Compare commits

...

182 Commits

Author SHA1 Message Date
b2f601df80 Make empty notification list transparent 2025-11-23 21:46:10 +01:00
25c3fbbef8 Fix toasts 2025-11-23 21:46:10 +01:00
eb9c35a9e5 Add inner bar implementation for notifications 2025-11-23 21:46:10 +01:00
bf000f409c Use a BarItem for the notification pill 2025-11-23 21:46:10 +01:00
ItsLemmy
e3c722fec8 VPN: Minimal support for wireguard + removed opening non existing panel. 2025-11-23 15:11:33 -05:00
Lysec
2f92445e8a Merge pull request #851 from lonerOrz/feat/mm
feat: Add circular progress bar to MediaMini widget
2025-11-23 21:01:33 +01:00
Ly-sec
59678022ac Launcher: hide ClipboardPreview if history is empty 2025-11-23 20:45:47 +01:00
loner
f56109ad35 feat: Add progress ring setting translations for all languages 2025-11-24 03:42:11 +08:00
loner
a5fdf67b05 fix: Optimize MediaMini widget progress ring display 2025-11-24 03:42:11 +08:00
loner
d04ae8a81d feat: Add circular progress bar to MediaMini widget 2025-11-24 03:42:11 +08:00
Ly-sec
81e9419e83 EmojiPlugin: fix emoji icon 2025-11-23 20:36:30 +01:00
Lysec
cbe9dc448b Merge pull request #844 from MrDowntempo/fix/cleaned-up-color-picker
Fixed button margins
2025-11-23 20:01:57 +01:00
Lysec
c15adb5322 Merge pull request #849 from acdcbyl/main
Matugen: Fix cava theme can't hot reload
2025-11-23 20:01:26 +01:00
Aiser
9d3aacc5f1 Matugen: Fix cava theme can't hot reload 2025-11-24 02:56:23 +08:00
Lysec
3f0392d137 Merge pull request #848 from acdcbyl/main
Matugen: Fix telegram theme
2025-11-23 19:47:46 +01:00
Aiser
1d75bf3aec Matugen: Fix telegram theme 2025-11-24 02:37:28 +08:00
Lysec
0d9c4974b3 Merge pull request #836 from acdcbyl/main
Matugen: Add Cava's Theme
2025-11-23 19:31:58 +01:00
Aiser
002df77b78 Matugen: Adjust cava's posthook 2025-11-24 02:23:21 +08:00
Lysec
0f83f25a2e Merge pull request #846 from lonerOrz/fix/cp
Fix shader compilation error in color picker
2025-11-23 19:03:11 +01:00
Lysec
8b6a6b6bce Merge pull request #845 from lonerOrz/feat/cub
feat: Added scrolling to the custom button settings page
2025-11-23 19:02:29 +01:00
loner
676942a942 Fix shader compilation error in color picker 2025-11-24 02:00:28 +08:00
Ly-sec
d6a6341e1a LockScreen: fix hibernate visibility check 2025-11-23 18:32:03 +01:00
loner
60280aa8d4 feat: Added scrolling to the custom button settings page 2025-11-24 00:35:06 +08:00
Corey Woodworth
bbde9f9b10 Fixed button margins 2025-11-23 09:53:27 -05:00
Ly-sec
faa074c330 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-11-23 13:08:34 +01:00
Ly-sec
72ce9953b6 BluetoothService: revert to old version 2025-11-23 13:08:29 +01:00
Lysec
7400801dbc Merge pull request #839 from lonerOrz/fix/osd
fix(brightness): Prevent brightness from getting stuck at minimum
2025-11-23 12:36:00 +01:00
loner
b9c12ec14c fix(brightness): Prevent brightness from getting stuck at minimum 2025-11-23 19:14:22 +08:00
Ly-sec
5187c8075b BluetoothService: cleanup 2025-11-23 11:34:32 +01:00
Ly-sec
cfffcdcd24 BluetoothService: more robust connection logic 2025-11-23 11:30:50 +01:00
Ly-sec
634a9b1a86 Notification: fix warning 2025-11-23 11:25:20 +01:00
Lysec
459f89e751 Merge pull request #834 from notiant/patch-3
Some changes for the notification layout & localize timestamps
2025-11-23 10:32:00 +01:00
Ly-sec
1a5888f277 LockScreen: make mouse cursor pointing when hovering over buttons 2025-11-23 10:03:06 +01:00
Aiser
33af5b9ab9 Matugen: Add Cava's Theme 2025-11-23 15:10:38 +08:00
ItsLemmy
4d13cfeedb Battery: BarIcon color changes when charging + Panel refinment (conditional PPD and some alignment) 2025-11-23 00:00:34 -05:00
ItsLemmy
0fe84273df Bluetooth: fix inverted toast messages. 2025-11-22 23:13:01 -05:00
notiant
e34248d5cb Some changes for the notification layout & localize timestamps 2025-11-23 00:51:04 +01:00
ItsLemmy
1ff64efc64 autofmt 2025-11-22 16:50:09 -05:00
Ly-sec
e108a3b45b Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-11-22 21:11:26 +01:00
Ly-sec
aa34a7ed36 Settings: add telegram
ProgramCheckerService: add check for telegram
2025-11-22 21:10:50 +01:00
ItsLemmy
190392ead7 Location: removed hardcoded max width 2025-11-22 15:10:22 -05:00
Ly-sec
cee39cce58 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-11-22 18:04:56 +01:00
Ly-sec
2035895c04 SessionMenuTab: fix warning 2025-11-22 18:04:39 +01:00
Lemmy
a61a40fd7c Merge pull request #811 from shouya/main
Show workspaces from the focused screen
2025-11-22 11:31:22 -05:00
ItsLemmy
3efee10a6b Autofmt 2025-11-22 11:26:15 -05:00
Lemmy
2d1034ece8 Merge pull request #822 from MrDowntempo/feat/SUPER-ColorPicker-DX-ALPHA-PlusPlus
Feat/super color picker dx alpha plus plus
2025-11-22 11:25:44 -05:00
ItsLemmy
69c60a6a5a BatteryPanel: show the powerprofile name in realtime + autofmt 2025-11-22 11:24:21 -05:00
ItsLemmy
1aac585d62 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-11-22 11:23:48 -05:00
ItsLemmy
a8ec22984a i18n 2025-11-22 11:23:45 -05:00
Ly-sec
7a3b488546 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-11-22 17:22:27 +01:00
Ly-sec
6b80da8e63 Merge remote-tracking branch 'origin/shell-state' 2025-11-22 17:21:47 +01:00
Lemmy
9911c722a0 Merge pull request #829 from art0rz/feat/battery-panel
Add BatteryPanel with charge level, power profile settings, prevent sleep toggle, battery health (if available)
2025-11-22 11:20:12 -05:00
ItsLemmy
04fd3b96b2 TaskBarGrouped: dont hide "ShowLabelsOnlyWhenOccupied" to avoid edge case when on an empty workspace. 2025-11-22 11:16:48 -05:00
art0rz
ba8733c5c0 Align battery keep awake naming and slider 2025-11-22 17:15:13 +01:00
art0rz
5cc71b4da2 Add BatteryPanel with charge level, power profile settings, prevent sleep toggle, battery health (if available) 2025-11-22 17:10:26 +01:00
Ly-sec
c3066e1dd5 SystemMonitor: fix vertical layout 2025-11-22 16:53:28 +01:00
ItsLemmy
5ee808e186 Autofmt 2025-11-22 10:44:20 -05:00
ItsLemmy
152c8efaa1 Panels Animation: centralizing more code for easier maintenance. 2025-11-22 10:43:57 -05:00
Lysec
b93b74ae59 Merge pull request #824 from lonerOrz/feat/emoji
Implement emoji picker
2025-11-22 16:12:28 +01:00
Ly-sec
8eaa5cc034 Settings: migration for shellstate is now handled in Settings.qml 2025-11-22 15:40:53 +01:00
ItsLemmy
75bc25747f Panels animation: better fix to break the binding look on animation duration. 2025-11-22 09:35:32 -05:00
Ly-sec
32927aa8a2 LockScreen: adjust wrong password popup 2025-11-22 15:06:52 +01:00
Ly-sec
afd156bbb8 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-11-22 14:50:21 +01:00
Ly-sec
24210697bf SetupWizard: check for WallpaperService 2025-11-22 14:50:17 +01:00
Lemmy
f7bdb3f059 Merge pull request #828 from notiant/patch-2
Fix some missing translations
2025-11-22 08:49:43 -05:00
Lysec
c8c6fd7568 PR_TEMPLATE: add interface scaling checkmark 2025-11-22 14:38:13 +01:00
Ly-sec
83d1cbacc7 autofmt 2025-11-22 13:52:45 +01:00
Ly-sec
74ba883dd8 initial commit 2025-11-22 13:51:58 +01:00
ItsLemmy
85ec32336b Panels: allow width animations with horizontal bars. 2025-11-22 07:49:01 -05:00
notiant
9ef788a354 Fix some missing translations 2025-11-22 13:21:01 +01:00
Lysec
01a26fd910 Merge pull request #827 from notiant/patch-1
LockScreen: make 'hibernate' optional
2025-11-22 13:14:53 +01:00
notiant
0293b8c8dd LockScreen: make 'hibernate' optional 2025-11-22 13:04:44 +01:00
Lysec
3914c32c96 Merge pull request #823 from acdcbyl/main
Matugen: Add Telegram's Theme
2025-11-22 13:04:29 +01:00
Lysec
4652691c4c Merge pull request #825 from lonerOrz/fix/tray
Fix inconsistent tray drawer behavior for different mouse buttons
2025-11-22 12:54:30 +01:00
shouya
1b706f8469 update sources for consistent naming 2025-11-22 20:49:55 +09:00
Lysec
679fd5c40e Merge pull request #826 from art0rz/fix/recording-button
Add screen recording loading feedback
2025-11-22 12:49:32 +01:00
Ly-sec
48c5435cef SetupWizard: ensure setuoCompleted is always being saved 2025-11-22 12:46:22 +01:00
shouya
5604d79961 update translations 2025-11-22 20:46:03 +09:00
shouya
7a64758ac8 declare default value for the settings 2025-11-22 19:58:11 +09:00
loner
880ae9c7b9 fix: Fix inconsistent tray drawer behavior for different mouse buttons 2025-11-22 18:03:33 +08:00
loner
c390e97c7a feat: Add emoji plugin translations for all supported languages 2025-11-22 17:34:44 +08:00
loner
8730eb0e71 feat: Add emoji usage tracking and sorting by frequency 2025-11-22 17:34:44 +08:00
loner
1dced9a7bc feat: Implement EmojiService 2025-11-22 17:34:44 +08:00
loner
aa7563838b feat: Implement emoji deduplication with user emoji priority 2025-11-22 17:34:44 +08:00
loner
6dc2bf5a16 feat: Add emoji picker plugin to launcher with category support 2025-11-22 17:34:44 +08:00
Aiser
0f650b36f7 Matugen: Add Telegram's Theme 2025-11-22 14:52:38 +08:00
Corey Woodworth
2c1c1da64c Translations done 2025-11-22 01:12:00 -05:00
Corey Woodworth
0ba80b10a9 removed 'Hex:' from i18n because of numerous translation issues 2025-11-22 01:12:00 -05:00
Corey Woodworth
89d7a63248 removed unneeded 'auto' since it's by default 2025-11-22 01:12:00 -05:00
Corey Woodworth
42f782abbf Update tooltip code, Use an Enum for editMode, setup strings for i18n 2025-11-22 01:12:00 -05:00
Corey Woodworth
bd1c9d30d9 I forgor to add the ColorPicker file 2025-11-22 01:12:00 -05:00
Corey Woodworth
724fed6001 Initial commit 2025-11-22 01:12:00 -05:00
ItsLemmy
823042b245 Panels: properly animate height with vertical bar + Bluetooth sizing refinement. 2025-11-22 00:33:42 -05:00
ItsLemmy
9c550af64e UpdateService: fix wrong changelog when updating from 3.2.0-dev to 3.2.0-git 2025-11-21 23:25:22 -05:00
ItsLemmy
1bf54de99c UpdateService: Remove potential -dev 2025-11-21 16:40:48 -05:00
ItsLemmy
7a68030f69 Notifications: ensure they are not sandwitched between panels
+ Always access lockScreen via panel service and removed deprecation
notice.
2025-11-21 15:18:38 -05:00
ItsLemmy
f46915d2c3 UpdateService: cleanup and use -git suffix instead of -dev. 2025-11-21 13:54:00 -05:00
ItsLemmy
50ebc77513 UpdateService: proper revert 2025-11-21 13:43:09 -05:00
ItsLemmy
522e7e4352 Default settings: update 2025-11-21 13:41:23 -05:00
ItsLemmy
9f9e1341fd Reapply "UpdateService: renamed "-dev" to "-git" for clarity."
This reverts commit c919c54a32.
2025-11-21 13:38:38 -05:00
ItsLemmy
c919c54a32 Revert "UpdateService: renamed "-dev" to "-git" for clarity."
This reverts commit 6387dcc6d4.
2025-11-21 13:38:29 -05:00
ItsLemmy
6387dcc6d4 UpdateService: renamed "-dev" to "-git" for clarity. 2025-11-21 13:28:42 -05:00
ItsLemmy
455014a39b Brightness: scroll-wheel was bypassing available control check. 2025-11-21 13:28:12 -05:00
ItsLemmy
a884f012d8 i18n + autoformat 2025-11-21 13:18:02 -05:00
Lemmy
c5b23cc291 Merge pull request #804 from Vortelf/feat/vpn-widget
VPN: Widget Implementation
2025-11-21 13:12:50 -05:00
Lemmy
04e46815f8 Merge pull request #808 from EmmetZ/brightness-panel
feat: add brightness panel for bar brightness widget
2025-11-21 13:11:05 -05:00
Lemmy
f3d1e1f3d1 Merge pull request #813 from alaughlin/dock-border-radius
Dock: make border radius configurable
2025-11-21 12:05:25 -05:00
Lemmy
e2aa4ca2f8 Merge pull request #807 from lonerOrz/feat/custombutton
Enhance custom button
2025-11-21 12:02:57 -05:00
Lemmy
d6edc55d16 Merge pull request #817 from MrDowntempo/feat/smarter_shader_compiler
shaders-compile.sh supports file list arguments
2025-11-21 11:57:05 -05:00
ItsLemmy
e5912760ca WiFi Panel: improved the layout with proper multiple sections and proper height calculation. 2025-11-21 11:46:14 -05:00
MrDowntempo
7d981fb55b Merge branch 'main' into feat/smarter_shader_compiler 2025-11-21 11:45:19 -05:00
Corey Woodworth
e97c46e96c shaders-compile.sh supports file list arguments 2025-11-21 11:36:51 -05:00
Lysec
c1afa199e3 Merge pull request #816 from LionHeartP/main
revert: 'Matugen/Discord: fix inbox alignment'
2025-11-21 17:23:28 +01:00
LionHeartP
530992a14b revert: 'Matugen/Discord: fix inbox alignment' 2025-11-21 18:13:13 +02:00
Lysec
5d9cfeb9d0 Merge pull request #815 from lonerOrz/fix/about-version
fix: Fix latestVersion on the about page
2025-11-21 16:27:20 +01:00
loner
8cb4711629 fix: Fix latestVersion on the about page 2025-11-21 23:06:36 +08:00
Ly-sec
2d856882d2 Changelog: remove changelogs.json 2025-11-21 15:58:34 +01:00
art0rz
f181bdf21c Add screen recording loading feedback 2025-11-21 15:52:29 +01:00
Lysec
665aa84f70 Merge pull request #814 from lonerOrz/fix/about
fix: Fix the rich text display on the About page
2025-11-21 15:45:17 +01:00
Ly-sec
b84452e04d Changelogs: overhaul 2025-11-21 15:44:41 +01:00
Ly-sec
d3c200f50c SchemeDownloader: download schemes to ~/.config/noctalia/colorschemes/
ColorSchemeService: check said folder for theming
TemplateProcessor: check said folder for theming
2025-11-21 15:32:02 +01:00
loner
a39fbb5639 fix: Fix the rich text display on the About page 2025-11-21 22:14:57 +08:00
Ly-sec
fe40758d4e SchemeDownloader: fix logger warning 2025-11-21 15:02:38 +01:00
Ly-sec
63331c1018 WidgetSetting: fixes not being able to type when opening settings through context menu 2025-11-21 14:02:24 +01:00
Ly-sec
9c955cdd39 Services/systemd: small update 2025-11-21 13:58:50 +01:00
Adam Laughlin
d9e0f2fc10 Dock: make border radius configurable 2025-11-21 07:45:37 -05:00
Georgi Velev
1cbc793087 VPN: Widget Implementation 2025-11-21 14:32:39 +02:00
Ly-sec
1a2ddbb9e3 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-11-21 13:27:13 +01:00
Ly-sec
e46c9cdf0e Battery: add visual indicator for low battery (same as SysMon) 2025-11-21 13:27:03 +01:00
Lysec
43cdc4494d Merge pull request #786 from lonerOrz/feat/clip
Add clipboard preview
2025-11-21 13:19:42 +01:00
Ly-sec
5ed4c97ee5 Tooltip: fix newline detection 2025-11-21 13:02:50 +01:00
Lysec
ddd3ae364c Merge pull request #812 from bokicoder/patch-1
i18n: improve chinese translation
2025-11-21 12:21:32 +01:00
bokicoder
3b793add39 i18n: improve chinese translation 2025-11-21 19:15:19 +08:00
Ly-sec
71f4a8eb49 NText: add optional RichText (default false) to fix calendar with specific languages 2025-11-21 11:09:03 +01:00
Ly-sec
2f735eda81 ChangelogPanel: nice formatting for changelogs
AboutTab: update version connection
GitHubService: cleanup, move changelog logic to UpdateService
UpdateService: use new changelog host
2025-11-21 11:01:59 +01:00
shouya
5f2a6ffc9d fix workspace.qml warning 2025-11-21 15:01:02 +09:00
shouya
211a9e4033 add translations for other languages 2025-11-21 14:53:59 +09:00
shouya
bcebaa0185 add english translation 2025-11-21 14:52:20 +09:00
shouya
8331d1954d show workspaces for the focused screen 2025-11-21 14:49:31 +09:00
loner
ee33da8348 i18n: fix: Add translations for custom button wheel actions 2025-11-21 13:14:57 +08:00
loner
f7d7d7ac15 fix: Stabilize custom button wheel command settings UI layout 2025-11-21 12:38:50 +08:00
ItsLemmy
972ac47c1b Bluetooth: smaller font for section name, similar to wifi. 2025-11-20 23:08:42 -05:00
ItsLemmy
0b0860a446 WiFi: improved classification and sorting 2025-11-20 23:06:56 -05:00
loner
e8a27acb63 fix: Left click behavior should only depend on left click settings 2025-11-21 11:06:51 +08:00
loner
694fefeebd feat: Custom buttons now support wheel actions 2025-11-21 10:58:15 +08:00
ItsLemmy
088431b20d Autoformatting + translations 2025-11-20 21:38:00 -05:00
ItsLemmy
63940703f8 TaskbarGrouped: Fixes, cleanup and improvements. 2025-11-20 21:37:02 -05:00
EmmetZ
e3c171840f feat: add brightness panel for bar brightness widget 2025-11-21 10:23:23 +08:00
loner
857d1dbbb6 feat: Update translation files for maxTextLength feature 2025-11-21 09:07:39 +08:00
loner
516fc47b68 feat: Replace hideTextInVerticalBar with maxTextLength object
- Replace boolean hideTextInVerticalBar with maxTextLength object that has
  separate horizontal and vertical properties for more flexible text length control
- Add NSpinBox controls in settings UI to configure both horizontal and
  vertical max text length independently
- Update CustomButton widget to use new maxTextLength structure and
  implement text scrolling based on direction-specific limits
- Set default values to 10 for both horizontal and vertical (was 20/0)
- Update translations and widget registry metadata accordingly
- When vertical maxTextLength is 0, text is completely hidden (preserving
  original hideTextInVerticalBar: true behavior)

This allows users to set different text length limits for horizontal and
vertical bar orientations, providing more granular control over text display.
2025-11-21 09:07:30 +08:00
loner
e549cfcb78 feat: Use maxTextLength to Limit Custom Button Text Length 2025-11-21 08:31:15 +08:00
loner
6a840769ed Fix(BarPillVertical): Improve vertical text positioning and spacing 2025-11-21 08:31:00 +08:00
loner
ec92295a98 Enhance custom tooltip parsing 2025-11-21 08:30:46 +08:00
Ly-sec
60d37576e0 LocationTab: properly trim whitespaces from first day of the week 2025-11-20 21:08:07 +01:00
Ly-sec
067bbf20bc ColorScheme/Rosepine: fix colors 2025-11-20 20:54:43 +01:00
Ly-sec
49aab3c487 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-11-20 20:45:25 +01:00
Ly-sec
868b14bbc3 AudioCard: fix elide 2025-11-20 20:45:21 +01:00
Lysec
b435d1f588 Merge pull request #806 from bokicoder/main
Nix: cleaner code
2025-11-20 20:19:34 +01:00
Ly-sec
1fc1fa36aa Matugen/Discord: fix inbox alignment (thanks @LionHeartP) 2025-11-20 19:56:18 +01:00
wxlyyy
04311f191f Nix: cleaner code 2025-11-21 02:28:56 +08:00
Ly-sec
0726e6b92f LockScreen: adjust button width depending on text length 2025-11-20 17:55:45 +01:00
Ly-sec
9a3d04249f Set version to dev 2025-11-20 17:42:59 +01:00
Ly-sec
edd4ba1b15 LockScreen: make digital font bigger 2025-11-20 17:39:01 +01:00
loner
e4e3b1b85c qml format 2025-11-19 16:07:08 +08:00
loner
4a0c2b7ef3 feat(i18n): Add clip preview translations for Simplified Chinese (zh-CN) 2025-11-19 16:07:07 +08:00
loner
5e2f8c1462 feat(i18n): Add clip preview translations for Ukrainian (uk-UA) 2025-11-19 16:07:07 +08:00
loner
97ba831cb4 feat(i18n): Add clip preview translations for Turkish (tr) 2025-11-19 16:07:07 +08:00
loner
5ade827a4c feat(i18n): Add clip preview translations for Russian (ru) 2025-11-19 16:07:07 +08:00
loner
03554120be feat(i18n): Add clip preview translations for Portuguese (pt) 2025-11-19 16:07:07 +08:00
loner
2917f02621 feat(i18n): Add clip preview translations for Dutch (nl) 2025-11-19 16:07:07 +08:00
loner
a18be7927c feat(i18n): Add clip preview translations for French (fr) 2025-11-19 16:07:07 +08:00
loner
9bf8fd16d6 feat(i18n): Add clip preview translations for Spanish (es) 2025-11-19 16:07:07 +08:00
loner
948c3c7e18 feat(i18n): Add clip preview translations for German (de) 2025-11-19 16:07:07 +08:00
loner
b2978113c5 feat(launcher): Integrate TextFormatter for enhanced preview UI 2025-11-19 16:07:07 +08:00
loner
87f62b288b feat(launcher): UI improvements for clipboard preview window 2025-11-19 16:07:07 +08:00
loner
ed373df99d feat(launcher): Add toggle for clip plugin preview 2025-11-19 16:07:07 +08:00
loner
529869f796 fix: External clipboard preview panel positioning and styling 2025-11-19 16:07:07 +08:00
loner
12766e411d widget: new fillMode and smt 2025-11-19 16:07:07 +08:00
loner
79f79e0cff feat: Add image preview logic 2025-11-19 16:07:07 +08:00
loner
ca89a0dc35 fix: Fix the proportions of the list and the preview 2025-11-19 16:07:07 +08:00
loner
6eaffb0e65 feat: implement full content preview with async loading in ClipboardPreview 2025-11-19 16:07:07 +08:00
loner
455ef3449e feat: clip preview 2025-11-19 16:07:07 +08:00
118 changed files with 6896 additions and 2427 deletions

View File

@@ -22,6 +22,7 @@ Describe how you tested your changes and mark the relevant items.
- [ ] Tested on Hyprland
- [ ] Tested on sway
- [ ] Tested with different bar positions and density settings
- [ ] Tested at different interface scaling values
- [ ] Tested with multiple monitors (if applicable)
## Screenshots / Videos

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.qmlls.ini
.zed
Bin/battery-manager/uninstall-battery-manager.sh
.idea

View File

@@ -8,7 +8,7 @@
"mOnTertiary": "#e0def4",
"mError": "#eb6f92",
"mOnError": "#191724",
"mSurface": "#1f1d2e",
"mSurface": "#191724",
"mOnSurface": "#e0def4",
"mSurfaceVariant": "#26233a",
"mOnSurfaceVariant": "#908caa",

View File

@@ -0,0 +1,74 @@
[
{"emoji": "😀", "name": "grinning face", "keywords": ["smile", "happy", "grin"], "category": "people"},
{"emoji": "😂", "name": "face with tears of joy", "keywords": ["laugh", "cry", "happy", "joy"], "category": "people"},
{"emoji": "😍", "name": "smiling face with heart-eyes", "keywords": ["love", "heart", "eyes", "smile"], "category": "people"},
{"emoji": "🤔", "name": "thinking face", "keywords": ["think", "ponder", "consider"], "category": "people"},
{"emoji": "😎", "name": "smiling face with sunglasses", "keywords": ["cool", "sunglasses", "smile"], "category": "people"},
{"emoji": "🥳", "name": "partying face", "keywords": ["party", "hat", "horn", "celebration"], "category": "people"},
{"emoji": "🤩", "name": "star-struck", "keywords": ["star", "eyes", "amazed", "wow"], "category": "people"},
{"emoji": "🤯", "name": "exploding head", "keywords": ["mind", "blown", "explode", "shocked"], "category": "people"},
{"emoji": "👍", "name": "thumbs up", "keywords": ["like", "good", "agree", "ok"], "category": "people"},
{"emoji": "👎", "name": "thumbs down", "keywords": ["dislike", "bad", "disagree", "no"], "category": "people"},
{"emoji": "🐱", "name": "cat face", "keywords": ["cat", "kitten", "pet", "meow"], "category": "animals"},
{"emoji": "🐶", "name": "dog face", "keywords": ["dog", "puppy", "pet", "woof"], "category": "animals"},
{"emoji": "🦊", "name": "fox face", "keywords": ["fox", "animal", "cute", "wild"], "category": "animals"},
{"emoji": "🐼", "name": "panda", "keywords": ["panda", "bear", "animal", "cute"], "category": "animals"},
{"emoji": "🦄", "name": "unicorn", "keywords": ["unicorn", "horse", "magic", "fantasy"], "category": "animals"},
{"emoji": "🦁", "name": "lion", "keywords": ["lion", "animal", "face", "majestic"], "category": "animals"},
{"emoji": "🐢", "name": "turtle", "keywords": ["turtle", "slow", "animal", "shell"], "category": "animals"},
{"emoji": "🐙", "name": "octopus", "keywords": ["octopus", "animal", "ocean", "sea"], "category": "animals"},
{"emoji": "🌻", "name": "sunflower", "keywords": ["sunflower", "flower", "nature", "yellow"], "category": "nature"},
{"emoji": "🌺", "name": "hibiscus", "keywords": ["hibiscus", "flower", "nature", "plant"], "category": "nature"},
{"emoji": "🌍", "name": "earth globe europe-africa", "keywords": ["earth", "world", "globe", "nature"], "category": "nature"},
{"emoji": "🌞", "name": "sun with face", "keywords": ["sun", "nature", "bright", "weather"], "category": "nature"},
{"emoji": "🌙", "name": "crescent moon", "keywords": ["moon", "night", "sky", "sleep"], "category": "nature"},
{"emoji": "🌈", "name": "rainbow", "keywords": ["rainbow", "color", "weather", "sky"], "category": "nature"},
{"emoji": "🔥", "name": "fire", "keywords": ["fire", "hot", "flame", "burn"], "category": "nature"},
{"emoji": "💧", "name": "droplet", "keywords": ["water", "drop", "drip", "liquid"], "category": "nature"},
{"emoji": "🍎", "name": "red apple", "keywords": ["apple", "fruit", "food", "red"], "category": "food"},
{"emoji": "🍕", "name": "pizza", "keywords": ["pizza", "food", "italian", "cheese"], "category": "food"},
{"emoji": " sushi", "name": "sushi", "keywords": ["sushi", "food", "japanese", "rice"], "category": "food"},
{"emoji": "🍔", "name": "hamburger", "keywords": ["hamburger", "food", "burger", "fast food"], "category": "food"},
{"emoji": "🍦", "name": "soft ice cream", "keywords": ["ice cream", "dessert", "food", "sweet"], "category": "food"},
{"emoji": "🍩", "name": "doughnut", "keywords": ["donut", "doughnut", "food", "sweet"], "category": "food"},
{"emoji": "🍪", "name": "cookie", "keywords": ["cookie", "food", "sweet", "biscuit"], "category": "food"},
{"emoji": "🍺", "name": "beer mug", "keywords": ["beer", "drink", "alcohol", "pub"], "category": "food"},
{"emoji": "🍷", "name": "wine glass", "keywords": ["wine", "drink", "alcohol", "glass"], "category": "food"},
{"emoji": "☕", "name": "hot beverage", "keywords": ["coffee", "hot", "drink", "cafe"], "category": "food"},
{"emoji": "⚽", "name": "soccer ball", "keywords": ["soccer", "football", "ball", "sport"], "category": "activity"},
{"emoji": "🏀", "name": "basketball", "keywords": ["basketball", "ball", "sport", "game"], "category": "activity"},
{"emoji": "🎯", "name": "direct hit", "keywords": ["target", "bullseye", "aim", "goal"], "category": "activity"},
{"emoji": "🎮", "name": "video game", "keywords": ["game", "video game", "play", "console"], "category": "activity"},
{"emoji": "🎲", "name": "game die", "keywords": ["dice", "game", "board", "random"], "category": "activity"},
{"emoji": "🎨", "name": "artist palette", "keywords": ["art", "paint", "colors", "creative"], "category": "activity"},
{"emoji": "🎤", "name": "microphone", "keywords": ["mic", "microphone", "sing", "karaoke"], "category": "activity"},
{"emoji": "🎬", "name": "clapper board", "keywords": ["movie", "film", "action", "director"], "category": "activity"},
{"emoji": "🚗", "name": "automobile", "keywords": ["car", "vehicle", "transport", "drive"], "category": "travel"},
{"emoji": "✈️", "name": "airplane", "keywords": ["plane", "flight", "travel", "fly"], "category": "travel"},
{"emoji": "🚀", "name": "rocket", "keywords": ["space", "launch", "fast", "ship"], "category": "travel"},
{"emoji": "🚲", "name": "bicycle", "keywords": ["bike", "cycle", "transport", "exercise"], "category": "travel"},
{"emoji": "🚂", "name": "locomotive", "keywords": ["train", "steam", "vehicle", "transport"], "category": "travel"},
{"emoji": "🚢", "name": "ship", "keywords": ["ship", "boat", "water", "transport"], "category": "travel"},
{"emoji": "🏠", "name": "house", "keywords": ["home", "house", "building", "residence"], "category": "objects"},
{"emoji": "🏢", "name": "office building", "keywords": ["office", "building", "work", "business"], "category": "objects"},
{"emoji": "🏥", "name": "hospital", "keywords": ["hospital", "medical", "health", "doctor"], "category": "objects"},
{"emoji": "🏦", "name": "bank", "keywords": ["bank", "money", "finance", "building"], "category": "objects"},
{"emoji": "🏪", "name": "convenience store", "keywords": ["store", "shop", "convenience", "grocery"], "category": "objects"},
{"emoji": "🎁", "name": "gift", "keywords": ["present", "gift", "box", "birthday"], "category": "objects"},
{"emoji": "💡", "name": "light bulb", "keywords": ["idea", "light", "bright", "thinking"], "category": "objects"},
{"emoji": "💻", "name": "laptop computer", "keywords": ["computer", "laptop", "pc", "work"], "category": "objects"},
{"emoji": "📱", "name": "mobile phone", "keywords": ["phone", "smartphone", "cellphone", "mobile"], "category": "objects"},
{"emoji": "🔑", "name": "key", "keywords": ["key", "password", "secret", "access"], "category": "objects"},
{"emoji": "🔒", "name": "locked", "keywords": ["lock", "secure", "private", "closed"], "category": "objects"},
{"emoji": "⭐", "name": "star", "keywords": ["star", "rating", "favorite", "bright"], "category": "symbols"},
{"emoji": "❤️", "name": "red heart", "keywords": ["heart", "love", "like", "affection"], "category": "symbols"},
{"emoji": "💯", "name": "hundred points", "keywords": ["percent", "perfect", "score", "100"], "category": "symbols"},
{"emoji": "©️", "name": "copyright", "keywords": ["copyright", "symbol", "c", "legal"], "category": "symbols"},
{"emoji": "®️", "name": "registered", "keywords": ["registered", "symbol", "r", "trademark"], "category": "symbols"},
{"emoji": "™️", "name": "trade mark", "keywords": ["trademark", "tm", "symbol", "mark"], "category": "symbols"},
{"emoji": "✔️", "name": "check mark", "keywords": ["check", "mark", "ok", "correct"], "category": "symbols"},
{"emoji": "❌", "name": "cross mark", "keywords": ["x", "cross", "mark", "no", "wrong"], "category": "symbols"},
{"emoji": "⚠️", "name": "warning", "keywords": ["warning", "exclamation", "caution", "alert"], "category": "symbols"},
{"emoji": "🎉", "name": "party popper", "keywords": ["party", "celebration", "tada", "congrats"], "category": "symbols"},
{"emoji": "🔔", "name": "bell", "keywords": ["bell", "sound", "notification", "ring"], "category": "symbols"}
]

View File

@@ -0,0 +1,16 @@
[color]
background = '{{colors.surface.default.hex}}'
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}}'

View File

@@ -0,0 +1,139 @@
// Material You theme for Telegram Desktop
// Generated by matugen
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

View File

@@ -253,4 +253,4 @@ div[class*="linkCalloutContainer"]:hover > span[class*="semibold"],
div[class*="linkCalloutContainer"]:hover > span[data-text-variant*="semibold"] {
color: var(--accent-1) !important;
opacity: 1 !important;
}
}

View File

@@ -1,7 +1,6 @@
[Unit]
Description=Noctalia Shell Service
Requisite=graphical-session.target
PartOf=graphical-session.target
BindsTo=graphical-session.target
After=graphical-session.target
[Service]
@@ -10,4 +9,4 @@ Restart=on-failure
RestartSec=1
[Install]
WantedBy=graphical-session.target
WantedBy=graphical-session.target

View File

@@ -123,10 +123,6 @@
"stream-description": "Geben Sie einen Befehl ein, der kontinuierlich ausgeführt werden soll."
},
"dynamic-text": "Dynamischer Text",
"hide-vertical": {
"description": "Wenn aktiviert, wird der Text aus der Befehlsausgabe nicht angezeigt, wenn sich die Leiste in einem vertikalen Layout befindet (links oder rechts).",
"label": "Text in vertikaler Leiste ausblenden"
},
"icon": {
"description": "Symbol aus der Bibliothek auswählen.",
"label": "Symbol"
@@ -136,6 +132,14 @@
"label": "Linksklick",
"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)",
"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)",
"label": "Max. Textlänge (vertikal)"
},
"middle-click": {
"description": "Befehl, der ausgeführt wird, wenn die Schaltfläche mit der mittleren Maustaste angeklickt wird.",
"label": "Mittelklick",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "Gestreamte Zeilen aus dem Befehl werden als Text auf der Schaltfläche angezeigt.",
"label": "Stream"
},
"wheel": {
"description": "Befehl, der bei Verwendung des Scrollrads ausgeführt wird.\nVerwenden Sie $delta für die Scrollrad-Delta im Befehl",
"label": "Scrollrad",
"update-text": "Anzeigetext beim Scrollen aktualisieren"
},
"wheel-down": {
"description": "Befehl, der ausgeführt wird, wenn das Scrollrad heruntergescrollt wird.",
"label": "Scrollrad runter Befehl"
},
"wheel-mode-separate": {
"description": "Separate Befehle für Scrollrad hoch und runter aktivieren",
"label": "Separate Scrollrad-Befehle"
},
"wheel-up": {
"description": "Befehl, der ausgeführt wird, wenn das Scrollrad hochgescrollt wird.",
"label": "Scrollrad hoch Befehl"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "Audio-Visualizer anzeigen, wenn Musik abgespielt wird.",
"label": "Visualizer anzeigen"
},
"show-progress-ring": {
"description": "Runden Fortschrittsindikator anzeigen, der den Titelfortschritt anzeigt.",
"label": "Fortschrittsring anzeigen"
},
"use-fixed-width": {
"description": "Wenn aktiviert, verwendet das Widget immer die maximale Breite, anstatt sich dynamisch an den Inhalt anzupassen.",
"label": "Feste Breite verwenden"
@@ -352,6 +377,10 @@
"description": "Arbeitsbereiche ohne Fenster nicht anzeigen.",
"label": "Unbesetzte ausblenden"
},
"follow-focused-screen": {
"description": "Zeigt Arbeitsbereiche vom aktuell fokussierten Bildschirm an, statt vom Bildschirm, auf dem sich die Leiste befindet.",
"label": "Fokussiertem Bildschirm folgen"
},
"label-mode": {
"description": "Wählen Sie, wie Arbeitsbereichs-Beschriftungen angezeigt werden.",
"label": "Beschriftungsmodus"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "Helligkeit",
"charge-level": "Ladestand",
"charging": "Wird geladen.",
"charging-rate": "Laderate: {rate} W.",
"discharging": "Wird entladen.",
"discharging-rate": "Entladerate: {rate} W.",
"health": "Zustand: {percent}%",
"idle": "Leerlauf.",
"inhibit-idle-description": "Hält das System wach.",
"inhibit-idle-label": "Wach halten",
"no-battery-detected": "Keine Batterie erkannt.",
"panel-title": "Akku",
"plugged-in": "Angeschlossen.",
"power-profile": "Energieprofil",
"time-left": "Verbleibende Zeit: {time}.",
"time-until-full": "Zeit bis vollständig geladen: {time}."
},
@@ -396,29 +431,29 @@
}
},
"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": {
"title": "Was ist neu in {version}",
"buttons": {
"discord": "Unserem Discord beitreten",
"dismiss": "Ok"
},
"empty": "Es sind noch keine Versionshinweise verfügbar.",
"highlight-title": "Highlights",
"section": {
"released": "Veröffentlicht am {date}",
"version": "Version {version}"
},
"subtitle": {
"fresh": "Danke, dass du Noctalia installiert hast! Das ist in diesem Build enthalten.",
"updated": "Aktualisiert von {previousVersion}"
},
"title": "Was ist neu in {version}",
"version": {
"new-user": "Neuinstallation"
},
"highlight-title": "Highlights",
"empty": "Es sind noch keine Versionshinweise verfügbar.",
"section": {
"version": "Version {version}",
"released": "Veröffentlicht am {date}"
},
"buttons": {
"discord": "Unserem Discord beitreten",
"feedback": "Feedback senden",
"dismiss": "Ok"
}
},
"error": {
"rate-limit": "GitHub-Limit erreicht. Bitte versuche es in ein paar Minuten erneut."
}
},
"clock": {
@@ -428,10 +463,12 @@
"activate-app": "{app} aktivieren",
"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": "Bitte nicht stören deaktivieren",
"disable-wifi": "WLAN deaktivieren",
"disconnect-vpn": "Verbindung zu {name} trennen",
"enable-bluetooth": "Bluetooth aktivieren",
"enable-dnd": "Nicht stören aktivieren",
"enable-wifi": "WLAN aktivieren",
@@ -471,7 +508,7 @@
"password": "Passwort eingeben...",
"restart": "Neu starten",
"shut-down": "Herunterfahren",
"suspend": "Ruhezustand",
"suspend": "Energie sparen",
"welcome-back": "Willkommen zurück,"
},
"notifications": {
@@ -480,6 +517,15 @@
"description": "Ihre Benachrichtigungen werden hier angezeigt, sobald sie eintreffen.",
"no-notifications": "Keine Benachrichtigungen",
"title": "Benachrichtigungen"
},
"time": {
"now": "jetzt",
"diffM": "vor 1 Minute",
"diffMM": "vor {diff} Minuten",
"diffH": "vor 1 Stunde",
"diffHH": "vor {diff} Stunden",
"diffD": "vor 1 Tag",
"diffDD": "vor {diff} Tagen"
}
},
"options": {
@@ -647,7 +693,11 @@
"clipboard-history-disabled-description": "Zwischenablageverlauf in den Einstellungen aktivieren oder cliphist installieren",
"clipboard-loading": "Lade Zwischenablageverlauf...",
"clipboard-loading-description": "Bitte warten",
"clipboard-search-description": "Zwischenablageverlauf durchsuchen"
"clipboard-search-description": "Zwischenablageverlauf durchsuchen",
"emoji": "Emoji-Auswahl",
"emoji-loading": "Lade Emojis...",
"emoji-loading-description": "Bitte warten",
"emoji-search-description": "Emojis suchen und kopieren"
},
"quickSettings": {
"bluetooth": {
@@ -726,11 +776,11 @@
"action-in-seconds": "{action} in {seconds} Sekunden...",
"hibernate": "Ruhezustand",
"lock": "Sperren",
"lock-and-suspend": "Sperren und Ruhezustand",
"lock-and-suspend": "Sperren und Energie sparen",
"logout": "Abmelden",
"reboot": "Neu starten",
"shutdown": "Herunterfahren",
"suspend": "Energiesparmodus",
"suspend": "Energie sparen",
"title": "Sitzungsmenü"
},
"settings": {
@@ -1018,7 +1068,15 @@
"description-missing": "Erfordert die Installation von {app}"
},
"spicetify": {
"description": "Schreibe {Dateipfad}. Das Comfy-Theme muss manuell installiert und aktiviert werden",
"description": "Schreibe {filepath}. Das Comfy-Theme muss manuell installiert und aktiviert werden",
"description-missing": "Benötigt die Installation von {app}"
},
"telegram": {
"description": "Schreibe {filepath}.",
"description-missing": "Benötigt die Installation von {app}"
},
"cava": {
"description": "Schreibe {filepath}.",
"description-missing": "Benötigt die Installation von {app}"
},
"vicinae": {
@@ -1217,6 +1275,10 @@
"description": "Hintergrund-Transparenz des Docks anpassen.",
"label": "Hintergrund-Transparenz"
},
"border-radius": {
"description": "Den Radius der Dock-Umrandung anpassen.",
"label": "Eckenradius"
},
"colorize-icons": {
"description": "Theme-Farben auf Dock-App-Symbole anwenden (nur nicht fokussierte Apps).",
"label": "Symbole einfärben"
@@ -1368,6 +1430,10 @@
"description": "Hintergrund-Transparenz des Starters anpassen.",
"label": "Hintergrund-Transparenz"
},
"clip-preview": {
"description": "Zeigt eine Vorschau des Inhalts der Zwischenablage an, wenn der Befehl >clip verwendet wird.",
"label": "Clip-Vorschau aktivieren"
},
"clipboard-history": {
"description": "Zugriff auf zuvor kopierte Elemente über den Launcher.",
"label": "Zwischenablageverlauf aktivieren"
@@ -1472,8 +1538,12 @@
"label": "Kompakter Sperrbildschirm"
},
"lock-on-suspend": {
"description": "Den Bildschirm beim Suspendieren des Systems automatisch sperren.",
"label": "Sperren beim Ruhezustand"
"description": "Den Bildschirm beim Energiesparen automatisch sperren.",
"label": "Sperren beim Energiesparen"
},
"show-hibernate": {
"description": "Die Option 'Ruhezustand' in den Energieaktionen anzeigen.",
"label": "Ruhezustand anzeigen"
},
"title": "Sperrbildschirm"
},
@@ -2038,6 +2108,10 @@
"title-matugen": "Matugen-Templating-Verarbeitung fehlgeschlagen",
"title-predefined": "Die Verarbeitung des vordefinierten Farbschemas ist fehlgeschlagen."
},
"vpn": {
"connected": "Verbunden mit '{name}'",
"disconnected": "Verbindung zu '{name}' getrennt"
},
"wallpaper-colors": {
"disabled": "Hintergrundbild-Farben deaktiviert",
"enabled": "Hintergrundbild-Farben aktiviert",
@@ -2071,6 +2145,7 @@
"input-muted": "Audio-Eingabe stummschalten",
"keep-awake": "Wach halten",
"keyboard-layout": "{layout} Tastaturlayout",
"manage-vpn": "VPN-Verbindungen verwalten",
"manage-wifi": "WLAN verwalten",
"microphone-volume-at": "Mikrofonlautstärke bei {volume}%.\nLinksklick für Einstellungen. Rechtsklick zum Stummschalten.\nScrollen zum Ändern der Lautstärke.",
"move-to-center-section": "Zur mittleren Sektion verschieben",
@@ -2205,23 +2280,11 @@
"widgets": {
"color-picker": {
"apply": "Anwenden",
"brightness": "Helligkeit",
"cancel": "Abbrechen",
"hex": {
"description": "Geben Sie einen hexadezimalen Farbcode ein.",
"label": "Hex-Farbe"
},
"palette": {
"description": "Wählen Sie aus einer großen Auswahl vordefinierter Farben.",
"label": "Palette"
},
"rgb": {
"description": "Passen Sie Rot-, Grün-, Blau- und Helligkeitswerte an.",
"label": "RGB-Werte"
},
"theme-colors": {
"description": "Schnellzugriff auf die Farbpalette Ihres Themes.",
"label": "Theme-Farben"
"label": "Palette",
"theme-colors": "Schneller Zugriff auf die Farbpalette Ihres Themes."
},
"title": "Farbauswahl"
},
@@ -2297,6 +2360,7 @@
},
"wifi": {
"panel": {
"available-networks": "Verfügbare Netzwerke",
"connect": "Verbinden",
"connected": "Verbunden",
"disabled": "WLAN ist deaktiviert",
@@ -2307,6 +2371,7 @@
"forget": "Vergessen",
"forget-network": "Dieses Netzwerk vergessen?",
"forgetting": "Wird vergessen...",
"known-networks": "Bekannte Netzwerke",
"no-networks": "Keine Netzwerke gefunden",
"password": "Passwort",
"saved": "Gespeichert",

View File

@@ -123,10 +123,6 @@
"stream-description": "Enter a command to run continuously."
},
"dynamic-text": "Dynamic text",
"hide-vertical": {
"description": "If enabled, the text from the command output will not be shown when the bar is in a vertical layout (left or right).",
"label": "Hide text in vertical bar"
},
"icon": {
"description": "Select an icon from the library.",
"label": "Icon"
@@ -136,6 +132,14 @@
"label": "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)",
"label": "Max text length (horizontal)"
},
"max-text-length-vertical": {
"description": "Maximum number of characters to show in vertical bar (0 to hide text)",
"label": "Max text length (vertical)"
},
"middle-click": {
"description": "Command to execute when the button is middle-clicked.",
"label": "Middle click",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "Streamed lines from the command will be displayed as text on the button.",
"label": "Stream"
},
"wheel": {
"description": "Command to execute when the scroll wheel is used.\nUse $delta for the scroll wheel delta in the command",
"label": "Scroll wheel",
"update-text": "Update displayed text on scroll"
},
"wheel-down": {
"description": "Command to execute when the scroll wheel is scrolled down.",
"label": "Wheel down command"
},
"wheel-mode-separate": {
"description": "Enable separate commands for wheel up and down",
"label": "Separate wheel commands"
},
"wheel-up": {
"description": "Command to execute when the scroll wheel is scrolled up.",
"label": "Wheel up command"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "Display an audio visualizer when music is playing.",
"label": "Show visualizer"
},
"show-progress-ring": {
"description": "Display a circular progress indicator showing track progress.",
"label": "Show progress ring"
},
"use-fixed-width": {
"description": "When enabled, the widget will always use the maximum width instead of dynamically adjusting to content.",
"label": "Use Fixed Width"
@@ -352,6 +377,10 @@
"description": "Don't display workspaces without windows.",
"label": "Hide unoccupied"
},
"follow-focused-screen": {
"description": "Display workspaces from the currently focused screen, rather than the screen where the bar is located.",
"label": "Follow Focused Screen"
},
"label-mode": {
"description": "Choose how workspace labels are displayed.",
"label": "Label Mode"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "Brightness",
"charge-level": "Charge level",
"charging": "Charging",
"charging-rate": "Charging rate: {rate} W",
"discharging": "Discharging",
"discharging-rate": "Discharging rate: {rate} W",
"health": "Health: {percent}%",
"idle": "Idle",
"inhibit-idle-description": "Keeps the system awake.",
"inhibit-idle-label": "Keep awake",
"no-battery-detected": "No battery detected",
"panel-title": "Battery",
"plugged-in": "Plugged in",
"power-profile": "Power profile",
"time-left": "Time left: {time}",
"time-until-full": "Time until full: {time}"
},
@@ -396,28 +431,29 @@
}
},
"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": {
"title": "What's new in {version}",
"buttons": {
"discord": "Join our Discord",
"dismiss": "Ok"
},
"empty": "Release notes are not available yet.",
"highlight-title": "Highlights",
"section": {
"released": "Released on {date}",
"version": "Version {version}"
},
"subtitle": {
"fresh": "Thanks for installing Noctalia! Here is whats included in this build.",
"updated": "Updated from {previousVersion}"
},
"title": "What's new in {version}",
"version": {
"new-user": "Fresh install"
},
"highlight-title": "Highlights",
"empty": "Release notes are not available yet.",
"section": {
"version": "Version {version}",
"released": "Released on {date}"
},
"buttons": {
"discord": "Join our Discord",
"dismiss": "Ok"
}
},
"error": {
"rate-limit": "GitHub rate limit exceeded. Please try again in a few minutes."
}
},
"clock": {
@@ -427,10 +463,12 @@
"activate-app": "Activate {app}",
"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}",
"enable-bluetooth": "Enable Bluetooth",
"enable-dnd": "Enable Do Not Disturb",
"enable-wifi": "Enable Wi-Fi",
@@ -479,6 +517,15 @@
"description": "Your notifications will show up here as they arrive.",
"no-notifications": "No notifications",
"title": "Notifications"
},
"time": {
"now": "now",
"diffM": "1 minute ago",
"diffMM": "{diff} minutes ago",
"diffH": "1 hour ago",
"diffHH": "{diff} hours ago",
"diffD": "1 day ago",
"diffDD": "{diff} days ago"
}
},
"options": {
@@ -546,7 +593,8 @@
"follow_bar": "Follow bar (default)",
"top_center": "Top center",
"top_left": "Top left",
"top_right": "Top right"
"top_right": "Top right",
"bar": "Bar (inline)"
}
},
"osd": {
@@ -646,7 +694,11 @@
"clipboard-history-disabled-description": "Enable clipboard history in settings or install cliphist",
"clipboard-loading": "Loading clipboard history...",
"clipboard-loading-description": "Please wait",
"clipboard-search-description": "Search clipboard history"
"clipboard-search-description": "Search clipboard history",
"emoji": "Emoji picker",
"emoji-loading": "Loading emojis...",
"emoji-loading-description": "Please wait",
"emoji-search-description": "Search and copy emojis"
},
"quickSettings": {
"bluetooth": {
@@ -1020,6 +1072,14 @@
"description": "Write {filepath}. Comfy theme needs to be installed and activated manually.",
"description-missing": "Requires {app} to be installed"
},
"telegram": {
"description": "Write {filepath}.",
"description-missing": "Requires {app} to be installed"
},
"cava": {
"description": "Write {filepath}.",
"description-missing": "Requires {app} to be installed"
},
"vicinae": {
"description": "Write {filepath} and reload",
"description-missing": "Requires {app} to be installed"
@@ -1216,6 +1276,10 @@
"description": "Adjust the dock's background opacity.",
"label": "Background opacity"
},
"border-radius": {
"description": "Adjust the dock's border radius.",
"label": "Border radius"
},
"colorize-icons": {
"description": "Apply theme colors to dock app icons (non-focused apps only).",
"label": "Colorize Icons"
@@ -1367,6 +1431,10 @@
"description": "Adjust the background opacity of the launcher.",
"label": "Background opacity"
},
"clip-preview": {
"description": "Show a preview of the clipboard content when using the >clip command.",
"label": "Enable clip preview"
},
"clipboard-history": {
"description": "Access previously copied items from the launcher.",
"label": "Enable clipboard history"
@@ -1474,6 +1542,10 @@
"description": "Automatically lock the screen when suspending the system.",
"label": "Lock on suspend"
},
"show-hibernate": {
"description": "Show the option 'hibernate' in the energy actions.",
"label": "Show hibernate"
},
"title": "Lock screen"
},
"network": {
@@ -2037,6 +2109,10 @@
"title-matugen": "Matugen templating processing failed",
"title-predefined": "Predefined color cheme processing failed"
},
"vpn": {
"connected": "Connected to '{name}'",
"disconnected": "Disconnected from '{name}'"
},
"wallpaper-colors": {
"disabled": "Wallpaper colors disabled",
"enabled": "Wallpaper colors enabled",
@@ -2070,6 +2146,7 @@
"input-muted": "Toggle input mute",
"keep-awake": "Keep awake",
"keyboard-layout": "{layout} keyboard layout",
"manage-vpn": "Manage VPN connections",
"manage-wifi": "Manage Wi-Fi",
"microphone-volume-at": "Microphone volume at {volume}%\nScroll to modify volume",
"move-to-center-section": "Move to center section",
@@ -2204,23 +2281,11 @@
"widgets": {
"color-picker": {
"apply": "Apply",
"brightness": "Brightness",
"cancel": "Cancel",
"hex": {
"description": "Enter a hexadecimal color code.",
"label": "Hex color"
},
"palette": {
"description": "Choose from a wide range of predefined colors.",
"label": "Palette"
},
"rgb": {
"description": "Adjust red, green, blue, and brightness values.",
"label": "RGB values"
},
"theme-colors": {
"description": "Quick access to your theme's color palette.",
"label": "Theme colors"
"label": "Palette",
"theme-colors": "Quick access to your theme's color palette."
},
"title": "Color picker"
},
@@ -2296,6 +2361,7 @@
},
"wifi": {
"panel": {
"available-networks": "Available Networks",
"connect": "Connect",
"connected": "Connected",
"disabled": "Wi-Fi is disabled",
@@ -2306,6 +2372,7 @@
"forget": "Forget",
"forget-network": "Forget this network?",
"forgetting": "Forgetting...",
"known-networks": "Known Networks",
"no-networks": "No networks found",
"password": "Password",
"saved": "Saved",

View File

@@ -123,10 +123,6 @@
"stream-description": "Introduce un comando para ejecutar continuamente."
},
"dynamic-text": "Texto dinámico",
"hide-vertical": {
"description": "Si está activado, el texto de la salida del comando no se mostrará cuando la barra esté en un diseño vertical (izquierda o derecha).",
"label": "Ocultar texto en barra vertical"
},
"icon": {
"description": "Selecciona un icono de la biblioteca.",
"label": "Icono"
@@ -136,6 +132,14 @@
"label": "Clic izquierdo",
"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)",
"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)",
"label": "Longitud máxima de texto (vertical)"
},
"middle-click": {
"description": "Comando a ejecutar cuando se hace clic medio en el botón.",
"label": "Clic medio",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "Las líneas transmitidas desde el comando se mostrarán como texto en el botón.",
"label": "Transmisión"
},
"wheel": {
"description": "Comando a ejecutar cuando se usa la rueda de desplazamiento.\nUsa $delta para el delta de la rueda de desplazamiento en el comando",
"label": "Rueda de desplazamiento",
"update-text": "Actualizar texto mostrado al desplazarse"
},
"wheel-down": {
"description": "Comando a ejecutar cuando la rueda de desplazamiento se desplaza hacia abajo.",
"label": "Comando de rueda hacia abajo"
},
"wheel-mode-separate": {
"description": "Habilitar comandos separados para rueda arriba y abajo",
"label": "Comandos de rueda separados"
},
"wheel-up": {
"description": "Comando a ejecutar cuando la rueda de desplazamiento se desplaza hacia arriba.",
"label": "Comando de rueda hacia arriba"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "Mostrar un visualizador de audio cuando se reproduce música.",
"label": "Mostrar visualizador"
},
"show-progress-ring": {
"description": "Mostrar un indicador de progreso circular que muestre el progreso de la pista.",
"label": "Mostrar anillo de progreso"
},
"use-fixed-width": {
"description": "Cuando está activado, el widget siempre usará el ancho máximo en lugar de ajustarse dinámicamente al contenido.",
"label": "Usar Ancho Fijo"
@@ -352,6 +377,10 @@
"description": "No mostrar espacios de trabajo sin ventanas.",
"label": "Ocultar desocupados"
},
"follow-focused-screen": {
"description": "Mostrar espacios de trabajo de la pantalla actualmente enfocada, en lugar de la pantalla donde se encuentra la barra.",
"label": "Seguir Pantalla Enfocada"
},
"label-mode": {
"description": "Elegir cómo se muestran las etiquetas de los espacios de trabajo.",
"label": "Modo de etiqueta"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "Brillo",
"charge-level": "Nivel de carga",
"charging": "Cargando.",
"charging-rate": "Tasa de carga: {rate} W.",
"discharging": "Descargando.",
"discharging-rate": "Tasa de descarga: {rate} W.",
"health": "Salud: {percent}%",
"idle": "Inactivo.",
"inhibit-idle-description": "Mantiene el sistema despierto.",
"inhibit-idle-label": "Mantener despierto",
"no-battery-detected": "No se detectó ninguna batería.",
"panel-title": "Batería",
"plugged-in": "Conectado.",
"power-profile": "Perfil de energía",
"time-left": "Tiempo restante: {time}.",
"time-until-full": "Tiempo hasta carga completa: {time}."
},
@@ -396,29 +431,29 @@
}
},
"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": {
"title": "Novedades en {version}",
"buttons": {
"discord": "Únete a nuestro Discord",
"dismiss": "Ok"
},
"empty": "Las notas de la versión aún no están disponibles.",
"highlight-title": "Cambios destacados",
"section": {
"released": "Publicado el {date}",
"version": "Versión {version}"
},
"subtitle": {
"fresh": "Gracias por instalar Noctalia. Esto es lo que incluye esta compilación.",
"updated": "Actualizado desde {previousVersion}"
},
"title": "Novedades en {version}",
"version": {
"new-user": "Instalación nueva"
},
"highlight-title": "Cambios destacados",
"empty": "Las notas de la versión aún no están disponibles.",
"section": {
"version": "Versión {version}",
"released": "Publicado el {date}"
},
"buttons": {
"discord": "Únete a nuestro Discord",
"feedback": "Enviar comentarios",
"dismiss": "Ok"
}
},
"error": {
"rate-limit": "Se alcanzó el límite de GitHub. Inténtalo de nuevo en unos minutos."
}
},
"clock": {
@@ -428,10 +463,12 @@
"activate-app": "Activar {app}",
"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}",
"enable-bluetooth": "Activar Bluetooth",
"enable-dnd": "Activar No molestar",
"enable-wifi": "Activar Wi-Fi",
@@ -480,6 +517,15 @@
"description": "Tus notificaciones aparecerán aquí a medida que lleguen.",
"no-notifications": "No hay notificaciones",
"title": "Notificaciones"
},
"time": {
"now": "ahora",
"diffM": "hace 1 minuto",
"diffMM": "hace {diff} minutos",
"diffH": "hace 1 hora",
"diffHH": "hace {diff} horas",
"diffD": "hace 1 día",
"diffDD": "hace {diff} días"
}
},
"options": {
@@ -647,7 +693,11 @@
"clipboard-history-disabled-description": "Activa el historial del portapapeles en la configuración o instala cliphist",
"clipboard-loading": "Cargando historial del portapapeles...",
"clipboard-loading-description": "Por favor espera",
"clipboard-search-description": "Buscar en el historial del portapapeles"
"clipboard-search-description": "Buscar en el historial del portapapeles",
"emoji": "Selector de emojis",
"emoji-loading": "Cargando emojis...",
"emoji-loading-description": "Por favor espera",
"emoji-search-description": "Buscar y copiar emojis"
},
"quickSettings": {
"bluetooth": {
@@ -1021,6 +1071,14 @@
"description": "Escribe {filepath}. El tema Comfy debe ser instalado y activado manualmente.",
"description-missing": "Requiere que {app} esté instalado/a."
},
"telegram": {
"description": "Escribe {filepath}.",
"description-missing": "Requiere que {app} esté instalado/a."
},
"cava": {
"description": "Escribe {filepath}.",
"description-missing": "Requiere que {app} esté instalado/a."
},
"vicinae": {
"description": "Escribir {filepath} y recargar",
"description-missing": "Requiere que {app} esté instalado"
@@ -1217,6 +1275,10 @@
"description": "Ajusta la opacidad del fondo del dock.",
"label": "Opacidad del fondo"
},
"border-radius": {
"description": "Ajustar el radio del borde del dock.",
"label": "Radio de borde"
},
"colorize-icons": {
"description": "Aplicar colores del tema a los iconos de aplicaciones del dock (solo aplicaciones no enfocadas).",
"label": "Colorear iconos"
@@ -1368,6 +1430,10 @@
"description": "Ajusta la opacidad del fondo del lanzador.",
"label": "Opacidad del fondo"
},
"clip-preview": {
"description": "Muestra una vista previa del contenido del portapapeles al usar el comando >clip.",
"label": "Activar vista previa del portapapeles"
},
"clipboard-history": {
"description": "Accede a los elementos copiados anteriormente desde el lanzador.",
"label": "Activar historial del portapapeles"
@@ -1475,6 +1541,10 @@
"description": "Bloquear la pantalla automáticamente al suspender el sistema.",
"label": "Bloquear al suspender"
},
"show-hibernate": {
"description": "Mostrar la opción 'hibernar' en las acciones de energía.",
"label": "Mostrar hibernar"
},
"title": "Pantalla de bloqueo"
},
"network": {
@@ -2038,6 +2108,10 @@
"title-matugen": "Falló el procesamiento de la plantilla Matugen.",
"title-predefined": "Falló el procesamiento del esquema de color predefinido."
},
"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",
@@ -2071,6 +2145,7 @@
"input-muted": "Silenciar entrada de audio",
"keep-awake": "Mantener despierto",
"keyboard-layout": "Distribución de teclado {layout}",
"manage-vpn": "Administrar conexiones VPN",
"manage-wifi": "Gestionar Wi-Fi",
"microphone-volume-at": "Volumen del micrófono al {volume}%.\nClic izquierdo para ajustes. Clic derecho para activar/desactivar el silencio.\nDesplázate para modificar el volumen.",
"move-to-center-section": "Mover a la sección central",
@@ -2205,23 +2280,11 @@
"widgets": {
"color-picker": {
"apply": "Aplicar",
"brightness": "Brillo",
"cancel": "Cancelar",
"hex": {
"description": "Ingresa un código de color hexadecimal.",
"label": "Color hexadecimal"
},
"palette": {
"description": "Elige entre una amplia gama de colores predefinidos.",
"label": "Paleta"
},
"rgb": {
"description": "Ajusta los valores de rojo, verde, azul y brillo.",
"label": "Valores RGB"
},
"theme-colors": {
"description": "Acceso rápido a la paleta de colores de tu tema.",
"label": "Colores del tema"
"label": "Paleta",
"theme-colors": "Acceso rápido a la paleta de colores de tu tema."
},
"title": "Selector de colores"
},
@@ -2297,6 +2360,7 @@
},
"wifi": {
"panel": {
"available-networks": "Redes disponibles",
"connect": "Conectar",
"connected": "Conectado",
"disabled": "Wi-Fi está desactivado",
@@ -2307,6 +2371,7 @@
"forget": "Olvidar",
"forget-network": "¿Olvidar esta red?",
"forgetting": "Olvidando...",
"known-networks": "Redes conocidas",
"no-networks": "No se encontraron redes",
"password": "Contraseña",
"saved": "Guardado",

View File

@@ -123,10 +123,6 @@
"stream-description": "Entrez une commande à exécuter en continu."
},
"dynamic-text": "Texte dynamique",
"hide-vertical": {
"description": "Si activé, le texte de la sortie de la commande ne sera pas affiché lorsque la barre est en disposition verticale (gauche ou droite).",
"label": "Masquer le texte dans la barre verticale"
},
"icon": {
"description": "Sélectionnez une icône dans la bibliothèque.",
"label": "Icône"
@@ -136,6 +132,14 @@
"label": "Clic gauche",
"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)",
"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)",
"label": "Longueur max du texte (vertical)"
},
"middle-click": {
"description": "Commande à exécuter quand le bouton est cliqué au milieu.",
"label": "Clic milieu",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "Les lignes diffusées depuis la commande seront affichées sous forme de texte sur le bouton.",
"label": "Flux"
},
"wheel": {
"description": "Commande à exécuter lorsque la molette est utilisée.\nUtilisez $delta pour le delta de la molette dans la commande",
"label": "Molette",
"update-text": "Mettre à jour le texte affiché au défilement"
},
"wheel-down": {
"description": "Commande à exécuter lorsque la molette est défilée vers le bas.",
"label": "Commande molette bas"
},
"wheel-mode-separate": {
"description": "Activer des commandes séparées pour la molette haut et bas",
"label": "Commandes de molette séparées"
},
"wheel-up": {
"description": "Commande à exécuter lorsque la molette est défilée vers le haut.",
"label": "Commande molette haut"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "Afficher un visualiseur audio quand la musique est en cours de lecture.",
"label": "Afficher le visualiseur"
},
"show-progress-ring": {
"description": "Afficher un indicateur de progression circulaire montrant la progression de la piste.",
"label": "Afficher l'anneau de progression"
},
"use-fixed-width": {
"description": "Lorsque activé, le widget utilisera toujours la largeur maximale au lieu de s'ajuster dynamiquement au contenu.",
"label": "Utiliser une Largeur Fixe"
@@ -352,6 +377,10 @@
"description": "Ne pas afficher les espaces de travail sans fenêtres.",
"label": "Masquer les inoccupés"
},
"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.",
"label": "Suivre l'Écran Ciblé"
},
"label-mode": {
"description": "Choisir comment les étiquettes d'espace de travail sont affichées.",
"label": "Mode d'étiquette"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "Luminosité",
"charge-level": "Niveau de charge",
"charging": "En charge.",
"charging-rate": "Taux de charge : {rate} W.",
"discharging": "En décharge.",
"discharging-rate": "Taux de décharge : {rate} W.",
"health": "État : {percent}%",
"idle": "Inactif.",
"inhibit-idle-description": "Maintient le système éveillé.",
"inhibit-idle-label": "Garder éveillé",
"no-battery-detected": "Aucune batterie détectée.",
"panel-title": "Batterie",
"plugged-in": "Branché.",
"power-profile": "Profil dalimentation",
"time-left": "Temps restant : {time}.",
"time-until-full": "Temps jusqu'à charge complète : {time}."
},
@@ -396,29 +431,29 @@
}
},
"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": {
"title": "Quoi de neuf dans {version}",
"buttons": {
"discord": "Rejoindre notre Discord",
"dismiss": "Ok"
},
"empty": "Les notes de version ne sont pas encore disponibles.",
"highlight-title": "Points importants",
"section": {
"released": "Publié le {date}",
"version": "Version {version}"
},
"subtitle": {
"fresh": "Merci davoir installé Noctalia ! Voici ce que contient cette version.",
"updated": "Mise à jour depuis {previousVersion}"
},
"title": "Quoi de neuf dans {version}",
"version": {
"new-user": "Nouvelle installation"
},
"highlight-title": "Points importants",
"empty": "Les notes de version ne sont pas encore disponibles.",
"section": {
"version": "Version {version}",
"released": "Publié le {date}"
},
"buttons": {
"discord": "Rejoindre notre Discord",
"feedback": "Envoyer un retour",
"dismiss": "Ok"
}
},
"error": {
"rate-limit": "Limite de GitHub atteinte. Réessayez dans quelques minutes."
}
},
"clock": {
@@ -428,10 +463,12 @@
"activate-app": "Activer {app}",
"clear-history": "Effacer l'historique",
"close-app": "Fermer {app}",
"connect-vpn": "Se connecter à {name}",
"cycle-visualizer": "Visualiseur de cycle",
"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}",
"enable-bluetooth": "Activer le Bluetooth",
"enable-dnd": "Activer le mode Ne pas déranger",
"enable-wifi": "Activer le Wi-Fi",
@@ -480,6 +517,15 @@
"description": "Vos notifications apparaîtront ici à mesure qu'elles arriveront.",
"no-notifications": "Aucune notification",
"title": "Notifications"
},
"time": {
"now": "maintenant",
"diffM": "il y a 1 minute",
"diffMM": "il y a {diff} minutes",
"diffH": "il y a 1 heure",
"diffHH": "il y a {diff} heures",
"diffD": "il y a 1 jour",
"diffDD": "il y a {diff} jours"
}
},
"options": {
@@ -647,7 +693,11 @@
"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-loading-description": "Veuillez patienter",
"clipboard-search-description": "Rechercher dans l'historique du presse-papiers"
"clipboard-search-description": "Rechercher dans l'historique du presse-papiers",
"emoji": "Sélecteur d'émojis",
"emoji-loading": "Chargement des émojis...",
"emoji-loading-description": "Veuillez patienter",
"emoji-search-description": "Rechercher et copier des émojis"
},
"quickSettings": {
"bluetooth": {
@@ -1021,6 +1071,14 @@
"description": "Écrire {filepath}. Le thème Comfy doit être installé et activé manuellement.",
"description-missing": "Nécessite l'installation de {app}"
},
"telegram": {
"description": "Écrire {filepath}.",
"description-missing": "Nécessite l'installation de {app}"
},
"cava": {
"description": "Écrire {filepath}.",
"description-missing": "Nécessite l'installation de {app}"
},
"vicinae": {
"description": "Écrire {filepath} et recharger",
"description-missing": "Nécessite que le lanceur {app} soit installé"
@@ -1217,6 +1275,10 @@
"description": "Ajustez l'opacité de l'arrière-plan du dock.",
"label": "Opacité de l'arrière-plan"
},
"border-radius": {
"description": "Ajuster le rayon de bordure du dock.",
"label": "Rayon de bordure"
},
"colorize-icons": {
"description": "Appliquer les couleurs du thème aux icônes d'applications du dock (applications non focalisées uniquement).",
"label": "Coloriser les icônes"
@@ -1368,6 +1430,10 @@
"description": "Ajustez l'opacité de l'arrière-plan du lanceur.",
"label": "Opacité de l'arrière-plan"
},
"clip-preview": {
"description": "Afficher un aperçu du contenu du presse-papiers lors de l'utilisation de la commande >clip.",
"label": "Activer l'aperçu du presse-papiers"
},
"clipboard-history": {
"description": "Accédez aux éléments précédemment copiés depuis le lanceur.",
"label": "Activer l'historique du presse-papiers"
@@ -1475,6 +1541,10 @@
"description": "Verrouiller automatiquement l'écran lors de la mise en veille du système.",
"label": "Verrouiller à la suspension"
},
"show-hibernate": {
"description": "Afficher loption 'hiberner' dans les actions dénergie.",
"label": "Afficher lhibernation"
},
"title": "Écran de verrouillage"
},
"network": {
@@ -2038,6 +2108,10 @@
"title-matugen": "Le traitement du modèle Matugen a échoué.",
"title-predefined": "Le traitement du schéma de couleurs prédéfini 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",
@@ -2071,6 +2145,7 @@
"input-muted": "Couper l'entrée audio",
"keep-awake": "Rester éveillé",
"keyboard-layout": "Disposition du clavier {layout}",
"manage-vpn": "Gérer les connexions VPN",
"manage-wifi": "Gérer le Wi-Fi",
"microphone-volume-at": "Volume du microphone à {volume}%.\nClic gauche pour les paramètres. Clic droit pour activer/désactiver le mode muet.\nFaites défiler pour modifier le volume.",
"move-to-center-section": "Déplacer vers la section centrale",
@@ -2205,23 +2280,11 @@
"widgets": {
"color-picker": {
"apply": "Appliquer",
"brightness": "Luminosité",
"cancel": "Annuler",
"hex": {
"description": "Entrez un code couleur hexadécimal.",
"label": "Couleur hexadécimale"
},
"palette": {
"description": "Choisissez parmi une large gamme de couleurs prédéfinies.",
"label": "Palette"
},
"rgb": {
"description": "Ajustez les valeurs de rouge, vert, bleu et de luminosité.",
"label": "Valeurs RVB"
},
"theme-colors": {
"description": "Accès rapide à la palette de couleurs de votre thème.",
"label": "Couleurs du thème"
"label": "Palette",
"theme-colors": "Accès rapide à la palette de couleurs de votre thème."
},
"title": "Sélecteur de couleurs"
},
@@ -2297,6 +2360,7 @@
},
"wifi": {
"panel": {
"available-networks": "Réseaux disponibles",
"connect": "Connecter",
"connected": "Connecté",
"disabled": "Le Wi-Fi est désactivé",
@@ -2307,6 +2371,7 @@
"forget": "Oublier",
"forget-network": "Oublier ce réseau ?",
"forgetting": "Oubli en cours...",
"known-networks": "Réseaux connus",
"no-networks": "Aucun réseau trouvé",
"password": "Mot de passe",
"saved": "Enregistré",

View File

@@ -123,10 +123,6 @@
"stream-description": "Voer een commando in dat continu wordt uitgevoerd."
},
"dynamic-text": "Dynamische tekst",
"hide-vertical": {
"description": "Indien ingeschakeld wordt de tekst uit de commando-uitvoer niet getoond wanneer de balk verticaal is (links of rechts).",
"label": "Tekst verbergen in verticale balk"
},
"icon": {
"description": "Selecteer een pictogram uit de bibliotheek.",
"label": "Pictogram"
@@ -136,6 +132,14 @@
"label": "Linkermuisklik",
"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)",
"label": "Max. tekstlengte (horizontaal)"
},
"max-text-length-vertical": {
"description": "Maximaal aantal tekens dat moet worden weergegeven in verticale balk (0 om tekst te verbergen)",
"label": "Max. tekstlengte (verticaal)"
},
"middle-click": {
"description": "Commando dat wordt uitgevoerd wanneer met de middelste muisknop op de knop wordt geklikt.",
"label": "Middelste muisklik",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "Gestreamde regels uit het commando worden als tekst op de knop weergegeven.",
"label": "Stream"
},
"wheel": {
"description": "Commando om uit te voeren wanneer het scrollwiel wordt gebruikt.\nGebruik $delta voor de scrollwiel-delta in het commando",
"label": "Scrollwiel",
"update-text": "Weergegeven tekst bij scrollen bijwerken"
},
"wheel-down": {
"description": "Commando om uit te voeren wanneer het scrollwiel omlaag wordt bewogen.",
"label": "Scrollwiel omlaag commando"
},
"wheel-mode-separate": {
"description": "Afzonderlijke commando's inschakelen voor scrollwiel omhoog en omlaag",
"label": "Afzonderlijke scrollwielcommando's"
},
"wheel-up": {
"description": "Commando om uit te voeren wanneer het scrollwiel omhoog wordt bewogen.",
"label": "Scrollwiel omhoog commando"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "Toon een audiovisualizer wanneer muziek wordt afgespeeld.",
"label": "Visualizer tonen"
},
"show-progress-ring": {
"description": "Toon een circulaire voortgangsindicator die het bestandsspoor voortgang toont.",
"label": "Voortgangscirkel tonen"
},
"use-fixed-width": {
"description": "Indien ingeschakeld gebruikt de widget altijd de maximale breedte in plaats van zich aan te passen aan de inhoud.",
"label": "Vaste breedte gebruiken"
@@ -352,6 +377,10 @@
"description": "Werkruimten zonder vensters niet weergeven.",
"label": "Ongebruikte verbergen"
},
"follow-focused-screen": {
"description": "Werkruimten weergeven van het momenteel gefocuste scherm, in plaats van het scherm waar de balk zich bevindt.",
"label": "Gefocust Scherm Volgen"
},
"label-mode": {
"description": "Kies hoe labels van werkruimten worden weergegeven.",
"label": "Labelmodus"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "Helderheid",
"charge-level": "Laadniveau",
"charging": "Opladen.",
"charging-rate": "Laadsnelheid: {rate} W.",
"discharging": "Ontladen.",
"discharging-rate": "Ontlaadsnelheid: {rate} W.",
"health": "Gezondheid: {percent}%",
"idle": "In rust.",
"inhibit-idle-description": "Houdt het systeem wakker.",
"inhibit-idle-label": "Wakker houden",
"no-battery-detected": "Geen batterij gedetecteerd.",
"panel-title": "Batterij",
"plugged-in": "Op netstroom.",
"power-profile": "Energieprofiel",
"time-left": "Resterende tijd: {time}.",
"time-until-full": "Tijd tot vol: {time}."
},
@@ -396,29 +431,29 @@
}
},
"changelog": {
"error": {
"fetch-failed": "Kan changeloggegevens niet laden. Probeer het later opnieuw.",
"rate-limit": "GitHub-limiet bereikt. Probeer het over enkele minuten opnieuw."
},
"panel": {
"title": "Wat is er nieuw in {version}",
"buttons": {
"discord": "Word lid van onze Discord",
"dismiss": "Ok"
},
"empty": "Er zijn nog geen release-opmerkingen beschikbaar.",
"highlight-title": "Hoogtepunten",
"section": {
"released": "Uitgebracht op {date}",
"version": "Versie {version}"
},
"subtitle": {
"fresh": "Bedankt voor het installeren van Noctalia! Dit zit er in deze build.",
"updated": "Bijgewerkt vanaf {previousVersion}"
},
"title": "Wat is er nieuw in {version}",
"version": {
"new-user": "Nieuwe installatie"
},
"highlight-title": "Hoogtepunten",
"empty": "Er zijn nog geen release-opmerkingen beschikbaar.",
"section": {
"version": "Versie {version}",
"released": "Uitgebracht op {date}"
},
"buttons": {
"discord": "Word lid van onze Discord",
"feedback": "Feedback verzenden",
"dismiss": "Ok"
}
},
"error": {
"rate-limit": "GitHub-limiet bereikt. Probeer het over enkele minuten opnieuw."
}
},
"clock": {
@@ -428,10 +463,12 @@
"activate-app": "Activeer {app}",
"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",
"enable-bluetooth": "Bluetooth inschakelen",
"enable-dnd": "Niet Storen inschakelen",
"enable-wifi": "Wi-Fi inschakelen",
@@ -480,6 +517,15 @@
"description": "Je meldingen verschijnen hier zodra ze binnenkomen.",
"no-notifications": "Geen meldingen",
"title": "Meldingen"
},
"time": {
"now": "nu",
"diffM": "1 minuut geleden",
"diffMM": "{diff} minuten geleden",
"diffH": "1 uur geleden",
"diffHH": "{diff} uur geleden",
"diffD": "1 dag geleden",
"diffDD": "{diff} dagen geleden"
}
},
"options": {
@@ -647,7 +693,11 @@
"clipboard-history-disabled-description": "Schakel klembordgeschiedenis in de instellingen in of installeer cliphist",
"clipboard-loading": "Klembordgeschiedenis laden...",
"clipboard-loading-description": "Even geduld",
"clipboard-search-description": "Zoek in klembordgeschiedenis"
"clipboard-search-description": "Zoek in klembordgeschiedenis",
"emoji": "Emoji-kiezer",
"emoji-loading": "Emoji's laden...",
"emoji-loading-description": "Even geduld",
"emoji-search-description": "Zoek en kopieer emoji's"
},
"quickSettings": {
"bluetooth": {
@@ -1021,6 +1071,14 @@
"description": "Schrijf {filepath}. Het Comfy-thema moet handmatig worden geïnstalleerd en geactiveerd.",
"description-missing": "Vereist dat {app} is geïnstalleerd."
},
"telegram": {
"description": "Schrijf {filepath}.",
"description-missing": "Vereist dat {app} is geïnstalleerd."
},
"cava": {
"description": "Schrijf {filepath}.",
"description-missing": "Vereist dat {app} is geïnstalleerd."
},
"vicinae": {
"description": "Schrijf {filepath} en herlaad.",
"description-missing": "Vereist dat {app} is geïnstalleerd."
@@ -1217,6 +1275,10 @@
"description": "Pas de achtergronddekking van de dock aan.",
"label": "Achtergronddekking"
},
"border-radius": {
"description": "Pas de randradius van het dock aan.",
"label": "Randradius"
},
"colorize-icons": {
"description": "Pas themakleuren toe op dock-pictogrammen (alleen niet-focuste apps).",
"label": "Pictogrammen inkleuren"
@@ -1368,6 +1430,10 @@
"description": "Pas de achtergronddekking van de launcher aan.",
"label": "Achtergronddekking"
},
"clip-preview": {
"description": "Toon een voorbeeld van de inhoud van het klembord bij gebruik van het >clip-commando.",
"label": "Klembordvoorbeeld inschakelen"
},
"clipboard-history": {
"description": "Toegang tot eerder gekopieerde items vanuit de launcher.",
"label": "Klembordgeschiedenis inschakelen"
@@ -1475,6 +1541,10 @@
"description": "Vergrendel het scherm automatisch wanneer het systeem wordt onderbroken.",
"label": "Vergrendelen bij onderbreken"
},
"show-hibernate": {
"description": "De optie 'sluimerstand' tonen in de energieacties.",
"label": "Sluimerstand tonen"
},
"title": "Vergrendelscherm"
},
"network": {
@@ -2038,6 +2108,10 @@
"title-matugen": "Matugen-sjabloonverwerking mislukt",
"title-predefined": "Verwerken van vooraf gedefinieerd kleurenschema mislukt"
},
"vpn": {
"connected": "Verbonden met '{name}'",
"disconnected": "Verbinding met '{name}' verbroken"
},
"wallpaper-colors": {
"disabled": "Achtergrondkleuren uitgeschakeld",
"enabled": "Achtergrondkleuren ingeschakeld",
@@ -2071,6 +2145,7 @@
"input-muted": "Invoermicrofoon dempen in-/uitschakelen",
"keep-awake": "Wakker houden",
"keyboard-layout": "{layout}-toetsenbordindeling",
"manage-vpn": "VPN-verbindingen beheren",
"manage-wifi": "Wi-Fi beheren",
"microphone-volume-at": "Microfoonvolume {volume}%\nLinks klikken voor instellingen. Rechts klikken om te dempen.\nScroll om het volume aan te passen.",
"move-to-center-section": "Verplaatsen naar middelste sectie",
@@ -2205,23 +2280,11 @@
"widgets": {
"color-picker": {
"apply": "Toepassen",
"brightness": "Helderheid",
"cancel": "Annuleren",
"hex": {
"description": "Voer een hexadecimale kleurcode in.",
"label": "Hex-kleur"
},
"palette": {
"description": "Kies uit een breed scala aan vooraf gedefinieerde kleuren.",
"label": "Palet"
},
"rgb": {
"description": "Pas de waarden voor rood, groen, blauw en helderheid aan.",
"label": "RGB-waarden"
},
"theme-colors": {
"description": "Snelle toegang tot het kleurenpalet van je thema.",
"label": "Themakleuren"
"label": "Palet",
"theme-colors": "Snelle toegang tot het kleurenpalet van je thema."
},
"title": "Kleurkiezer"
},
@@ -2297,6 +2360,7 @@
},
"wifi": {
"panel": {
"available-networks": "Beschikbare netwerken",
"connect": "Verbinden",
"connected": "Verbonden",
"disabled": "Wi-Fi is uitgeschakeld",
@@ -2307,6 +2371,7 @@
"forget": "Vergeten",
"forget-network": "Dit netwerk vergeten?",
"forgetting": "Vergeten...",
"known-networks": "Bekende netwerken",
"no-networks": "Geen netwerken gevonden",
"password": "Wachtwoord",
"saved": "Opgeslagen",

View File

@@ -123,10 +123,6 @@
"stream-description": "Insira um comando para executar continuamente."
},
"dynamic-text": "Texto dinâmico",
"hide-vertical": {
"description": "Se ativado, o texto da saída do comando não será exibido quando a barra estiver em um layout vertical (esquerda ou direita).",
"label": "Ocultar texto na barra vertical"
},
"icon": {
"description": "Selecione um ícone da biblioteca.",
"label": "Ícone"
@@ -136,6 +132,14 @@
"label": "Clique esquerdo",
"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)",
"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)",
"label": "Comprimento máximo do texto (vertical)"
},
"middle-click": {
"description": "Comando a executar quando o botão é clicado com o botão do meio.",
"label": "Clique do meio",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "As linhas transmitidas do comando serão exibidas como texto no botão.",
"label": "Transmissão"
},
"wheel": {
"description": "Comando a executar quando a roda de rolagem é usada.\nUse $delta para o delta da roda de rolagem no comando",
"label": "Roda de rolagem",
"update-text": "Atualizar texto exibido ao rolar"
},
"wheel-down": {
"description": "Comando a executar quando a roda de rolagem é rolada para baixo.",
"label": "Comando de roda para baixo"
},
"wheel-mode-separate": {
"description": "Ativar comandos separados para roda para cima e para baixo",
"label": "Comandos de roda separados"
},
"wheel-up": {
"description": "Comando a executar quando a roda de rolagem é rolada para cima.",
"label": "Comando de roda para cima"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "Exibir um visualizador de áudio quando música está sendo reproduzida.",
"label": "Mostrar visualizador"
},
"show-progress-ring": {
"description": "Exibir um indicador de progresso circular mostrando o progresso da faixa.",
"label": "Mostrar anel de progresso"
},
"use-fixed-width": {
"description": "Quando ativado, o widget sempre usará a largura máxima em vez de ajustar dinamicamente ao conteúdo.",
"label": "Usar Largura Fixa"
@@ -352,6 +377,10 @@
"description": "Não exibir áreas de trabalho sem janelas.",
"label": "Ocultar desocupados"
},
"follow-focused-screen": {
"description": "Exibir áreas de trabalho da tela atualmente em foco, em vez da tela onde a barra está localizada.",
"label": "Seguir Tela em Foco"
},
"label-mode": {
"description": "Escolher como os rótulos de espaço de trabalho são exibidos.",
"label": "Modo de rótulo"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "Brilho",
"charge-level": "Nível de carga",
"charging": "Carregando.",
"charging-rate": "Taxa de carregamento: {rate} W.",
"discharging": "Descarregando.",
"discharging-rate": "Taxa de descarregamento: {rate} W.",
"health": "Saúde: {percent}%",
"idle": "Ocioso.",
"inhibit-idle-description": "Mantém o sistema acordado.",
"inhibit-idle-label": "Manter acordado",
"no-battery-detected": "Nenhuma bateria detectada.",
"panel-title": "Bateria",
"plugged-in": "Conectado.",
"power-profile": "Perfil de energia",
"time-left": "Tempo restante: {time}.",
"time-until-full": "Tempo até carga completa: {time}."
},
@@ -396,29 +431,29 @@
}
},
"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": {
"title": "Novidades na {version}",
"buttons": {
"discord": "Entre no nosso Discord",
"dismiss": "Ok"
},
"empty": "As notas da versão ainda não estão disponíveis.",
"highlight-title": "Destaques",
"section": {
"released": "Lançado em {date}",
"version": "Versão {version}"
},
"subtitle": {
"fresh": "Obrigado por instalar o Noctalia! Veja o que está incluído nesta compilação.",
"updated": "Atualizado a partir da {previousVersion}"
},
"title": "Novidades na {version}",
"version": {
"new-user": "Nova instalação"
},
"highlight-title": "Destaques",
"empty": "As notas da versão ainda não estão disponíveis.",
"section": {
"version": "Versão {version}",
"released": "Lançado em {date}"
},
"buttons": {
"discord": "Entre no nosso Discord",
"feedback": "Enviar feedback",
"dismiss": "Ok"
}
},
"error": {
"rate-limit": "Limite do GitHub atingido. Tente novamente em alguns minutos."
}
},
"clock": {
@@ -428,10 +463,12 @@
"activate-app": "Ativar {app}",
"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 o Não Perturbe",
"disable-wifi": "Desativar Wi-Fi",
"disconnect-vpn": "Desconectar {name}",
"enable-bluetooth": "Ativar Bluetooth",
"enable-dnd": "Ativar Não Perturbe",
"enable-wifi": "Ativar Wi-Fi",
@@ -480,6 +517,15 @@
"description": "Suas notificações aparecerão aqui assim que chegarem.",
"no-notifications": "Nenhuma notificação",
"title": "Notificações"
},
"time": {
"now": "agora",
"diffM": "há 1 minuto",
"diffMM": "há {diff} minutos",
"diffH": "há 1 hora",
"diffHH": "há {diff} horas",
"diffD": "há 1 dia",
"diffDD": "há {diff} dias"
}
},
"options": {
@@ -647,7 +693,11 @@
"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-loading-description": "Por favor, aguarde",
"clipboard-search-description": "Pesquisar no histórico da área de transferência"
"clipboard-search-description": "Pesquisar no histórico da área de transferência",
"emoji": "Seletor de emojis",
"emoji-loading": "Carregando emojis...",
"emoji-loading-description": "Por favor, aguarde",
"emoji-search-description": "Buscar e copiar emojis"
},
"quickSettings": {
"bluetooth": {
@@ -1021,6 +1071,14 @@
"description": "Escreva em {filepath}. O tema Comfy precisa ser instalado e ativado manualmente.",
"description-missing": "Requer que o {app} esteja instalado."
},
"telegram": {
"description": "Escreva em {filepath}.",
"description-missing": "Requer que o {app} esteja instalado."
},
"cava": {
"description": "Escreva em {filepath}.",
"description-missing": "Requer que o {app} esteja instalado."
},
"vicinae": {
"description": "Escrever {filepath} e recarregar",
"description-missing": "Requer que o {app} esteja instalado"
@@ -1217,6 +1275,10 @@
"description": "Ajuste a opacidade do fundo da dock.",
"label": "Opacidade do fundo"
},
"border-radius": {
"description": "Ajustar o raio da borda da dock.",
"label": "Raio da borda"
},
"colorize-icons": {
"description": "Aplicar cores do tema aos ícones de aplicativos da dock (apenas aplicativos não focados).",
"label": "Colorir ícones"
@@ -1368,6 +1430,10 @@
"description": "Ajuste a opacidade do fundo do lançador.",
"label": "Opacidade do fundo"
},
"clip-preview": {
"description": "Mostra uma pré-visualização do conteúdo da área de transferência ao usar o comando >clip.",
"label": "Ativar pré-visualização da área de transferência"
},
"clipboard-history": {
"description": "Acesse itens copiados anteriormente a partir do lançador.",
"label": "Ativar histórico da área de transferência"
@@ -1475,6 +1541,10 @@
"description": "Bloquear a tela automaticamente ao suspender o sistema.",
"label": "Bloquear ao suspender"
},
"show-hibernate": {
"description": "Mostrar a opção 'hibernar' nas ações de energia.",
"label": "Mostrar hibernar"
},
"title": "Tela de bloqueio"
},
"network": {
@@ -2038,6 +2108,10 @@
"title-matugen": "Falha no processamento do template Matugen",
"title-predefined": "O processamento do esquema de cores predefinido falhou."
},
"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",
@@ -2071,6 +2145,7 @@
"input-muted": "Silenciar entrada de áudio",
"keep-awake": "Manter acordado",
"keyboard-layout": "Layout de teclado {layout}",
"manage-vpn": "Gerenciar conexões VPN",
"manage-wifi": "Gerenciar Wi-Fi",
"microphone-volume-at": "Volume do microfone em {volume}%.\nClique esquerdo para configurações. Clique direito para ativar/desativar o mudo.\nRole para modificar o volume.",
"move-to-center-section": "Mover para a seção central",
@@ -2205,23 +2280,11 @@
"widgets": {
"color-picker": {
"apply": "Aplicar",
"brightness": "Brilho",
"cancel": "Cancelar",
"hex": {
"description": "Digite um código de cor hexadecimal.",
"label": "Cor hexadecimal"
},
"palette": {
"description": "Escolha entre uma vasta gama de cores predefinidas.",
"label": "Paleta"
},
"rgb": {
"description": "Ajuste os valores de vermelho, verde, azul e brilho.",
"label": "Valores RGB"
},
"theme-colors": {
"description": "Acesso rápido à paleta de cores do seu tema.",
"label": "Cores do tema"
"label": "Paleta",
"theme-colors": "Acesso rápido à paleta de cores do seu tema."
},
"title": "Seletor de cores"
},
@@ -2297,6 +2360,7 @@
},
"wifi": {
"panel": {
"available-networks": "Redes Disponíveis",
"connect": "Conectar",
"connected": "Conectado",
"disabled": "O Wi-Fi está desativado",
@@ -2307,6 +2371,7 @@
"forget": "Esquecer",
"forget-network": "Esquecer esta rede?",
"forgetting": "Esquecendo...",
"known-networks": "Redes Conhecidas",
"no-networks": "Nenhuma rede encontrada",
"password": "Senha",
"saved": "Salva",

View File

@@ -123,10 +123,6 @@
"stream-description": "Введите команду для непрерывного выполнения."
},
"dynamic-text": "Динамический текст",
"hide-vertical": {
"description": "Если включено, текст из вывода команды не будет отображаться, когда панель находится в вертикальном макете (слева или справа).",
"label": "Скрывать текст в вертикальной панели"
},
"icon": {
"description": "Выберите иконку из библиотеки.",
"label": "Иконка"
@@ -136,6 +132,14 @@
"label": "Клик левой кнопкой",
"update-text": "Обновить отображаемый текст по левому клику"
},
"max-text-length-horizontal": {
"description": "Максимальное количество символов для отображения в горизонтальной панели (0 для скрытия текста)",
"label": "Макс. длина текста (горизонтально)"
},
"max-text-length-vertical": {
"description": "Максимальное количество символов для отображения в вертикальной панели (0 для скрытия текста)",
"label": "Макс. длина текста (вертикально)"
},
"middle-click": {
"description": "Команда для выполнения при нажатии средней кнопкой мыши.",
"label": "Клик средней кнопкой",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "Потоковые строки из команды будут отображаться как текст на кнопке.",
"label": "Поток"
},
"wheel": {
"description": "Команда для выполнения при использовании колеса прокрутки.\nИспользуйте $delta для дельты колеса прокрутки в команде",
"label": "Колесо прокрутки",
"update-text": "Обновить отображаемый текст при прокрутке"
},
"wheel-down": {
"description": "Команда для выполнения при прокрутке колеса вниз.",
"label": "Команда прокрутки колеса вниз"
},
"wheel-mode-separate": {
"description": "Включить раздельные команды для колеса прокрутки вверх и вниз",
"label": "Раздельные команды колеса прокрутки"
},
"wheel-up": {
"description": "Команда для выполнения при прокрутке колеса вверх.",
"label": "Команда прокрутки колеса вверх"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "Отображать аудиовизуализатор при воспроизведении музыки.",
"label": "Показывать визуализатор"
},
"show-progress-ring": {
"description": "Отображать круговой индикатор прогресса воспроизведения трека.",
"label": "Показывать кольцо прогресса"
},
"use-fixed-width": {
"description": "Если включено, виджет всегда будет использовать максимальную ширину вместо динамической подстройки под содержимое.",
"label": "Использовать фиксированную ширину"
@@ -352,6 +377,10 @@
"description": "Не отображать рабочие пространства без окон.",
"label": "Скрыть незанятые"
},
"follow-focused-screen": {
"description": "Отображать рабочие пространства с текущего активного экрана, а не с экрана, на котором расположена панель.",
"label": "Следовать за Активным Экраном"
},
"label-mode": {
"description": "Выберите, как отображаются метки рабочих пространств.",
"label": "Режим метки"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "Яркость",
"charge-level": "Уровень заряда",
"charging": "Зарядка.",
"charging-rate": "Скорость зарядки: {rate} Вт.",
"discharging": "Разрядка.",
"discharging-rate": "Скорость разрядки: {rate} Вт.",
"health": "Здоровье: {percent}%",
"idle": "Простой.",
"inhibit-idle-description": "Не дает системе уснуть.",
"inhibit-idle-label": "Не давать засыпать",
"no-battery-detected": "Батарея не обнаружена.",
"panel-title": "Батарея",
"plugged-in": "Подключено.",
"power-profile": "Профиль питания",
"time-left": "Осталось времени: {time}.",
"time-until-full": "Время до полной зарядки: {time}."
},
@@ -396,29 +431,29 @@
}
},
"changelog": {
"error": {
"fetch-failed": "Не удалось загрузить данные журнала изменений. Пожалуйста, попробуйте позже.",
"rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут."
},
"panel": {
"title": "Что нового в {version}",
"buttons": {
"discord": "Присоединиться к нашему Discord",
"dismiss": "Ок"
},
"empty": "Примечания к выпуску пока недоступны.",
"highlight-title": "Основные изменения",
"section": {
"released": "Выпущено {date}",
"version": "Версия {version}"
},
"subtitle": {
"fresh": "Спасибо за установку Noctalia! Вот что входит в этот билд.",
"updated": "Обновлено с {previousVersion}"
},
"title": "Что нового в {version}",
"version": {
"new-user": "Новая установка"
},
"highlight-title": "Основные изменения",
"empty": "Примечания к выпуску пока недоступны.",
"section": {
"version": "Версия {version}",
"released": "Выпущено {date}"
},
"buttons": {
"discord": "Присоединиться к нашему Discord",
"feedback": "Отправить отзыв",
"dismiss": "Ок"
}
},
"error": {
"rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут."
}
},
"clock": {
@@ -428,10 +463,12 @@
"activate-app": "Активировать {app}",
"clear-history": "Очистить историю",
"close-app": "Закрыть {app}",
"connect-vpn": "Подключиться к {name}",
"cycle-visualizer": "Визуализатор циклов",
"disable-bluetooth": "Отключить Bluetooth",
"disable-dnd": "Отключить режим \"Не беспокоить\"",
"disable-wifi": "Отключить Wi-Fi",
"disconnect-vpn": "Отключить {name}",
"enable-bluetooth": "Включить Bluetooth",
"enable-dnd": "Не беспокоить",
"enable-wifi": "Включить Wi-Fi",
@@ -480,6 +517,15 @@
"description": "Ваши уведомления будут появляться здесь по мере их поступления.",
"no-notifications": "Нет уведомлений",
"title": "Уведомления"
},
"time": {
"now": "сейчас",
"diffM": "1 минуту назад",
"diffMM": "{diff} минут назад",
"diffH": "1 час назад",
"diffHH": "{diff} часов назад",
"diffD": "1 день назад",
"diffDD": "{diff} дней назад"
}
},
"options": {
@@ -647,7 +693,11 @@
"clipboard-history-disabled-description": "Включите историю буфера обмена в настройках или установите cliphist",
"clipboard-loading": "Загрузка истории буфера обмена...",
"clipboard-loading-description": "Пожалуйста, подождите",
"clipboard-search-description": "Поиск в истории буфера обмена"
"clipboard-search-description": "Поиск в истории буфера обмена",
"emoji": "Выбор эмодзи",
"emoji-loading": "Загрузка эмодзи...",
"emoji-loading-description": "Пожалуйста, подождите",
"emoji-search-description": "Поиск и копирование эмодзи"
},
"quickSettings": {
"bluetooth": {
@@ -1021,6 +1071,14 @@
"description": "Записать {filepath}. Тему Comfy нужно установить и активировать вручную.",
"description-missing": "Требуется установка {app}"
},
"telegram": {
"description": "Записать {filepath}.",
"description-missing": "Требуется установка {app}"
},
"cava": {
"description": "Записать {filepath}.",
"description-missing": "Требуется установка {app}"
},
"vicinae": {
"description": "Записать {filepath} и перезагрузить",
"description-missing": "Требуется установка {app}"
@@ -1217,6 +1275,10 @@
"description": "Настройка непрозрачности фона панели приложений.",
"label": "Непрозрачность фона"
},
"border-radius": {
"description": "Измените радиус границы дока.",
"label": "Радиус скругления границы"
},
"colorize-icons": {
"description": "Применить цвета темы к иконкам приложений на панели (только для нефокусированных приложений).",
"label": "Раскрасить иконки"
@@ -1368,6 +1430,10 @@
"description": "Настройка непрозрачности фона запуска.",
"label": "Непрозрачность фона"
},
"clip-preview": {
"description": "Показывать предварительный просмотр содержимого буфера обмена при использовании команды >clip.",
"label": "Включить предварительный просмотр буфера обмена"
},
"clipboard-history": {
"description": "Доступ к ранее скопированным элементам из запуска.",
"label": "Включить историю буфера обмена"
@@ -1475,6 +1541,10 @@
"description": "Автоматически блокировать экран при приостановке работы системы.",
"label": "Блокировать при приостановке"
},
"show-hibernate": {
"description": "Показывать опцию 'спящий режим' в действиях питания.",
"label": "Показывать спящий режим"
},
"title": "Экран блокировки"
},
"network": {
@@ -2038,6 +2108,10 @@
"title-matugen": "Сбой обработки шаблонов Matugen",
"title-predefined": "Сбой обработки предопределенной цветовой схемы"
},
"vpn": {
"connected": "Подключено к '{name}'",
"disconnected": "Отключено от '{name}'"
},
"wallpaper-colors": {
"disabled": "Цвета обоев отключены",
"enabled": "Цвета обоев включены",
@@ -2071,6 +2145,7 @@
"input-muted": "Переключить заглушение ввода",
"keep-awake": "Не засыпать",
"keyboard-layout": "Раскладка клавиатуры {layout}",
"manage-vpn": "Управлять VPN-подключениями",
"manage-wifi": "Управление Wi-Fi",
"microphone-volume-at": "Громкость микрофона {volume}%\nЛевый клик для настроек. Правый клик для переключения заглушения.\nПрокрутка для изменения громкости.",
"move-to-center-section": "Переместить в центральную секцию",
@@ -2205,23 +2280,11 @@
"widgets": {
"color-picker": {
"apply": "Применить",
"brightness": "Яркость",
"cancel": "Отмена",
"hex": {
"description": "Введите шестнадцатеричный код цвета.",
"label": "Hex-цвет"
},
"palette": {
"description": "Выберите из широкого диапазона предопределенных цветов.",
"label": "Палитра"
},
"rgb": {
"description": "Настройте значения красного, зеленого, синего и яркости.",
"label": "Значения RGB"
},
"theme-colors": {
"description": "Быстрый доступ к цветовой палитре вашей темы.",
"label": "Цвета темы"
"label": "Палитра",
"theme-colors": "Быстрый доступ к цветовой палитре вашей темы."
},
"title": "Выбор цвета"
},
@@ -2297,6 +2360,7 @@
},
"wifi": {
"panel": {
"available-networks": "Доступные сети",
"connect": "Подключить",
"connected": "Подключено",
"disabled": "Wi-Fi отключен",
@@ -2307,6 +2371,7 @@
"forget": "Забыть",
"forget-network": "Забыть эту сеть?",
"forgetting": "Забывание...",
"known-networks": "Известные сети",
"no-networks": "Сети не найдены",
"password": "Пароль",
"saved": "Сохранено",

View File

@@ -123,10 +123,6 @@
"stream-description": "Sürekli çalıştırılacak bir komut girin."
},
"dynamic-text": "Dinamik metin",
"hide-vertical": {
"description": "Etkinleştirilirse, komut çıktısındaki metin, çubuk dikey düzende (sol veya sağ) olduğunda gösterilmeyecektir.",
"label": "Dikey çubukta metni gizle"
},
"icon": {
"description": "Kütüphaneden bir ikon seçin.",
"label": "İkon"
@@ -136,6 +132,14 @@
"label": "Sol tıklama",
"update-text": "Sol tıklamayla görüntülenen metni güncelle"
},
"max-text-length-horizontal": {
"description": "Yatay çubukta gösterilecek maksimum karakter sayısı (metni gizlemek için 0)",
"label": "Maks. metin uzunluğu (yatay)"
},
"max-text-length-vertical": {
"description": "Dikey çubukta gösterilecek maksimum karakter sayısı (metni gizlemek için 0)",
"label": "Maks. metin uzunluğu (dikey)"
},
"middle-click": {
"description": "Butona orta tıklandığında yürütülecek komut.",
"label": "Orta tıklama",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "Komuttan gelen akış satırları butonda metin olarak gösterilecektir.",
"label": "Akış"
},
"wheel": {
"description": "Kaydırma tekerleği kullanıldığında yürütülecek komut.\nKomutta kaydırma tekerleği deltası için $delta kullanın",
"label": "Kaydırma tekerleği",
"update-text": "Kaydırmada gösterilen metni güncelle"
},
"wheel-down": {
"description": "Kaydırma tekerleği aşağı kaydırıldığında yürütülecek komut.",
"label": "Kaydırma tekerleği aşağı komutu"
},
"wheel-mode-separate": {
"description": "Kaydırma tekerleği yukarı ve aşağı için ayrı komutları etkinleştir",
"label": "Ayrı kaydırma tekerleği komutları"
},
"wheel-up": {
"description": "Kaydırma tekerleği yukarı kaydırıldığında yürütülecek komut.",
"label": "Kaydırma tekerleği yukarı komutu"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "Müzik çalarken bir ses görselleştirici göster.",
"label": "Görselleştiriciyi göster"
},
"show-progress-ring": {
"description": "Parça ilerlemesini gösteren dairesel bir ilerleme göstergesi gösterin.",
"label": "İlerleme halkası göster"
},
"use-fixed-width": {
"description": "Etkinleştirildiğinde, widget dinamik olarak içerik göre ayarlamak yerine her zaman maksimum genişliği kullanır.",
"label": "Sabit Genişlik Kullan"
@@ -352,6 +377,10 @@
"description": "Penceresi olmayan çalışma alanlarını gösterme.",
"label": "Dolu olmayanları gizle"
},
"follow-focused-screen": {
"description": "Çubuğun bulunduğu ekran yerine, şu anda odaklanmış ekrandaki çalışma alanlarını göster.",
"label": "Odaklanmış Ekranı Takip Et"
},
"label-mode": {
"description": "Çalışma alanı etiketlerinin nasıl gösterileceğini seçin.",
"label": "Etiket Modu"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "Parlaklık",
"charge-level": "Şarj seviyesi",
"charging": "Şarj oluyor.",
"charging-rate": "Şarj oranı: {rate} W.",
"discharging": "Deşarj oluyor.",
"discharging-rate": "Deşarj oranı: {rate} W.",
"health": "Sağlık: {percent}%",
"idle": "Boşta.",
"inhibit-idle-description": "Sistemi uyanık tutar.",
"inhibit-idle-label": "Uyanık tut",
"no-battery-detected": "Pil tespit edilmedi.",
"panel-title": "Pil",
"plugged-in": "Prize takılı.",
"power-profile": "Güç profili",
"time-left": "Kalan süre: {time}.",
"time-until-full": "Dolma süresi: {time}."
},
@@ -396,29 +431,29 @@
}
},
"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": {
"title": "{version} sürümünde neler yeni",
"buttons": {
"discord": "Discord sunucumuza katıl",
"dismiss": "Tamam"
},
"empty": "Sürüm notları henüz hazır değil.",
"highlight-title": "Öne çıkanlar",
"section": {
"released": "{date} tarihinde yayımlandı",
"version": "Sürüm {version}"
},
"subtitle": {
"fresh": "Noctaliayı kurduğun için teşekkürler! Bu sürümde gelenler bunlar.",
"updated": "{previousVersion} sürümünden güncellendi"
},
"title": "{version} sürümünde neler yeni",
"version": {
"new-user": "Yeni kurulum"
},
"highlight-title": "Öne çıkanlar",
"empty": "Sürüm notları henüz hazır değil.",
"section": {
"version": "Sürüm {version}",
"released": "{date} tarihinde yayımlandı"
},
"buttons": {
"discord": "Discord sunucumuza katıl",
"feedback": "Geri bildirim gönder",
"dismiss": "Tamam"
}
},
"error": {
"rate-limit": "GitHub sınırına ulaşıldı. Lütfen birkaç dakika sonra tekrar dene."
}
},
"clock": {
@@ -428,10 +463,12 @@
"activate-app": "{app}'i etkinleştir",
"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": "Wi-Fi'ı kapat",
"disconnect-vpn": "{name} bağlantısını kes",
"enable-bluetooth": "Bluetooth'u etkinleştir",
"enable-dnd": "Rahatsız Etmeyin'i Etkinleştir",
"enable-wifi": "Wi-Fi'ı etkinleştir",
@@ -480,6 +517,15 @@
"description": "Bildirimler geldikçe burada görünecek.",
"no-notifications": "Bildirim yok",
"title": "Bildirimler"
},
"time": {
"now": "şimdi",
"diffM": "1 dakika önce",
"diffMM": "{diff} dakika önce",
"diffH": "1 saat önce",
"diffHH": "{diff} saat önce",
"diffD": "1 gün önce",
"diffDD": "{diff} gün önce"
}
},
"options": {
@@ -647,7 +693,11 @@
"clipboard-history-disabled-description": "Panoya geçmişini ayarlarda etkinleştir veya cliphist kur",
"clipboard-loading": "Panoya geçmişi yükleniyor...",
"clipboard-loading-description": "Lütfen bekleyin",
"clipboard-search-description": "Panoya geçmişini ara"
"clipboard-search-description": "Panoya geçmişini ara",
"emoji": "Emoji seçici",
"emoji-loading": "Emojiler yükleniyor...",
"emoji-loading-description": "Lütfen bekleyin",
"emoji-search-description": "Emoji arama ve kopyalama"
},
"quickSettings": {
"bluetooth": {
@@ -1021,6 +1071,14 @@
"description": "{filepath} dosyasına yaz. Comfy temasının kurulu ve manuel olarak etkinleştirilmiş olması gerekir.",
"description-missing": "Kurulum için {app} gereklidir"
},
"telegram": {
"description": "{filepath} dosyasına yaz.",
"description-missing": "Kurulum için {app} gereklidir"
},
"cava": {
"description": "{filepath} dosyasına yaz.",
"description-missing": "Kurulum için {app} gereklidir"
},
"vicinae": {
"description": "{filepath} dosyasına yaz ve yeniden yükle",
"description-missing": "Kurulum için {app} gereklidir"
@@ -1217,6 +1275,10 @@
"description": "Dock'un arka plan opaklığını ayarlayın.",
"label": "Arka plan opaklığı"
},
"border-radius": {
"description": "Dock'un kenar yarıçapını ayarla.",
"label": "Kenar yarıçapı"
},
"colorize-icons": {
"description": "Dock uygulama simgelerine tema renklerini uygulayın (sadece odaklanılmamış uygulamalar).",
"label": "Simgeleri Renklendir"
@@ -1368,6 +1430,10 @@
"description": "Başlatıcının arka plan opaklığını ayarlayın.",
"label": "Arka plan opaklığı"
},
"clip-preview": {
"description": ">clip komutu kullanılırken panodaki içeriğin önizlemesini gösterir.",
"label": "Panoyu önizlemeyi etkinleştir"
},
"clipboard-history": {
"description": "Başlatıcıdan daha önce kopyalanan öğelere erişin.",
"label": "Pano geçmişini etkinleştir"
@@ -1475,6 +1541,10 @@
"description": "Sistemi askıya alırken otomatik olarak ekranı kilitler.",
"label": "Askıya alırken kilitle"
},
"show-hibernate": {
"description": "Güç işlemlerinde 'hazırda beklet' seçeneğini göster.",
"label": "Hazırda beklet seçeneğini göster"
},
"title": "Ekran Kilit"
},
"network": {
@@ -2038,6 +2108,10 @@
"title-matugen": "Matugen şablon işleme başarısız oldu.",
"title-predefined": "Önceden tanımlanmış renk şeması işleme başarısız oldu."
},
"vpn": {
"connected": "'{name}' ile bağlantı kuruldu",
"disconnected": "'{name}' bağlantısı kesildi"
},
"wallpaper-colors": {
"disabled": "Duvar kağıdı renkleri devre dışı",
"enabled": "Duvar kağıdı renkleri etkin",
@@ -2071,6 +2145,7 @@
"input-muted": "Giriş sessizliğini değiştir",
"keep-awake": "Uyanık kal",
"keyboard-layout": "{layout} klavye düzeni",
"manage-vpn": "VPN bağlantılarını yönet",
"manage-wifi": "Wi-Fi yönet",
"microphone-volume-at": "Mikrofon sesi %{volume}\nAyarlar için sol tık. Sessize almak için sağ tık.\nSesi değiştirmek için kaydırın.",
"move-to-center-section": "Orta bölüme taşı",
@@ -2205,23 +2280,11 @@
"widgets": {
"color-picker": {
"apply": "Uygula",
"brightness": "Parlaklık",
"cancel": "İptal",
"hex": {
"description": "Onaltılık renk kodu girin.",
"label": "Hex renk"
},
"palette": {
"description": "Önceden tanımlı renkler geniş yelpazesinden seçin.",
"label": "Palet"
},
"rgb": {
"description": "Kırmızı, yeşil, mavi ve parlaklık değerlerini ayarlayın.",
"label": "RGB değerleri"
},
"theme-colors": {
"description": "Temanızın renk paletine hızlı erişim.",
"label": "Tema renkleri"
"label": "Palet",
"theme-colors": "Temanızın renk paletine hızlı erişim."
},
"title": "Renk seçici"
},
@@ -2297,6 +2360,7 @@
},
"wifi": {
"panel": {
"available-networks": "Kullanılabilir Ağlar",
"connect": "Bağlan",
"connected": "Bağlı",
"disabled": "Wi-Fi devre dışı",
@@ -2307,6 +2371,7 @@
"forget": "Unut",
"forget-network": "Bu ağı unut?",
"forgetting": "Unutuluyor...",
"known-networks": "Bilinen Ağlar",
"no-networks": "Ağ bulunamadı",
"password": "Şifre",
"saved": "Kaydedildi",

View File

@@ -123,10 +123,6 @@
"stream-description": "Введіть команду для безперервного запуску."
},
"dynamic-text": "Динамічний текст",
"hide-vertical": {
"description": "Якщо увімкнено, текст з виводу команди не відображатиметься, коли панель знаходиться у вертикальному розташуванні (ліворуч або праворуч).",
"label": "Приховати текст у вертикальній панелі"
},
"icon": {
"description": "Вибрати значок з бібліотеки.",
"label": "Значок"
@@ -136,6 +132,14 @@
"label": "Лівий клік",
"update-text": "Оновити текст, що відображається, при натисканні лівою кнопкою миші"
},
"max-text-length-horizontal": {
"description": "Максимальна кількість символів для відображення в горизонтальній панелі (0 щоб приховати текст)",
"label": "Макс. довжина тексту (горизонтально)"
},
"max-text-length-vertical": {
"description": "Максимальна кількість символів для відображення в вертикальній панелі (0 щоб приховати текст)",
"label": "Макс. довжина тексту (вертикально)"
},
"middle-click": {
"description": "Команда для виконання при середньому кліку на кнопку.",
"label": "Середній клік",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "Потокові рядки з команди відображатимуться як текст на кнопці.",
"label": "Потік"
},
"wheel": {
"description": "Команда для виконання при використанні колеса прокрутки.\nВикористовуйте $delta для дельти колеса прокрутки в команді",
"label": "Колесо прокрутки",
"update-text": "Оновити відображуваний текст при прокрутці"
},
"wheel-down": {
"description": "Команда для виконання при прокрутці колеса вниз.",
"label": "Команда прокрутки колеса вниз"
},
"wheel-mode-separate": {
"description": "Увімкнути окремі команди для колеса прокрутки вгору та вниз",
"label": "Окремі команди колеса прокрутки"
},
"wheel-up": {
"description": "Команда для виконання при прокрутці колеса вгору.",
"label": "Команда прокрутки колеса вгору"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "Відображати аудіовізуалізатор під час відтворення музики.",
"label": "Показувати візуалізатор"
},
"show-progress-ring": {
"description": "Відображати круговий індикатор прогресу, що показує просування треку.",
"label": "Показувати кільце прогресу"
},
"use-fixed-width": {
"description": "Коли увімкнено, віджет завжди використовуватиме максимальну ширину замість динамічного налаштування до вмісту.",
"label": "Використовувати фіксовану ширину"
@@ -352,6 +377,10 @@
"description": "Не відображати робочі простори без вікон.",
"label": "Приховати незайняті"
},
"follow-focused-screen": {
"description": "Відображати робочі простори з поточного активного екрана, а не з екрана, на якому розташована панель.",
"label": "Слідувати за Активним Екраном"
},
"label-mode": {
"description": "Виберіть, як відображаються мітки робочих просторів.",
"label": "Режим міток"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "Яскравість",
"charge-level": "Рівень заряду",
"charging": "Зарядка.",
"charging-rate": "Швидкість зарядки: {rate} Вт.",
"discharging": "Розрядка.",
"discharging-rate": "Швидкість розрядки: {rate} Вт.",
"health": "Здоров'я: {percent}%",
"idle": "Бездіяльність.",
"inhibit-idle-description": "Підтримує систему активною.",
"inhibit-idle-label": "Не давати заснути",
"no-battery-detected": "Батарею не виявлено.",
"panel-title": "Батарея",
"plugged-in": "Підключено.",
"power-profile": "Профіль живлення",
"time-left": "Залишилось часу: {time}.",
"time-until-full": "Час до повного заряду: {time}."
},
@@ -396,29 +431,29 @@
}
},
"changelog": {
"error": {
"fetch-failed": "Не вдалося завантажити дані журналу змін. Будь ласка, спробуйте пізніше.",
"rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин."
},
"panel": {
"title": "Що нового у {version}",
"buttons": {
"discord": "Приєднатися до нашого Discord",
"dismiss": "Ок"
},
"empty": "Примітки до релізу ще недоступні.",
"highlight-title": "Основні зміни",
"section": {
"released": "Випущено {date}",
"version": "Версія {version}"
},
"subtitle": {
"fresh": "Дякуємо, що встановили Noctalia! Ось що містить цей білд.",
"updated": "Оновлено з {previousVersion}"
},
"title": "Що нового у {version}",
"version": {
"new-user": "Нове встановлення"
},
"highlight-title": "Основні зміни",
"empty": "Примітки до релізу ще недоступні.",
"section": {
"version": "Версія {version}",
"released": "Випущено {date}"
},
"buttons": {
"discord": "Приєднатися до нашого Discord",
"feedback": "Надіслати відгук",
"dismiss": "Ок"
}
},
"error": {
"rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин."
}
},
"clock": {
@@ -428,10 +463,12 @@
"activate-app": "Активувати {app}",
"clear-history": "Очистити історію",
"close-app": "Закрити {app}",
"connect-vpn": "Підключитися до {name}",
"cycle-visualizer": "Візуалізатор циклів",
"disable-bluetooth": "Вимкнути Bluetooth",
"disable-dnd": "Вимкнути режим \"Не турбувати\"",
"disable-wifi": "Вимкнути Wi-Fi",
"disconnect-vpn": "Відключити {name}",
"enable-bluetooth": "Увімкнути Bluetooth",
"enable-dnd": "Увімкнути режим \"Не турбувати\"",
"enable-wifi": "Увімкнути Wi-Fi",
@@ -480,6 +517,15 @@
"description": "Ваші сповіщення з'являтимуться тут по мірі надходження.",
"no-notifications": "Немає сповіщень",
"title": "Сповіщення"
},
"time": {
"now": "зараз",
"diffM": "1 хвилину тому",
"diffMM": "{diff} хвилин тому",
"diffH": "1 годину тому",
"diffHH": "{diff} годин тому",
"diffD": "1 день тому",
"diffDD": "{diff} днів тому"
}
},
"options": {
@@ -647,7 +693,11 @@
"clipboard-history-disabled-description": "Увімкніть історію буфера обміну в налаштуваннях або встановіть cliphist",
"clipboard-loading": "Завантаження історії буфера обміну...",
"clipboard-loading-description": "Зачекайте, будь ласка",
"clipboard-search-description": "Пошук в історії буфера обміну"
"clipboard-search-description": "Пошук в історії буфера обміну",
"emoji": "Обрати емодзі",
"emoji-loading": "Завантаження емодзі...",
"emoji-loading-description": "Зачекайте, будь ласка",
"emoji-search-description": "Пошук і копіювання емодзі"
},
"quickSettings": {
"bluetooth": {
@@ -1021,6 +1071,14 @@
"description": "Записати {filepath}. Тему Comfy потрібно встановити та активувати вручну.",
"description-missing": "Потрібна установка {app}"
},
"telegram": {
"description": "Записати {filepath}.",
"description-missing": "Потрібна установка {app}"
},
"cava": {
"description": "Записати {filepath}.",
"description-missing": "Потрібна установка {app}"
},
"vicinae": {
"description": "Записати {filepath} та перезавантажити",
"description-missing": "Потрібна установка {app}"
@@ -1217,6 +1275,10 @@
"description": "Налаштуйте непрозорість фону дока.",
"label": "Непрозорість фону"
},
"border-radius": {
"description": "Налаштуйте радіус заокруглення країв док-панелі.",
"label": "Радіус заокруглення"
},
"colorize-icons": {
"description": "Застосувати кольори теми до значків програм у доці (тільки неактивні програми).",
"label": "Розфарбувати значки"
@@ -1368,6 +1430,10 @@
"description": "Налаштуйте непрозорість фону запускача.",
"label": "Непрозорість фону"
},
"clip-preview": {
"description": "Показувати попередній перегляд вмісту буфера обміну при використанні команди >clip.",
"label": "Увімкнути попередній перегляд буфера обміну"
},
"clipboard-history": {
"description": "Отримати доступ до раніше скопійованих елементів із запускача.",
"label": "Увімкнути історію буфера обміну"
@@ -1475,6 +1541,10 @@
"description": "Автоматично блокувати екран при призупиненні системи.",
"label": "Блокувати при призупиненні"
},
"show-hibernate": {
"description": "Показувати опцію 'сплячий режим' у діях живлення.",
"label": "Показувати сплячий режим"
},
"title": "Екран блокування"
},
"network": {
@@ -2038,6 +2108,10 @@
"title-matugen": "Помилка обробки шаблонів Matugen",
"title-predefined": "Помилка обробки попередньо визначеної колірної схеми"
},
"vpn": {
"connected": "Підключено до '{name}'",
"disconnected": "Відключено від '{name}'"
},
"wallpaper-colors": {
"disabled": "Кольори шпалер вимкнено",
"enabled": "Кольори шпалер увімкнено",
@@ -2071,6 +2145,7 @@
"input-muted": "Перемкнути вимкнення входу",
"keep-awake": "Не спати",
"keyboard-layout": "Розкладка клавіатури {layout}",
"manage-vpn": "Керувати підключеннями VPN",
"manage-wifi": "Керувати Wi-Fi",
"microphone-volume-at": "Гучність мікрофона на {volume}%\nЛівий клік для налаштувань. Правий клік для вимкнення звуку.\nПрокрутка для зміни гучності.",
"move-to-center-section": "Перемістити в центральну секцію",
@@ -2205,23 +2280,11 @@
"widgets": {
"color-picker": {
"apply": "Застосувати",
"brightness": "Яскравість",
"cancel": "Скасувати",
"hex": {
"description": "Введіть шістнадцятковий код кольору.",
"label": "Hex-колір"
},
"palette": {
"description": "Виберіть із широкого діапазону попередньо визначених кольорів.",
"label": "Палітра"
},
"rgb": {
"description": "Налаштуйте значення червоного, зеленого, синього та яскравості.",
"label": "Значення RGB"
},
"theme-colors": {
"description": "Швидкий доступ до колірної палітри вашої теми.",
"label": "Кольори теми"
"label": "Палітра",
"theme-colors": "Швидкий доступ до колірної палітри вашої теми."
},
"title": "Вибір кольору"
},
@@ -2297,6 +2360,7 @@
},
"wifi": {
"panel": {
"available-networks": "Доступні мережі",
"connect": "Підключити",
"connected": "Підключено",
"disabled": "Wi-Fi вимкнено",
@@ -2307,6 +2371,7 @@
"forget": "Забути",
"forget-network": "Забути цю мережу?",
"forgetting": "Забування...",
"known-networks": "Відомі мережі",
"no-networks": "Мереж не знайдено",
"password": "Пароль",
"saved": "Збережено",

View File

@@ -123,10 +123,6 @@
"stream-description": "输入一个要持续运行的命令。"
},
"dynamic-text": "动态文本",
"hide-vertical": {
"description": "如果启用,当栏处于垂直布局(左或右)时,将不显示命令输出的文本。",
"label": "在垂直栏中隐藏文本"
},
"icon": {
"description": "从库中选择图标。",
"label": "图标"
@@ -136,6 +132,14 @@
"label": "左键点击",
"update-text": "左键单击时更新显示的文本"
},
"max-text-length-horizontal": {
"description": "在水平栏中显示的最大字符数0 为隐藏文本)",
"label": "最大文本长度(水平)"
},
"max-text-length-vertical": {
"description": "在垂直栏中显示的最大字符数0 为隐藏文本)",
"label": "最大文本长度(垂直)"
},
"middle-click": {
"description": "中键点击按钮时执行的命令。",
"label": "中键点击",
@@ -157,6 +161,23 @@
"text-stream": {
"description": "来自命令的流式输出行将作为文本显示在按钮上。",
"label": "流"
},
"wheel": {
"description": "使用滚轮时执行的命令。\n在命令中使用 $delta 表示滚轮增量",
"label": "滚轮",
"update-text": "滚轮滚动时更新显示的文本"
},
"wheel-down": {
"description": "滚轮向下滚动时执行的命令。",
"label": "滚轮向下命令"
},
"wheel-mode-separate": {
"description": "为滚轮向上和向下启用单独的命令",
"label": "分开滚轮命令"
},
"wheel-up": {
"description": "滚轮向上滚动时执行的命令。",
"label": "滚轮向上命令"
}
},
"dialog": {
@@ -210,6 +231,10 @@
"description": "播放音乐时显示音频可视化器。",
"label": "显示可视化器"
},
"show-progress-ring": {
"description": "显示显示曲目进度的圆形进度指示器。",
"label": "显示进度环"
},
"use-fixed-width": {
"description": "启用后,小部件将始终使用最大宽度,而不根据内容动态调整。",
"label": "使用固定宽度"
@@ -228,10 +253,10 @@
"notification-history": {
"hide-badge-when-zero": {
"description": "当没有未读通知时隐藏通知徽章。",
"label": "时隐藏徽章"
"label": "时隐藏徽章"
},
"show-unread-badge": {
"description": "显示示未读通知数量的徽章。",
"description": "显示一个用于展示未读通知数量的徽章。",
"label": "显示未读徽章"
}
},
@@ -352,6 +377,10 @@
"description": "不显示没有窗口的工作区。",
"label": "隐藏未占用"
},
"follow-focused-screen": {
"description": "显示当前焦点屏幕的工作区,而不是任务栏所在屏幕的工作区。",
"label": "跟随焦点屏幕"
},
"label-mode": {
"description": "选择工作区标签的显示方式。",
"label": "标签模式"
@@ -360,14 +389,20 @@
}
},
"battery": {
"brightness": "亮度",
"charge-level": "电量",
"charging": "正在充电。",
"charging-rate": "充电速率:{rate} W。",
"discharging": "正在放电。",
"discharging-rate": "放电速率:{rate} W。",
"health": "健康状况:{percent}%",
"idle": "空闲。",
"inhibit-idle-description": "保持系统唤醒。",
"inhibit-idle-label": "保持唤醒",
"no-battery-detected": "未检测到电池。",
"panel-title": "电池",
"plugged-in": "已接通电源。",
"power-profile": "电源模式",
"time-left": "剩余时间:{time}。",
"time-until-full": "充满所需时间:{time}。"
},
@@ -396,29 +431,29 @@
}
},
"changelog": {
"error": {
"fetch-failed": "无法加载更新日志数据,请稍后再试。",
"rate-limit": "已达到 GitHub 速率限制,请稍后再试。"
},
"panel": {
"title": "{version} 有哪些更新",
"buttons": {
"discord": "加入我们的 Discord",
"dismiss": "确定"
},
"empty": "暂时没有可用的发行说明。",
"highlight-title": "重点更新",
"section": {
"released": "{date} 发布",
"version": "版本 {version}"
},
"subtitle": {
"fresh": "感谢安装 Noctalia以下是本次构建包含的内容。",
"updated": "已从 {previousVersion} 更新"
},
"title": "{version} 有哪些更新",
"version": {
"new-user": "全新安装"
},
"highlight-title": "重点更新",
"empty": "暂时没有可用的发行说明。",
"section": {
"version": "版本 {version}",
"released": "{date} 发布"
},
"buttons": {
"discord": "加入我们的 Discord",
"feedback": "发送反馈",
"dismiss": "确定"
}
},
"error": {
"rate-limit": "已达到 GitHub 速率限制,请稍后再试。"
}
},
"clock": {
@@ -428,22 +463,24 @@
"activate-app": "激活 {app}",
"clear-history": "清除历史记录",
"close-app": "关闭 {app}",
"cycle-visualizer": "周期可视化工具",
"connect-vpn": "连接 {name}",
"cycle-visualizer": "切换可视化器样式",
"disable-bluetooth": "禁用蓝牙",
"disable-dnd": "关闭勿扰模式",
"disable-wifi": "禁用Wi-Fi",
"disconnect-vpn": "断开 {name}",
"enable-bluetooth": "启用蓝牙",
"enable-dnd": "启用勿扰模式",
"enable-wifi": "启用 Wi-Fi",
"next": "下一",
"next": "下一",
"open-calendar": "打开日历",
"open-display-settings": "显示设置",
"open-launcher": "打开启动器",
"open-mixer": "音频调音台",
"open-mixer": "音频混音器",
"open-settings": "打开设置",
"pause": "暂停",
"play": "",
"previous": "上一",
"play": "播放",
"previous": "上一",
"random-wallpaper": "随机壁纸",
"toggle-mute": "切换静音",
"widget-settings": "小部件设置"
@@ -480,6 +517,15 @@
"description": "您的通知将在到达时显示在此处。",
"no-notifications": "无通知",
"title": "通知"
},
"time": {
"now": "现在",
"diffM": "1 分钟前",
"diffMM": "{diff} 分钟前",
"diffH": "1 小时前",
"diffHH": "{diff} 小时前",
"diffD": "1 天前",
"diffDD": "{diff} 天前"
}
},
"options": {
@@ -647,7 +693,11 @@
"clipboard-history-disabled-description": "在设置中启用剪贴板历史记录或安装 cliphist",
"clipboard-loading": "正在加载剪贴板历史记录...",
"clipboard-loading-description": "请稍候",
"clipboard-search-description": "搜索剪贴板历史记录"
"clipboard-search-description": "搜索剪贴板历史记录",
"emoji": "表情符号选择器",
"emoji-loading": "正在加载表情符号...",
"emoji-loading-description": "请稍候",
"emoji-search-description": "搜索和复制表情符号"
},
"quickSettings": {
"bluetooth": {
@@ -1021,6 +1071,14 @@
"description": "写入 {filepath}。Comfy 主题需要手动安装和激活。",
"description-missing": "需要安装 {app}"
},
"telegram": {
"description": "写入 {filepath}。",
"description-missing": "需要安装 {app}"
},
"cava": {
"description": "写入 {filepath}。",
"description-missing": "需要安装 {app}"
},
"vicinae": {
"description": "写入 {filepath} 并重新加载",
"description-missing": "需要安装 {app}"
@@ -1217,6 +1275,10 @@
"description": "调整 Dock 的背景不透明度。",
"label": "背景不透明度"
},
"border-radius": {
"description": "调整程序坞的边框半径。",
"label": "边框半径"
},
"colorize-icons": {
"description": "将主题颜色应用到 Dock 应用图标(仅限非聚焦应用)。",
"label": "着色图标"
@@ -1368,6 +1430,10 @@
"description": "调整启动器的背景不透明度。",
"label": "背景不透明度"
},
"clip-preview": {
"description": "在使用 >clip 命令时显示剪贴板内容的预览。",
"label": "启用剪贴板预览"
},
"clipboard-history": {
"description": "从启动器访问之前复制的项目。",
"label": "启用剪贴板历史记录"
@@ -1475,6 +1541,10 @@
"description": "系统挂起时自动锁定屏幕。",
"label": "挂起时锁定"
},
"show-hibernate": {
"description": "在电源操作中显示'休眠'选项。",
"label": "显示休眠"
},
"title": "锁屏"
},
"network": {
@@ -2038,6 +2108,10 @@
"title-matugen": "Matugen模板处理失败",
"title-predefined": "预定义的颜色方案处理失败"
},
"vpn": {
"connected": "已连接到“{name}”",
"disconnected": "已断开与“{name}”的连接"
},
"wallpaper-colors": {
"disabled": "壁纸颜色已禁用",
"enabled": "壁纸颜色已启用",
@@ -2071,6 +2145,7 @@
"input-muted": "静音输入设备",
"keep-awake": "保持唤醒",
"keyboard-layout": "{layout} 键盘布局",
"manage-vpn": "管理 VPN 连接",
"manage-wifi": "管理 Wi-Fi",
"microphone-volume-at": "麦克风音量 {volume}%\n左键点击进入设置。右键点击切换静音。\n滚动滚轮调节音量。",
"move-to-center-section": "移动到中央部分",
@@ -2205,23 +2280,11 @@
"widgets": {
"color-picker": {
"apply": "应用",
"brightness": "亮度",
"cancel": "取消",
"hex": {
"description": "输入十六进制颜色代码。",
"label": "十六进制颜色"
},
"palette": {
"description": "从各种预定义颜色中选择。",
"label": "调色板"
},
"rgb": {
"description": "调整红、绿、蓝和亮度值。",
"label": "RGB 值"
},
"theme-colors": {
"description": "快速访问您的主题调色板。",
"label": "主题颜色"
"label": "调色板",
"theme-colors": "快速访问您主题的调色板。"
},
"title": "颜色选择器"
},
@@ -2297,6 +2360,7 @@
},
"wifi": {
"panel": {
"available-networks": "可用网络",
"connect": "连接",
"connected": "已连接",
"disabled": "Wi-Fi 已禁用",
@@ -2307,6 +2371,7 @@
"forget": "忘记",
"forget-network": "忘记此网络?",
"forgetting": "正在忘记...",
"known-networks": "已知网络",
"no-networks": "未找到网络",
"password": "密码",
"saved": "已保存",

View File

@@ -70,6 +70,7 @@
"animationDisabled": false,
"compactLockScreen": false,
"lockOnSuspend": true,
"showHibernateOnLockScreen": false,
"enableShadows": true,
"shadowDirection": "bottom_right",
"shadowOffsetX": 2,
@@ -87,9 +88,6 @@
"panelsAttachedToBar": true,
"settingsPanelAttachToBar": false
},
"changelog": {
"lastSeenVersion": ""
},
"location": {
"name": "Tokyo",
"weatherEnabled": true,
@@ -143,6 +141,7 @@
},
"appLauncher": {
"enableClipboardHistory": false,
"enableClipPreview": true,
"position": "center",
"pinnedExecs": [],
"useApp2Unit": false,
@@ -223,6 +222,7 @@
"enabled": true,
"displayMode": "always_visible",
"backgroundOpacity": 1,
"radiusRatio": 0.1,
"floatingRatio": 1,
"size": 1,
"onlySameOutput": true,
@@ -292,7 +292,8 @@
"visualizerType": "linear",
"visualizerQuality": "high",
"mprisBlacklist": [],
"preferredPlayer": ""
"preferredPlayer": "",
"externalMixer": "pwvucontrol || pavucontrol"
},
"brightness": {
"brightnessStep": 5,
@@ -325,6 +326,7 @@
"walker": false,
"code": false,
"spicetify": false,
"telegram": false,
"enableUserTemplates": false
},
"nightLight": {
@@ -336,6 +338,9 @@
"manualSunrise": "06:30",
"manualSunset": "18:30"
},
"changelog": {
"lastSeenVersion": ""
},
"hooks": {
"enabled": false,
"wallpaperChange": "",

View File

@@ -4,7 +4,7 @@
if [ "$#" -ne 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}" >&2
echo "Usage: $0 {kitty|ghostty|foot|alacritty|wezterm|fuzzel|walker|pywalfox|cava}" >&2
exit 1
fi
@@ -200,6 +200,58 @@ pywalfox)
pywalfox update
;;
cava)
echo "🎨 Applying 'noctalia' theme to 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
echo "[color] section found, checking theme setting..."
# Check if theme is already set to noctalia under [color]
if sed -n '/^\[color\]/,/^\[/p' "$CONFIG_FILE" | grep -q '^theme = "noctalia"'; then
echo "Theme already set to noctalia under [color], skipping modification."
else
# Check if theme line exists under [color] section
if sed -n '/^\[color\]/,/^\[/p' "$CONFIG_FILE" | grep -q '^theme = '; then
# Replace existing theme line under [color]
sed -i '/^\[color\]/,/^\[/{s/^theme = .*/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
fi
else
echo "[color] section not found, adding it with theme..."
# 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
if pgrep -f cava >/dev/null; then
echo "Reloading cava configuration..."
pkill -USR1 cava
echo "✅ Cava reloaded successfully"
else
if [ "$THEME_MODIFIED" = true ]; then
echo "✅ Configuration updated. Start cava to see the changes."
else
echo "✅ Configuration already correct."
fi
fi
else
echo "Error: cava config file not found at $CONFIG_FILE" >&2
exit 1
fi
;;
*)
# Handle unknown application names.
echo "Error: Unknown application '$APP_NAME'." >&2

View File

@@ -15,22 +15,59 @@ fi
# Create the destination directory if it doesn't exist.
mkdir -p "$DEST_DIR"
# Loop through all files in the source directory ending with .frag
for shader in "$SOURCE_DIR"*.frag; do
# Check if a file was found (to handle the case of no .frag files).
if [ -f "$shader" ]; then
# Get the base name of the file (e.g., wp_fade).
shader_name=$(basename "$shader" .frag)
# Array to hold the list of full paths to the shaders.
SHADERS_TO_COMPILE=()
# Construct the output path for the compiled shader.
output_path="$DEST_DIR$shader_name.frag.qsb"
# Specific files mode.
if [ "$#" -gt 0 ]; then
# Construct and run the qsb command.
/usr/lib/qt6/bin/qsb --qt6 -o "$output_path" "$shader"
# Loop through all command-line arguments ($@ holds all arguments).
for SINGLE_FILE in "$@"; do
# Print a message to confirm compilation.
echo "Compiled $shader to $output_path"
# 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.
/usr/lib/qt6/bin/qsb --qt6 -o "$output_path" "$shader"
# Print a message to confirm compilation.
echo "Compiled $(basename "$shader") to $output_path"
done
echo "Shader compilation complete."
echo "Shader compilation complete."

View File

@@ -215,6 +215,7 @@ Singleton {
property bool animationDisabled: false
property bool compactLockScreen: false
property bool lockOnSuspend: true
property bool showHibernateOnLockScreen: false
property bool enableShadows: true
property string shadowDirection: "bottom_right"
property int shadowOffsetX: 2
@@ -296,6 +297,7 @@ Singleton {
// applauncher
property JsonObject appLauncher: JsonObject {
property bool enableClipboardHistory: false
property bool enableClipPreview: true
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
property string position: "center"
property list<string> pinnedExecs: []
@@ -385,6 +387,7 @@ Singleton {
property bool enabled: true
property string displayMode: "always_visible" // "always_visible", "auto_hide", "exclusive"
property real backgroundOpacity: 1.0
property real radiusRatio: 0.1
property real floatingRatio: 1.0
property real size: 1
property bool onlySameOutput: true
@@ -504,6 +507,8 @@ Singleton {
property bool walker: false
property bool code: false
property bool spicetify: false
property bool telegram: false
property bool cava: false
property bool enableUserTemplates: false
}
@@ -792,6 +797,220 @@ Singleton {
Logger.w("Settings", "Failed to read raw JSON for dimDesktop migration:", error);
}
}
// -----------------
// 8th. Migrate ShellState-related files from old cache files to ShellState
// This consolidates migrations that were previously in individual service files
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
migrateShellStateFiles();
} else {
// Wait for ShellState to be ready
Qt.callLater(() => {
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
migrateShellStateFiles();
}
});
}
}
// -----------------------------------------------------
// Migrate old cache files to ShellState
function migrateShellStateFiles() {
// Migrate display.json → ShellState (CompositorService)
migrateDisplayFile();
// Migrate notifications-state.json → ShellState (NotificationService)
migrateNotificationsStateFile();
// Migrate changelog-state.json → ShellState (UpdateService)
migrateChangelogStateFile();
// Migrate color-schemes-list.json → ShellState (SchemeDownloader)
migrateColorSchemesListFile();
// Migrate wallpaper paths from Settings → ShellState (WallpaperService)
migrateWallpaperPaths();
}
function migrateDisplayFile() {
// Check if ShellState already has display data
const cached = ShellState.getDisplay();
if (cached && Object.keys(cached).length > 0) {
return; // Already migrated
}
const oldDisplayPath = cacheDir + "display.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
FileView {
id: migrationView
path: "${oldDisplayPath}"
printErrors: false
adapter: JsonAdapter {
property var displays: ({})
}
onLoaded: {
if (adapter.displays && Object.keys(adapter.displays).length > 0) {
ShellState.setDisplay(adapter.displays);
Logger.i("Settings", "Migrated display.json to ShellState");
}
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "displayMigrationView");
}
function migrateNotificationsStateFile() {
// Check if ShellState already has notifications state
const cached = ShellState.getNotificationsState();
if (cached && cached.lastSeenTs && cached.lastSeenTs > 0) {
return; // Already migrated
}
// Also check Settings for lastSeenTs
if (adapter.notifications && adapter.notifications.lastSeenTs) {
ShellState.setNotificationsState({
lastSeenTs: adapter.notifications.lastSeenTs
});
Logger.i("Settings", "Migrated notifications lastSeenTs from Settings to ShellState");
return;
}
const oldStatePath = cacheDir + "notifications-state.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
FileView {
id: migrationView
path: "${oldStatePath}"
printErrors: false
adapter: JsonAdapter {
property real lastSeenTs: 0
}
onLoaded: {
if (adapter.lastSeenTs && adapter.lastSeenTs > 0) {
ShellState.setNotificationsState({
lastSeenTs: adapter.lastSeenTs
});
Logger.i("Settings", "Migrated notifications-state.json to ShellState");
}
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "notificationsMigrationView");
}
function migrateChangelogStateFile() {
// Check if ShellState already has changelog state
const cached = ShellState.getChangelogState();
if (cached && cached.lastSeenVersion && cached.lastSeenVersion !== "") {
return; // Already migrated
}
// Also check Settings for lastSeenVersion
if (adapter.changelog && adapter.changelog.lastSeenVersion) {
ShellState.setChangelogState({
lastSeenVersion: adapter.changelog.lastSeenVersion
});
Logger.i("Settings", "Migrated changelog lastSeenVersion from Settings to ShellState");
return;
}
const oldChangelogPath = cacheDir + "changelog-state.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
FileView {
id: migrationView
path: "${oldChangelogPath}"
printErrors: false
adapter: JsonAdapter {
property string lastSeenVersion: ""
}
onLoaded: {
if (adapter.lastSeenVersion && adapter.lastSeenVersion !== "") {
ShellState.setChangelogState({
lastSeenVersion: adapter.lastSeenVersion
});
Logger.i("Settings", "Migrated changelog-state.json to ShellState");
}
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "changelogMigrationView");
}
function migrateColorSchemesListFile() {
// Check if ShellState already has color schemes list
const cached = ShellState.getColorSchemesList();
if (cached && cached.schemes && cached.schemes.length > 0) {
return; // Already migrated
}
const oldSchemesPath = cacheDir + "color-schemes-list.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
FileView {
id: migrationView
path: "${oldSchemesPath}"
printErrors: false
adapter: JsonAdapter {
property var schemes: []
property real timestamp: 0
}
onLoaded: {
if (adapter.schemes && adapter.schemes.length > 0) {
ShellState.setColorSchemesList({
schemes: adapter.schemes,
timestamp: adapter.timestamp || 0
});
Logger.i("Settings", "Migrated color-schemes-list.json to ShellState");
}
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "schemesMigrationView");
}
function migrateWallpaperPaths() {
// Check if ShellState already has wallpaper paths
const cached = ShellState.getWallpapers();
if (cached && Object.keys(cached).length > 0) {
return; // Already migrated
}
// Migrate from Settings wallpaper.monitors
var monitors = adapter.wallpaper.monitors || [];
if (monitors.length > 0) {
var wallpapers = {};
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name && monitors[i].wallpaper) {
wallpapers[monitors[i].name] = monitors[i].wallpaper;
}
}
if (Object.keys(wallpapers).length > 0) {
ShellState.setWallpapers(wallpapers);
Logger.i("Settings", "Migrated wallpaper paths from Settings to ShellState");
}
}
}
// -----------------------------------------------------
@@ -828,4 +1047,4 @@ Singleton {
const widgetAfter = JSON.stringify(widget);
return (widgetAfter !== widgetBefore);
}
}
}

182
Commons/ShellState.qml Normal file
View File

@@ -0,0 +1,182 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// 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
})
// WallpaperService: current wallpapers per screen
property var wallpapers: ({})
}
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;
}
}
}
// Debounced save timer
Timer {
id: saveTimer
interval: 300
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
};
}
// Wallpapers (WallpaperService)
function setWallpapers(wallpapersData) {
adapter.wallpapers = wallpapersData;
save();
}
function getWallpapers() {
return adapter.wallpapers || {};
}
}

View File

@@ -100,11 +100,23 @@ Singleton {
return "";
const diff = Date.now() - date.getTime();
if (diff < 60000)
return "now";
return I18n.tr("notifications.time.now");
if (diff < 120000)
return I18n.tr("notifications.time.diffM");
if (diff < 3600000)
return `${Math.floor(diff / 60000)}m ago`;
return I18n.tr("notifications.time.diffMM", {
"diff": Math.floor(diff / 60000)
});
if (diff < 7200000)
return I18n.tr("notifications.time.diffH");
if (diff < 86400000)
return `${Math.floor(diff / 3600000)}h ago`;
return `${Math.floor(diff / 86400000)}d ago`;
return I18n.tr("notifications.time.diffHH", {
"diff": Math.floor(diff / 3600000)
});
if (diff < 172800000)
return I18n.tr("notifications.time.diffD");
return I18n.tr("notifications.time.diffDD", {
"diff": Math.floor(diff / 86400000)
});
}
}

187
Helpers/ColorList.js Normal file
View File

@@ -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" }
]

34
Helpers/TextFormatter.js Normal file
View File

@@ -0,0 +1,34 @@
.pragma library
/**
* Wrap text in a nicely styled HTML container for display
* @param {string} text - The text to display
* @returns {string} HTML string
*/
function wrapTextForDisplay(text) {
// Escape HTML special characters
const escapeHtml = (s) =>
s.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
return `
<div style="
font-family: 'Fira Code', 'Courier New', monospace;
white-space: pre-wrap;
background: linear-gradient(135deg, #2c3e50, #34495e);
color: #ecf0f1;
padding: 16px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
overflow-x: auto;
line-height: 1.5;
font-size: 14px;
border: 1px solid #3d566e;
">
${escapeHtml(text)}
</div>
`;
}

View File

@@ -20,6 +20,8 @@ Item {
property bool oppositeDirection: false
property bool hovered: false
property bool rotateText: false
property color customBackgroundColor: Qt.rgba(0, 0, 0, 0)
property color customTextIconColor: Qt.rgba(0, 0, 0, 0)
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
@@ -57,6 +59,8 @@ Item {
hovered: root.hovered
density: root.density
rotateText: root.rotateText
customBackgroundColor: root.customBackgroundColor
customTextIconColor: root.customTextIconColor
onShown: root.shown()
onHidden: root.hidden()
onEntered: root.entered()
@@ -82,6 +86,8 @@ Item {
oppositeDirection: root.oppositeDirection
hovered: root.hovered
density: root.density
customBackgroundColor: root.customBackgroundColor
customTextIconColor: root.customTextIconColor
onShown: root.shown()
onHidden: root.hidden()
onEntered: root.entered()

View File

@@ -20,6 +20,8 @@ Item {
property bool forceClose: false
property bool oppositeDirection: false
property bool hovered: false
property color customBackgroundColor: Qt.rgba(0, 0, 0, 0)
property color customTextIconColor: Qt.rgba(0, 0, 0, 0)
// Effective shown state (true if hovered/animated open or forced)
readonly property bool revealed: !forceClose && (forceOpen || showPill)
@@ -78,7 +80,7 @@ Item {
width: root.width
height: pillHeight
radius: halfPillHeight
color: hovered ? Color.mHover : Style.capsuleColor
color: hovered ? (customBackgroundColor.a > 0 ? Qt.lighter(customBackgroundColor, 1.1) : Color.mHover) : (customBackgroundColor.a > 0 ? customBackgroundColor : Style.capsuleColor)
anchors.verticalCenter: parent.verticalCenter
readonly property int halfPillHeight: Math.round(pillHeight * 0.5)
@@ -129,7 +131,7 @@ Item {
pointSize: textSize
applyUiScale: false
font.weight: Style.fontWeightBold
color: hovered ? Color.mOnHover : (forceOpen ? Color.mOnSurface : Color.mPrimary)
color: hovered ? (customTextIconColor.a > 0 ? customTextIconColor : Color.mOnHover) : (customTextIconColor.a > 0 ? customTextIconColor : (forceOpen ? Color.mOnSurface : Color.mPrimary))
visible: revealed
}
@@ -163,7 +165,7 @@ Item {
icon: root.icon
pointSize: iconSize
applyUiScale: false
color: hovered ? Color.mOnHover : Color.mOnSurface
color: hovered ? (customTextIconColor.a > 0 ? customTextIconColor : Color.mOnHover) : (customTextIconColor.a > 0 ? customTextIconColor : Color.mOnSurface)
// Center horizontally
x: (iconCircle.width - width) / 2
// Center vertically accounting for font metrics

View File

@@ -21,6 +21,8 @@ Item {
property bool oppositeDirection: false
property bool hovered: false
property bool rotateText: false
property color customBackgroundColor: Qt.rgba(0, 0, 0, 0)
property color customTextIconColor: Qt.rgba(0, 0, 0, 0)
// Bar position detection for pill direction
readonly property string barPosition: Settings.data.bar.position
@@ -49,10 +51,9 @@ Item {
// Sizing logic for vertical bars
readonly property int buttonSize: Style.capsuleHeight
readonly property int pillHeight: buttonSize
readonly property int pillPaddingVertical: 3 * 2 // Very precise adjustment don't replace by Style.margin
readonly property int pillOverlap: Math.round(buttonSize * 0.5)
readonly property int maxPillWidth: rotateText ? Math.max(buttonSize, Math.round(textItem.implicitHeight + pillPaddingVertical * 2)) : buttonSize
readonly property int maxPillHeight: rotateText ? Math.max(1, Math.round(textItem.implicitWidth + pillPaddingVertical * 2 + Math.round(iconCircle.height / 4))) : Math.max(1, Math.round(textItem.implicitHeight + pillPaddingVertical * 4))
readonly property int maxPillWidth: rotateText ? Math.max(buttonSize, Math.round(textItem.implicitHeight + Style.marginM * 2)) : buttonSize
readonly property int maxPillHeight: rotateText ? Math.max(1, Math.round(textItem.implicitWidth + Style.marginM * 2 + Math.round(iconCircle.height / 4))) : Math.max(1, Math.round(textItem.implicitHeight + Style.marginM * 2))
readonly property real iconSize: {
switch (root.density) {
@@ -91,7 +92,7 @@ Item {
width: buttonSize
height: revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize
radius: halfButtonSize
color: hovered ? Color.mHover : Style.capsuleColor
color: hovered ? (customBackgroundColor.a > 0 ? Qt.lighter(customBackgroundColor, 1.1) : Color.mHover) : (customBackgroundColor.a > 0 ? customBackgroundColor : Style.capsuleColor)
readonly property int halfButtonSize: Math.round(buttonSize * 0.5)
@@ -130,7 +131,7 @@ Item {
id: textItem
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: rotateText ? Math.round(iconCircle.height / 4) : getVerticalCenterOffset()
anchors.verticalCenterOffset: openDownward ? Style.marginXXS : -Style.marginXXS
rotation: rotateText ? -90 : 0
text: root.text + root.suffix
family: Settings.data.ui.fontFixed
@@ -139,15 +140,12 @@ Item {
font.weight: Style.fontWeightMedium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: hovered ? Color.mOnHover : (forceOpen ? Color.mOnSurface : Color.mPrimary)
color: hovered ? (customTextIconColor.a > 0 ? customTextIconColor : Color.mOnHover) : (customTextIconColor.a > 0 ? customTextIconColor : (forceOpen ? Color.mOnSurface : Color.mPrimary))
visible: revealed
function getVerticalCenterOffset() {
var offset = openDownward ? Math.round(pillPaddingVertical * 0.75) : -Math.round(pillPaddingVertical * 0.75);
if (forceOpen) {
offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS;
}
return offset;
// A small, symmetrical offset to push the text slightly away from the icon's edge.
return openDownward ? Style.marginXS : -Style.marginXS;
}
}
Behavior on width {
@@ -189,7 +187,7 @@ Item {
icon: root.icon
pointSize: iconSize
applyUiScale: false
color: hovered ? Color.mOnHover : Color.mOnSurface
color: hovered ? (customTextIconColor.a > 0 ? customTextIconColor : Color.mOnHover) : (customTextIconColor.a > 0 ? customTextIconColor : Color.mOnSurface)
// Center horizontally
x: (iconCircle.width - width) / 2
// Center vertically accounting for font metrics

View File

@@ -34,10 +34,11 @@ Item {
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold
readonly property bool isLowBattery: !charging && percent <= warningThreshold
// Test mode
readonly property bool testMode: false
readonly property int testPercent: 100
readonly property int testPercent: 15
readonly property bool testCharging: false
// Main properties
@@ -120,6 +121,9 @@ Item {
autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery)
customBackgroundColor: isLowBattery ? Color.mError : Qt.rgba(0, 0, 0, 0)
customTextIconColor: isLowBattery ? Color.mOnError : charging ? Color.mPrimary : Qt.rgba(0, 0, 0, 0)
tooltipText: {
let lines = [];
if (testMode) {
@@ -165,6 +169,7 @@ Item {
}
return lines.join("\n");
}
onClicked: PanelService.getPanel("batteryPanel", screen)?.toggle(this)
onRightClicked: {
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {

View File

@@ -133,8 +133,9 @@ Item {
onWheel: function (angle) {
var monitor = getMonitor();
if (!monitor)
if (!monitor || !monitor.brightnessControlAvailable)
return;
if (angle > 0) {
monitor.increaseBrightness();
} else if (angle < 0) {
@@ -142,11 +143,7 @@ Item {
}
}
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel.requestedTab = SettingsPanel.Tab.Display;
settingsPanel.open();
}
onClicked: PanelService.getPanel("brightnessPanel", screen)?.toggle(this)
onRightClicked: {
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);

View File

@@ -39,15 +39,19 @@ Item {
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 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 hideTextInVerticalBar: widgetSettings.hideTextInVerticalBar !== undefined ? widgetSettings.hideTextInVerticalBar : (widgetMetadata.hideTextInVerticalBar || false)
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
readonly property bool shouldShowText: !isVerticalBar || !hideTextInVerticalBar
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec || (wheelMode === "unified" && wheelExec) || (wheelMode === "separate" && (wheelUpExec || wheelDownExec)))
implicitWidth: pill.width
implicitHeight: pill.height
@@ -58,9 +62,9 @@ Item {
screen: root.screen
oppositeDirection: BarService.getPillDirection(root)
icon: _dynamicIcon !== "" ? _dynamicIcon : customIcon
text: shouldShowText ? _dynamicText : ""
text: (!isVerticalBar || currentMaxTextLength > 0) ? _dynamicText : ""
density: Settings.data.bar.density
rotateText: isVerticalBar && !hideTextInVerticalBar
rotateText: isVerticalBar && currentMaxTextLength > 0
autoHide: false
forceOpen: _dynamicText !== ""
tooltipText: {
@@ -76,6 +80,16 @@ Item {
if (middleClickExec !== "") {
tooltipLines.push(`Middle click: ${middleClickExec}.`);
}
if (wheelMode === "unified" && wheelExec !== "") {
tooltipLines.push(`Wheel: ${wheelExec}.`);
} else if (wheelMode === "separate") {
if (wheelUpExec !== "") {
tooltipLines.push(`Wheel up: ${wheelUpExec}.`);
}
if (wheelDownExec !== "") {
tooltipLines.push(`Wheel down: ${wheelDownExec}.`);
}
}
}
if (_dynamicTooltip !== "") {
@@ -95,6 +109,7 @@ Item {
onClicked: root.onClicked()
onRightClicked: root.onRightClicked()
onMiddleClicked: root.onMiddleClicked()
onWheel: delta => root.onWheel(delta)
}
// Internal state for dynamic text
@@ -102,12 +117,32 @@ Item {
property string _dynamicIcon: ""
property string _dynamicTooltip: ""
// 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: shouldShowText && !textStream && textCommand && textCommand.length > 0
running: (!isVerticalBar || currentMaxTextLength > 0) && !textStream && textCommand && textCommand.length > 0
triggeredOnStart: true
onTriggered: root.runTextCommand()
}
@@ -116,10 +151,61 @@ Item {
Timer {
id: restartTimer
interval: 1000
running: shouldShowText && textStream && !textProc.running
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)
@@ -162,16 +248,33 @@ Item {
let tooltip = parsed.tooltip || "";
if (checkCollapse(text)) {
_scrollState.originalText = "";
_dynamicText = "";
_dynamicIcon = "";
_dynamicTooltip = "";
_scrollState.needsScrolling = false;
_scrollState.phase = 0;
_scrollState.phaseCounter = 0;
return;
}
_dynamicText = text;
_scrollState.originalText = text;
_scrollState.needsScrolling = text.length > currentMaxTextLength && currentMaxTextLength > 0;
if (_scrollState.needsScrolling) {
// Start with the beginning of the text
_dynamicText = text.substring(0, currentMaxTextLength);
_scrollState.phase = 0; // Start at phase 0 (static beginning)
_scrollState.phaseCounter = 0;
_scrollState.offset = 0;
scrollTimer.start(); // Start the scrolling timer
} else {
_dynamicText = text;
scrollTimer.stop();
}
_dynamicIcon = icon;
_dynamicTooltip = toHtml(tooltip);
_scrollState.offset = 0;
return;
} catch (e) {
Logger.w("CustomButton", `Failed to parse JSON. Content: "${lineToParse}"`);
@@ -179,15 +282,32 @@ Item {
}
if (checkCollapse(contentStr)) {
_scrollState.originalText = "";
_dynamicText = "";
_dynamicIcon = "";
_dynamicTooltip = "";
_scrollState.needsScrolling = false;
_scrollState.phase = 0;
_scrollState.phaseCounter = 0;
return;
}
_dynamicText = contentStr;
_scrollState.originalText = contentStr;
_scrollState.needsScrolling = contentStr.length > currentMaxTextLength && currentMaxTextLength > 0;
if (_scrollState.needsScrolling) {
// Start with the beginning of the text
_dynamicText = contentStr.substring(0, currentMaxTextLength);
_scrollState.phase = 0; // Start at phase 0 (static beginning)
_scrollState.phaseCounter = 0;
_scrollState.offset = 0;
scrollTimer.start(); // Start the scrolling timer
} else {
_dynamicText = contentStr;
scrollTimer.stop();
}
_dynamicIcon = "";
_dynamicTooltip = toHtml(contentStr);
_scrollState.offset = 0;
}
function checkCollapse(text) {
@@ -215,8 +335,8 @@ Item {
if (leftClickExec) {
Quickshell.execDetached(["sh", "-c", leftClickExec]);
Logger.i("CustomButton", `Executing command: ${leftClickExec}`);
} else if (!hasExec && !leftClickUpdateText) {
// No script was defined, open settings
} else if (!leftClickUpdateText) {
// No left click script was defined, open settings
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel.requestedTab = SettingsPanel.Tab.Bar;
settingsPanel.open();
@@ -247,17 +367,19 @@ Item {
}
function toHtml(str) {
const htmlRegex = /<\/?[a-zA-Z][\s\S]*>/;
const htmlTagRegex = /<\/?[a-zA-Z][^>]*>/g;
const placeholders = [];
let i = 0;
const protectedStr = str.replace(htmlTagRegex, tag => {
placeholders.push(tag);
return `___HTML_TAG_${i++}___`;
});
if (htmlRegex.test(str)) {
return str;
}
let escaped = protectedStr.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/\r\n|\r|\n/g, "<br/>");
const escaped = str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
escaped = escaped.replace(/___HTML_TAG_(\d+)___/g, (_, index) => placeholders[Number(index)]);
const withBreaks = escaped.replace(/\r\n|\r|\n/g, "<br/>");
return withBreaks;
return escaped;
}
function runTextCommand() {
@@ -268,4 +390,99 @@ Item {
textProc.command = ["sh", "-lc", textCommand];
textProc.running = true;
}
function onWheel(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", "-c", 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", "-c", 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();
}
}
}
}
}

View File

@@ -42,6 +42,7 @@ Item {
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? 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
// Maximum widget width with user settings support
readonly property real maxWidth: (widgetSettings.maxWidth !== undefined) ? widgetSettings.maxWidth : Math.max(widgetMetadata.maxWidth, screen ? screen.width * 0.06 : 0)
@@ -321,14 +322,79 @@ Item {
Layout.preferredWidth: Math.round(21 * scaling)
Layout.preferredHeight: Math.round(21 * scaling)
// Background for progress circle
Rectangle {
anchors.fill: parent
radius: width / 2
color: Color.transparent
}
// Progress circle
Canvas {
id: progressCanvas
anchors.fill: parent
anchors.margins: 0 // Align exactly with parent to avoid clipping
visible: showProgressRing // Control visibility with setting
z: 0 // Behind the album art
// Calculate progress ratio: 0 to 1
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));
}
onProgressRatioChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
var centerX = width / 2;
var centerY = height / 2;
var radius = Math.min(width, height) / 2 - (1.25 * scaling); // Larger radius, accounting for line width to approach edge
ctx.reset();
// Background circle (full track, not played yet)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.lineWidth = 3 * scaling; // Thicker line width based on scaling property
ctx.strokeStyle = Qt.alpha(Color.mOnSurface, 0.4); // More opaque for better visibility
ctx.stroke();
// Progress arc (played portion)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progressRatio * 2 * Math.PI);
ctx.lineWidth = 3 * scaling; // Thicker line width based on scaling property
ctx.strokeStyle = Color.mPrimary; // Use primary color for progress
ctx.lineCap = "round";
ctx.stroke();
}
}
// Connection to update progress when media position changes
Connections {
target: MediaService
function onCurrentPositionChanged() {
progressCanvas.requestPaint();
}
function onTrackLengthChanged() {
progressCanvas.requestPaint();
}
}
NImageCircled {
id: trackArt
anchors.fill: parent
anchors.margins: showProgressRing ? (2.5 * scaling) : 0.5 // Make album art smaller only when progress ring is visible, scaled with widget
imagePath: MediaService.trackArtUrl
fallbackIcon: MediaService.isPlaying ? "media-pause" : "media-play"
fallbackIconSize: 10
borderWidth: 0
border.color: Color.transparent
z: 1 // In front of the progress circle
}
}
}
@@ -484,6 +550,52 @@ Item {
}
}
// Progress circle for vertical layout - follows background radius
Canvas {
id: progressCanvasVertical
anchors.fill: parent
anchors.margins: 0 // Align with parent container (mainContainer which matches mediaMini)
visible: isVerticalBar && showProgressRing // Control visibility with setting
z: 0 // Behind other content
// Calculate progress ratio: 0 to 1
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));
}
onProgressRatioChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
var centerX = width / 2;
var centerY = height / 2;
// Align with mediaMini radius which is circular in vertical mode
var radius = Math.min(width, height) / 2 - 4; // Position ring near the outer edge of background
ctx.reset();
// Background circle (full track, not played yet)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.lineWidth = 1.5 * scaling; // Line width based on scaling property, thinner for vertical layout
ctx.strokeStyle = Qt.alpha(Color.mOnSurface, 0.4); // More opaque for better visibility
ctx.stroke();
// Progress arc (played portion)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progressRatio * 2 * Math.PI);
ctx.lineWidth = 1.5 * scaling; // Line width based on scaling property, thinner for vertical layout
ctx.strokeStyle = Color.mPrimary; // Use primary color for progress
ctx.lineCap = "round";
ctx.stroke();
}
}
// Vertical layout for left/right bars - icon only
Item {
id: verticalLayout
@@ -507,10 +619,22 @@ Item {
pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
z: 1 // In front of the progress circle
}
}
}
// Connection to update vertical progress when media position changes
Connections {
target: MediaService
function onCurrentPositionChanged() {
progressCanvasVertical.requestPaint();
}
function onTrackLengthChanged() {
progressCanvasVertical.requestPaint();
}
}
// Mouse area for hover detection
MouseArea {
id: mouseArea

View File

@@ -144,10 +144,10 @@ Item {
forceClose: displayMode === "alwaysHide"
tooltipText: 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);
})()
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
const displayVolume = Math.min(maxVolume, AudioService.inputVolume);
return Math.round(displayVolume * 100);
})()
})
onWheel: function (delta) {

View File

@@ -9,7 +9,7 @@ import qs.Services.System
import qs.Services.UI
import qs.Widgets
NIconButton {
Item {
id: root
property ShellScreen screen
@@ -33,6 +33,9 @@ NIconButton {
readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge
readonly property bool hideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero
implicitWidth: pill.width
implicitHeight: pill.height
function computeUnreadCount() {
var since = NotificationService.lastSeenTs;
var count = 0;
@@ -46,17 +49,6 @@ NIconButton {
return count;
}
baseSize: Style.capsuleHeight
applyUiScale: false
density: Settings.data.bar.density
icon: NotificationService.doNotDisturb ? "bell-off" : "bell"
tooltipText: NotificationService.doNotDisturb ? I18n.tr("tooltips.open-notification-history-disable-dnd") : I18n.tr("tooltips.open-notification-history-enable-dnd")
tooltipDirection: BarService.getTooltipDirection()
colorBg: Style.capsuleColor
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
NPopupContextMenu {
id: contextMenu
@@ -94,37 +86,79 @@ NIconButton {
}
}
onClicked: {
var panel = PanelService.getPanel("notificationHistoryPanel", screen);
panel?.toggle(this);
}
BarPill {
id: pill
onRightClicked: {
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
const pos = BarService.getContextMenuPosition(root, contextMenu.implicitWidth, contextMenu.implicitHeight);
contextMenu.openAtItem(root, pos.x, pos.y);
popupMenuWindow.showContextMenu(contextMenu);
property string currentNotif
Connections {
target: NotificationService.activeList
function onCountChanged() {
// keep current text a bit longer for the animation
if (NotificationService.activeList.count > 0) {
var notif = NotificationService.activeList.get(0)
var summary = notif.summary.trim()
var body = notif.body.trim()
pill.currentNotif = `${summary}: ${body}`.replace(/\n/g, " ")
}
}
}
}
Loader {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 2
anchors.topMargin: 1
z: 2
active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0)
sourceComponent: Rectangle {
id: badge
readonly property int count: computeUnreadCount()
height: 8
width: height
radius: height / 2
color: Color.mError
border.color: Color.mSurface
border.width: Style.borderS
visible: count > 0 || !hideWhenZero
Component.onCompleted: {
function dismiss(notificationId) {
if (Settings.data.notifications?.location == "bar") {
NotificationService.dismissActiveNotification(notificationId)
}
}
NotificationService.animateAndRemove.connect(dismiss);
}
screen: root.screen
density: Settings.data.bar.density
oppositeDirection: BarService.getPillDirection(root)
icon: NotificationService.doNotDisturb ? "bell-off" : "bell"
tooltipText: NotificationService.doNotDisturb ? I18n.tr("tooltips.open-notification-history-disable-dnd") : I18n.tr("tooltips.open-notification-history-enable-dnd")
text: currentNotif
forceOpen: Settings.data.notifications?.location == "bar" && NotificationService.activeList.count > 0
// prevent open via mouse over
forceClose: NotificationService.activeList.count == 0
opacity: NotificationService.doNotDisturb || computeUnreadCount() > 0 ? 100 : 0
onClicked: {
var panel = PanelService.getPanel("notificationHistoryPanel", screen);
panel?.toggle(this);
}
onRightClicked: {
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
const pos = BarService.getContextMenuPosition(root, contextMenu.implicitWidth, contextMenu.implicitHeight);
contextMenu.openAtItem(root, pos.x, pos.y);
popupMenuWindow.showContextMenu(contextMenu);
}
}
Loader {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 2
anchors.topMargin: 1
z: 2
active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0)
sourceComponent: Rectangle {
id: badge
readonly property int count: computeUnreadCount()
height: 8
width: height
radius: height / 2
color: Color.mError
border.color: Color.mSurface
border.width: Style.borderS
visible: count > 0 || !hideWhenZero
}
}
}
}

View File

@@ -1,3 +1,4 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.Media
@@ -11,7 +12,7 @@ NIconButton {
property ShellScreen screen
icon: "camera-video"
icon: ScreenRecorderService.isPending ? "" : "camera-video"
tooltipText: ScreenRecorderService.isRecording ? I18n.tr("tooltips.click-to-stop-recording") : I18n.tr("tooltips.click-to-start-recording")
tooltipDirection: BarService.getTooltipDirection()
density: Settings.data.bar.density
@@ -31,4 +32,32 @@ NIconButton {
}
onClicked: handleClick()
// Custom spinner shown only during pending start
NIcon {
id: pendingSpinner
icon: "loader-2"
visible: ScreenRecorderService.isPending
pointSize: {
switch (root.density) {
case "compact":
return Math.max(1, root.width * 0.65);
default:
return Math.max(1, root.width * 0.48);
}
}
applyUiScale: root.applyUiScale
color: root.enabled && root.hovering ? colorFgHover : colorFg
anchors.centerIn: parent
transformOrigin: Item.Center
RotationAnimation on rotation {
running: ScreenRecorderService.isPending
from: 0
to: 360
duration: Style.animationSlow
loops: Animation.Infinite
onStopped: pendingSpinner.rotation = 0
}
}
}

View File

@@ -240,6 +240,7 @@ Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
Layout.fillWidth: isVertical
implicitWidth: iconSize
implicitHeight: iconSize
@@ -314,6 +315,7 @@ Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
Layout.fillWidth: isVertical
implicitWidth: iconSize
implicitHeight: iconSize
@@ -381,6 +383,7 @@ Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
Layout.fillWidth: isVertical
implicitWidth: iconSize
implicitHeight: iconSize
@@ -429,13 +432,20 @@ Rectangle {
rowSpacing: Style.marginXXS
columnSpacing: Style.marginXXS
NIcon {
icon: "download-speed"
pointSize: iconSize
applyUiScale: false
Item {
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
Layout.fillWidth: isVertical
implicitWidth: iconSize
implicitHeight: iconSize
NIcon {
icon: "download-speed"
pointSize: iconSize
applyUiScale: false
anchors.centerIn: parent
}
}
NText {
@@ -472,13 +482,20 @@ Rectangle {
rowSpacing: Style.marginXXS
columnSpacing: Style.marginXXS
NIcon {
icon: "upload-speed"
pointSize: iconSize
applyUiScale: false
Item {
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
Layout.fillWidth: isVertical
implicitWidth: iconSize
implicitHeight: iconSize
NIcon {
icon: "upload-speed"
pointSize: iconSize
applyUiScale: false
anchors.centerIn: parent
}
}
NText {
@@ -530,15 +547,22 @@ Rectangle {
rowSpacing: Style.marginXXS
columnSpacing: Style.marginXXS
NIcon {
icon: "storage"
pointSize: iconSize
applyUiScale: false
Item {
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
// Invert color when disk indicator active (vertical uses highlight colors)
color: isVertical ? (diskCritical ? criticalColor : (diskWarning ? warningColor : Color.mOnSurface)) : ((diskWarning || diskCritical) ? Color.mSurfaceVariant : Color.mOnSurface)
Layout.fillWidth: isVertical
implicitWidth: iconSize
implicitHeight: iconSize
NIcon {
icon: "storage"
pointSize: iconSize
applyUiScale: false
anchors.centerIn: parent
// Invert color when disk indicator active (vertical uses highlight colors)
color: isVertical ? (diskCritical ? criticalColor : (diskWarning ? warningColor : Color.mOnSurface)) : ((diskWarning || diskCritical) ? Color.mSurfaceVariant : Color.mOnSurface)
}
}
NText {

View File

@@ -34,11 +34,13 @@ Item {
}
return {};
}
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
readonly property int characterCount: 2
readonly property bool showLabelsOnlyWhenOccupied: (widgetSettings.showLabelsOnlyWhenOccupied !== undefined) ? widgetSettings.showLabelsOnlyWhenOccupied : true
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
readonly property bool showLabelsOnlyWhenOccupied: (widgetSettings.showLabelsOnlyWhenOccupied !== undefined) ? widgetSettings.showLabelsOnlyWhenOccupied : widgetMetadata.showLabelsOnlyWhenOccupied
readonly property bool colorizeIcons: (widgetSettings.colorizeIcons !== undefined) ? widgetSettings.colorizeIcons : widgetMetadata.colorizeIcons
property ListModel localWorkspaces: ListModel {}
property real masterProgress: 0.0
property bool effectsActive: false
@@ -270,9 +272,22 @@ Item {
hoverEnabled: true
enabled: !hasWindows
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
CompositorService.switchToWorkspace(workspaceModel);
}
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: mouse => {
if (mouse.button === Qt.LeftButton) {
CompositorService.switchToWorkspace(workspaceModel);
} else if (mouse.button === Qt.RightButton) {
TooltipService.hide();
root.selectedWindow = "";
root.selectedAppName = "";
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
const pos = BarService.getContextMenuPosition(container, contextMenu.implicitWidth, contextMenu.implicitHeight);
contextMenu.openAtItem(container, pos.x, pos.y);
popupMenuWindow.showContextMenu(contextMenu);
}
}
}
}
Flow {
@@ -345,25 +360,25 @@ Item {
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: function (mouse) {
if (!model) {
return;
}
onPressed: mouse => {
if (!model) {
return;
}
if (mouse.button === Qt.LeftButton) {
CompositorService.focusWindow(model);
} else if (mouse.button === Qt.RightButton) {
TooltipService.hide();
root.selectedWindow = model;
root.selectedAppName = CompositorService.getCleanAppName(model.appId, model.title);
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
const pos = BarService.getContextMenuPosition(taskbarItem, contextMenu.implicitWidth, contextMenu.implicitHeight);
contextMenu.openAtItem(taskbarItem, pos.x, pos.y);
popupMenuWindow.showContextMenu(contextMenu);
}
}
}
if (mouse.button === Qt.LeftButton) {
CompositorService.focusWindow(model);
} else if (mouse.button === Qt.RightButton) {
TooltipService.hide();
root.selectedWindow = model;
root.selectedAppName = CompositorService.getCleanAppName(model.appId, model.title);
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
const pos = BarService.getContextMenuPosition(taskbarItem, contextMenu.implicitWidth, contextMenu.implicitHeight);
contextMenu.openAtItem(taskbarItem, pos.x, pos.y);
popupMenuWindow.showContextMenu(contextMenu);
}
}
}
onEntered: {
taskbarItem.itemHovered = true;
TooltipService.show(taskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection());
@@ -407,7 +422,11 @@ Item {
if (hasWindows)
return Color.mSecondary;
return Qt.alpha(Color.mOutline, 0.3);
if (Settings.data.colorSchemes.darkMode) {
return Qt.darker(Color.mSecondary, 1.5);
} else {
return Qt.lighter(Color.mSecondary, 1.5);
}
}
scale: workspaceModel.isActive ? 1.0 : 0.9
@@ -472,21 +491,10 @@ Item {
return Color.mOnPrimary;
if (workspaceModel.isUrgent)
return Color.mOnError;
if (hasWindows)
return Color.mOnSecondary;
// if (hasWindows)
// return Color.mOnSecondary;
return Color.mOnSurface;
}
opacity: {
if (workspaceModel.isFocused)
return 1.0;
if (workspaceModel.isUrgent)
return 0.9;
if (hasWindows)
return 0.8;
return 0.6;
return Color.mOnSecondary;
}
Behavior on opacity {

139
Modules/Bar/Widgets/VPN.qml Normal file
View File

@@ -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]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section];
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex];
}
}
return {};
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
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("context-menu.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("context-menu.connect-vpn", {
"name": conn.name
}),
"action": "connect:" + conn.uuid,
"icon": "shield-lock"
});
}
items.push({
"label": I18n.tr("context-menu.widget-settings"),
"action": "widget-settings",
"icon": "settings"
});
return items;
}
onTriggered: action => {
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
popupMenuWindow.close();
}
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
density: Settings.data.bar.density
oppositeDirection: BarService.getPillDirection(root)
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: {
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
const pos = BarService.getContextMenuPosition(pill, contextMenu.implicitWidth, contextMenu.implicitHeight);
contextMenu.openAtItem(pill, pos.x, pos.y);
popupMenuWindow.showContextMenu(contextMenu);
}
}
tooltipText: {
if (pill.text !== "") {
return pill.text;
}
return I18n.tr("tooltips.manage-vpn");
}
}
}

View File

@@ -127,10 +127,10 @@ Item {
forceClose: displayMode === "alwaysHide"
tooltipText: 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);
})()
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
const displayVolume = Math.min(maxVolume, AudioService.volume);
return Math.round(displayVolume * 100);
})()
})
onWheel: function (delta) {

View File

@@ -46,6 +46,7 @@ Item {
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
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: isVertical ? 2 : ((widgetSettings.characterCount !== undefined) ? widgetSettings.characterCount : widgetMetadata.characterCount)
property bool isDestroying: false
@@ -162,10 +163,20 @@ Item {
function refreshWorkspaces() {
localWorkspaces.clear();
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) {
for (var i = 0; i < CompositorService.workspaces.count; i++) {
const ws = CompositorService.workspaces.get(i);
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
if ((followFocusedScreen && ws.output.toLowerCase() == focusedOutput) || (!followFocusedScreen && ws.output.toLowerCase() == screen.name.toLowerCase())) {
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) {
continue;
}

View File

@@ -299,7 +299,7 @@ Loader {
height: Math.round(iconSize * 1.5)
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
anchors.centerIn: parent
radius: Style.radiusL
radius: height * 0.5 * Settings.data.dock.radiusRatio
border.width: Style.borderS
border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity)

View File

@@ -73,7 +73,7 @@ Scope {
root.unlocked();
} else {
Logger.i("LockContext", "Authentication failed");
errorMessage = "Authentication failed";
errorMessage = I18n.tr("lock-screen.authentication-failed");
showFailure = true;
root.failed();
}

View File

@@ -20,21 +20,19 @@ import qs.Widgets
import qs.Widgets.AudioSpectrum
Loader {
id: lockScreen
id: root
active: false
// Track if triggered via deprecated IPC call
property bool triggeredViaDeprecatedCall: false
Component.onCompleted: {
// Register with panel service
PanelService.lockScreen = this;
}
Timer {
id: unloadAfterUnlockTimer
interval: 250
repeat: false
onTriggered: {
lockScreen.active = false;
// Reset the deprecation flag when unlocking
lockScreen.triggeredViaDeprecatedCall = false;
}
onTriggered: root.active = false
}
function scheduleUnloadAfterUnlock() {
@@ -49,7 +47,7 @@ Loader {
id: lockContext
onUnlocked: {
lockSession.locked = false;
lockScreen.scheduleUnloadAfterUnlock();
root.scheduleUnloadAfterUnlock();
lockContext.currentText = "";
}
onFailed: {
@@ -59,7 +57,7 @@ Loader {
WlSessionLock {
id: lockSession
locked: lockScreen.active
locked: root.active
WlSessionLockSurface {
readonly property var now: Time.now
@@ -377,76 +375,20 @@ Loader {
backgroundColor: Color.mSurface
clockColor: Color.mOnSurface
secondHandColor: Color.mPrimary
}
}
}
// Deprecation warning (shown above error notification)
Rectangle {
width: Math.min(650, parent.width - 40)
implicitHeight: deprecationContent.implicitHeight + 24
height: implicitHeight
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 320 : 400) * Style.uiScaleRatio
radius: Style.radiusL
color: Qt.alpha(Color.mTertiary, 0.95)
border.color: Color.mTertiary
border.width: 2
visible: lockScreen.triggeredViaDeprecatedCall
opacity: visible ? 1.0 : 0.0
ColumnLayout {
id: deprecationContent
anchors.fill: parent
anchors.margins: 12
spacing: 6
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 8
NIcon {
icon: "alert-triangle"
pointSize: Style.fontSizeL
color: Color.mOnTertiary
}
NText {
text: "Deprecated IPC Call"
color: Color.mOnTertiary
pointSize: Style.fontSizeL
font.weight: Font.Bold
}
}
NText {
text: "The 'lockScreen toggle' IPC call is deprecated. Use 'lockScreen lock' instead."
color: Color.mOnTertiary
pointSize: Style.fontSizeM
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
hoursFontSize: Style.fontSizeL
minutesFontSize: Style.fontSizeL
}
}
}
// Error notification
Rectangle {
width: 450
height: 60
width: errorRowLayout.implicitWidth + Style.marginXL * 1.5
height: 50
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 240 : 320) * Style.uiScaleRatio
radius: 30
anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 280 : 360) * Style.uiScaleRatio
radius: Style.radiusL
color: Color.mError
border.color: Color.mError
border.width: 1
@@ -454,6 +396,7 @@ Loader {
opacity: visible ? 1.0 : 0.0
RowLayout {
id: errorRowLayout
anchors.centerIn: parent
spacing: 10
@@ -550,7 +493,7 @@ Loader {
// Bottom container with weather, password input and controls
Rectangle {
width: 750
id: bottomContainer
height: Settings.data.general.compactLockScreen ? 120 : 220
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
@@ -558,6 +501,61 @@ Loader {
radius: Style.radiusL
color: Color.mSurface
// Measure text widths to determine minimum button width (for container width calculation)
Item {
id: buttonRowTextMeasurer
visible: false
property real iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
property real fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
property real spacing: 6
property real padding: 18 // Approximate horizontal padding per button
// Measure all button text widths
Text {
id: logoutText
text: I18n.tr("session-menu.logout")
font.pointSize: buttonRowTextMeasurer.fontSize
font.weight: Font.Medium
}
Text {
id: suspendText
text: I18n.tr("session-menu.suspend")
font.pointSize: buttonRowTextMeasurer.fontSize
font.weight: Font.Medium
}
Text {
id: hibernateText
text: I18n.tr("session-menu.hibernate")
font.pointSize: buttonRowTextMeasurer.fontSize
font.weight: Font.Medium
}
Text {
id: rebootText
text: I18n.tr("session-menu.reboot")
font.pointSize: buttonRowTextMeasurer.fontSize
font.weight: Font.Medium
}
Text {
id: shutdownText
text: I18n.tr("session-menu.shutdown")
font.pointSize: buttonRowTextMeasurer.fontSize
font.weight: Font.Medium
}
// Calculate maximum width needed
property real maxTextWidth: Math.max(logoutText.implicitWidth, Math.max(suspendText.implicitWidth, Math.max(hibernateText.implicitWidth, Math.max(rebootText.implicitWidth, shutdownText.implicitWidth))))
property real minButtonWidth: maxTextWidth + iconSize + spacing + padding
}
// Calculate minimum width based on button requirements
// Button row needs: margins + buttons (4 or 5 depending on hibernate visibility) + spacings + margins
// Plus ColumnLayout margins (14 on each side = 28 total)
// Add extra buffer to ensure password input has proper padding
property int buttonCount: Settings.data.general.showHibernateOnLockScreen ? 5 : 4
property int spacingCount: buttonCount - 1
property real minButtonRowWidth: buttonRowTextMeasurer.minButtonWidth > 0 ? (buttonCount * buttonRowTextMeasurer.minButtonWidth) + (spacingCount * 10) + 40 + (2 * Style.marginM) + 28 + (2 * Style.marginM) : 750
width: Math.max(750, minButtonRowWidth)
ColumnLayout {
anchors.fill: parent
anchors.margins: 14
@@ -1026,6 +1024,7 @@ Loader {
id: eyeButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: parent.parent.passwordVisible = !parent.parent.passwordVisible
}
@@ -1062,6 +1061,7 @@ Loader {
id: submitButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: lockContext.tryUnlock()
}
}
@@ -1091,6 +1091,7 @@ Loader {
Rectangle {
Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24
color: logoutButtonArea.containsMouse ? Color.mHover : "transparent"
@@ -1119,6 +1120,7 @@ Loader {
id: logoutButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: CompositorService.logout()
}
@@ -1139,6 +1141,7 @@ Loader {
Rectangle {
Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24
color: suspendButtonArea.containsMouse ? Color.mHover : "transparent"
@@ -1167,6 +1170,7 @@ Loader {
id: suspendButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: CompositorService.suspend()
}
@@ -1187,11 +1191,13 @@ Loader {
Rectangle {
Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24
color: hibernateButtonArea.containsMouse ? Color.mHover : "transparent"
border.color: Color.mOutline
border.width: 1
visible: Settings.data.general.showHibernateOnLockScreen
RowLayout {
anchors.centerIn: parent
@@ -1215,6 +1221,7 @@ Loader {
id: hibernateButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: CompositorService.hibernate()
}
@@ -1235,6 +1242,7 @@ Loader {
Rectangle {
Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24
color: rebootButtonArea.containsMouse ? Color.mHover : "transparent"
@@ -1263,6 +1271,7 @@ Loader {
id: rebootButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: CompositorService.reboot()
}
@@ -1283,6 +1292,7 @@ Loader {
Rectangle {
Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24
color: shutdownButtonArea.containsMouse ? Color.mError : "transparent"
@@ -1311,6 +1321,7 @@ Loader {
id: shutdownButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: CompositorService.shutdown()
}

View File

@@ -69,6 +69,13 @@ Item {
backgroundColor: panelBackgroundColor
}
// Battery
PanelBackground {
panel: root.windowRoot.batteryPanelPlaceholder
shapeContainer: backgroundsShape
backgroundColor: panelBackgroundColor
}
// Bluetooth
PanelBackground {
panel: root.windowRoot.bluetoothPanelPlaceholder
@@ -76,6 +83,13 @@ Item {
backgroundColor: panelBackgroundColor
}
// Brightness
PanelBackground {
panel: root.windowRoot.brightnessPanelPlaceholder
shapeContainer: backgroundsShape
backgroundColor: panelBackgroundColor
}
// Calendar
PanelBackground {
panel: root.windowRoot.calendarPanelPlaceholder

View File

@@ -10,7 +10,9 @@ import qs.Commons
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.Calendar
import qs.Modules.Panels.Changelog
import qs.Modules.Panels.ControlCenter
@@ -32,7 +34,9 @@ PanelWindow {
// Expose panels as readonly property aliases
readonly property alias audioPanel: audioPanel
readonly property alias batteryPanel: batteryPanel
readonly property alias bluetoothPanel: bluetoothPanel
readonly property alias brightnessPanel: brightnessPanel
readonly property alias calendarPanel: calendarPanel
readonly property alias changelogPanel: changelogPanel
readonly property alias controlCenterPanel: controlCenterPanel
@@ -47,7 +51,9 @@ PanelWindow {
// Expose panel placeholders for AllBackgrounds
readonly property var audioPanelPlaceholder: audioPanel.panelPlaceholder
readonly property var batteryPanelPlaceholder: batteryPanel.panelPlaceholder
readonly property var bluetoothPanelPlaceholder: bluetoothPanel.panelPlaceholder
readonly property var brightnessPanelPlaceholder: brightnessPanel.panelPlaceholder
readonly property var calendarPanelPlaceholder: calendarPanel.panelPlaceholder
readonly property var changelogPanelPlaceholder: changelogPanel.panelPlaceholder
readonly property var controlCenterPanelPlaceholder: controlCenterPanel.panelPlaceholder
@@ -165,12 +171,24 @@ PanelWindow {
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")

View File

@@ -47,6 +47,10 @@ Item {
property bool isPanelVisible: false
property bool isClosing: false
property bool opacityFadeComplete: false
property bool sizeAnimationComplete: false
// Derived state: track opening transition
readonly property bool isOpening: isPanelVisible && !isClosing && !sizeAnimationComplete
// Content size (set by SmartPanelWindow when content size changes)
property real contentPreferredWidth: 0
@@ -563,6 +567,9 @@ Item {
readonly property bool shouldAnimateWidth: !shouldAnimateHeight && (animateFromLeft || animateFromRight)
readonly property bool shouldAnimateHeight: animateFromTop || animateFromBottom
// Track whether we're in an initial open/close state transition vs normal content resizing
readonly property bool isStateTransition: root.isOpening || root.isClosing
// Current animated width/height
readonly property real currentWidth: {
if (isClosing && opacityFadeComplete && shouldAnimateWidth)
@@ -603,11 +610,10 @@ Item {
Behavior on width {
NumberAnimation {
duration: {
if (!panelBackground.shouldAnimateWidth)
return 0;
return root.isClosing ? Style.animationFast : Style.animationNormal;
}
// 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: (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
}
@@ -615,11 +621,10 @@ Item {
Behavior on height {
NumberAnimation {
duration: {
if (!panelBackground.shouldAnimateHeight)
return 0;
return root.isClosing ? Style.animationFast : Style.animationNormal;
}
// 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: (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
}

View File

@@ -29,10 +29,13 @@ PanelWindow {
// Use Top layer (same as MainScreen) for proper event handling
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
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");

View File

@@ -99,6 +99,9 @@ PanelWindow {
onOpacityFadeCompleteChanged: {
placeholder.opacityFadeComplete = opacityFadeComplete;
}
onSizeAnimationCompleteChanged: {
placeholder.sizeAnimationComplete = sizeAnimationComplete;
}
// Panel control functions
function toggle(buttonItem, buttonName) {

View File

@@ -21,8 +21,8 @@ Variants {
property ListModel notificationModel: NotificationService.activeList
// Loader is active when there are notifications
active: notificationModel.count > 0 || delayTimer.running
// Always create window (but with 0x0 dimensions when no notifications)
active: Settings.data.notifications?.location != "bar"
// Keep loader active briefly after last notification to allow animations to complete
Timer {
@@ -104,8 +104,8 @@ Variants {
margins.left: isLeft ? barOffsetLeft : 0
margins.right: isRight ? barOffsetRight : 0
implicitWidth: notifWidth
implicitHeight: notificationStack.implicitHeight + Style.marginL
implicitWidth: (notificationModel.count > 0 || delayTimer.running) ? notifWidth : 0
implicitHeight: (notificationModel.count > 0 || delayTimer.running) ? (notificationStack.implicitHeight + Style.marginL) : 0
property var animateConnection: null
@@ -393,23 +393,22 @@ Variants {
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
Layout.margins: Style.marginM
Layout.leftMargin: Style.marginM * 2
Layout.rightMargin: Style.marginM * 2
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
ColumnLayout {
NImageCircled {
Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio)
Layout.preferredHeight: Math.round(40 * Style.uiScaleRatio)
Layout.alignment: Qt.AlignTop
Layout.topMargin: 30
Layout.alignment: Qt.AlignVCenter
imagePath: model.originalImage || ""
borderColor: Color.transparent
borderWidth: 0
fallbackIcon: "bell"
fallbackIconSize: 24
}
Item {
Layout.fillHeight: true
}
}
ColumnLayout {
@@ -430,9 +429,18 @@ Variants {
}
NText {
text: `${model.appName || I18n.tr("system.unknown-app")} · ${Time.formatRelativeTime(model.timestamp)}`
color: Color.mSecondary
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 {

View File

@@ -0,0 +1,296 @@
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.Power
import qs.Widgets
SmartPanel {
id: root
preferredWidth: Math.round(360 * Style.uiScaleRatio)
preferredHeight: Math.round(460 * Style.uiScaleRatio)
readonly property var battery: UPower.displayDevice
readonly property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent
readonly property int percent: isReady ? Math.round(battery.percentage * 100) : -1
readonly property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
readonly property bool healthSupported: isReady && battery.healthSupported
readonly property bool healthAvailable: healthSupported
readonly property int healthPercent: healthAvailable ? Math.round(battery.healthPercentage) : -1
readonly property bool powerProfileAvailable: PowerProfileService.available
readonly property var powerProfiles: [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
readonly property string timeText: {
if (!isReady)
return I18n.tr("battery.no-battery-detected");
if (charging && battery.timeToFull > 0) {
return I18n.tr("battery.time-until-full", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToFull)
});
}
if (!charging && battery.timeToEmpty > 0) {
return I18n.tr("battery.time-left", {
"time": Time.formatVagueHumanReadableDuration(battery.timeToEmpty)
});
}
return I18n.tr("battery.idle");
}
readonly property string iconName: BatteryService.getIcon(percent, charging, isReady)
readonly property bool profilesAvailable: PowerProfileService.available
property int profileIndex: profileToIndex(PowerProfileService.profile)
property bool manualInhibitActive: manualInhibitorEnabled()
panelContent: Item {
property real contentPreferredHeight: mainLayout.implicitHeight + Style.marginL * 2
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// HEADER
NBox {
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight + (Style.marginM * 2)
RowLayout {
id: headerRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NIcon {
pointSize: Style.fontSizeXXL
color: root.charging ? Color.mPrimary : Color.mOnSurface
icon: iconName
}
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NText {
text: I18n.tr("battery.panel-title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NText {
text: timeText
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
wrapMode: Text.Wrap
Layout.fillWidth: true
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
}
}
}
// Charge level + health/time
NBox {
Layout.fillWidth: true
height: chargeLayout.implicitHeight + Style.marginL * 2
ColumnLayout {
id: chargeLayout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
ColumnLayout {
NText {
text: I18n.tr("battery.charge-level")
color: Color.mOnSurface
pointSize: Style.fontSizeS
}
Rectangle {
Layout.fillWidth: true
height: Math.round(8 * Style.uiScaleRatio)
radius: height / 2
color: Color.mSurfaceVariant
Rectangle {
anchors.verticalCenter: parent.verticalCenter
height: parent.height
radius: parent.radius
width: {
var ratio = Math.max(0, Math.min(1, percent / 100));
return parent.width * ratio;
}
color: Color.mPrimary
}
}
}
NText {
text: percent >= 0 ? `${percent}%` : "--"
color: Color.mOnSurface
pointSize: Style.fontSizeS
font.weight: Style.fontWeightBold
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
visible: healthAvailable
NText {
text: I18n.tr("battery.health", {
"percent": healthPercent
})
color: Color.mOnSurface
pointSize: Style.fontSizeS
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
}
}
}
}
// Power profile and idle inhibit controls
NBox {
Layout.fillWidth: true
height: controlsLayout.implicitHeight + Style.marginM * 2
ColumnLayout {
id: controlsLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
ColumnLayout {
id: ppd
visible: root.powerProfileAvailable
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: PowerProfileService.getIcon()
pointSize: Style.fontSizeM
color: Color.mPrimary
}
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
value: profileIndex
enabled: profilesAvailable
onPressedChanged: (pressed, v) => {
if (!pressed) {
setProfileByIndex(v);
}
}
onMoved: v => {
profileIndex = v;
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: manualInhibitActive ? "keep-awake-on" : "keep-awake-off"
pointSize: Style.fontSizeL
color: manualInhibitActive ? Color.mPrimary : Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NToggle {
Layout.fillWidth: true
checked: manualInhibitActive
label: I18n.tr("battery.inhibit-idle-label")
description: I18n.tr("battery.inhibit-idle-description")
onToggled: function (checked) {
if (checked) {
IdleInhibitorService.addManualInhibitor(null);
} else {
IdleInhibitorService.removeManualInhibitor();
}
manualInhibitActive = checked;
}
}
}
}
}
}
}
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 manualInhibitorEnabled() {
return IdleInhibitorService.activeInhibitors && IdleInhibitorService.activeInhibitors.indexOf("manual") >= 0;
}
Connections {
target: IdleInhibitorService
function onIsInhibitedChanged() {
manualInhibitActive = manualInhibitorEnabled();
}
}
Timer {
id: inhibitorPoll
interval: 1000
repeat: true
running: true
onTriggered: manualInhibitActive = manualInhibitorEnabled()
}
Connections {
target: PowerProfileService
function onProfileChanged() {
profileIndex = profileToIndex(PowerProfileService.profile);
}
}
}

View File

@@ -27,7 +27,7 @@ NBox {
NText {
text: root.label
pointSize: Style.fontSizeL
pointSize: Style.fontSizeS
color: Color.mSecondary
font.weight: Style.fontWeightMedium
visible: root.model.length > 0

View File

@@ -18,7 +18,11 @@ SmartPanel {
panelContent: Rectangle {
color: Color.transparent
property real contentPreferredHeight: !(BluetoothService.adapter && BluetoothService.adapter.enabled) ? Math.min(preferredHeight, Math.max(280 * Style.uiScaleRatio, mainColumn.implicitHeight + Style.marginL * 2)) : (mainColumn.implicitHeight + Style.marginL * 2)
// Calculate content height based on header + devices list (or minimum for empty states)
property real headerHeight: headerRow.implicitHeight + Style.marginM * 2
property real devicesHeight: devicesList.implicitHeight
property real calculatedHeight: headerHeight + devicesHeight + Style.marginL * 2 + Style.marginM
property real contentPreferredHeight: (BluetoothService.adapter && BluetoothService.adapter.enabled) ? Math.min(root.preferredHeight, Math.max(280 * Style.uiScaleRatio, calculatedHeight)) : Math.min(root.preferredHeight, 280 * Style.uiScaleRatio)
ColumnLayout {
id: mainColumn
@@ -83,6 +87,7 @@ SmartPanel {
// Adapter not available of disabled
NBox {
id: disabledBox
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
Layout.fillWidth: true
Layout.fillHeight: true
@@ -135,6 +140,7 @@ SmartPanel {
contentWidth: availableWidth
ColumnLayout {
id: devicesList
width: parent.width
spacing: Style.marginM

View File

@@ -0,0 +1,148 @@
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.Widgets
SmartPanel {
id: root
preferredWidth: Math.round(340 * Style.uiScaleRatio)
preferredHeight: Math.round(420 * Style.uiScaleRatio)
function getIcon(brightness) {
return brightness <= 0.5 ? "brightness-low" : "brightness-high";
}
panelContent: Item {
property real contentPreferredHeight: mainColumn.implicitHeight + Style.marginL * 2
ColumnLayout {
id: mainColumn
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// HEADER
NBox {
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight + (Style.marginM * 2)
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("settings.display.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close();
}
}
}
}
NScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
// AudioService Devices
ColumnLayout {
spacing: Style.marginM
width: parent.width
Repeater {
model: Quickshell.screens || []
delegate: NBox {
Layout.fillWidth: true
Layout.preferredHeight: outputColumn.implicitHeight + (Style.marginM * 2)
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
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: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
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"
}
}
}
}
}
}
}
}
}
}

View File

@@ -21,8 +21,8 @@ SmartPanel {
readonly property bool hasPreviousVersion: previousVersion && previousVersion.length > 0
readonly property var releaseHighlights: UpdateService.releaseHighlights || []
readonly property string subtitleText: hasPreviousVersion ? I18n.tr("changelog.panel.subtitle.updated", {
"previousVersion": previousVersion
}) : I18n.tr("changelog.panel.subtitle.fresh")
"previousVersion": previousVersion
}) : I18n.tr("changelog.panel.subtitle.fresh")
panelContent: Rectangle {
color: Color.mSurfaceVariant
@@ -51,8 +51,8 @@ SmartPanel {
NText {
text: I18n.tr("changelog.panel.title", {
"version": currentVersion || UpdateService.currentVersion
})
"version": currentVersion || UpdateService.currentVersion
})
pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold
color: Color.mPrimary
@@ -128,12 +128,6 @@ SmartPanel {
width: parent.width
spacing: Style.marginM
NText {
text: I18n.tr("changelog.panel.highlight-title")
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
visible: UpdateService.fetchError !== ""
text: UpdateService.fetchError
@@ -141,62 +135,37 @@ SmartPanel {
wrapMode: Text.WordWrap
}
Repeater {
model: releaseHighlights
delegate: ColumnLayout {
width: parent.width
spacing: Style.marginS
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("changelog.panel.section.version", {
"version": modelData.version || I18n.tr("system.unknown-version")
})
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
Repeater {
model: releaseHighlights
delegate: ColumnLayout {
width: parent.width
spacing: Style.marginXS
NText {
visible: modelData.date && modelData.date.length > 0
text: I18n.tr("changelog.panel.section.released", {
"date": root.formatReleaseDate(modelData.date)
})
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeXS
}
Repeater {
model: modelData.entries
delegate: RowLayout {
width: parent.width
spacing: Style.marginS
Rectangle {
width: Style.marginXL
height: Style.marginXL
radius: Style.radiusS
color: Qt.alpha(Color.mPrimary, 0.12)
NIcon {
anchors.centerIn: parent
icon: "check"
color: Color.mPrimary
pointSize: Style.fontSizeM
Repeater {
model: modelData.entries
delegate: NText {
readonly property int headingLevel: root.headingLevel(modelData)
text: {
if (modelData.length === 0)
return "\u00A0";
if (headingLevel > 0)
return modelData.replace(/^#+\s+/, "");
return modelData;
}
}
NText {
text: modelData
color: Color.mOnSurface
wrapMode: Text.WordWrap
elide: Text.ElideNone
textFormat: Text.PlainText
color: headingLevel > 0 ? Color.mPrimary : Color.mOnSurface
font.weight: headingLevel > 0 ? Style.fontWeightBold : Style.fontWeightMedium
pointSize: headingLevel === 1 ? Style.fontSizeXXL : headingLevel === 2 ? Style.fontSizeXL : Style.fontSizeM
Layout.fillWidth: true
}
}
}
NDivider {
Layout.fillWidth: true
visible: index < releaseHighlights.length - 1
}
}
}
@@ -240,10 +209,19 @@ SmartPanel {
}
}
function headingLevel(text) {
if (!text)
return 0;
const trimmed = text.trim();
if (trimmed.length === 0)
return 0;
const match = trimmed.match(/^(#+)\s+/);
if (!match)
return 0;
return Math.min(match[1].length, 2);
}
onClosed: {
if (GitHubService && GitHubService.clearReleaseCache) {
GitHubService.clearReleaseCache();
}
if (UpdateService && UpdateService.changelogCurrentVersion) {
UpdateService.markChangelogSeen(UpdateService.changelogCurrentVersion);
}
@@ -262,4 +240,3 @@ SmartPanel {
}
}
}

View File

@@ -117,6 +117,7 @@ NBox {
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
Layout.fillWidth: true
Layout.preferredWidth: 0
}
}
@@ -165,6 +166,7 @@ NBox {
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
Layout.fillWidth: true
Layout.preferredWidth: 0
}
}

View File

@@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../../../Helpers/TextFormatter.js" as TextFormatter
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: contentColumn.implicitHeight + Style.marginL * 2
Connections {
target: previewPanel
function onCurrentItemChanged() {
fullContent = "";
imageDataUrl = "";
loadingFullContent = false;
isImageContent = currentItem && currentItem.isImage;
if (currentItem && currentItem.clipboardId) {
if (isImageContent) {
imageDataUrl = ClipboardService.getImageData(currentItem.clipboardId) || "";
loadingFullContent = !imageDataUrl;
if (!imageDataUrl && currentItem.mime) {
ClipboardService.decodeToDataUrl(currentItem.clipboardId, currentItem.mime, null);
}
} else {
loadingFullContent = true;
ClipboardService.decode(currentItem.clipboardId, function (content) {
fullContent = TextFormatter.wrapTextForDisplay(content);
loadingFullContent = false;
});
}
}
}
}
readonly property int _rev: ClipboardService.revision
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;
}
}
}
}
}
Rectangle {
anchors.fill: parent
color: Color.mSurface || "#f5f5f5"
border.color: Color.mOutlineVariant || "#cccccc"
border.width: 1
radius: Style.radiusM
ColumnLayout {
id: contentColumn
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginS
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant || "#e0e0e0"
border.color: Color.mOutline || "#aaaaaa"
border.width: 1
radius: Style.radiusS
BusyIndicator {
anchors.centerIn: parent
running: loadingFullContent
visible: loadingFullContent
width: Style.baseWidgetSize
height: width
}
Item {
anchors.fill: parent
anchors.margins: Style.marginS
NImageRounded {
anchors.fill: parent
imagePath: imageDataUrl
visible: isImageContent && !loadingFullContent && imageDataUrl !== ""
imageRadius: Style.radiusS
imageFillMode: Image.PreserveAspectFit
}
ScrollView {
anchors.fill: parent
clip: true
visible: !isImageContent && !loadingFullContent
TextArea {
text: fullContent
readOnly: true
wrapMode: Text.Wrap
textFormat: TextArea.RichText // Enable HTML rendering
font.pointSize: Style.fontSizeM // Adjust font size for readability
color: Color.mOnSurface // Consistent text color
}
}
}
}
}
}
}

View File

@@ -13,8 +13,14 @@ import qs.Widgets
SmartPanel {
id: root
readonly property bool previewActive: searchText.startsWith(">clip") && Settings.data.appLauncher.enableClipPreview && ClipboardService.items.length > 0 && selectedIndex >= 0 && results[selectedIndex] && results[selectedIndex].clipboardId
// Panel configuration
preferredWidth: Math.round(500 * Style.uiScaleRatio)
readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio)
readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio)
readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2)
preferredWidth: totalBaseWidth
preferredHeight: Math.round(600 * Style.uiScaleRatio)
preferredWidthRatio: 0.3
preferredHeightRatio: 0.5
@@ -214,6 +220,14 @@ SmartPanel {
}
}
EmojiPlugin {
id: emojiPlugin
Component.onCompleted: {
registerPlugin(this);
Logger.d("Launcher", "Registered: EmojiPlugin");
}
}
// Navigation functions
function selectNextWrapped() {
if (results.length > 0) {
@@ -262,13 +276,50 @@ SmartPanel {
}
}
// UI
panelContent: Rectangle {
id: ui
color: Color.transparent
opacity: resultsReady ? 1.0 : 0.0
// Global MouseArea to detect mouse movement
// Preview Panel (external)
NBox {
id: previewBox
visible: root.previewActive
width: root.previewPanelWidth
height: Math.round(400 * Style.uiScaleRatio)
x: ui.width + Style.marginM
y: Math.max(Style.marginL // Minimum y is the top margin of the content area
, Math.min(resultsList.mapToItem(ui, 0, (root.selectedIndex * (root.entryHeight + resultsList.spacing)) - resultsList.contentY).y, ui.height - previewBox.height - Style.marginL // Maximum y, considering bottom margin
))
z: -1 // Draw behind main panel content if it ever overlaps
opacity: visible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Loader {
id: clipboardPreviewLoader
anchors.fill: parent
active: root.previewActive
source: active ? "./ClipboardPreview.qml" : ""
onLoaded: {
if (selectedIndex >= 0 && results[selectedIndex] && item) {
item.currentItem = results[selectedIndex];
}
}
onItemChanged: {
if (item && selectedIndex >= 0 && results[selectedIndex]) {
item.currentItem = results[selectedIndex];
}
}
}
}
MouseArea {
id: mouseMovementDetector
anchors.fill: parent
@@ -282,7 +333,6 @@ SmartPanel {
property bool initialized: false
onPositionChanged: mouse => {
// Store initial position
if (!initialized) {
lastX = mouse.x;
lastY = mouse.y;
@@ -290,7 +340,6 @@ SmartPanel {
return;
}
// Check if mouse actually moved
const deltaX = Math.abs(mouse.x - lastX);
const deltaY = Math.abs(mouse.y - lastY);
if (deltaX > 1 || deltaY > 1) {
@@ -300,7 +349,6 @@ SmartPanel {
}
}
// Reset when launcher opens
Connections {
target: root
function onOpened() {
@@ -329,298 +377,304 @@ SmartPanel {
}
}
ColumnLayout {
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
anchors.margins: Style.marginL // Apply overall margins here
spacing: Style.marginM // Apply spacing between elements here
NTextInput {
id: searchInput
Layout.fillWidth: true
fontSize: Style.fontSizeL
fontWeight: Style.fontWeightSemiBold
text: searchText
placeholderText: I18n.tr("placeholders.search-launcher")
onTextChanged: searchText = text
Component.onCompleted: {
if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus();
// Intercept Tab keys before TextField handles them
searchInput.inputItem.Keys.onPressed.connect(function (event) {
if (event.key === Qt.Key_Tab) {
root.onTabPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Backtab) {
root.onBackTabPressed();
event.accepted = true;
}
});
}
}
}
// Results list
NListView {
id: resultsList
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
Layout.fillWidth: true
// Left Pane
ColumnLayout {
id: leftPane
Layout.fillHeight: true
spacing: Style.marginXXS
model: results
currentIndex: selectedIndex
cacheBuffer: resultsList.height * 2
onCurrentIndexChanged: {
cancelFlick();
if (currentIndex >= 0) {
positionViewAtIndex(currentIndex, ListView.Contain);
Layout.preferredWidth: root.listPanelWidth
spacing: Style.marginM
NTextInput {
id: searchInput
Layout.fillWidth: true
fontSize: Style.fontSizeL
fontWeight: Style.fontWeightSemiBold
text: searchText
placeholderText: I18n.tr("placeholders.search-launcher")
onTextChanged: searchText = text
Component.onCompleted: {
if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus();
// Intercept Tab keys before TextField handles them
searchInput.inputItem.Keys.onPressed.connect(function (event) {
if (event.key === Qt.Key_Tab) {
root.onTabPressed();
event.accepted = true;
} else if (event.key === Qt.Key_Backtab) {
root.onBackTabPressed();
event.accepted = true;
}
});
}
}
}
onModelChanged: {}
delegate: Rectangle {
id: entry
NListView {
id: resultsList
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex)
// Accessor for app id
property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
// Pin helpers
function togglePin(appId) {
if (!appId)
return;
let arr = (Settings.data.dock.pinnedApps || []).slice();
const idx = arr.indexOf(appId);
if (idx >= 0)
arr.splice(idx, 1);
else
arr.push(appId);
Settings.data.dock.pinnedApps = arr;
}
function isPinned(appId) {
const arr = Settings.data.dock.pinnedApps || [];
return appId && arr.indexOf(appId) >= 0;
}
// Property to reliably track the current item's ID.
// This changes whenever the delegate is recycled for a new item.
property var currentClipboardId: modelData.isImage ? modelData.clipboardId : ""
// When this delegate is assigned a new image item, trigger the decode.
onCurrentClipboardIdChanged: {
// Check if it's a valid ID and if the data isn't already cached.
if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) {
ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null);
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Style.marginXXS
model: results
currentIndex: selectedIndex
cacheBuffer: resultsList.height * 2
onCurrentIndexChanged: {
cancelFlick();
if (currentIndex >= 0) {
positionViewAtIndex(currentIndex, ListView.Contain);
}
if (clipboardPreviewLoader.item) {
clipboardPreviewLoader.item.currentItem = results[currentIndex] || null;
}
}
onModelChanged: {}
width: resultsList.width - Style.marginS
implicitHeight: entryHeight
radius: Style.radiusM
color: entry.isSelected ? Color.mHover : Color.mSurface
delegate: Rectangle {
id: entry
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex)
property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
// Pin helpers
function togglePin(appId) {
if (!appId)
return;
let arr = (Settings.data.dock.pinnedApps || []).slice();
const idx = arr.indexOf(appId);
if (idx >= 0)
arr.splice(idx, 1);
else
arr.push(appId);
Settings.data.dock.pinnedApps = arr;
}
}
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
function isPinned(appId) {
const arr = Settings.data.dock.pinnedApps || [];
return appId && arr.indexOf(appId) >= 0;
}
// Top row - Main entry content with pin button
RowLayout {
Layout.fillWidth: true
// Property to reliably track the current item's ID.
// This changes whenever the delegate is recycled for a new item.
property var currentClipboardId: modelData.isImage ? modelData.clipboardId : ""
// When this delegate is assigned a new image item, trigger the decode.
onCurrentClipboardIdChanged: {
// Check if it's a valid ID and if the data isn't already cached.
if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) {
ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null);
}
}
width: resultsList.width - Style.marginS
implicitHeight: entryHeight
radius: Style.radiusM
color: entry.isSelected ? Color.mHover : Color.mSurface
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
// Icon badge or Image preview
Rectangle {
Layout.preferredWidth: badgeSize
Layout.preferredHeight: badgeSize
radius: Style.radiusM
color: Color.mSurfaceVariant
// Top row - Main entry content with pin button
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
// Image preview for clipboard images
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: modelData.isImage
imageRadius: Style.radiusM
// This property creates a dependency on the service's revision counter
readonly property int _rev: ClipboardService.revision
// Fetches from the service's cache.
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
imagePath: {
_rev;
return ClipboardService.getImageData(modelData.clipboardId) || "";
}
// Loading indicator
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
}
}
// Error fallback
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true;
imagePreview.visible = false;
}
}
}
// Icon fallback
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS
visible: !modelData.isImage || imagePreview.status === Image.Error
active: visible
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== ""
asynchronous: true
}
}
}
// Fallback text if no icon and no image
NText {
anchors.centerIn: parent
visible: !imagePreview.visible && !iconLoader.visible
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
pointSize: Style.fontSizeXXL
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
// Image type indicator overlay
// Icon badge or Image preview or Emoji
Rectangle {
visible: modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: formatLabel.width + 6
height: formatLabel.height + 2
Layout.preferredWidth: badgeSize
Layout.preferredHeight: badgeSize
radius: Style.radiusM
color: Color.mSurfaceVariant
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return "";
const desc = modelData.description || "";
const parts = desc.split(" • ");
return parts[0] || "IMG";
// Image preview for clipboard images
NImageRounded {
id: imagePreview
anchors.fill: parent
visible: modelData.isImage && !modelData.emojiChar
imageRadius: Style.radiusM
// This property creates a dependency on the service's revision counter
readonly property int _rev: ClipboardService.revision
// Fetches from the service's cache.
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
imagePath: {
_rev;
return ClipboardService.getImageData(modelData.clipboardId) || "";
}
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;
}
}
}
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS
visible: !modelData.isImage && !modelData.emojiChar || (modelData.isImage && imagePreview.status === Image.Error)
active: visible
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== "" && !modelData.emojiChar
asynchronous: true
}
}
}
// Emoji display - takes precedence when emojiChar is present
NText {
id: emojiDisplay
anchors.centerIn: parent
visible: modelData.emojiChar ? true : (!imagePreview.visible && !iconLoader.visible)
text: modelData.emojiChar ? modelData.emojiChar : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?")
pointSize: modelData.emojiChar ? Style.fontSizeXXXL : Style.fontSizeXXL // Larger font for emojis
font.weight: Style.fontWeightBold
color: modelData.emojiChar ? Color.mOnSurface : Color.mOnPrimary // Different color for emojis
}
// Image type indicator overlay
Rectangle {
visible: modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
width: formatLabel.width + 6
height: formatLabel.height + 2
radius: Style.radiusM
color: Color.mSurfaceVariant
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return "";
const desc = modelData.description || "";
const parts = desc.split(" • ");
return parts[0] || "IMG";
}
pointSize: Style.fontSizeXXS
color: Color.mPrimary
}
pointSize: Style.fontSizeXXS
color: Color.mPrimary
}
}
}
// 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
// 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
Layout.fillWidth: true
}
NText {
text: modelData.description || ""
pointSize: Style.fontSizeS
color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
}
}
NText {
text: modelData.description || ""
pointSize: Style.fontSizeS
color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
// Pin/Unpin action icon button
NIconButton {
visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0)
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
icon: entry.isPinned(entry.appId) ? "unpin" : "pin"
tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin")
onClicked: entry.togglePin(entry.appId)
}
}
// Pin/Unpin action icon button
NIconButton {
visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0)
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
icon: entry.isPinned(entry.appId) ? "unpin" : "pin"
tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin")
onClicked: entry.togglePin(entry.appId)
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (!root.ignoreMouseHover) {
selectedIndex = index;
MouseArea {
id: mouseArea
anchors.fill: parent
z: -1
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (!root.ignoreMouseHover) {
selectedIndex = index;
}
}
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
selectedIndex = index;
root.activate();
mouse.accepted = true;
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
selectedIndex = index;
root.activate();
mouse.accepted = true;
}
}
}
acceptedButtons: Qt.LeftButton
acceptedButtons: Qt.LeftButton
}
}
}
}
NDivider {
Layout.fillWidth: true
}
// Status
NText {
Layout.fillWidth: true
text: {
if (results.length === 0)
return searchText ? "No results" : "";
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : "";
return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`;
NDivider {
Layout.fillWidth: true
}
NText {
Layout.fillWidth: true
text: {
if (results.length === 0)
return searchText ? "No results" : "";
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : "";
return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`;
}
pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignCenter
}
pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignCenter
}
}
}

View File

@@ -204,12 +204,9 @@ Item {
return results;
}
// Helper: Format image clipboard entry
function formatImageEntry(item) {
const meta = parseImageMeta(item.preview);
// The launcher's delegate will now be responsible for fetching the image data.
// This function's role is to provide the necessary metadata for that request.
return {
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
"description": meta ? `${meta.fmt} ${meta.size}` : item.mime || "Image data",
@@ -218,22 +215,20 @@ Item {
"imageWidth": meta ? meta.w : 0,
"imageHeight": meta ? meta.h : 0,
"clipboardId": item.id,
"mime": item.mime
"mime": item.mime,
"preview": item.preview
};
}
// Helper: Format text clipboard entry with preview
function formatTextEntry(item) {
const preview = (item.preview || "").trim();
const lines = preview.split('\n').filter(l => l.trim());
// Use first line as title, limit length
let title = lines[0] || "Empty text";
if (title.length > 60) {
title = title.substring(0, 57) + "...";
}
// Use second line or character count as description
let description = "";
if (lines.length > 1) {
description = lines[1];
@@ -250,11 +245,12 @@ Item {
"name": title,
"description": description,
"icon": "text-x-generic",
"isImage": false
"isImage": false,
"clipboardId": item.id,
"preview": preview
};
}
// Helper: Parse image metadata from 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);
@@ -271,8 +267,6 @@ Item {
};
}
// Public method to get image data for a clipboard item
// This can be called by the launcher when rendering
function getImageForItem(clipboardId) {
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null;
}

View File

@@ -0,0 +1,96 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.Keyboard
Item {
id: root
// Plugin metadata
property string name: I18n.tr("plugins.emoji")
property var launcher: null
property bool handleSearch: false
// Force update results when emoji service loads
Connections {
target: EmojiService
function onLoadedChanged() {
if (EmojiService.loaded && root.launcher) {
// Update launcher results to refresh the UI
root.launcher?.updateResults();
}
}
}
// Initialize plugin
function init() {
Logger.i("EmojiPlugin", "Initialized");
}
// Check if this plugin handles the command
function handleCommand(searchText) {
return searchText.startsWith(">emoji");
}
// Return available commands when user types ">"
function commands() {
return [
{
"name": ">emoji",
"description": I18n.tr("plugins.emoji-search-description"),
"icon": "face-smile",
"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("plugins.emoji-loading"),
"description": I18n.tr("plugins.emoji-loading-description"),
"icon": "view-refresh",
"isImage": false,
"onActivate": function () {}
}
];
}
const query = searchText.slice(6).trim();
const emojis = EmojiService.search(query);
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,
"emojiChar": emojiChar,
"onActivate": function () {
EmojiService.copy(emojiChar);
launcher.close();
}
};
}
}

View File

@@ -238,13 +238,16 @@ SmartPanel {
NText {
text: model.appName || "Unknown App"
pointSize: Style.fontSizeXS
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: Time.formatRelativeTime(model.timestamp)
pointSize: Style.fontSizeXS
color: Color.mSecondary
textFormat: Text.PlainText
text: " " + Time.formatRelativeTime(model.timestamp)
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
anchors.bottom: parent.bottom
}
}

View File

@@ -29,6 +29,8 @@ Popup {
if (widgetData && widgetId) {
loadWidgetSettings();
}
// Request focus to ensure keyboard input works
forceActiveFocus();
}
background: Rectangle {
@@ -40,71 +42,108 @@ Popup {
border.width: Style.borderM
}
contentItem: ColumnLayout {
id: content
contentItem: FocusScope {
id: focusScope
focus: true
width: parent.width
spacing: Style.marginM
// Title
RowLayout {
Layout.fillWidth: true
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("tooltips.close")
onClicked: root.close()
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Settings based on widget type
// Will be triggered via settingsLoader.setSource()
Loader {
id: settingsLoader
Layout.fillWidth: true
}
// Action buttons
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
ColumnLayout {
id: content
anchors.fill: parent
spacing: Style.marginM
Item {
// Title
RowLayout {
Layout.fillWidth: true
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("tooltips.close")
onClicked: root.close()
}
}
NButton {
text: I18n.tr("bar.widget-settings.dialog.cancel")
outlined: true
onClicked: root.close()
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
NButton {
text: I18n.tr("bar.widget-settings.dialog.apply")
icon: "check"
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings();
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
root.close();
// 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;
}
}
// Action buttons
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
spacing: Style.marginM
Item {
Layout.fillWidth: true
}
NButton {
text: I18n.tr("bar.widget-settings.dialog.cancel")
outlined: true
onClicked: root.close()
}
NButton {
text: I18n.tr("bar.widget-settings.dialog.apply")
icon: "check"
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings();
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
root.close();
}
}
}
}

View File

@@ -16,7 +16,8 @@ ColumnLayout {
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 bool valueHideTextInVerticalBar: widgetData.hideTextInVerticalBar !== undefined ? widgetData.hideTextInVerticalBar : widgetMetadata.hideTextInVerticalBar
property int valueMaxTextLengthHorizontal: widgetData?.maxTextLength?.horizontal ?? widgetMetadata?.maxTextLength?.horizontal
property int valueMaxTextLengthVertical: widgetData?.maxTextLength?.vertical ?? widgetMetadata?.maxTextLength?.vertical
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
@@ -27,173 +28,306 @@ ColumnLayout {
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.hideTextInVerticalBar = valueHideTextInVerticalBar;
settings.maxTextLength = {
"horizontal": valueMaxTextLengthHorizontal,
"vertical": valueMaxTextLengthVertical
};
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10);
return settings;
}
RowLayout {
spacing: Style.marginM
NScrollView {
Layout.preferredWidth: Math.round(600 * Style.uiScaleRatio)
Layout.preferredHeight: Math.round(700 * Style.uiScaleRatio)
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
padding: Style.marginL
focus: true
NLabel {
label: I18n.tr("bar.widget-settings.custom-button.icon.label")
description: I18n.tr("bar.widget-settings.custom-button.icon.description")
ColumnLayout {
width: parent.width
spacing: Style.marginM
RowLayout {
spacing: Style.marginM
NLabel {
label: I18n.tr("bar.widget-settings.custom-button.icon.label")
description: I18n.tr("bar.widget-settings.custom-button.icon.description")
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: valueIcon
pointSize: Style.fontSizeXL
visible: valueIcon !== ""
}
NButton {
text: I18n.tr("bar.widget-settings.custom-button.browse")
onClicked: iconPicker.open()
}
}
NIconPicker {
id: iconPicker
initialIcon: valueIcon
onIconSelected: function (iconName) {
valueIcon = iconName;
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.left-click.label")
description: I18n.tr("bar.widget-settings.custom-button.left-click.description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
}
NToggle {
id: leftClickUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(leftClickUpdateText, I18n.tr("bar.widget-settings.custom-button.left-click.update-text"), "auto")
onExited: TooltipService.hide()
checked: widgetData?.leftClickUpdateText ?? widgetMetadata.leftClickUpdateText
onToggled: isChecked => checked = isChecked
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.right-click.label")
description: I18n.tr("bar.widget-settings.custom-button.right-click.description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
}
NToggle {
id: rightClickUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(rightClickUpdateText, I18n.tr("bar.widget-settings.custom-button.right-click.update-text"), "auto")
onExited: TooltipService.hide()
checked: widgetData?.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText
onToggled: isChecked => checked = isChecked
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.middle-click.label")
description: I18n.tr("bar.widget-settings.custom-button.middle-click.description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
}
NToggle {
id: middleClickUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(middleClickUpdateText, I18n.tr("bar.widget-settings.custom-button.middle-click.update-text"), "auto")
onExited: TooltipService.hide()
checked: widgetData?.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText
onToggled: isChecked => checked = isChecked
}
}
// Wheel command settings
NToggle {
id: separateWheelToggle
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.wheel-mode-separate.label", "Separate wheel commands")
description: I18n.tr("bar.widget-settings.custom-button.wheel-mode-separate.description", "Enable separate commands for wheel up and down")
property bool internalChecked: (widgetData?.wheelMode || widgetMetadata?.wheelMode || "unified") === "separate"
checked: internalChecked
onToggled: checked => {
internalChecked = checked;
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.preferredWidth: parent.width
RowLayout {
id: unifiedWheelLayout
visible: !separateWheelToggle.checked
spacing: Style.marginM
NTextInput {
id: wheelExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.wheel.label")
description: I18n.tr("bar.widget-settings.custom-button.wheel.description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.wheelExec || widgetMetadata?.wheelExec || ""
}
NToggle {
id: wheelUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(wheelUpdateText, I18n.tr("bar.widget-settings.custom-button.wheel.update-text"), "auto")
onExited: TooltipService.hide()
checked: widgetData?.wheelUpdateText ?? widgetMetadata?.wheelUpdateText
onToggled: isChecked => checked = isChecked
}
}
ColumnLayout {
id: separatedWheelLayout
Layout.fillWidth: true
visible: separateWheelToggle.checked
RowLayout {
spacing: Style.marginM
NTextInput {
id: wheelUpExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.wheel-up.label")
description: I18n.tr("bar.widget-settings.custom-button.wheel-up.description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.wheelUpExec || widgetMetadata?.wheelUpExec || ""
}
NToggle {
id: wheelUpUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(wheelUpUpdateText, I18n.tr("bar.widget-settings.custom-button.wheel.update-text"), "auto")
onExited: TooltipService.hide()
checked: (widgetData?.wheelUpUpdateText !== undefined) ? widgetData.wheelUpUpdateText : (widgetMetadata?.wheelUpUpdateText ?? false)
onToggled: isChecked => checked = isChecked
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: wheelDownExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.wheel-down.label")
description: I18n.tr("bar.widget-settings.custom-button.wheel-down.description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.wheelDownExec || widgetMetadata?.wheelDownExec || ""
}
NToggle {
id: wheelDownUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(wheelDownUpdateText, I18n.tr("bar.widget-settings.custom-button.wheel.update-text"), "auto")
onExited: TooltipService.hide()
checked: (widgetData?.wheelDownUpdateText !== undefined) ? widgetData.wheelDownUpdateText : (widgetMetadata?.wheelDownUpdateText ?? false)
onToggled: isChecked => checked = isChecked
}
}
}
}
NDivider {
Layout.fillWidth: true
}
NHeader {
label: I18n.tr("bar.widget-settings.custom-button.dynamic-text")
}
NSpinBox {
label: I18n.tr("bar.widget-settings.custom-button.max-text-length-horizontal.label", "Max text length (horizontal)")
description: I18n.tr("bar.widget-settings.custom-button.max-text-length-horizontal.description", "Maximum number of characters to show in horizontal bar (0 to hide text)")
from: 0
to: 100
value: valueMaxTextLengthHorizontal
onValueChanged: valueMaxTextLengthHorizontal = value
}
NSpinBox {
label: I18n.tr("bar.widget-settings.custom-button.max-text-length-vertical.label", "Max text length (vertical)")
description: I18n.tr("bar.widget-settings.custom-button.max-text-length-vertical.description", "Maximum number of characters to show in vertical bar (0 to hide text)")
from: 0
to: 100
value: valueMaxTextLengthVertical
onValueChanged: valueMaxTextLengthVertical = value
}
NToggle {
id: textStreamInput
label: I18n.tr("bar.widget-settings.custom-button.text-stream.label")
description: I18n.tr("bar.widget-settings.custom-button.text-stream.description")
checked: valueTextStream
onToggled: checked => valueTextStream = checked
}
NToggle {
id: parseJsonInput
label: I18n.tr("bar.widget-settings.custom-button.parse-json.label", "Parse output as JSON")
description: I18n.tr("bar.widget-settings.custom-button.parse-json.description", "Parse the command output as a JSON object to dynamically set text and icon.")
checked: valueParseJson
onToggled: checked => valueParseJson = checked
}
NTextInput {
id: textCommandInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.display-command-output.label")
description: valueTextStream ? I18n.tr("bar.widget-settings.custom-button.display-command-output.stream-description") : I18n.tr("bar.widget-settings.custom-button.display-command-output.description")
placeholderText: I18n.tr("placeholders.command-example")
text: widgetData?.textCommand || widgetMetadata.textCommand
}
NTextInput {
id: textCollapseInput
Layout.fillWidth: true
visible: valueTextStream
label: I18n.tr("bar.widget-settings.custom-button.collapse-condition.label")
description: I18n.tr("bar.widget-settings.custom-button.collapse-condition.description")
placeholderText: I18n.tr("placeholders.enter-text-to-collapse")
text: widgetData?.textCollapse || widgetMetadata.textCollapse
}
NTextInput {
id: textIntervalInput
Layout.fillWidth: true
visible: !valueTextStream
label: I18n.tr("bar.widget-settings.custom-button.refresh-interval.label")
description: I18n.tr("bar.widget-settings.custom-button.refresh-interval.description")
placeholderText: String(widgetMetadata.textIntervalMs || 3000)
text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : ""
}
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: valueIcon
pointSize: Style.fontSizeXL
visible: valueIcon !== ""
}
NButton {
text: I18n.tr("bar.widget-settings.custom-button.browse")
onClicked: iconPicker.open()
}
}
NIconPicker {
id: iconPicker
initialIcon: valueIcon
onIconSelected: function (iconName) {
valueIcon = iconName;
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.left-click.label")
description: I18n.tr("bar.widget-settings.custom-button.left-click.description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
}
NToggle {
id: leftClickUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(leftClickUpdateText, I18n.tr("bar.widget-settings.custom-button.left-click.update-text"), "auto")
onExited: TooltipService.hide()
checked: widgetData?.leftClickUpdateText ?? widgetMetadata.leftClickUpdateText
onToggled: isChecked => checked = isChecked
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.right-click.label")
description: I18n.tr("bar.widget-settings.custom-button.right-click.description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
}
NToggle {
id: rightClickUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(rightClickUpdateText, I18n.tr("bar.widget-settings.custom-button.right-click.update-text"), "auto")
onExited: TooltipService.hide()
checked: widgetData?.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText
onToggled: isChecked => checked = isChecked
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.middle-click.label")
description: I18n.tr("bar.widget-settings.custom-button.middle-click.description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
}
NToggle {
id: middleClickUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(middleClickUpdateText, I18n.tr("bar.widget-settings.custom-button.middle-click.update-text"), "auto")
onExited: TooltipService.hide()
checked: widgetData?.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText
onToggled: isChecked => checked = isChecked
}
}
NDivider {
Layout.fillWidth: true
}
NHeader {
label: I18n.tr("bar.widget-settings.custom-button.dynamic-text")
}
NToggle {
label: I18n.tr("bar.widget-settings.custom-button.hide-vertical.label", "Hide text in vertical bar")
description: I18n.tr("bar.widget-settings.custom-button.hide-vertical.description", "If enabled, the text from the command output will not be shown when the bar is in a vertical layout (left or right).")
checked: valueHideTextInVerticalBar
onToggled: checked => valueHideTextInVerticalBar = checked
}
NToggle {
id: textStreamInput
label: I18n.tr("bar.widget-settings.custom-button.text-stream.label")
description: I18n.tr("bar.widget-settings.custom-button.text-stream.description")
checked: valueTextStream
onToggled: checked => valueTextStream = checked
}
NToggle {
id: parseJsonInput
label: I18n.tr("bar.widget-settings.custom-button.parse-json.label", "Parse output as JSON")
description: I18n.tr("bar.widget-settings.custom-button.parse-json.description", "Parse the command output as a JSON object to dynamically set text and icon.")
checked: valueParseJson
onToggled: checked => valueParseJson = checked
}
NTextInput {
id: textCommandInput
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.custom-button.display-command-output.label")
description: valueTextStream ? I18n.tr("bar.widget-settings.custom-button.display-command-output.stream-description") : I18n.tr("bar.widget-settings.custom-button.display-command-output.description")
placeholderText: I18n.tr("placeholders.command-example")
text: widgetData?.textCommand || widgetMetadata.textCommand
}
NTextInput {
id: textCollapseInput
Layout.fillWidth: true
visible: valueTextStream
label: I18n.tr("bar.widget-settings.custom-button.collapse-condition.label")
description: I18n.tr("bar.widget-settings.custom-button.collapse-condition.description")
placeholderText: I18n.tr("placeholders.enter-text-to-collapse")
text: widgetData?.textCollapse || widgetMetadata.textCollapse
}
NTextInput {
id: textIntervalInput
Layout.fillWidth: true
visible: !valueTextStream
label: I18n.tr("bar.widget-settings.custom-button.refresh-interval.label")
description: I18n.tr("bar.widget-settings.custom-button.refresh-interval.description")
placeholderText: String(widgetMetadata.textIntervalMs || 3000)
text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : ""
}
}

View File

@@ -23,6 +23,7 @@ ColumnLayout {
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 valueShowProgressRing: widgetData.showProgressRing !== undefined ? widgetData.showProgressRing : widgetMetadata.showProgressRing
Component.onCompleted: {
if (widgetData && widgetData.hideMode !== undefined) {
@@ -41,6 +42,7 @@ ColumnLayout {
settings.scrollingMode = valueScrollingMode;
settings.maxWidth = parseInt(widthInput.text) || widgetMetadata.maxWidth;
settings.useFixedWidth = valueUseFixedWidth;
settings.showProgressRing = valueShowProgressRing;
return settings;
}
@@ -130,6 +132,13 @@ ColumnLayout {
onToggled: checked => valueUseFixedWidth = checked
}
NToggle {
label: I18n.tr("bar.widget-settings.media-mini.show-progress-ring.label")
description: I18n.tr("bar.widget-settings.media-mini.show-progress-ring.description")
checked: valueShowProgressRing
onToggled: checked => valueShowProgressRing = checked
}
NComboBox {
label: I18n.tr("bar.widget-settings.media-mini.scrolling-mode.label")
description: I18n.tr("bar.widget-settings.media-mini.scrolling-mode.description")

View File

@@ -12,22 +12,28 @@ ColumnLayout {
property var widgetData: null
property var widgetMetadata: null
property string valueLabelMode: widgetData.labelMode !== undefined ? widgetData.labelMode : (widgetMetadata ? widgetMetadata.labelMode : "index")
property bool valueHideUnoccupied: widgetData.hideUnoccupied !== undefined ? widgetData.hideUnoccupied : (widgetMetadata ? widgetMetadata.hideUnoccupied : false)
property bool valueShowWorkspaceNumbers: widgetData.showWorkspaceNumbers !== undefined ? widgetData.showWorkspaceNumbers : (widgetMetadata ? widgetMetadata.showWorkspaceNumbers : true)
property bool valueShowNumbersOnlyWhenOccupied: widgetData.showNumbersOnlyWhenOccupied !== undefined ? widgetData.showNumbersOnlyWhenOccupied : (widgetMetadata ? widgetMetadata.showNumbersOnlyWhenOccupied : true)
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : (widgetMetadata ? widgetMetadata.colorizeIcons : false)
property bool valueHideUnoccupied: widgetData.hideUnoccupied !== undefined ? widgetData.hideUnoccupied : widgetMetadata.hideUnoccupied
property string valueLabelMode: widgetData.labelMode !== undefined ? widgetData.labelMode : widgetMetadata.labelMode
property bool valueShowLabelsOnlyWhenOccupied: widgetData.showLabelsOnlyWhenOccupied !== undefined ? widgetData.showLabelsOnlyWhenOccupied : widgetMetadata.showLabelsOnlyWhenOccupied
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.labelMode = valueLabelMode;
settings.hideUnoccupied = valueHideUnoccupied;
settings.showWorkspaceNumbers = valueShowWorkspaceNumbers;
settings.showNumbersOnlyWhenOccupied = valueShowNumbersOnlyWhenOccupied;
settings.labelMode = valueLabelMode;
settings.showLabelsOnlyWhenOccupied = valueShowLabelsOnlyWhenOccupied;
settings.colorizeIcons = valueColorizeIcons;
return settings;
}
NToggle {
label: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.label")
description: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.description")
checked: valueHideUnoccupied
onToggled: checked => valueHideUnoccupied = checked
}
NComboBox {
id: labelModeCombo
label: I18n.tr("bar.widget-settings.workspace.label-mode.label")
@@ -50,16 +56,17 @@ ColumnLayout {
"name": I18n.tr("options.workspace-labels.index+name")
}
]
currentKey: widgetData.labelMode || widgetMetadata.labelMode
currentKey: widgetData.labelMode
onSelected: key => valueLabelMode = key
minimumWidth: 200
}
NToggle {
label: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.label")
description: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.description")
checked: valueHideUnoccupied
onToggled: checked => valueHideUnoccupied = checked
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.taskbar-grouped.show-labels-only-when-occupied.label")
description: I18n.tr("bar.widget-settings.taskbar-grouped.show-labels-only-when-occupied.description")
checked: root.valueShowLabelsOnlyWhenOccupied
onToggled: checked => root.valueShowLabelsOnlyWhenOccupied = checked
}
NToggle {
@@ -69,13 +76,4 @@ ColumnLayout {
checked: root.valueColorizeIcons
onToggled: checked => root.valueColorizeIcons = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.widget-settings.taskbar-grouped.show-labels-only-when-occupied.label")
description: I18n.tr("bar.widget-settings.taskbar-grouped.show-labels-only-when-occupied.description")
checked: root.valueShowNumbersOnlyWhenOccupied
onToggled: checked => root.valueShowNumbersOnlyWhenOccupied = checked
visible: root.valueShowWorkspaceNumbers
}
}

View File

@@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
property var widgetData: null
property var widgetMetadata: null
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.displayMode = valueDisplayMode;
return settings;
}
NComboBox {
label: I18n.tr("bar.widget-settings.battery.display-mode.label")
description: I18n.tr("bar.widget-settings.battery.display-mode.description")
minimumWidth: 134
model: [
{
"key": "onhover",
"name": I18n.tr("options.display-mode.on-hover")
},
{
"key": "alwaysShow",
"name": I18n.tr("options.display-mode.always-show")
},
{
"key": "alwaysHide",
"name": I18n.tr("options.display-mode.always-hide")
}
]
currentKey: root.valueDisplayMode
onSelected: key => root.valueDisplayMode = key
}
}

View File

@@ -14,6 +14,7 @@ ColumnLayout {
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
function saveSettings() {
@@ -21,6 +22,7 @@ ColumnLayout {
settings.labelMode = valueLabelMode;
settings.hideUnoccupied = valueHideUnoccupied;
settings.characterCount = valueCharacterCount;
settings.followFocusedScreen = valueFollowFocusedScreen;
return settings;
}
@@ -51,6 +53,13 @@ ColumnLayout {
minimumWidth: 200
}
NToggle {
label: I18n.tr("bar.widget-settings.workspace.follow-focused-screen.label")
description: I18n.tr("bar.widget-settings.workspace.follow-focused-screen.description")
checked: valueFollowFocusedScreen
onToggled: checked => valueFollowFocusedScreen = checked
}
NToggle {
label: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.label")
description: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.description")

View File

@@ -146,6 +146,7 @@ ColumnLayout {
}) : I18n.tr("settings.about.contributors.section.description_plural", {
"count": root.contributors.length
})
enableDescriptionRichText: true
}
GridView {

View File

@@ -835,6 +835,42 @@ ColumnLayout {
}
}
}
NCheckbox {
label: "Telegram"
description: ProgramCheckerService.telegramAvailable ? I18n.tr("settings.color-scheme.templates.programs.telegram.description", {
"filepath": "~/.config/telegram-desktop/themes/noctalia.tdesktop-theme"
}) : I18n.tr("settings.color-scheme.templates.programs.telegram.description-missing", {
"app": "telegram"
})
checked: Settings.data.templates.telegram
enabled: ProgramCheckerService.telegramAvailable
opacity: ProgramCheckerService.telegramAvailable ? 1.0 : 0.6
onToggled: checked => {
if (ProgramCheckerService.telegramAvailable) {
Settings.data.templates.telegram = checked;
AppThemeService.generate();
}
}
}
NCheckbox {
label: "Cava"
description: ProgramCheckerService.cavaAvailable ? I18n.tr("settings.color-scheme.templates.programs.cava.description", {
"filepath": "~/.config/cava/themes/noctalia"
}) : I18n.tr("settings.color-scheme.templates.programs.cava.description-missing", {
"app": "cava"
})
checked: Settings.data.templates.cava
enabled: ProgramCheckerService.cavaAvailable
opacity: ProgramCheckerService.cavaAvailable ? 1.0 : 0.6
onToggled: checked => {
if (ProgramCheckerService.cavaAvailable) {
Settings.data.templates.cava = checked;
AppThemeService.generate();
}
}
}
}
// Miscellaneous
NCollapsible {

View File

@@ -26,8 +26,7 @@ Popup {
property var schemeColorsCache: ({})
property int cacheVersion: 0
// Cache for available schemes list
property string schemesCacheFile: Settings.cacheDir + "color-schemes-list.json"
// 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)
@@ -99,34 +98,6 @@ Popup {
xhr.send();
}
// Cache file for schemes list
FileView {
id: schemesCacheFileView
path: schemesCacheFile
printErrors: false
JsonAdapter {
id: schemesCacheAdapter
property var schemes: []
property real timestamp: 0
}
onLoaded: {
loadSchemesFromCache();
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
// Cache doesn't exist, fetch from API (only if popup is open)
if (root.visible) {
Qt.callLater(() => {
fetchAvailableSchemesFromAPI();
});
}
}
}
}
background: Rectangle {
color: Color.mSurface
radius: Style.radiusL
@@ -135,58 +106,67 @@ Popup {
}
function loadSchemesFromCache() {
const now = Time.timestamp;
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 (!schemesCacheAdapter.timestamp || (now >= schemesCacheAdapter.timestamp + schemesCacheUpdateFrequency)) {
// 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 (schemesCacheAdapter.schemes && schemesCacheAdapter.schemes.length > 0) {
availableSchemes = schemesCacheAdapter.schemes;
hasInitialData = true;
fetching = false;
// 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 - schemesCacheAdapter.timestamp) / 60);
Logger.d("ColorSchemeDownload", "Loading cached schemes (age:", ageMinutes, "minutes)");
const ageMinutes = Math.round((now - cachedTimestamp) / 60);
Logger.d("ColorSchemeDownload", "Loading cached schemes from ShellState (age:", ageMinutes, "minutes)");
if (schemesCacheAdapter.schemes && schemesCacheAdapter.schemes.length > 0) {
availableSchemes = schemesCacheAdapter.schemes;
hasInitialData = true;
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");
if (cachedSchemes.length > 0) {
availableSchemes = cachedSchemes;
hasInitialData = true;
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() {
schemesCacheAdapter.schemes = availableSchemes;
schemesCacheAdapter.timestamp = Time.timestamp;
// Ensure cache directory exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]);
Qt.callLater(() => {
schemesCacheFileView.writeAdapter();
Logger.d("ColorSchemeDownload", "Schemes list saved to cache");
});
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() {
@@ -194,19 +174,11 @@ Popup {
return;
}
// Path is set when popup becomes visible, so FileView will start loading
// Try to load from cache first
if (schemesCacheFileView.loaded) {
// Try to load from ShellState cache first
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
loadSchemesFromCache();
} else if (schemesCacheFileView.path) {
// Cache file path is set but not loaded yet, wait for it to load
// The FileView will trigger loadSchemesFromCache() when loaded
// But if it fails, we should fetch from API
if (!schemesCacheFileView.loading) {
schemesCacheFileView.reload();
}
} else {
// No cache file path, fetch directly from API
// ShellState not ready, fetch directly from API
fetchAvailableSchemesFromAPI();
}
}
@@ -521,7 +493,7 @@ Popup {
return;
}
var targetDir = ColorSchemeService.schemesDirectory + "/" + schemeName;
var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName;
var downloadScript = "mkdir -p '" + targetDir + "'\n";
// Build download script for all files
@@ -552,6 +524,7 @@ Popup {
var downloadProcess = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
Process {
id: downloadProcess
command: ["sh", "-c", ` + JSON.stringify(downloadScript) + `]
@@ -611,6 +584,17 @@ Popup {
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;
@@ -623,7 +607,8 @@ Popup {
var deletedSchemeDisplayName = ColorSchemeService.getBasename(schemeName);
var needsReset = (currentScheme === deletedSchemeDisplayName);
var targetDir = ColorSchemeService.schemesDirectory + "/" + schemeName;
// Only allow deleting downloaded schemes, not preinstalled ones
var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName;
var deleteScript = "rm -rf '" + targetDir + "'";
var deleteProcess = Qt.createQmlObject(`
@@ -712,7 +697,25 @@ Popup {
}
onAvailableSchemesChanged: preFetchSchemeColors()
onVisibleChanged: 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
@@ -896,12 +899,14 @@ Popup {
NIconButton {
property bool isDownloading: downloading && downloadingScheme === schemeRow.schemeName
property bool isInstalled: root.isSchemeInstalled(schemeRow.schemeName)
property bool isDownloaded: root.isSchemeDownloaded(schemeRow.schemeName)
icon: isDownloading ? "" : (isInstalled ? "trash" : "download")
tooltipText: isDownloading ? I18n.tr("settings.color-scheme.download.downloading") : (isInstalled ? I18n.tr("settings.color-scheme.download.delete") : I18n.tr("settings.color-scheme.download.download"))
icon: isDownloading ? "" : (isDownloaded ? "trash" : "download")
tooltipText: isDownloading ? I18n.tr("settings.color-scheme.download.downloading") : (isDownloaded ? I18n.tr("settings.color-scheme.download.delete") : I18n.tr("settings.color-scheme.download.download"))
enabled: !downloading
Layout.alignment: Qt.AlignVCenter
onClicked: isInstalled ? root.deleteScheme(schemeRow.schemeName) : root.downloadScheme(modelData)
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
@@ -938,4 +943,4 @@ Popup {
}
}
}
}
}

View File

@@ -81,6 +81,25 @@ ColumnLayout {
}
}
ColumnLayout {
visible: Settings.data.dock.enabled
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("settings.dock.appearance.border-radius.label")
description: I18n.tr("settings.dock.appearance.border-radius.description")
}
NValueSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.dock.radiusRatio
onMoved: value => Settings.data.dock.radiusRatio = value
text: Math.floor(Settings.data.dock.radiusRatio * 100) + "%"
}
}
ColumnLayout {
visible: Settings.data.dock.enabled
spacing: Style.marginXXS

View File

@@ -65,6 +65,13 @@ ColumnLayout {
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
}
NToggle {
label: I18n.tr("settings.launcher.settings.clip-preview.label")
description: I18n.tr("settings.launcher.settings.clip-preview.description")
checked: Settings.data.appLauncher.enableClipPreview
onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked
}
NToggle {
label: I18n.tr("settings.launcher.settings.sort-by-usage.label")
description: I18n.tr("settings.launcher.settings.sort-by-usage.description")

View File

@@ -37,7 +37,6 @@ ColumnLayout {
LocationService.resetWeather();
}
}
Layout.maximumWidth: 420
}
NText {
@@ -145,17 +144,17 @@ ColumnLayout {
},
{
"key": "6",
"name": I18n.locale.dayName(6, Locale.LongFormat)
"name": I18n.locale.dayName(6, Locale.LongFormat).trim()
} // Saturday
,
{
"key": "0",
"name": I18n.locale.dayName(0, Locale.LongFormat)
"name": I18n.locale.dayName(0, Locale.LongFormat).trim()
} // Sunday
,
{
"key": "1",
"name": I18n.locale.dayName(1, Locale.LongFormat)
"name": I18n.locale.dayName(1, Locale.LongFormat).trim()
} // Monday
]
onSelected: key => Settings.data.location.firstDayOfWeek = parseInt(key)

View File

@@ -22,6 +22,13 @@ ColumnLayout {
onToggled: checked => Settings.data.general.compactLockScreen = checked
}
NToggle {
label: I18n.tr("settings.lock-screen.show-hibernate.label")
description: I18n.tr("settings.lock-screen.show-hibernate.description")
checked: Settings.data.general.showHibernateOnLockScreen
onToggled: checked => Settings.data.general.showHibernateOnLockScreen = checked
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL

View File

@@ -74,6 +74,10 @@ ColumnLayout {
{
"key": "bottom_right",
"name": I18n.tr("options.launcher.position.bottom_right")
},
{
"key": "bar",
"name": I18n.tr("options.launcher.position.bar")
}
]
currentKey: Settings.data.notifications.location || "top_right"

View File

@@ -63,6 +63,21 @@ ColumnLayout {
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.onCompleted: {
entriesModel = [];
@@ -213,9 +228,6 @@ ColumnLayout {
clip: true
model: entriesModel
// Store reference to root's saveEntries function
property var saveEntriesFunc: root.saveEntries
delegate: Item {
id: delegateItem
width: listView.width
@@ -228,7 +240,6 @@ ColumnLayout {
property int dragStartY: 0
property int dragStartIndex: -1
property int dragTargetIndex: -1
property var saveEntriesFunc: listView.saveEntriesFunc
Rectangle {
anchors.fill: parent
@@ -310,11 +321,7 @@ ColumnLayout {
onReleased: {
preventStealing = false;
if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) {
var newModel = entriesModel.slice();
var item = newModel.splice(delegateItem.dragStartIndex, 1)[0];
newModel.splice(delegateItem.dragTargetIndex, 0, item);
entriesModel = newModel;
root.saveEntries();
root.reorderEntries(delegateItem.dragStartIndex, delegateItem.dragTargetIndex);
}
delegateItem.dragging = false;
delegateItem.dragStartIndex = -1;
@@ -361,12 +368,9 @@ ColumnLayout {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
var newModel = entriesModel.slice();
newModel[index] = Object.assign({}, newModel[index], {
"enabled": !modelData.enabled
});
entriesModel = newModel;
delegateItem.saveEntriesFunc();
root.updateEntry(index, {
"enabled": !modelData.enabled
});
}
}
}
@@ -395,12 +399,9 @@ ColumnLayout {
NToggle {
checked: modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true
onToggled: function (checked) {
var newModel = entriesModel.slice();
newModel[delegateItem.index] = Object.assign({}, newModel[delegateItem.index], {
"countdownEnabled": checked
});
entriesModel = newModel;
delegateItem.saveEntriesFunc();
root.updateEntry(delegateItem.index, {
"countdownEnabled": checked
});
}
}
}

View File

@@ -24,11 +24,24 @@ SmartPanel {
property int currentStep: 0
property int totalSteps: 5
property bool isCompleting: false
onOpened: function () {
selectedScaleRatio = Settings.data.general.scaleRatio;
selectedBarPosition = Settings.data.bar.position;
selectedWallpaperDirectory = Settings.data.wallpaper.directory || Settings.defaultWallpapersDirectory;
isCompleting = false;
}
Connections {
target: Settings
function onSettingsSaved() {
if (isCompleting) {
Logger.i("SetupWizard", "Settings saved, closing panel");
isCompleting = false;
root.close();
}
}
}
// Setup wizard data
@@ -366,34 +379,64 @@ SmartPanel {
}
function completeSetup() {
Logger.i("SetupWizard", "Completing setup with selected options");
if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) {
Settings.data.wallpaper.directory = selectedWallpaperDirectory;
WallpaperService.refreshWallpapersList();
if (isCompleting) {
Logger.w("SetupWizard", "completeSetup() called while already completing, ignoring");
return;
}
if (selectedWallpaper !== "") {
WallpaperService.changeWallpaper(selectedWallpaper, undefined);
try {
Logger.i("SetupWizard", "Completing setup with selected options");
isCompleting = true;
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;
Settings.data.setupCompleted = true;
// Save settings immediately and wait for settingsSaved signal before closing
Settings.saveImmediate();
Logger.i("SetupWizard", "Setup completed successfully, waiting for settings save confirmation");
// 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;
}
}
Settings.data.general.scaleRatio = selectedScaleRatio;
Settings.data.bar.position = selectedBarPosition;
Settings.data.setupCompleted = true;
Settings.saveImmediate();
Logger.i("SetupWizard", "Setup completed successfully");
root.close();
Timer {
id: closeTimer
interval: 2000
onTriggered: {
if (isCompleting) {
Logger.w("SetupWizard", "Settings save timeout, closing panel anyway");
isCompleting = false;
root.close();
}
}
}
function applyWallpaperSettings() {
if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) {
Settings.data.wallpaper.directory = selectedWallpaperDirectory;
WallpaperService.refreshWallpapersList();
}
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);
if (selectedWallpaper !== "") {
WallpaperService.changeWallpaper(selectedWallpaper, undefined);
}
}
}

View File

@@ -184,9 +184,15 @@ SmartPanel {
if (!modelData.onlyMenu) {
modelData.activate();
}
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
PanelService.openedPanel.close();
}
} else if (mouse.button === Qt.MiddleButton) {
// Middle click: activate with middle button
modelData.secondaryActivate && modelData.secondaryActivate();
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
PanelService.openedPanel.close();
}
} else if (mouse.button === Qt.RightButton) {
// Right click: open context menu
TooltipService.hideImmediately();

View File

@@ -0,0 +1,370 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Networking
import qs.Widgets
NBox {
id: root
property string label: ""
property var model: []
property string passwordSsid: ""
property string expandedSsid: ""
signal passwordRequested(string ssid)
signal passwordSubmitted(string ssid, string password)
signal passwordCancelled
signal forgetRequested(string ssid)
signal forgetConfirmed(string ssid)
signal forgetCancelled
Layout.fillWidth: true
Layout.preferredHeight: column.implicitHeight + Style.marginM * 2
visible: root.model.length > 0
ColumnLayout {
id: column
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NText {
text: root.label
pointSize: Style.fontSizeS
color: Color.mSecondary
font.weight: Style.fontWeightBold
visible: root.model.length > 0
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
}
Repeater {
model: root.model
Rectangle {
id: networkItem
Layout.fillWidth: true
Layout.leftMargin: Style.marginXS
Layout.rightMargin: Style.marginXS
implicitHeight: netColumn.implicitHeight + (Style.marginM * 2)
radius: Style.radiusM
border.width: Style.borderS
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
opacity: (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0
color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
}
}
ColumnLayout {
id: netColumn
width: parent.width - (Style.marginM * 2)
x: Style.marginM
y: Style.marginM
spacing: Style.marginS
// Main row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: NetworkService.signalIcon(modelData.signal, modelData.connected)
pointSize: Style.fontSizeXXL
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
NText {
text: modelData.ssid
pointSize: Style.fontSizeM
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
color: Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginXS
NText {
text: I18n.tr("system.signal-strength", {
"signal": modelData.signal
})
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
Item {
Layout.preferredWidth: Style.marginXXS
}
// Status badges
Rectangle {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
color: Color.mPrimary
radius: height * 0.5
width: connectedText.implicitWidth + (Style.marginS * 2)
height: connectedText.implicitHeight + (Style.marginXXS * 2)
NText {
id: connectedText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.connected")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.disconnectingFrom === modelData.ssid
color: Color.mError
radius: height * 0.5
width: disconnectingText.implicitWidth + (Style.marginS * 2)
height: disconnectingText.implicitHeight + (Style.marginXXS * 2)
NText {
id: disconnectingText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.disconnecting")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.forgettingNetwork === modelData.ssid
color: Color.mError
radius: height * 0.5
width: forgettingText.implicitWidth + (Style.marginS * 2)
height: forgettingText.implicitHeight + (Style.marginXXS * 2)
NText {
id: forgettingText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.forgetting")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: modelData.cached && !modelData.connected && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
color: Color.transparent
border.color: Color.mOutline
border.width: Style.borderS
radius: height * 0.5
width: savedText.implicitWidth + (Style.marginS * 2)
height: savedText.implicitHeight + (Style.marginXXS * 2)
NText {
id: savedText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.saved")
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
}
}
}
// Action area
RowLayout {
spacing: Style.marginS
NBusyIndicator {
visible: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid
running: visible
color: Color.mPrimary
size: Style.baseWidgetSize * 0.5
}
NIconButton {
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
icon: "trash"
tooltipText: I18n.tr("tooltips.forget-network")
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.forgetRequested(modelData.ssid)
}
NButton {
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && root.passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
text: {
if (modelData.existing || modelData.cached)
return I18n.tr("wifi.panel.connect");
if (!NetworkService.isSecured(modelData.security))
return I18n.tr("wifi.panel.connect");
return I18n.tr("wifi.panel.password");
}
outlined: !hovered
fontSize: Style.fontSizeXS
enabled: !NetworkService.connecting
onClicked: {
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
NetworkService.connect(modelData.ssid);
} else {
root.passwordRequested(modelData.ssid);
}
}
}
NButton {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
text: I18n.tr("wifi.panel.disconnect")
outlined: !hovered
fontSize: Style.fontSizeXS
backgroundColor: Color.mError
onClicked: NetworkService.disconnect(modelData.ssid)
}
}
}
// Password input
Rectangle {
visible: root.passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: passwordRow.implicitHeight + Style.marginS * 2
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
radius: Style.radiusS
RowLayout {
id: passwordRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginM
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.radiusXS
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
focus: visible
passwordCharacter: "●"
onVisibleChanged: if (visible) {
text = "";
forceActiveFocus();
}
onAccepted: {
if (text && !NetworkService.connecting) {
root.passwordSubmitted(modelData.ssid, text);
}
}
NText {
visible: parent.text.length === 0
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("wifi.panel.enter-password")
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeS
}
}
}
NButton {
text: I18n.tr("wifi.panel.connect")
fontSize: Style.fontSizeXXS
enabled: pwdInput.text.length > 0 && !NetworkService.connecting
outlined: true
onClicked: root.passwordSubmitted(modelData.ssid, pwdInput.text)
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.passwordCancelled()
}
}
}
// Forget network
Rectangle {
visible: root.expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: forgetRow.implicitHeight + Style.marginS * 2
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 ? false : true
onClicked: root.forgetConfirmed(modelData.ssid)
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.forgetCancelled()
}
}
}
}
}
}
}
}

View File

@@ -14,15 +14,71 @@ SmartPanel {
preferredHeight: Math.round(500 * Style.uiScaleRatio)
property string passwordSsid: ""
property string passwordInput: ""
property string expandedSsid: ""
property bool hasHadNetworks: false
onOpened: NetworkService.scan()
// Computed network lists
readonly property var knownNetworks: {
if (!Settings.data.network.wifiEnabled)
return [];
const nets = Object.values(NetworkService.networks);
const known = nets.filter(n => n.connected || n.existing || n.cached);
// Sort: connected first, then by signal strength
known.sort((a, b) => {
if (a.connected !== b.connected)
return b.connected - a.connected;
return b.signal - a.signal;
});
return known;
}
readonly property var availableNetworks: {
if (!Settings.data.network.wifiEnabled)
return [];
const nets = Object.values(NetworkService.networks);
const available = nets.filter(n => !n.connected && !n.existing && !n.cached);
// Sort by signal strength
available.sort((a, b) => b.signal - a.signal);
return available;
}
onOpened: {
hasHadNetworks = false;
NetworkService.scan();
}
onKnownNetworksChanged: {
if (knownNetworks.length > 0)
hasHadNetworks = true;
}
onAvailableNetworksChanged: {
if (availableNetworks.length > 0)
hasHadNetworks = true;
}
Connections {
target: Settings.data.network
function onWifiEnabledChanged() {
if (!Settings.data.network.wifiEnabled)
root.hasHadNetworks = false;
}
}
panelContent: Rectangle {
color: Color.transparent
property real contentPreferredHeight: Math.min(preferredHeight, Math.max(280 * Style.uiScaleRatio, mainColumn.implicitHeight + Style.marginL * 2))
// Calculate content height based on header + networks list (or minimum for empty states)
property real headerHeight: headerRow.implicitHeight + Style.marginM * 2
property real networksHeight: networksList.implicitHeight
property real calculatedHeight: headerHeight + networksHeight + Style.marginL * 2 + Style.marginM
property real contentPreferredHeight: Settings.data.network.wifiEnabled && Object.keys(NetworkService.networks).length > 0 ? Math.min(root.preferredHeight, Math.max(280 * Style.uiScaleRatio, calculatedHeight)) : Math.min(root.preferredHeight, 280 * Style.uiScaleRatio)
ColumnLayout {
id: mainColumn
@@ -116,14 +172,13 @@ SmartPanel {
}
}
// Main content area
// WiFi disabled state
NBox {
visible: !Settings.data.network.wifiEnabled
Layout.fillWidth: true
Layout.fillHeight: true
// WiFi disabled state
ColumnLayout {
visible: !Settings.data.network.wifiEnabled
anchors.fill: parent
anchors.margins: Style.marginM
@@ -158,10 +213,15 @@ SmartPanel {
Layout.fillHeight: true
}
}
}
// Scanning state (show when no networks and we haven't had any yet)
NBox {
visible: Settings.data.network.wifiEnabled && Object.keys(NetworkService.networks).length === 0 && !root.hasHadNetworks
Layout.fillWidth: true
Layout.fillHeight: true
// Scanning state
ColumnLayout {
visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginL
@@ -188,374 +248,15 @@ SmartPanel {
Layout.fillHeight: true
}
}
}
// Networks list container
NScrollView {
visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0)
anchors.fill: parent
anchors.margins: Style.marginM
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
// Empty state when no networks (only show after we've had networks before, meaning a real empty result)
NBox {
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 && root.hasHadNetworks
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
width: parent.width
spacing: Style.marginM
// Network list
Repeater {
model: {
if (!Settings.data.network.wifiEnabled)
return [];
const nets = Object.values(NetworkService.networks);
return nets.sort((a, b) => {
if (a.connected !== b.connected)
return b.connected - a.connected;
return b.signal - a.signal;
});
}
Rectangle {
Layout.fillWidth: true
implicitHeight: netColumn.implicitHeight + (Style.marginM * 2)
radius: Style.radiusM
// Add opacity for operations in progress
opacity: (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0
color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface
border.width: Style.borderS
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
// Smooth opacity animation
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
}
}
ColumnLayout {
id: netColumn
width: parent.width - (Style.marginM * 2)
x: Style.marginM
y: Style.marginM
spacing: Style.marginS
// Main row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: NetworkService.signalIcon(modelData.signal, modelData.connected)
pointSize: Style.fontSizeXXL
color: modelData.connected ? Color.mPrimary : Color.mOnSurface
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
NText {
text: modelData.ssid
pointSize: Style.fontSizeM
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
color: Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginXS
NText {
text: I18n.tr("system.signal-strength", {
"signal": modelData.signal
})
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NText {
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
Item {
Layout.preferredWidth: Style.marginXXS
}
// Update the status badges area (around line 237)
Rectangle {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
color: Color.mPrimary
radius: height * 0.5
width: connectedText.implicitWidth + (Style.marginS * 2)
height: connectedText.implicitHeight + (Style.marginXXS * 2)
NText {
id: connectedText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.connected")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.disconnectingFrom === modelData.ssid
color: Color.mError
radius: height * 0.5
width: disconnectingText.implicitWidth + (Style.marginS * 2)
height: disconnectingText.implicitHeight + (Style.marginXXS * 2)
NText {
id: disconnectingText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.disconnecting")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.forgettingNetwork === modelData.ssid
color: Color.mError
radius: height * 0.5
width: forgettingText.implicitWidth + (Style.marginS * 2)
height: forgettingText.implicitHeight + (Style.marginXXS * 2)
NText {
id: forgettingText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.forgetting")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: modelData.cached && !modelData.connected && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
color: Color.transparent
border.color: Color.mOutline
border.width: Style.borderS
radius: height * 0.5
width: savedText.implicitWidth + (Style.marginS * 2)
height: savedText.implicitHeight + (Style.marginXXS * 2)
NText {
id: savedText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.saved")
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
}
}
}
// Action area
RowLayout {
spacing: Style.marginS
NBusyIndicator {
visible: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid
running: visible
color: Color.mPrimary
size: Style.baseWidgetSize * 0.5
}
NIconButton {
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
icon: "trash"
tooltipText: I18n.tr("tooltips.forget-network")
baseSize: Style.baseWidgetSize * 0.8
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
}
NButton {
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
text: {
if (modelData.existing || modelData.cached)
return I18n.tr("wifi.panel.connect");
if (!NetworkService.isSecured(modelData.security))
return I18n.tr("wifi.panel.connect");
return I18n.tr("wifi.panel.password");
}
outlined: !hovered
fontSize: Style.fontSizeXS
enabled: !NetworkService.connecting
onClicked: {
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
NetworkService.connect(modelData.ssid);
} else {
passwordSsid = modelData.ssid;
passwordInput = "";
expandedSsid = "";
}
}
}
NButton {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
text: I18n.tr("wifi.panel.disconnect")
outlined: !hovered
fontSize: Style.fontSizeXS
backgroundColor: Color.mError
onClicked: NetworkService.disconnect(modelData.ssid)
}
}
}
// Password input
Rectangle {
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: passwordRow.implicitHeight + Style.marginS * 2
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
radius: Style.radiusS
RowLayout {
id: passwordRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginM
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.radiusXS
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
text: passwordInput
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS
color: Color.mOnSurface
echoMode: TextInput.Password
selectByMouse: true
focus: visible
passwordCharacter: "●"
onTextChanged: passwordInput = text
onVisibleChanged: if (visible)
forceActiveFocus()
onAccepted: {
if (text && !NetworkService.connecting) {
NetworkService.connect(passwordSsid, text);
passwordSsid = "";
passwordInput = "";
}
}
NText {
visible: parent.text.length === 0
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("wifi.panel.enter-password")
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeS
}
}
}
NButton {
text: I18n.tr("wifi.panel.connect")
fontSize: Style.fontSizeXXS
enabled: passwordInput.length > 0 && !NetworkService.connecting
outlined: true
onClicked: {
NetworkService.connect(passwordSsid, passwordInput);
passwordSsid = "";
passwordInput = "";
}
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
passwordSsid = "";
passwordInput = "";
}
}
}
}
// Forget network
Rectangle {
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: forgetRow.implicitHeight + Style.marginS * 2
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 ? false : true
onClicked: {
NetworkService.forget(modelData.ssid);
expandedSsid = "";
}
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: expandedSsid = ""
}
}
}
}
}
}
}
}
// Empty state when no networks
ColumnLayout {
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent
spacing: Style.marginL
@@ -589,6 +290,66 @@ SmartPanel {
}
}
}
// Networks list container (no NBox wrapper)
NScrollView {
visible: Settings.data.network.wifiEnabled && Object.keys(NetworkService.networks).length > 0
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
ColumnLayout {
id: networksList
width: parent.width
spacing: Style.marginM
WiFiNetworksList {
label: I18n.tr("wifi.panel.known-networks")
model: root.knownNetworks
passwordSsid: root.passwordSsid
expandedSsid: root.expandedSsid
onPasswordRequested: ssid => {
root.passwordSsid = ssid;
root.expandedSsid = "";
}
onPasswordSubmitted: (ssid, password) => {
NetworkService.connect(ssid, password);
root.passwordSsid = "";
}
onPasswordCancelled: root.passwordSsid = ""
onForgetRequested: ssid => root.expandedSsid = root.expandedSsid === ssid ? "" : ssid
onForgetConfirmed: ssid => {
NetworkService.forget(ssid);
root.expandedSsid = "";
}
onForgetCancelled: root.expandedSsid = ""
}
WiFiNetworksList {
label: I18n.tr("wifi.panel.available-networks")
model: root.availableNetworks
passwordSsid: root.passwordSsid
expandedSsid: root.expandedSsid
onPasswordRequested: ssid => {
root.passwordSsid = ssid;
root.expandedSsid = "";
}
onPasswordSubmitted: (ssid, password) => {
NetworkService.connect(ssid, password);
root.passwordSsid = "";
}
onPasswordCancelled: root.passwordSsid = ""
onForgetRequested: ssid => root.expandedSsid = root.expandedSsid === ssid ? "" : ssid
onForgetConfirmed: ssid => {
NetworkService.forget(ssid);
root.expandedSsid = "";
}
onForgetCancelled: root.expandedSsid = ""
}
}
}
}
}
}

View File

@@ -100,9 +100,9 @@ Item {
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.marginM
anchors.left: parent.left
anchors.leftMargin: Style.marginM * 2
anchors.leftMargin: Style.marginM * 3
anchors.right: parent.right
anchors.rightMargin: Style.marginM * 2
anchors.rightMargin: Style.marginM * 3
spacing: Style.marginL
// Icon

View File

@@ -134,7 +134,11 @@ Item {
screen: root.screen
// Parse location setting
readonly property string location: Settings.data.notifications?.location || "top_right"
readonly property string location: {
if (Settings.data.notifications?.location == "bar")
return "top_right"
return 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")

View File

@@ -122,8 +122,11 @@ PopupWindow {
hideImmediately();
}
// Convert \n to <br> for RichText format
const processedText = tipText.replace(/\n/g, '<br>');
// Set properties
text = tipText;
text = processedText;
targetItem = target;
// Find the correct screen dimensions based on target's global position
@@ -326,7 +329,9 @@ PopupWindow {
// Update text function
function updateText(newText) {
if (visible && targetItem) {
text = newText;
// Convert \n to <br> for RichText format
const processedText = newText.replace(/\n/g, '<br>');
text = processedText;
// Recalculate dimensions
const tipWidth = Math.min(tooltipText.implicitWidth + (padding * 2), maxWidth);

View File

@@ -32,21 +32,26 @@ Singleton {
// Backend service loader
property var backend: null
// Cache file path
property string displayCachePath: ""
Component.onCompleted: {
// Setup cache path (needs Settings to be available)
// Load display scales from ShellState
Qt.callLater(() => {
if (typeof Settings !== 'undefined' && Settings.cacheDir) {
displayCachePath = Settings.cacheDir + "display.json";
displayCacheFileView.path = displayCachePath;
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");
@@ -99,28 +104,21 @@ Singleton {
}
}
// Cache FileView for display scales
FileView {
id: displayCacheFileView
printErrors: false
watchChanges: false
adapter: JsonAdapter {
id: displayCacheAdapter
property var displays: ({})
}
onLoaded: {
// Load cached display scales
displayScales = displayCacheAdapter.displays || {};
// 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;
// Logger.i("CompositorService", "Loaded display scales from cache:", JSON.stringify(displayScales))
}
onLoadFailed: {
// Cache doesn't exist yet, will be created on first update
displayScalesLoaded = true;
// Logger.i("CompositorService", "No display cache found, will create on first update")
}
}
@@ -234,12 +232,12 @@ Singleton {
// Save display scales to cache
function saveDisplayScalesToCache() {
if (!displayCachePath) {
return;
try {
ShellState.setDisplay(displayScales);
Logger.d("CompositorService", "Saved display scales to ShellState");
} catch (error) {
Logger.e("CompositorService", "Failed to save display scales:", error);
}
displayCacheAdapter.displays = displayScales;
displayCacheFileView.writeAdapter();
}
// Public function to get scale for a specific display

View File

@@ -120,24 +120,8 @@ Item {
// New preferred method - lock the screen
function lock() {
// Only lock if not already locked (prevents the red screen issue)
// Note: No unlock via IPC for security reasons
if (!lockScreen.active) {
lockScreen.triggeredViaDeprecatedCall = false;
lockScreen.active = true;
}
}
// Deprecated: Use 'lockScreen lock' instead
function toggle() {
// Mark as triggered via deprecated call - warning will show in lock screen
lockScreen.triggeredViaDeprecatedCall = true;
// Log deprecation warning for users checking logs
Logger.w("IPC", "The 'lockScreen toggle' IPC call is deprecated. Use 'lockScreen lock' instead.");
// Still functional for backward compatibility
if (!lockScreen.active) {
lockScreen.active = true;
if (!PanelService.lockScreen.active) {
PanelService.lockScreen.active = true;
}
}
}

View File

@@ -283,7 +283,7 @@ Singleton {
function increaseBrightness(): void {
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness;
// Enforce minimum brightness if enabled
if (Settings.data.brightness.enforceMinimum && value <= minBrightnessValue) {
if (Settings.data.brightness.enforceMinimum && value < minBrightnessValue) {
setBrightnessDebounced(Math.max(stepSize, minBrightnessValue));
} else {
// Normal brightness increase

View File

@@ -0,0 +1,259 @@
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
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 popular/recently used emojis, fallback to all emojis sorted by usage
return _getPopularEmojis(50);
}
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;
}
// Get popular emojis sorted by usage count
function _getPopularEmojis(limit) {
// Create array of emojis with their usage counts
const emojisWithUsage = emojis.map(emoji => {
return {
emoji: emoji,
usageCount: usageCounts[emoji.emoji] || 0
};
});
// Sort by usage count (descending), then by name
emojisWithUsage.sort((a, b) => {
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(item => item.emoji);
}
// Record emoji usage
function recordUsage(emojiChar) {
if (emojiChar) {
const currentCount = usageCounts[emojiChar] || 0;
usageCounts[emojiChar] = currentCount + 1;
_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`]);
}
}
}

View File

@@ -207,7 +207,7 @@ Singleton {
root.coordinatesReady = true;
isFetchingWeather = false;
Logger.i("Location", "Cached weather to disk - stable coordinates updated");
Logger.d("Location", "Cached weather to disk - stable coordinates updated");
} catch (e) {
errorCallback("Location", "Failed to parse weather data");
}

View File

@@ -94,11 +94,11 @@ Singleton {
if (vol > maxVolume) {
root.isClampingOutput = true;
Qt.callLater(() => {
if (root.sink?.audio) {
root.sink.audio.volume = maxVolume;
}
root.isClampingOutput = false;
});
if (root.sink?.audio) {
root.sink.audio.volume = maxVolume;
}
root.isClampingOutput = false;
});
return;
}
}
@@ -131,11 +131,11 @@ Singleton {
if (vol > maxVolume) {
root.isClampingInput = true;
Qt.callLater(() => {
if (root.source?.audio) {
root.source.audio.volume = maxVolume;
}
root.isClampingInput = false;
});
if (root.source?.audio) {
root.source.audio.volume = maxVolume;
}
root.isClampingInput = false;
});
return;
}
}

View File

@@ -70,10 +70,10 @@ Singleton {
root.lastBluetoothBlocked = root.blocked;
if (bluetoothBlockedToggled) {
checkWifiBlocked.running = true;
} else if (adapter.enabled) {
} else if (adapter.state === BluetoothAdapterState.Enabled) {
ToastService.showNotice(I18n.tr("bluetooth.panel.title"), I18n.tr("toast.bluetooth.enabled"), "bluetooth");
discoveryTimer.running = true;
} else {
} else if (adapter.state === BluetoothAdapterState.Disabled) {
ToastService.showNotice(I18n.tr("bluetooth.panel.title"), I18n.tr("toast.bluetooth.disabled"), "bluetooth-off");
}
}

View File

@@ -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 = "";
}
}
}
}

View File

@@ -5,22 +5,18 @@ import Quickshell
import Quickshell.Io
import qs.Commons
// GitHub API logic and caching
// 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
property bool isReleasesFetching: false
readonly property alias data: adapter // Used to access via GitHubService.data.xxx.yyy
// Public properties for easy access
property string latestVersion: I18n.tr("system.unknown-version")
property var contributors: []
property string releaseNotes: ""
property var releases: []
property string releaseFetchError: ""
FileView {
id: githubDataFileView
@@ -48,8 +44,6 @@ Singleton {
property string version: I18n.tr("system.unknown-version")
property var contributors: []
property string releaseNotes: ""
property var releases: []
property real timestamp: 0
}
}
@@ -71,15 +65,6 @@ Singleton {
if (data.contributors) {
root.contributors = data.contributors;
}
if (data.releaseNotes) {
root.releaseNotes = data.releaseNotes;
}
if (data.releases && data.releases.length > 0) {
root.releases = data.releases;
} else {
Logger.d("GitHub", "Cached releases missing, scheduling fetch");
needsRefetch = true;
}
if (needsRefetch) {
fetchFromGitHub();
@@ -96,14 +81,13 @@ Singleton {
isFetchingData = true;
versionProcess.running = true;
contributorsProcess.running = true;
fetchAllReleases();
}
// --------------------------------
function saveData() {
data.timestamp = Time.timestamp;
Logger.d("GitHub", "Saving data to cache file:", githubDataFile);
Logger.d("GitHub", "Data to save - version:", data.version, "contributors:", data.contributors.length, "notes length:", data.releaseNotes ? data.releaseNotes.length : 0, "release count:", data.releases ? data.releases.length : 0);
Logger.d("GitHub", "Data to save - version:", data.version, "contributors:", data.contributors.length);
// Ensure cache directory exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]);
@@ -115,25 +99,25 @@ Singleton {
});
}
// --------------------------------
function checkAndSaveData() {
// Only save when all processes are finished
if (!versionProcess.running && !contributorsProcess.running) {
root.isFetchingData = false;
root.saveData();
}
}
// --------------------------------
function resetCache() {
data.version = I18n.tr("system.unknown-version");
data.contributors = [];
data.releaseNotes = "";
data.releases = [];
data.timestamp = 0;
// Try to fetch immediately
fetchFromGitHub();
}
function clearReleaseCache() {
Logger.d("GitHub", "Clearing cached release data");
data.releases = [];
root.releases = [];
githubDataFileView.writeAdapter();
}
Process {
id: versionProcess
@@ -152,15 +136,9 @@ Singleton {
Logger.d("GitHub", "Latest version fetched from GitHub:", version);
} else if (data.message) {
Logger.w("GitHub", "Latest release fetch warning:", data.message);
handleRateLimitError(data.message);
} else {
Logger.w("GitHub", "No tag_name in GitHub response");
}
if (data.body) {
root.data.releaseNotes = data.body;
root.releaseNotes = root.data.releaseNotes;
}
} else {
Logger.w("GitHub", "Empty response from GitHub API");
}
@@ -206,94 +184,4 @@ Singleton {
}
}
}
// --------------------------------
function fetchAllReleases(page, accumulator) {
if (isReleasesFetching && page === undefined) {
return;
}
const perPage = 100;
var currentPage = page || 1;
var releasesAccumulator = accumulator || [];
isReleasesFetching = true;
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState === XMLHttpRequest.DONE) {
if (request.status >= 200 && request.status < 300) {
try {
const responseText = request.responseText || "";
const parsed = responseText ? JSON.parse(responseText) : [];
if (Array.isArray(parsed) && parsed.length > 0) {
const mapped = parsed.map(rel => ({
"version": rel.tag_name || "",
"createdAt": rel.published_at || rel.created_at || "",
"body": rel.body || ""
})).filter(rel => rel.version !== "");
releasesAccumulator = releasesAccumulator.concat(mapped);
if (parsed.length === perPage) {
fetchAllReleases(currentPage + 1, releasesAccumulator);
return;
}
}
finalizeReleaseFetch(releasesAccumulator);
} catch (error) {
Logger.e("GitHub", "Failed to parse releases:", error);
finalizeReleaseFetch([]);
}
} else {
if (request.status === 403) {
handleRateLimitError();
}
Logger.e("GitHub", "Failed to fetch releases, status:", request.status);
finalizeReleaseFetch([]);
}
}
};
const url = `https://api.github.com/repos/noctalia-dev/noctalia-shell/releases?per_page=${perPage}&page=${currentPage}`;
request.open("GET", url);
request.send();
}
function finalizeReleaseFetch(releasesList) {
isReleasesFetching = false;
if (releasesList && releasesList.length > 0) {
releasesList.sort(function (a, b) {
const dateA = a.createdAt ? Date.parse(a.createdAt) : 0;
const dateB = b.createdAt ? Date.parse(b.createdAt) : 0;
return dateB - dateA;
});
root.data.releases = releasesList;
root.releases = releasesList;
releaseFetchError = "";
Logger.d("GitHub", "Fetched releases:", releasesList.length);
} else {
root.data.releases = [];
root.releases = [];
if (!releaseFetchError) {
Logger.w("GitHub", "No releases fetched");
}
}
checkAndSaveData();
}
// --------------------------------
function checkAndSaveData() {
// Only save when all processes are finished
if (!versionProcess.running && !contributorsProcess.running && !isReleasesFetching) {
root.isFetchingData = false;
root.saveData();
}
}
function handleRateLimitError(message) {
const limitMessage = message && message.length > 0 ? message : "API rate limit exceeded";
Logger.w("GitHub", "Rate limit warning:", limitMessage);
releaseFetchError = I18n.tr("changelog.error.rate-limit");
}
}

View File

@@ -11,10 +11,15 @@ Singleton {
id: root
// Version properties
property string baseVersion: "3.2.0"
property bool isDevelopment: false
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`
property string changelogStateFile: Quickshell.env("NOCTALIA_CHANGELOG_STATE_FILE") || (Settings.cacheDir + "changelog-state.json")
readonly property string baseVersion: "3.2.0"
readonly property bool isDevelopment: true
readonly property string developmentSuffix: "-git"
readonly property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + developmentSuffix}`
// 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://noctalia.dev:7777/upgradelog"
// Changelog properties
property bool initialized: false
@@ -24,16 +29,13 @@ Singleton {
property string previousVersion: ""
property string changelogCurrentVersion: ""
property var releaseHighlights: []
property string releaseNotesUrl: ""
property string discordUrl: "https://discord.noctalia.dev"
property string lastShownVersion: ""
property bool popupScheduled: false
property string feedbackUrl: Quickshell.env("NOCTALIA_CHANGELOG_FEEDBACK_URL") || ""
property string fetchError: ""
property string changelogLastSeenVersion: ""
property bool changelogStateLoaded: false
property bool pendingShowRequest: false
// Fix for FileView race condition
property bool saveInProgress: false
property bool pendingSave: false
@@ -41,59 +43,27 @@ Singleton {
signal popupQueued(string fromVersion, string toVersion)
// Internal helpers
function getVersion() {
return root.currentVersion;
}
function checkForUpdates() {
// TODO: Implement update checking logic
Logger.i("UpdateService", "Checking for updates...");
}
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: GitHubService ? GitHubService : null
function onReleaseNotesChanged() {
rebuildHighlights();
}
function onReleasesChanged() {
rebuildHighlights();
}
function onReleaseFetchErrorChanged() {
fetchError = GitHubService ? GitHubService.releaseFetchError : "";
}
}
FileView {
id: changelogStateFileView
path: root.changelogStateFile
printErrors: false
onLoaded: loadChangelogState()
onLoadFailed: error => {
if (error === 2) {
// File doesn't exist, create it
debouncedSaveChangelogState();
} else {
Logger.e("UpdateService", "Failed to load changelog state file:", error);
target: typeof ShellState !== 'undefined' ? ShellState : null
function onIsLoadedChanged() {
if (ShellState.isLoaded) {
loadChangelogState();
}
changelogStateLoaded = true;
if (pendingShowRequest) {
pendingShowRequest = false;
Qt.callLater(root.showLatestChangelog);
}
}
JsonAdapter {
id: changelogStateAdapter
property string lastSeenVersion: ""
}
}
@@ -108,7 +78,7 @@ Singleton {
function handleChangelogRequest() {
const fromVersion = changelogFromVersion || "";
const toVersion = changelogToVersion || "";
if (!toVersion)
return;
@@ -120,71 +90,72 @@ Singleton {
previousVersion = fromVersion;
changelogCurrentVersion = toVersion;
fetchError = GitHubService ? GitHubService.releaseFetchError : "";
releaseHighlights = buildReleaseHighlights(previousVersion, changelogCurrentVersion);
releaseNotesUrl = buildReleaseNotesUrl(toVersion);
// Fetch the upgrade log from the server
fetchUpgradeLog(fromVersion, toVersion);
popupScheduled = true;
root.popupQueued(previousVersion, changelogCurrentVersion);
clearChangelogRequest();
openWhenReady();
}
function rebuildHighlights() {
if (!changelogCurrentVersion)
return;
fetchError = GitHubService ? GitHubService.releaseFetchError : "";
releaseHighlights = buildReleaseHighlights(previousVersion, changelogCurrentVersion);
}
function fetchUpgradeLog(fromVersion, toVersion) {
// Use the last seen version, or default to v3.0.0 if this is a fresh install
let from = fromVersion || changelogLastSeenVersion || "v3.0.0";
let to = toVersion;
function buildReleaseHighlights(fromVersion, toVersion) {
const releases = GitHubService && GitHubService.releases ? GitHubService.releases : [];
const selected = [];
const fromNorm = normalizeVersion(fromVersion);
const toNorm = normalizeVersion(toVersion);
// Remove potential legacy -dev stuff
// TODO: remove in 2026!
from = from.replace("-dev", "");
to = to.replace("-dev", "");
if (releases.length > 0) {
for (var i = 0; i < releases.length; i++) {
const rel = releases[i];
const tag = rel.version || "";
const tagNorm = normalizeVersion(tag);
if (!tagNorm)
continue;
// Strip suffix from versions
from = from.replace(root.developmentSuffix, "");
to = to.replace(root.developmentSuffix, "");
if (toNorm && compareVersions(tagNorm, toNorm) > 0) {
continue;
}
if (fromNorm && compareVersions(tagNorm, fromNorm) <= 0) {
break;
}
const entries = parseReleaseNotes(rel.body);
if (entries.length === 0)
continue;
selected.push({
"version": tag,
"date": rel.createdAt || "",
"entries": entries
});
}
// 'from' always need to be before 'to'
// handle edge case that will show up as we changed -dev to -git
if (from === to) {
from = "v3.0.0";
}
if (selected.length === 0 && toVersion) {
const fallback = parseReleaseNotes(GitHubService ? GitHubService.releaseNotes : "");
if (fallback.length > 0) {
selected.push({
"version": toVersion,
"date": "",
"entries": fallback
});
fetchError = "";
}
}
Logger.d("UpdateService", "Fetching upgrade log", "from:", from, "to:", to);
return selected;
const url = `${upgradeLogBaseUrl}/${from}/${to}`;
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) {
const content = request.responseText || "";
Logger.d("UpdateService", "Successfully fetched upgrade log, parsing...");
const entries = parseReleaseNotes(content);
Logger.d("UpdateService", "Parsed entries count:", entries.length);
releaseHighlights = [
{
"version": toVersion,
"date": "",
"entries": entries
}
];
fetchError = "";
openWhenReady();
} else {
Logger.e("UpdateService", "Failed to fetch upgrade log");
Logger.e("UpdateService", "Status:", request.status);
Logger.e("UpdateService", "Status text:", request.statusText);
Logger.e("UpdateService", "Response:", request.responseText);
fetchError = I18n.tr("changelog.error.fetch-failed");
releaseHighlights = [];
openWhenReady();
}
}
};
request.open("GET", url);
request.send();
}
function normalizeVersion(version) {
@@ -217,13 +188,6 @@ Singleton {
return 0;
}
function buildReleaseNotesUrl(version) {
if (!version)
return "";
const tag = version.startsWith("v") ? version : `v${version}`;
return `https://github.com/noctalia-dev/noctalia-shell/releases/tag/${tag}`;
}
function parseReleaseNotes(body) {
if (!body)
return [];
@@ -232,32 +196,16 @@ Singleton {
var entries = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line)
continue;
if (line.startsWith("- ") || line.startsWith("* ")) {
const text = cleanEntry(line.substring(2).trim());
if (text.length > 0 && !isVersionLine(text) && !isIgnoredEntry(text)) {
entries.push(text);
}
}
if (entries.length >= 6)
break;
const line = lines[i];
entries.push(line);
}
var uniqueEntries = [];
var seen = {};
for (var j = 0; j < entries.length; j++) {
const key = entries[j].toLowerCase();
if (seen[key])
continue;
seen[key] = true;
uniqueEntries.push(entries[j]);
// Remove trailing blank lines
while (entries.length > 0 && entries[entries.length - 1].trim().length === 0) {
entries.pop();
}
return uniqueEntries;
return entries;
}
function isVersionLine(text) {
@@ -322,12 +270,6 @@ Singleton {
lastShownVersion = changelogCurrentVersion;
}
function openReleaseNotes() {
if (!releaseNotesUrl)
return;
Quickshell.execDetached(["xdg-open", releaseNotesUrl]);
}
function openDiscord() {
if (!discordUrl)
return;
@@ -374,12 +316,11 @@ Singleton {
function loadChangelogState() {
try {
changelogLastSeenVersion = changelogStateAdapter.lastSeenVersion || "";
if (!changelogLastSeenVersion && Settings.data && Settings.data.changelog && Settings.data.changelog.lastSeenVersion) {
changelogLastSeenVersion = Settings.data.changelog.lastSeenVersion;
debouncedSaveChangelogState();
Logger.i("UpdateService", "Migrated changelog lastSeenVersion from settings to cache");
}
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);
}
@@ -411,26 +352,16 @@ Singleton {
saveInProgress = true;
try {
changelogStateAdapter.lastSeenVersion = changelogLastSeenVersion || "";
// Ensure cache directory exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]);
// Small delay to ensure directory creation completes
Qt.callLater(() => {
try {
changelogStateFileView.writeAdapter();
saveInProgress = false;
// Check if another save was queued while we were saving
if (pendingSave) {
Qt.callLater(executeSave);
}
} catch (writeError) {
Logger.e("UpdateService", "Failed to write changelog state:", writeError);
saveInProgress = false;
}
});
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;
@@ -441,4 +372,4 @@ Singleton {
// Immediate save (backward compatibility)
debouncedSaveChangelogState();
}
}
}

View File

@@ -17,7 +17,6 @@ Singleton {
property int maxVisible: 5
property int maxHistory: 100
property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json")
property string stateFile: Settings.cacheDir + "notifications-state.json"
// State
property real lastSeenTs: 0
@@ -138,6 +137,22 @@ Singleton {
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 {
@@ -471,23 +486,6 @@ Singleton {
}
}
// Persistence - State
FileView {
id: stateFileView
path: stateFile
printErrors: false
onLoaded: loadState()
onLoadFailed: error => {
if (error === 2)
writeAdapter();
}
JsonAdapter {
id: stateAdapter
property real lastSeenTs: 0
}
}
Timer {
id: saveTimer
interval: 200
@@ -546,13 +544,11 @@ Singleton {
function loadState() {
try {
root.lastSeenTs = stateAdapter.lastSeenTs || 0;
const notifState = ShellState.getNotificationsState();
root.lastSeenTs = notifState.lastSeenTs || 0;
if (root.lastSeenTs === 0 && Settings.data.notifications && Settings.data.notifications.lastSeenTs) {
root.lastSeenTs = Settings.data.notifications.lastSeenTs;
saveState();
Logger.i("Notifications", "Migrated lastSeenTs from settings to state file");
}
// Migration is now handled in Settings.qml
Logger.d("Notifications", "Loaded state from ShellState");
} catch (e) {
Logger.e("Notifications", "Load state failed:", e);
}
@@ -560,8 +556,10 @@ Singleton {
function saveState() {
try {
stateAdapter.lastSeenTs = root.lastSeenTs;
stateFileView.writeAdapter();
ShellState.setNotificationsState({
lastSeenTs: root.lastSeenTs
});
Logger.d("Notifications", "Saved state to ShellState");
} catch (e) {
Logger.e("Notifications", "Save state failed:", e);
}

View File

@@ -27,6 +27,8 @@ Singleton {
property bool codeAvailable: false
property bool gnomeCalendarAvailable: false
property bool spicetifyAvailable: false
property bool telegramAvailable: false
property bool cavaAvailable: false
// Discord client auto-detection
property var availableDiscordClients: []
@@ -181,7 +183,9 @@ Singleton {
"wlsunsetAvailable": ["which", "wlsunset"],
"codeAvailable": ["which", "code"],
"gnomeCalendarAvailable": ["which", "gnome-calendar"],
"spicetifyAvailable": ["which", "spicetify"]
"spicetifyAvailable": ["which", "spicetify"],
"telegramAvailable": ["which", "telegram-desktop"],
"cavaAvailable": ["which","cava"]
})
// Internal tracking

View File

@@ -13,6 +13,7 @@ Singleton {
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"
Connections {
@@ -43,8 +44,11 @@ Singleton {
Logger.d("ColorScheme", "Load colorScheme");
scanning = true;
schemes = [];
// Use find command to locate all scheme.json files
findProcess.command = ["find", schemesDirectory, "-name", "*.json", "-type", "f"];
// 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", schemesDirectory, downloadedSchemesDirectory, "-name", "*.json", "-type", "f"];
findProcess.running = true;
}
@@ -81,7 +85,17 @@ Singleton {
} else if (schemeName === "Tokyo Night") {
schemeName = "Tokyo-Night";
}
return schemesDirectory + "/" + schemeName + "/" + schemeName + ".json";
// 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) {
@@ -199,6 +213,7 @@ Singleton {
FileView {
id: colorsWriter
path: colorsJsonFilePath
printErrors: false
onSaved:
// Logger.i("ColorScheme", "Colors saved")

View File

@@ -5,6 +5,7 @@ import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.System
import qs.Services.Theming
import qs.Services.UI
Singleton {
@@ -329,7 +330,26 @@ Singleton {
extension = ".toml";
}
return `${Quickshell.shellDir}/Assets/ColorScheme/${colorScheme}/terminal/${terminal}/${colorScheme}-${mode}${extension}`;
const fileName = `${colorScheme}-${mode}${extension}`;
const relativePath = `terminal/${terminal}/${fileName}`;
// Try to find the scheme in the loaded schemes list to determine which directory it's in
for (let i = 0; i < ColorSchemeService.schemes.length; i++) {
const schemeJsonPath = ColorSchemeService.schemes[i];
// Check if this is the scheme we're looking for
if (schemeJsonPath.indexOf(`/${colorScheme}/`) !== -1 || schemeJsonPath.indexOf(`/${colorScheme}.json`) !== -1) {
// Extract the scheme directory from the JSON path
// JSON path is like: /path/to/scheme/SchemeName/SchemeName.json
// We need: /path/to/scheme/SchemeName/terminal/...
const schemeDir = schemeJsonPath.substring(0, schemeJsonPath.lastIndexOf('/'));
return `${schemeDir}/${relativePath}`;
}
}
// Fallback: try downloaded first, then preinstalled
const downloadedPath = `${ColorSchemeService.downloadedSchemesDirectory}/${colorScheme}/${relativePath}`;
const preinstalledPath = `${ColorSchemeService.schemesDirectory}/${colorScheme}/${relativePath}`;
return preinstalledPath;
}
// ================================================================================

View File

@@ -208,6 +208,29 @@ Singleton {
}
],
"postProcess": () => `spicetify -q apply --no-restart`
},
{
"id": "telegram",
"name": "Telegram",
"category": "applications",
"input": "telegram.tdesktop-theme",
"outputs": [
{
"path": "~/.config/telegram-desktop/themes/noctalia.tdesktop-theme"
}
]
},
{
"id": "cava",
"name": "Cava",
"category": "applications",
"input": "cava.ini",
"outputs": [
{
"path": "~/.config/cava/themes/noctalia"
}
],
"postProcess": () => `${colorsApplyScript} cava`
}
]

View File

@@ -330,8 +330,11 @@ Singleton {
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();
});

View File

@@ -36,6 +36,7 @@ Singleton {
"TaskbarGrouped": taskbarGroupedComponent,
"Tray": trayComponent,
"Volume": volumeComponent,
"VPN": vpnComponent,
"WiFi": wiFiComponent,
"WallpaperSelector": wallpaperSelectorComponent,
"Workspace": workspaceComponent
@@ -58,12 +59,13 @@ Singleton {
"SessionMenu": "WidgetSettings/SessionMenuSettings.qml",
"Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
"TaskbarGrouped": "WidgetSettings/TaskbarGroupedSettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml",
"WiFi": "WidgetSettings/WiFiSettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
"Taskbar": "WidgetSettings/TaskbarSettings.qml",
"Tray": "WidgetSettings/TraySettings.qml"
"TaskbarGrouped": "WidgetSettings/TaskbarGroupedSettings.qml",
"Tray": "WidgetSettings/TraySettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml",
"VPN": "WidgetSettings/VPNSettings.qml",
"WiFi": "WidgetSettings/WiFiSettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml"
})
property var widgetMetadata: ({
@@ -124,7 +126,17 @@ Singleton {
"textIntervalMs": 3000,
"textCollapse": "",
"parseJson": false,
"hideTextInVerticalBar": false
"wheelExec": "",
"wheelUpExec": "",
"wheelDownExec": "",
"wheelMode": "unified",
"wheelUpdateText": false,
"wheelUpUpdateText": false,
"wheelDownUpdateText": false,
"maxTextLength": {
"horizontal": 10,
"vertical": 10
}
},
"KeyboardLayout": {
"allowUserSettings": true,
@@ -149,6 +161,7 @@ Singleton {
"showAlbumArt": false,
"showArtistFirst": true,
"showVisualizer": false,
"showProgressRing": true,
"visualizerType": "linear"
},
"Microphone": {
@@ -188,11 +201,9 @@ Singleton {
},
"TaskbarGrouped": {
"allowUserSettings": true,
"showWorkspaceNumbers": true,
"showNumbersOnlyWhenOccupied": true,
"labelMode": "index",
"hideUnoccupied": false,
"characterCount": 2,
"labelMode": "index",
"showLabelsOnlyWhenOccupied": true,
"colorizeIcons": false
},
"Tray": {
@@ -202,6 +213,10 @@ Singleton {
"pinned": [],
"drawerEnabled": true
},
"VPN": {
"allowUserSettings": true,
"displayMode": "onhover"
},
"WiFi": {
"allowUserSettings": true,
"displayMode": "onhover"
@@ -209,6 +224,7 @@ Singleton {
"Workspace": {
"allowUserSettings": true,
"labelMode": "index",
"followFocusedScreen": false,
"hideUnoccupied": false,
"characterCount": 2
},
@@ -291,6 +307,9 @@ Singleton {
property Component volumeComponent: Component {
Volume {}
}
property Component vpnComponent: Component {
VPN {}
}
property Component wiFiComponent: Component {
WiFi {}
}

Some files were not shown because too many files have changed in this diff Show More