Compare commits

...

95 Commits

Author SHA1 Message Date
Ly-sec
74ba883dd8 initial commit 2025-11-22 13:51:58 +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
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
loner
880ae9c7b9 fix: Fix inconsistent tray drawer behavior for different mouse buttons 2025-11-22 18:03:33 +08:00
Aiser
0f650b36f7 Matugen: Add Telegram's Theme 2025-11-22 14:52:38 +08: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
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
84 changed files with 4023 additions and 1782 deletions

View File

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

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

@@ -1,7 +1,6 @@
[Unit] [Unit]
Description=Noctalia Shell Service Description=Noctalia Shell Service
Requisite=graphical-session.target BindsTo=graphical-session.target
PartOf=graphical-session.target
After=graphical-session.target After=graphical-session.target
[Service] [Service]

View File

@@ -123,10 +123,6 @@
"stream-description": "Geben Sie einen Befehl ein, der kontinuierlich ausgeführt werden soll." "stream-description": "Geben Sie einen Befehl ein, der kontinuierlich ausgeführt werden soll."
}, },
"dynamic-text": "Dynamischer Text", "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": { "icon": {
"description": "Symbol aus der Bibliothek auswählen.", "description": "Symbol aus der Bibliothek auswählen.",
"label": "Symbol" "label": "Symbol"
@@ -136,6 +132,14 @@
"label": "Linksklick", "label": "Linksklick",
"update-text": "Text auf Linksklick aktualisieren" "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": { "middle-click": {
"description": "Befehl, der ausgeführt wird, wenn die Schaltfläche mit der mittleren Maustaste angeklickt wird.", "description": "Befehl, der ausgeführt wird, wenn die Schaltfläche mit der mittleren Maustaste angeklickt wird.",
"label": "Mittelklick", "label": "Mittelklick",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "Gestreamte Zeilen aus dem Befehl werden als Text auf der Schaltfläche angezeigt.", "description": "Gestreamte Zeilen aus dem Befehl werden als Text auf der Schaltfläche angezeigt.",
"label": "Stream" "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": { "dialog": {
@@ -396,29 +417,29 @@
} }
}, },
"changelog": { "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": { "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": { "subtitle": {
"fresh": "Danke, dass du Noctalia installiert hast! Das ist in diesem Build enthalten.", "fresh": "Danke, dass du Noctalia installiert hast! Das ist in diesem Build enthalten.",
"updated": "Aktualisiert von {previousVersion}" "updated": "Aktualisiert von {previousVersion}"
}, },
"title": "Was ist neu in {version}",
"version": { "version": {
"new-user": "Neuinstallation" "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": { "clock": {
@@ -428,10 +449,12 @@
"activate-app": "{app} aktivieren", "activate-app": "{app} aktivieren",
"clear-history": "Verlauf löschen", "clear-history": "Verlauf löschen",
"close-app": "{app} schließen", "close-app": "{app} schließen",
"connect-vpn": "Mit {name} verbinden",
"cycle-visualizer": "Zyklus-Visualisierer", "cycle-visualizer": "Zyklus-Visualisierer",
"disable-bluetooth": "Bluetooth deaktivieren", "disable-bluetooth": "Bluetooth deaktivieren",
"disable-dnd": "Bitte nicht stören deaktivieren", "disable-dnd": "Bitte nicht stören deaktivieren",
"disable-wifi": "WLAN deaktivieren", "disable-wifi": "WLAN deaktivieren",
"disconnect-vpn": "Verbindung zu {name} trennen",
"enable-bluetooth": "Bluetooth aktivieren", "enable-bluetooth": "Bluetooth aktivieren",
"enable-dnd": "Nicht stören aktivieren", "enable-dnd": "Nicht stören aktivieren",
"enable-wifi": "WLAN aktivieren", "enable-wifi": "WLAN aktivieren",
@@ -1018,7 +1041,11 @@
"description-missing": "Erfordert die Installation von {app}" "description-missing": "Erfordert die Installation von {app}"
}, },
"spicetify": { "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}" "description-missing": "Benötigt die Installation von {app}"
}, },
"vicinae": { "vicinae": {
@@ -1217,6 +1244,10 @@
"description": "Hintergrund-Transparenz des Docks anpassen.", "description": "Hintergrund-Transparenz des Docks anpassen.",
"label": "Hintergrund-Transparenz" "label": "Hintergrund-Transparenz"
}, },
"border-radius": {
"description": "Den Radius der Dock-Umrandung anpassen.",
"label": "Eckenradius"
},
"colorize-icons": { "colorize-icons": {
"description": "Theme-Farben auf Dock-App-Symbole anwenden (nur nicht fokussierte Apps).", "description": "Theme-Farben auf Dock-App-Symbole anwenden (nur nicht fokussierte Apps).",
"label": "Symbole einfärben" "label": "Symbole einfärben"
@@ -1368,6 +1399,10 @@
"description": "Hintergrund-Transparenz des Starters anpassen.", "description": "Hintergrund-Transparenz des Starters anpassen.",
"label": "Hintergrund-Transparenz" "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": { "clipboard-history": {
"description": "Zugriff auf zuvor kopierte Elemente über den Launcher.", "description": "Zugriff auf zuvor kopierte Elemente über den Launcher.",
"label": "Zwischenablageverlauf aktivieren" "label": "Zwischenablageverlauf aktivieren"
@@ -1475,6 +1510,10 @@
"description": "Den Bildschirm beim Suspendieren des Systems automatisch sperren.", "description": "Den Bildschirm beim Suspendieren des Systems automatisch sperren.",
"label": "Sperren beim Ruhezustand" "label": "Sperren beim Ruhezustand"
}, },
"show-hibernate": {
"description": "Die Option 'Ruhezustand' in den Energieaktionen anzeigen.",
"label": "Ruhezustand anzeigen"
},
"title": "Sperrbildschirm" "title": "Sperrbildschirm"
}, },
"network": { "network": {
@@ -2038,6 +2077,10 @@
"title-matugen": "Matugen-Templating-Verarbeitung fehlgeschlagen", "title-matugen": "Matugen-Templating-Verarbeitung fehlgeschlagen",
"title-predefined": "Die Verarbeitung des vordefinierten Farbschemas ist fehlgeschlagen." "title-predefined": "Die Verarbeitung des vordefinierten Farbschemas ist fehlgeschlagen."
}, },
"vpn": {
"connected": "Verbunden mit '{name}'",
"disconnected": "Verbindung zu '{name}' getrennt"
},
"wallpaper-colors": { "wallpaper-colors": {
"disabled": "Hintergrundbild-Farben deaktiviert", "disabled": "Hintergrundbild-Farben deaktiviert",
"enabled": "Hintergrundbild-Farben aktiviert", "enabled": "Hintergrundbild-Farben aktiviert",
@@ -2071,6 +2114,7 @@
"input-muted": "Audio-Eingabe stummschalten", "input-muted": "Audio-Eingabe stummschalten",
"keep-awake": "Wach halten", "keep-awake": "Wach halten",
"keyboard-layout": "{layout} Tastaturlayout", "keyboard-layout": "{layout} Tastaturlayout",
"manage-vpn": "VPN-Verbindungen verwalten",
"manage-wifi": "WLAN 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.", "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", "move-to-center-section": "Zur mittleren Sektion verschieben",
@@ -2297,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "Verfügbare Netzwerke",
"connect": "Verbinden", "connect": "Verbinden",
"connected": "Verbunden", "connected": "Verbunden",
"disabled": "WLAN ist deaktiviert", "disabled": "WLAN ist deaktiviert",
@@ -2307,6 +2352,7 @@
"forget": "Vergessen", "forget": "Vergessen",
"forget-network": "Dieses Netzwerk vergessen?", "forget-network": "Dieses Netzwerk vergessen?",
"forgetting": "Wird vergessen...", "forgetting": "Wird vergessen...",
"known-networks": "Bekannte Netzwerke",
"no-networks": "Keine Netzwerke gefunden", "no-networks": "Keine Netzwerke gefunden",
"password": "Passwort", "password": "Passwort",
"saved": "Gespeichert", "saved": "Gespeichert",

View File

@@ -123,10 +123,6 @@
"stream-description": "Enter a command to run continuously." "stream-description": "Enter a command to run continuously."
}, },
"dynamic-text": "Dynamic text", "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": { "icon": {
"description": "Select an icon from the library.", "description": "Select an icon from the library.",
"label": "Icon" "label": "Icon"
@@ -136,6 +132,14 @@
"label": "Left click", "label": "Left click",
"update-text": "Update displayed text on 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": { "middle-click": {
"description": "Command to execute when the button is middle-clicked.", "description": "Command to execute when the button is middle-clicked.",
"label": "Middle click", "label": "Middle click",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "Streamed lines from the command will be displayed as text on the button.", "description": "Streamed lines from the command will be displayed as text on the button.",
"label": "Stream" "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": { "dialog": {
@@ -396,28 +417,29 @@
} }
}, },
"changelog": { "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": { "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": { "subtitle": {
"fresh": "Thanks for installing Noctalia! Here is whats included in this build.", "fresh": "Thanks for installing Noctalia! Here is whats included in this build.",
"updated": "Updated from {previousVersion}" "updated": "Updated from {previousVersion}"
}, },
"title": "What's new in {version}",
"version": { "version": {
"new-user": "Fresh install" "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": { "clock": {
@@ -427,10 +449,12 @@
"activate-app": "Activate {app}", "activate-app": "Activate {app}",
"clear-history": "Clear history", "clear-history": "Clear history",
"close-app": "Close {app}", "close-app": "Close {app}",
"connect-vpn": "Connect to {name}",
"cycle-visualizer": "Cycle visualizer", "cycle-visualizer": "Cycle visualizer",
"disable-bluetooth": "Disable Bluetooth", "disable-bluetooth": "Disable Bluetooth",
"disable-dnd": "Disable Do Not Disturb", "disable-dnd": "Disable Do Not Disturb",
"disable-wifi": "Disable Wi-Fi", "disable-wifi": "Disable Wi-Fi",
"disconnect-vpn": "Disconnect {name}",
"enable-bluetooth": "Enable Bluetooth", "enable-bluetooth": "Enable Bluetooth",
"enable-dnd": "Enable Do Not Disturb", "enable-dnd": "Enable Do Not Disturb",
"enable-wifi": "Enable Wi-Fi", "enable-wifi": "Enable Wi-Fi",
@@ -1020,6 +1044,10 @@
"description": "Write {filepath}. Comfy theme needs to be installed and activated manually.", "description": "Write {filepath}. Comfy theme needs to be installed and activated manually.",
"description-missing": "Requires {app} to be installed" "description-missing": "Requires {app} to be installed"
}, },
"telegram": {
"description": "Write {filepath}.",
"description-missing": "Requires {app} to be installed"
},
"vicinae": { "vicinae": {
"description": "Write {filepath} and reload", "description": "Write {filepath} and reload",
"description-missing": "Requires {app} to be installed" "description-missing": "Requires {app} to be installed"
@@ -1216,6 +1244,10 @@
"description": "Adjust the dock's background opacity.", "description": "Adjust the dock's background opacity.",
"label": "Background opacity" "label": "Background opacity"
}, },
"border-radius": {
"description": "Adjust the dock's border radius.",
"label": "Border radius"
},
"colorize-icons": { "colorize-icons": {
"description": "Apply theme colors to dock app icons (non-focused apps only).", "description": "Apply theme colors to dock app icons (non-focused apps only).",
"label": "Colorize Icons" "label": "Colorize Icons"
@@ -1367,6 +1399,10 @@
"description": "Adjust the background opacity of the launcher.", "description": "Adjust the background opacity of the launcher.",
"label": "Background opacity" "label": "Background opacity"
}, },
"clip-preview": {
"description": "Show a preview of the clipboard content when using the >clip command.",
"label": "Enable clip preview"
},
"clipboard-history": { "clipboard-history": {
"description": "Access previously copied items from the launcher.", "description": "Access previously copied items from the launcher.",
"label": "Enable clipboard history" "label": "Enable clipboard history"
@@ -1474,6 +1510,10 @@
"description": "Automatically lock the screen when suspending the system.", "description": "Automatically lock the screen when suspending the system.",
"label": "Lock on suspend" "label": "Lock on suspend"
}, },
"show-hibernate": {
"description": "Show the option 'hibernate' in the energy actions.",
"label": "Show hibernate"
},
"title": "Lock screen" "title": "Lock screen"
}, },
"network": { "network": {
@@ -2037,6 +2077,10 @@
"title-matugen": "Matugen templating processing failed", "title-matugen": "Matugen templating processing failed",
"title-predefined": "Predefined color cheme processing failed" "title-predefined": "Predefined color cheme processing failed"
}, },
"vpn": {
"connected": "Connected to '{name}'",
"disconnected": "Disconnected from '{name}'"
},
"wallpaper-colors": { "wallpaper-colors": {
"disabled": "Wallpaper colors disabled", "disabled": "Wallpaper colors disabled",
"enabled": "Wallpaper colors enabled", "enabled": "Wallpaper colors enabled",
@@ -2070,6 +2114,7 @@
"input-muted": "Toggle input mute", "input-muted": "Toggle input mute",
"keep-awake": "Keep awake", "keep-awake": "Keep awake",
"keyboard-layout": "{layout} keyboard layout", "keyboard-layout": "{layout} keyboard layout",
"manage-vpn": "Manage VPN connections",
"manage-wifi": "Manage Wi-Fi", "manage-wifi": "Manage Wi-Fi",
"microphone-volume-at": "Microphone volume at {volume}%\nScroll to modify volume", "microphone-volume-at": "Microphone volume at {volume}%\nScroll to modify volume",
"move-to-center-section": "Move to center section", "move-to-center-section": "Move to center section",
@@ -2296,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "Available Networks",
"connect": "Connect", "connect": "Connect",
"connected": "Connected", "connected": "Connected",
"disabled": "Wi-Fi is disabled", "disabled": "Wi-Fi is disabled",
@@ -2306,6 +2352,7 @@
"forget": "Forget", "forget": "Forget",
"forget-network": "Forget this network?", "forget-network": "Forget this network?",
"forgetting": "Forgetting...", "forgetting": "Forgetting...",
"known-networks": "Known Networks",
"no-networks": "No networks found", "no-networks": "No networks found",
"password": "Password", "password": "Password",
"saved": "Saved", "saved": "Saved",

View File

@@ -123,10 +123,6 @@
"stream-description": "Introduce un comando para ejecutar continuamente." "stream-description": "Introduce un comando para ejecutar continuamente."
}, },
"dynamic-text": "Texto dinámico", "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": { "icon": {
"description": "Selecciona un icono de la biblioteca.", "description": "Selecciona un icono de la biblioteca.",
"label": "Icono" "label": "Icono"
@@ -136,6 +132,14 @@
"label": "Clic izquierdo", "label": "Clic izquierdo",
"update-text": "Actualizar el texto mostrado al hacer 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": { "middle-click": {
"description": "Comando a ejecutar cuando se hace clic medio en el botón.", "description": "Comando a ejecutar cuando se hace clic medio en el botón.",
"label": "Clic medio", "label": "Clic medio",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "Las líneas transmitidas desde el comando se mostrarán como texto en el botón.", "description": "Las líneas transmitidas desde el comando se mostrarán como texto en el botón.",
"label": "Transmisió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": { "dialog": {
@@ -396,29 +417,29 @@
} }
}, },
"changelog": { "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": { "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": { "subtitle": {
"fresh": "Gracias por instalar Noctalia. Esto es lo que incluye esta compilación.", "fresh": "Gracias por instalar Noctalia. Esto es lo que incluye esta compilación.",
"updated": "Actualizado desde {previousVersion}" "updated": "Actualizado desde {previousVersion}"
}, },
"title": "Novedades en {version}",
"version": { "version": {
"new-user": "Instalación nueva" "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": { "clock": {
@@ -428,10 +449,12 @@
"activate-app": "Activar {app}", "activate-app": "Activar {app}",
"clear-history": "Borrar historial", "clear-history": "Borrar historial",
"close-app": "Cerrar {app}", "close-app": "Cerrar {app}",
"connect-vpn": "Conectarse a {name}",
"cycle-visualizer": "Visualizador de ciclos", "cycle-visualizer": "Visualizador de ciclos",
"disable-bluetooth": "Desactivar Bluetooth", "disable-bluetooth": "Desactivar Bluetooth",
"disable-dnd": "Desactivar No molestar", "disable-dnd": "Desactivar No molestar",
"disable-wifi": "Desactivar Wi-Fi", "disable-wifi": "Desactivar Wi-Fi",
"disconnect-vpn": "Desconectar {name}",
"enable-bluetooth": "Activar Bluetooth", "enable-bluetooth": "Activar Bluetooth",
"enable-dnd": "Activar No molestar", "enable-dnd": "Activar No molestar",
"enable-wifi": "Activar Wi-Fi", "enable-wifi": "Activar Wi-Fi",
@@ -1021,6 +1044,10 @@
"description": "Escribe {filepath}. El tema Comfy debe ser instalado y activado manualmente.", "description": "Escribe {filepath}. El tema Comfy debe ser instalado y activado manualmente.",
"description-missing": "Requiere que {app} esté instalado/a." "description-missing": "Requiere que {app} esté instalado/a."
}, },
"telegram": {
"description": "Escribe {filepath}.",
"description-missing": "Requiere que {app} esté instalado/a."
},
"vicinae": { "vicinae": {
"description": "Escribir {filepath} y recargar", "description": "Escribir {filepath} y recargar",
"description-missing": "Requiere que {app} esté instalado" "description-missing": "Requiere que {app} esté instalado"
@@ -1217,6 +1244,10 @@
"description": "Ajusta la opacidad del fondo del dock.", "description": "Ajusta la opacidad del fondo del dock.",
"label": "Opacidad del fondo" "label": "Opacidad del fondo"
}, },
"border-radius": {
"description": "Ajustar el radio del borde del dock.",
"label": "Radio de borde"
},
"colorize-icons": { "colorize-icons": {
"description": "Aplicar colores del tema a los iconos de aplicaciones del dock (solo aplicaciones no enfocadas).", "description": "Aplicar colores del tema a los iconos de aplicaciones del dock (solo aplicaciones no enfocadas).",
"label": "Colorear iconos" "label": "Colorear iconos"
@@ -1368,6 +1399,10 @@
"description": "Ajusta la opacidad del fondo del lanzador.", "description": "Ajusta la opacidad del fondo del lanzador.",
"label": "Opacidad del fondo" "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": { "clipboard-history": {
"description": "Accede a los elementos copiados anteriormente desde el lanzador.", "description": "Accede a los elementos copiados anteriormente desde el lanzador.",
"label": "Activar historial del portapapeles" "label": "Activar historial del portapapeles"
@@ -1475,6 +1510,10 @@
"description": "Bloquear la pantalla automáticamente al suspender el sistema.", "description": "Bloquear la pantalla automáticamente al suspender el sistema.",
"label": "Bloquear al suspender" "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" "title": "Pantalla de bloqueo"
}, },
"network": { "network": {
@@ -2038,6 +2077,10 @@
"title-matugen": "Falló el procesamiento de la plantilla Matugen.", "title-matugen": "Falló el procesamiento de la plantilla Matugen.",
"title-predefined": "Falló el procesamiento del esquema de color predefinido." "title-predefined": "Falló el procesamiento del esquema de color predefinido."
}, },
"vpn": {
"connected": "Conectado a '{name}'",
"disconnected": "Desconectado de '{name}'"
},
"wallpaper-colors": { "wallpaper-colors": {
"disabled": "Colores del fondo de pantalla desactivados", "disabled": "Colores del fondo de pantalla desactivados",
"enabled": "Colores del fondo de pantalla activados", "enabled": "Colores del fondo de pantalla activados",
@@ -2071,6 +2114,7 @@
"input-muted": "Silenciar entrada de audio", "input-muted": "Silenciar entrada de audio",
"keep-awake": "Mantener despierto", "keep-awake": "Mantener despierto",
"keyboard-layout": "Distribución de teclado {layout}", "keyboard-layout": "Distribución de teclado {layout}",
"manage-vpn": "Administrar conexiones VPN",
"manage-wifi": "Gestionar Wi-Fi", "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.", "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", "move-to-center-section": "Mover a la sección central",
@@ -2297,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "Redes disponibles",
"connect": "Conectar", "connect": "Conectar",
"connected": "Conectado", "connected": "Conectado",
"disabled": "Wi-Fi está desactivado", "disabled": "Wi-Fi está desactivado",
@@ -2307,6 +2352,7 @@
"forget": "Olvidar", "forget": "Olvidar",
"forget-network": "¿Olvidar esta red?", "forget-network": "¿Olvidar esta red?",
"forgetting": "Olvidando...", "forgetting": "Olvidando...",
"known-networks": "Redes conocidas",
"no-networks": "No se encontraron redes", "no-networks": "No se encontraron redes",
"password": "Contraseña", "password": "Contraseña",
"saved": "Guardado", "saved": "Guardado",

View File

@@ -123,10 +123,6 @@
"stream-description": "Entrez une commande à exécuter en continu." "stream-description": "Entrez une commande à exécuter en continu."
}, },
"dynamic-text": "Texte dynamique", "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": { "icon": {
"description": "Sélectionnez une icône dans la bibliothèque.", "description": "Sélectionnez une icône dans la bibliothèque.",
"label": "Icône" "label": "Icône"
@@ -136,6 +132,14 @@
"label": "Clic gauche", "label": "Clic gauche",
"update-text": "Mettre à jour le texte affiché au 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": { "middle-click": {
"description": "Commande à exécuter quand le bouton est cliqué au milieu.", "description": "Commande à exécuter quand le bouton est cliqué au milieu.",
"label": "Clic milieu", "label": "Clic milieu",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "Les lignes diffusées depuis la commande seront affichées sous forme de texte sur le bouton.", "description": "Les lignes diffusées depuis la commande seront affichées sous forme de texte sur le bouton.",
"label": "Flux" "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": { "dialog": {
@@ -396,29 +417,29 @@
} }
}, },
"changelog": { "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": { "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": { "subtitle": {
"fresh": "Merci davoir installé Noctalia ! Voici ce que contient cette version.", "fresh": "Merci davoir installé Noctalia ! Voici ce que contient cette version.",
"updated": "Mise à jour depuis {previousVersion}" "updated": "Mise à jour depuis {previousVersion}"
}, },
"title": "Quoi de neuf dans {version}",
"version": { "version": {
"new-user": "Nouvelle installation" "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": { "clock": {
@@ -428,10 +449,12 @@
"activate-app": "Activer {app}", "activate-app": "Activer {app}",
"clear-history": "Effacer l'historique", "clear-history": "Effacer l'historique",
"close-app": "Fermer {app}", "close-app": "Fermer {app}",
"connect-vpn": "Se connecter à {name}",
"cycle-visualizer": "Visualiseur de cycle", "cycle-visualizer": "Visualiseur de cycle",
"disable-bluetooth": "Désactiver le Bluetooth", "disable-bluetooth": "Désactiver le Bluetooth",
"disable-dnd": "Désactiver le mode Ne pas déranger", "disable-dnd": "Désactiver le mode Ne pas déranger",
"disable-wifi": "Désactiver le Wi-Fi", "disable-wifi": "Désactiver le Wi-Fi",
"disconnect-vpn": "Se déconnecter de {name}",
"enable-bluetooth": "Activer le Bluetooth", "enable-bluetooth": "Activer le Bluetooth",
"enable-dnd": "Activer le mode Ne pas déranger", "enable-dnd": "Activer le mode Ne pas déranger",
"enable-wifi": "Activer le Wi-Fi", "enable-wifi": "Activer le Wi-Fi",
@@ -1021,6 +1044,10 @@
"description": "Écrire {filepath}. Le thème Comfy doit être installé et activé manuellement.", "description": "Écrire {filepath}. Le thème Comfy doit être installé et activé manuellement.",
"description-missing": "Nécessite l'installation de {app}" "description-missing": "Nécessite l'installation de {app}"
}, },
"telegram": {
"description": "Écrire {filepath}.",
"description-missing": "Nécessite l'installation de {app}"
},
"vicinae": { "vicinae": {
"description": "Écrire {filepath} et recharger", "description": "Écrire {filepath} et recharger",
"description-missing": "Nécessite que le lanceur {app} soit installé" "description-missing": "Nécessite que le lanceur {app} soit installé"
@@ -1217,6 +1244,10 @@
"description": "Ajustez l'opacité de l'arrière-plan du dock.", "description": "Ajustez l'opacité de l'arrière-plan du dock.",
"label": "Opacité de l'arrière-plan" "label": "Opacité de l'arrière-plan"
}, },
"border-radius": {
"description": "Ajuster le rayon de bordure du dock.",
"label": "Rayon de bordure"
},
"colorize-icons": { "colorize-icons": {
"description": "Appliquer les couleurs du thème aux icônes d'applications du dock (applications non focalisées uniquement).", "description": "Appliquer les couleurs du thème aux icônes d'applications du dock (applications non focalisées uniquement).",
"label": "Coloriser les icônes" "label": "Coloriser les icônes"
@@ -1368,6 +1399,10 @@
"description": "Ajustez l'opacité de l'arrière-plan du lanceur.", "description": "Ajustez l'opacité de l'arrière-plan du lanceur.",
"label": "Opacité de l'arrière-plan" "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": { "clipboard-history": {
"description": "Accédez aux éléments précédemment copiés depuis le lanceur.", "description": "Accédez aux éléments précédemment copiés depuis le lanceur.",
"label": "Activer l'historique du presse-papiers" "label": "Activer l'historique du presse-papiers"
@@ -1475,6 +1510,10 @@
"description": "Verrouiller automatiquement l'écran lors de la mise en veille du système.", "description": "Verrouiller automatiquement l'écran lors de la mise en veille du système.",
"label": "Verrouiller à la suspension" "label": "Verrouiller à la suspension"
}, },
"show-hibernate": {
"description": "Afficher loption 'hiberner' dans les actions dénergie.",
"label": "Afficher lhibernation"
},
"title": "Écran de verrouillage" "title": "Écran de verrouillage"
}, },
"network": { "network": {
@@ -2038,6 +2077,10 @@
"title-matugen": "Le traitement du modèle Matugen a échoué.", "title-matugen": "Le traitement du modèle Matugen a échoué.",
"title-predefined": "Le traitement du schéma de couleurs prédéfini 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": { "wallpaper-colors": {
"disabled": "Couleurs du fond d'écran désactivées", "disabled": "Couleurs du fond d'écran désactivées",
"enabled": "Couleurs du fond d'écran activées", "enabled": "Couleurs du fond d'écran activées",
@@ -2071,6 +2114,7 @@
"input-muted": "Couper l'entrée audio", "input-muted": "Couper l'entrée audio",
"keep-awake": "Rester éveillé", "keep-awake": "Rester éveillé",
"keyboard-layout": "Disposition du clavier {layout}", "keyboard-layout": "Disposition du clavier {layout}",
"manage-vpn": "Gérer les connexions VPN",
"manage-wifi": "Gérer le Wi-Fi", "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.", "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", "move-to-center-section": "Déplacer vers la section centrale",
@@ -2297,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "Réseaux disponibles",
"connect": "Connecter", "connect": "Connecter",
"connected": "Connecté", "connected": "Connecté",
"disabled": "Le Wi-Fi est désactivé", "disabled": "Le Wi-Fi est désactivé",
@@ -2307,6 +2352,7 @@
"forget": "Oublier", "forget": "Oublier",
"forget-network": "Oublier ce réseau ?", "forget-network": "Oublier ce réseau ?",
"forgetting": "Oubli en cours...", "forgetting": "Oubli en cours...",
"known-networks": "Réseaux connus",
"no-networks": "Aucun réseau trouvé", "no-networks": "Aucun réseau trouvé",
"password": "Mot de passe", "password": "Mot de passe",
"saved": "Enregistré", "saved": "Enregistré",

View File

@@ -123,10 +123,6 @@
"stream-description": "Voer een commando in dat continu wordt uitgevoerd." "stream-description": "Voer een commando in dat continu wordt uitgevoerd."
}, },
"dynamic-text": "Dynamische tekst", "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": { "icon": {
"description": "Selecteer een pictogram uit de bibliotheek.", "description": "Selecteer een pictogram uit de bibliotheek.",
"label": "Pictogram" "label": "Pictogram"
@@ -136,6 +132,14 @@
"label": "Linkermuisklik", "label": "Linkermuisklik",
"update-text": "Tekst bijwerken bij linksklik" "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": { "middle-click": {
"description": "Commando dat wordt uitgevoerd wanneer met de middelste muisknop op de knop wordt geklikt.", "description": "Commando dat wordt uitgevoerd wanneer met de middelste muisknop op de knop wordt geklikt.",
"label": "Middelste muisklik", "label": "Middelste muisklik",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "Gestreamde regels uit het commando worden als tekst op de knop weergegeven.", "description": "Gestreamde regels uit het commando worden als tekst op de knop weergegeven.",
"label": "Stream" "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": { "dialog": {
@@ -396,29 +417,29 @@
} }
}, },
"changelog": { "changelog": {
"error": {
"fetch-failed": "Kan changeloggegevens niet laden. Probeer het later opnieuw.",
"rate-limit": "GitHub-limiet bereikt. Probeer het over enkele minuten opnieuw."
},
"panel": { "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": { "subtitle": {
"fresh": "Bedankt voor het installeren van Noctalia! Dit zit er in deze build.", "fresh": "Bedankt voor het installeren van Noctalia! Dit zit er in deze build.",
"updated": "Bijgewerkt vanaf {previousVersion}" "updated": "Bijgewerkt vanaf {previousVersion}"
}, },
"title": "Wat is er nieuw in {version}",
"version": { "version": {
"new-user": "Nieuwe installatie" "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": { "clock": {
@@ -428,10 +449,12 @@
"activate-app": "Activeer {app}", "activate-app": "Activeer {app}",
"clear-history": "Geschiedenis wissen", "clear-history": "Geschiedenis wissen",
"close-app": "Sluit {app}", "close-app": "Sluit {app}",
"connect-vpn": "Verbinding maken met {name}",
"cycle-visualizer": "Cyclusvisualisatie", "cycle-visualizer": "Cyclusvisualisatie",
"disable-bluetooth": "Bluetooth uitschakelen", "disable-bluetooth": "Bluetooth uitschakelen",
"disable-dnd": "Niet Storen uitschakelen", "disable-dnd": "Niet Storen uitschakelen",
"disable-wifi": "Wi-Fi uitschakelen", "disable-wifi": "Wi-Fi uitschakelen",
"disconnect-vpn": "Verbinding met {name} verbreken",
"enable-bluetooth": "Bluetooth inschakelen", "enable-bluetooth": "Bluetooth inschakelen",
"enable-dnd": "Niet Storen inschakelen", "enable-dnd": "Niet Storen inschakelen",
"enable-wifi": "Wi-Fi inschakelen", "enable-wifi": "Wi-Fi inschakelen",
@@ -1021,6 +1044,10 @@
"description": "Schrijf {filepath}. Het Comfy-thema moet handmatig worden geïnstalleerd en geactiveerd.", "description": "Schrijf {filepath}. Het Comfy-thema moet handmatig worden geïnstalleerd en geactiveerd.",
"description-missing": "Vereist dat {app} is geïnstalleerd." "description-missing": "Vereist dat {app} is geïnstalleerd."
}, },
"telegram": {
"description": "Schrijf {filepath}.",
"description-missing": "Vereist dat {app} is geïnstalleerd."
},
"vicinae": { "vicinae": {
"description": "Schrijf {filepath} en herlaad.", "description": "Schrijf {filepath} en herlaad.",
"description-missing": "Vereist dat {app} is geïnstalleerd." "description-missing": "Vereist dat {app} is geïnstalleerd."
@@ -1217,6 +1244,10 @@
"description": "Pas de achtergronddekking van de dock aan.", "description": "Pas de achtergronddekking van de dock aan.",
"label": "Achtergronddekking" "label": "Achtergronddekking"
}, },
"border-radius": {
"description": "Pas de randradius van het dock aan.",
"label": "Randradius"
},
"colorize-icons": { "colorize-icons": {
"description": "Pas themakleuren toe op dock-pictogrammen (alleen niet-focuste apps).", "description": "Pas themakleuren toe op dock-pictogrammen (alleen niet-focuste apps).",
"label": "Pictogrammen inkleuren" "label": "Pictogrammen inkleuren"
@@ -1368,6 +1399,10 @@
"description": "Pas de achtergronddekking van de launcher aan.", "description": "Pas de achtergronddekking van de launcher aan.",
"label": "Achtergronddekking" "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": { "clipboard-history": {
"description": "Toegang tot eerder gekopieerde items vanuit de launcher.", "description": "Toegang tot eerder gekopieerde items vanuit de launcher.",
"label": "Klembordgeschiedenis inschakelen" "label": "Klembordgeschiedenis inschakelen"
@@ -1475,6 +1510,10 @@
"description": "Vergrendel het scherm automatisch wanneer het systeem wordt onderbroken.", "description": "Vergrendel het scherm automatisch wanneer het systeem wordt onderbroken.",
"label": "Vergrendelen bij onderbreken" "label": "Vergrendelen bij onderbreken"
}, },
"show-hibernate": {
"description": "De optie 'sluimerstand' tonen in de energieacties.",
"label": "Sluimerstand tonen"
},
"title": "Vergrendelscherm" "title": "Vergrendelscherm"
}, },
"network": { "network": {
@@ -2038,6 +2077,10 @@
"title-matugen": "Matugen-sjabloonverwerking mislukt", "title-matugen": "Matugen-sjabloonverwerking mislukt",
"title-predefined": "Verwerken van vooraf gedefinieerd kleurenschema mislukt" "title-predefined": "Verwerken van vooraf gedefinieerd kleurenschema mislukt"
}, },
"vpn": {
"connected": "Verbonden met '{name}'",
"disconnected": "Verbinding met '{name}' verbroken"
},
"wallpaper-colors": { "wallpaper-colors": {
"disabled": "Achtergrondkleuren uitgeschakeld", "disabled": "Achtergrondkleuren uitgeschakeld",
"enabled": "Achtergrondkleuren ingeschakeld", "enabled": "Achtergrondkleuren ingeschakeld",
@@ -2071,6 +2114,7 @@
"input-muted": "Invoermicrofoon dempen in-/uitschakelen", "input-muted": "Invoermicrofoon dempen in-/uitschakelen",
"keep-awake": "Wakker houden", "keep-awake": "Wakker houden",
"keyboard-layout": "{layout}-toetsenbordindeling", "keyboard-layout": "{layout}-toetsenbordindeling",
"manage-vpn": "VPN-verbindingen beheren",
"manage-wifi": "Wi-Fi 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.", "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", "move-to-center-section": "Verplaatsen naar middelste sectie",
@@ -2297,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "Beschikbare netwerken",
"connect": "Verbinden", "connect": "Verbinden",
"connected": "Verbonden", "connected": "Verbonden",
"disabled": "Wi-Fi is uitgeschakeld", "disabled": "Wi-Fi is uitgeschakeld",
@@ -2307,6 +2352,7 @@
"forget": "Vergeten", "forget": "Vergeten",
"forget-network": "Dit netwerk vergeten?", "forget-network": "Dit netwerk vergeten?",
"forgetting": "Vergeten...", "forgetting": "Vergeten...",
"known-networks": "Bekende netwerken",
"no-networks": "Geen netwerken gevonden", "no-networks": "Geen netwerken gevonden",
"password": "Wachtwoord", "password": "Wachtwoord",
"saved": "Opgeslagen", "saved": "Opgeslagen",

View File

@@ -123,10 +123,6 @@
"stream-description": "Insira um comando para executar continuamente." "stream-description": "Insira um comando para executar continuamente."
}, },
"dynamic-text": "Texto dinâmico", "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": { "icon": {
"description": "Selecione um ícone da biblioteca.", "description": "Selecione um ícone da biblioteca.",
"label": "Ícone" "label": "Ícone"
@@ -136,6 +132,14 @@
"label": "Clique esquerdo", "label": "Clique esquerdo",
"update-text": "Atualizar texto exibido ao clicar com o botão 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": { "middle-click": {
"description": "Comando a executar quando o botão é clicado com o botão do meio.", "description": "Comando a executar quando o botão é clicado com o botão do meio.",
"label": "Clique do meio", "label": "Clique do meio",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "As linhas transmitidas do comando serão exibidas como texto no botão.", "description": "As linhas transmitidas do comando serão exibidas como texto no botão.",
"label": "Transmissã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": { "dialog": {
@@ -396,29 +417,29 @@
} }
}, },
"changelog": { "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": { "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": { "subtitle": {
"fresh": "Obrigado por instalar o Noctalia! Veja o que está incluído nesta compilação.", "fresh": "Obrigado por instalar o Noctalia! Veja o que está incluído nesta compilação.",
"updated": "Atualizado a partir da {previousVersion}" "updated": "Atualizado a partir da {previousVersion}"
}, },
"title": "Novidades na {version}",
"version": { "version": {
"new-user": "Nova instalação" "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": { "clock": {
@@ -428,10 +449,12 @@
"activate-app": "Ativar {app}", "activate-app": "Ativar {app}",
"clear-history": "Limpar histórico", "clear-history": "Limpar histórico",
"close-app": "Fechar {app}", "close-app": "Fechar {app}",
"connect-vpn": "Conectar-se a {name}",
"cycle-visualizer": "Visualizador de ciclo", "cycle-visualizer": "Visualizador de ciclo",
"disable-bluetooth": "Desativar Bluetooth", "disable-bluetooth": "Desativar Bluetooth",
"disable-dnd": "Desativar o Não Perturbe", "disable-dnd": "Desativar o Não Perturbe",
"disable-wifi": "Desativar Wi-Fi", "disable-wifi": "Desativar Wi-Fi",
"disconnect-vpn": "Desconectar {name}",
"enable-bluetooth": "Ativar Bluetooth", "enable-bluetooth": "Ativar Bluetooth",
"enable-dnd": "Ativar Não Perturbe", "enable-dnd": "Ativar Não Perturbe",
"enable-wifi": "Ativar Wi-Fi", "enable-wifi": "Ativar Wi-Fi",
@@ -1021,6 +1044,10 @@
"description": "Escreva em {filepath}. O tema Comfy precisa ser instalado e ativado manualmente.", "description": "Escreva em {filepath}. O tema Comfy precisa ser instalado e ativado manualmente.",
"description-missing": "Requer que o {app} esteja instalado." "description-missing": "Requer que o {app} esteja instalado."
}, },
"telegram": {
"description": "Escreva em {filepath}.",
"description-missing": "Requer que o {app} esteja instalado."
},
"vicinae": { "vicinae": {
"description": "Escrever {filepath} e recarregar", "description": "Escrever {filepath} e recarregar",
"description-missing": "Requer que o {app} esteja instalado" "description-missing": "Requer que o {app} esteja instalado"
@@ -1217,6 +1244,10 @@
"description": "Ajuste a opacidade do fundo da dock.", "description": "Ajuste a opacidade do fundo da dock.",
"label": "Opacidade do fundo" "label": "Opacidade do fundo"
}, },
"border-radius": {
"description": "Ajustar o raio da borda da dock.",
"label": "Raio da borda"
},
"colorize-icons": { "colorize-icons": {
"description": "Aplicar cores do tema aos ícones de aplicativos da dock (apenas aplicativos não focados).", "description": "Aplicar cores do tema aos ícones de aplicativos da dock (apenas aplicativos não focados).",
"label": "Colorir ícones" "label": "Colorir ícones"
@@ -1368,6 +1399,10 @@
"description": "Ajuste a opacidade do fundo do lançador.", "description": "Ajuste a opacidade do fundo do lançador.",
"label": "Opacidade do fundo" "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": { "clipboard-history": {
"description": "Acesse itens copiados anteriormente a partir do lançador.", "description": "Acesse itens copiados anteriormente a partir do lançador.",
"label": "Ativar histórico da área de transferência" "label": "Ativar histórico da área de transferência"
@@ -1475,6 +1510,10 @@
"description": "Bloquear a tela automaticamente ao suspender o sistema.", "description": "Bloquear a tela automaticamente ao suspender o sistema.",
"label": "Bloquear ao suspender" "label": "Bloquear ao suspender"
}, },
"show-hibernate": {
"description": "Mostrar a opção 'hibernar' nas ações de energia.",
"label": "Mostrar hibernar"
},
"title": "Tela de bloqueio" "title": "Tela de bloqueio"
}, },
"network": { "network": {
@@ -2038,6 +2077,10 @@
"title-matugen": "Falha no processamento do template Matugen", "title-matugen": "Falha no processamento do template Matugen",
"title-predefined": "O processamento do esquema de cores predefinido falhou." "title-predefined": "O processamento do esquema de cores predefinido falhou."
}, },
"vpn": {
"connected": "Conectado a '{name}'",
"disconnected": "Desconectado de '{name}'"
},
"wallpaper-colors": { "wallpaper-colors": {
"disabled": "Cores do papel de parede desativadas", "disabled": "Cores do papel de parede desativadas",
"enabled": "Cores do papel de parede ativadas", "enabled": "Cores do papel de parede ativadas",
@@ -2071,6 +2114,7 @@
"input-muted": "Silenciar entrada de áudio", "input-muted": "Silenciar entrada de áudio",
"keep-awake": "Manter acordado", "keep-awake": "Manter acordado",
"keyboard-layout": "Layout de teclado {layout}", "keyboard-layout": "Layout de teclado {layout}",
"manage-vpn": "Gerenciar conexões VPN",
"manage-wifi": "Gerenciar Wi-Fi", "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.", "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", "move-to-center-section": "Mover para a seção central",
@@ -2297,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "Redes Disponíveis",
"connect": "Conectar", "connect": "Conectar",
"connected": "Conectado", "connected": "Conectado",
"disabled": "O Wi-Fi está desativado", "disabled": "O Wi-Fi está desativado",
@@ -2307,6 +2352,7 @@
"forget": "Esquecer", "forget": "Esquecer",
"forget-network": "Esquecer esta rede?", "forget-network": "Esquecer esta rede?",
"forgetting": "Esquecendo...", "forgetting": "Esquecendo...",
"known-networks": "Redes Conhecidas",
"no-networks": "Nenhuma rede encontrada", "no-networks": "Nenhuma rede encontrada",
"password": "Senha", "password": "Senha",
"saved": "Salva", "saved": "Salva",

View File

@@ -123,10 +123,6 @@
"stream-description": "Введите команду для непрерывного выполнения." "stream-description": "Введите команду для непрерывного выполнения."
}, },
"dynamic-text": "Динамический текст", "dynamic-text": "Динамический текст",
"hide-vertical": {
"description": "Если включено, текст из вывода команды не будет отображаться, когда панель находится в вертикальном макете (слева или справа).",
"label": "Скрывать текст в вертикальной панели"
},
"icon": { "icon": {
"description": "Выберите иконку из библиотеки.", "description": "Выберите иконку из библиотеки.",
"label": "Иконка" "label": "Иконка"
@@ -136,6 +132,14 @@
"label": "Клик левой кнопкой", "label": "Клик левой кнопкой",
"update-text": "Обновить отображаемый текст по левому клику" "update-text": "Обновить отображаемый текст по левому клику"
}, },
"max-text-length-horizontal": {
"description": "Максимальное количество символов для отображения в горизонтальной панели (0 для скрытия текста)",
"label": "Макс. длина текста (горизонтально)"
},
"max-text-length-vertical": {
"description": "Максимальное количество символов для отображения в вертикальной панели (0 для скрытия текста)",
"label": "Макс. длина текста (вертикально)"
},
"middle-click": { "middle-click": {
"description": "Команда для выполнения при нажатии средней кнопкой мыши.", "description": "Команда для выполнения при нажатии средней кнопкой мыши.",
"label": "Клик средней кнопкой", "label": "Клик средней кнопкой",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "Потоковые строки из команды будут отображаться как текст на кнопке.", "description": "Потоковые строки из команды будут отображаться как текст на кнопке.",
"label": "Поток" "label": "Поток"
},
"wheel": {
"description": "Команда для выполнения при использовании колеса прокрутки.\nИспользуйте $delta для дельты колеса прокрутки в команде",
"label": "Колесо прокрутки",
"update-text": "Обновить отображаемый текст при прокрутке"
},
"wheel-down": {
"description": "Команда для выполнения при прокрутке колеса вниз.",
"label": "Команда прокрутки колеса вниз"
},
"wheel-mode-separate": {
"description": "Включить раздельные команды для колеса прокрутки вверх и вниз",
"label": "Раздельные команды колеса прокрутки"
},
"wheel-up": {
"description": "Команда для выполнения при прокрутке колеса вверх.",
"label": "Команда прокрутки колеса вверх"
} }
}, },
"dialog": { "dialog": {
@@ -396,29 +417,29 @@
} }
}, },
"changelog": { "changelog": {
"error": {
"fetch-failed": "Не удалось загрузить данные журнала изменений. Пожалуйста, попробуйте позже.",
"rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут."
},
"panel": { "panel": {
"title": "Что нового в {version}", "buttons": {
"discord": "Присоединиться к нашему Discord",
"dismiss": "Ок"
},
"empty": "Примечания к выпуску пока недоступны.",
"highlight-title": "Основные изменения",
"section": {
"released": "Выпущено {date}",
"version": "Версия {version}"
},
"subtitle": { "subtitle": {
"fresh": "Спасибо за установку Noctalia! Вот что входит в этот билд.", "fresh": "Спасибо за установку Noctalia! Вот что входит в этот билд.",
"updated": "Обновлено с {previousVersion}" "updated": "Обновлено с {previousVersion}"
}, },
"title": "Что нового в {version}",
"version": { "version": {
"new-user": "Новая установка" "new-user": "Новая установка"
},
"highlight-title": "Основные изменения",
"empty": "Примечания к выпуску пока недоступны.",
"section": {
"version": "Версия {version}",
"released": "Выпущено {date}"
},
"buttons": {
"discord": "Присоединиться к нашему Discord",
"feedback": "Отправить отзыв",
"dismiss": "Ок"
} }
},
"error": {
"rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут."
} }
}, },
"clock": { "clock": {
@@ -428,10 +449,12 @@
"activate-app": "Активировать {app}", "activate-app": "Активировать {app}",
"clear-history": "Очистить историю", "clear-history": "Очистить историю",
"close-app": "Закрыть {app}", "close-app": "Закрыть {app}",
"connect-vpn": "Подключиться к {name}",
"cycle-visualizer": "Визуализатор циклов", "cycle-visualizer": "Визуализатор циклов",
"disable-bluetooth": "Отключить Bluetooth", "disable-bluetooth": "Отключить Bluetooth",
"disable-dnd": "Отключить режим \"Не беспокоить\"", "disable-dnd": "Отключить режим \"Не беспокоить\"",
"disable-wifi": "Отключить Wi-Fi", "disable-wifi": "Отключить Wi-Fi",
"disconnect-vpn": "Отключить {name}",
"enable-bluetooth": "Включить Bluetooth", "enable-bluetooth": "Включить Bluetooth",
"enable-dnd": "Не беспокоить", "enable-dnd": "Не беспокоить",
"enable-wifi": "Включить Wi-Fi", "enable-wifi": "Включить Wi-Fi",
@@ -1021,6 +1044,10 @@
"description": "Записать {filepath}. Тему Comfy нужно установить и активировать вручную.", "description": "Записать {filepath}. Тему Comfy нужно установить и активировать вручную.",
"description-missing": "Требуется установка {app}" "description-missing": "Требуется установка {app}"
}, },
"telegram": {
"description": "Записать {filepath}.",
"description-missing": "Требуется установка {app}"
},
"vicinae": { "vicinae": {
"description": "Записать {filepath} и перезагрузить", "description": "Записать {filepath} и перезагрузить",
"description-missing": "Требуется установка {app}" "description-missing": "Требуется установка {app}"
@@ -1217,6 +1244,10 @@
"description": "Настройка непрозрачности фона панели приложений.", "description": "Настройка непрозрачности фона панели приложений.",
"label": "Непрозрачность фона" "label": "Непрозрачность фона"
}, },
"border-radius": {
"description": "Измените радиус границы дока.",
"label": "Радиус скругления границы"
},
"colorize-icons": { "colorize-icons": {
"description": "Применить цвета темы к иконкам приложений на панели (только для нефокусированных приложений).", "description": "Применить цвета темы к иконкам приложений на панели (только для нефокусированных приложений).",
"label": "Раскрасить иконки" "label": "Раскрасить иконки"
@@ -1368,6 +1399,10 @@
"description": "Настройка непрозрачности фона запуска.", "description": "Настройка непрозрачности фона запуска.",
"label": "Непрозрачность фона" "label": "Непрозрачность фона"
}, },
"clip-preview": {
"description": "Показывать предварительный просмотр содержимого буфера обмена при использовании команды >clip.",
"label": "Включить предварительный просмотр буфера обмена"
},
"clipboard-history": { "clipboard-history": {
"description": "Доступ к ранее скопированным элементам из запуска.", "description": "Доступ к ранее скопированным элементам из запуска.",
"label": "Включить историю буфера обмена" "label": "Включить историю буфера обмена"
@@ -1475,6 +1510,10 @@
"description": "Автоматически блокировать экран при приостановке работы системы.", "description": "Автоматически блокировать экран при приостановке работы системы.",
"label": "Блокировать при приостановке" "label": "Блокировать при приостановке"
}, },
"show-hibernate": {
"description": "Показывать опцию 'спящий режим' в действиях питания.",
"label": "Показывать спящий режим"
},
"title": "Экран блокировки" "title": "Экран блокировки"
}, },
"network": { "network": {
@@ -2038,6 +2077,10 @@
"title-matugen": "Сбой обработки шаблонов Matugen", "title-matugen": "Сбой обработки шаблонов Matugen",
"title-predefined": "Сбой обработки предопределенной цветовой схемы" "title-predefined": "Сбой обработки предопределенной цветовой схемы"
}, },
"vpn": {
"connected": "Подключено к '{name}'",
"disconnected": "Отключено от '{name}'"
},
"wallpaper-colors": { "wallpaper-colors": {
"disabled": "Цвета обоев отключены", "disabled": "Цвета обоев отключены",
"enabled": "Цвета обоев включены", "enabled": "Цвета обоев включены",
@@ -2071,6 +2114,7 @@
"input-muted": "Переключить заглушение ввода", "input-muted": "Переключить заглушение ввода",
"keep-awake": "Не засыпать", "keep-awake": "Не засыпать",
"keyboard-layout": "Раскладка клавиатуры {layout}", "keyboard-layout": "Раскладка клавиатуры {layout}",
"manage-vpn": "Управлять VPN-подключениями",
"manage-wifi": "Управление Wi-Fi", "manage-wifi": "Управление Wi-Fi",
"microphone-volume-at": "Громкость микрофона {volume}%\nЛевый клик для настроек. Правый клик для переключения заглушения.\nПрокрутка для изменения громкости.", "microphone-volume-at": "Громкость микрофона {volume}%\nЛевый клик для настроек. Правый клик для переключения заглушения.\nПрокрутка для изменения громкости.",
"move-to-center-section": "Переместить в центральную секцию", "move-to-center-section": "Переместить в центральную секцию",
@@ -2297,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "Доступные сети",
"connect": "Подключить", "connect": "Подключить",
"connected": "Подключено", "connected": "Подключено",
"disabled": "Wi-Fi отключен", "disabled": "Wi-Fi отключен",
@@ -2307,6 +2352,7 @@
"forget": "Забыть", "forget": "Забыть",
"forget-network": "Забыть эту сеть?", "forget-network": "Забыть эту сеть?",
"forgetting": "Забывание...", "forgetting": "Забывание...",
"known-networks": "Известные сети",
"no-networks": "Сети не найдены", "no-networks": "Сети не найдены",
"password": "Пароль", "password": "Пароль",
"saved": "Сохранено", "saved": "Сохранено",

View File

@@ -123,10 +123,6 @@
"stream-description": "Sürekli çalıştırılacak bir komut girin." "stream-description": "Sürekli çalıştırılacak bir komut girin."
}, },
"dynamic-text": "Dinamik metin", "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": { "icon": {
"description": "Kütüphaneden bir ikon seçin.", "description": "Kütüphaneden bir ikon seçin.",
"label": "İkon" "label": "İkon"
@@ -136,6 +132,14 @@
"label": "Sol tıklama", "label": "Sol tıklama",
"update-text": "Sol tıklamayla görüntülenen metni güncelle" "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": { "middle-click": {
"description": "Butona orta tıklandığında yürütülecek komut.", "description": "Butona orta tıklandığında yürütülecek komut.",
"label": "Orta tıklama", "label": "Orta tıklama",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "Komuttan gelen akış satırları butonda metin olarak gösterilecektir.", "description": "Komuttan gelen akış satırları butonda metin olarak gösterilecektir.",
"label": "Akış" "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": { "dialog": {
@@ -396,29 +417,29 @@
} }
}, },
"changelog": { "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": { "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": { "subtitle": {
"fresh": "Noctaliayı kurduğun için teşekkürler! Bu sürümde gelenler bunlar.", "fresh": "Noctaliayı kurduğun için teşekkürler! Bu sürümde gelenler bunlar.",
"updated": "{previousVersion} sürümünden güncellendi" "updated": "{previousVersion} sürümünden güncellendi"
}, },
"title": "{version} sürümünde neler yeni",
"version": { "version": {
"new-user": "Yeni kurulum" "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": { "clock": {
@@ -428,10 +449,12 @@
"activate-app": "{app}'i etkinleştir", "activate-app": "{app}'i etkinleştir",
"clear-history": "Geçmişi temizle", "clear-history": "Geçmişi temizle",
"close-app": "{app}'i kapat", "close-app": "{app}'i kapat",
"connect-vpn": "{name} bağlantısına bağlan",
"cycle-visualizer": "Döngü görselleştirici", "cycle-visualizer": "Döngü görselleştirici",
"disable-bluetooth": "Bluetooth'u kapat", "disable-bluetooth": "Bluetooth'u kapat",
"disable-dnd": "Rahatsız Etmeyin'i Kapat", "disable-dnd": "Rahatsız Etmeyin'i Kapat",
"disable-wifi": "Wi-Fi'ı kapat", "disable-wifi": "Wi-Fi'ı kapat",
"disconnect-vpn": "{name} bağlantısını kes",
"enable-bluetooth": "Bluetooth'u etkinleştir", "enable-bluetooth": "Bluetooth'u etkinleştir",
"enable-dnd": "Rahatsız Etmeyin'i Etkinleştir", "enable-dnd": "Rahatsız Etmeyin'i Etkinleştir",
"enable-wifi": "Wi-Fi'ı etkinleştir", "enable-wifi": "Wi-Fi'ı etkinleştir",
@@ -1021,6 +1044,10 @@
"description": "{filepath} dosyasına yaz. Comfy temasının kurulu ve manuel olarak etkinleştirilmiş olması gerekir.", "description": "{filepath} dosyasına yaz. Comfy temasının kurulu ve manuel olarak etkinleştirilmiş olması gerekir.",
"description-missing": "Kurulum için {app} gereklidir" "description-missing": "Kurulum için {app} gereklidir"
}, },
"telegram": {
"description": "{filepath} dosyasına yaz.",
"description-missing": "Kurulum için {app} gereklidir"
},
"vicinae": { "vicinae": {
"description": "{filepath} dosyasına yaz ve yeniden yükle", "description": "{filepath} dosyasına yaz ve yeniden yükle",
"description-missing": "Kurulum için {app} gereklidir" "description-missing": "Kurulum için {app} gereklidir"
@@ -1217,6 +1244,10 @@
"description": "Dock'un arka plan opaklığını ayarlayın.", "description": "Dock'un arka plan opaklığını ayarlayın.",
"label": "Arka plan opaklığı" "label": "Arka plan opaklığı"
}, },
"border-radius": {
"description": "Dock'un kenar yarıçapını ayarla.",
"label": "Kenar yarıçapı"
},
"colorize-icons": { "colorize-icons": {
"description": "Dock uygulama simgelerine tema renklerini uygulayın (sadece odaklanılmamış uygulamalar).", "description": "Dock uygulama simgelerine tema renklerini uygulayın (sadece odaklanılmamış uygulamalar).",
"label": "Simgeleri Renklendir" "label": "Simgeleri Renklendir"
@@ -1368,6 +1399,10 @@
"description": "Başlatıcının arka plan opaklığını ayarlayın.", "description": "Başlatıcının arka plan opaklığını ayarlayın.",
"label": "Arka plan opaklığı" "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": { "clipboard-history": {
"description": "Başlatıcıdan daha önce kopyalanan öğelere erişin.", "description": "Başlatıcıdan daha önce kopyalanan öğelere erişin.",
"label": "Pano geçmişini etkinleştir" "label": "Pano geçmişini etkinleştir"
@@ -1475,6 +1510,10 @@
"description": "Sistemi askıya alırken otomatik olarak ekranı kilitler.", "description": "Sistemi askıya alırken otomatik olarak ekranı kilitler.",
"label": "Askıya alırken kilitle" "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" "title": "Ekran Kilit"
}, },
"network": { "network": {
@@ -2038,6 +2077,10 @@
"title-matugen": "Matugen şablon işleme başarısız oldu.", "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." "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": { "wallpaper-colors": {
"disabled": "Duvar kağıdı renkleri devre dışı", "disabled": "Duvar kağıdı renkleri devre dışı",
"enabled": "Duvar kağıdı renkleri etkin", "enabled": "Duvar kağıdı renkleri etkin",
@@ -2071,6 +2114,7 @@
"input-muted": "Giriş sessizliğini değiştir", "input-muted": "Giriş sessizliğini değiştir",
"keep-awake": "Uyanık kal", "keep-awake": "Uyanık kal",
"keyboard-layout": "{layout} klavye düzeni", "keyboard-layout": "{layout} klavye düzeni",
"manage-vpn": "VPN bağlantılarını yönet",
"manage-wifi": "Wi-Fi 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.", "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şı", "move-to-center-section": "Orta bölüme taşı",
@@ -2297,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "Kullanılabilir Ağlar",
"connect": "Bağlan", "connect": "Bağlan",
"connected": "Bağlı", "connected": "Bağlı",
"disabled": "Wi-Fi devre dışı", "disabled": "Wi-Fi devre dışı",
@@ -2307,6 +2352,7 @@
"forget": "Unut", "forget": "Unut",
"forget-network": "Bu ağı unut?", "forget-network": "Bu ağı unut?",
"forgetting": "Unutuluyor...", "forgetting": "Unutuluyor...",
"known-networks": "Bilinen Ağlar",
"no-networks": "Ağ bulunamadı", "no-networks": "Ağ bulunamadı",
"password": "Şifre", "password": "Şifre",
"saved": "Kaydedildi", "saved": "Kaydedildi",

View File

@@ -123,10 +123,6 @@
"stream-description": "Введіть команду для безперервного запуску." "stream-description": "Введіть команду для безперервного запуску."
}, },
"dynamic-text": "Динамічний текст", "dynamic-text": "Динамічний текст",
"hide-vertical": {
"description": "Якщо увімкнено, текст з виводу команди не відображатиметься, коли панель знаходиться у вертикальному розташуванні (ліворуч або праворуч).",
"label": "Приховати текст у вертикальній панелі"
},
"icon": { "icon": {
"description": "Вибрати значок з бібліотеки.", "description": "Вибрати значок з бібліотеки.",
"label": "Значок" "label": "Значок"
@@ -136,6 +132,14 @@
"label": "Лівий клік", "label": "Лівий клік",
"update-text": "Оновити текст, що відображається, при натисканні лівою кнопкою миші" "update-text": "Оновити текст, що відображається, при натисканні лівою кнопкою миші"
}, },
"max-text-length-horizontal": {
"description": "Максимальна кількість символів для відображення в горизонтальній панелі (0 щоб приховати текст)",
"label": "Макс. довжина тексту (горизонтально)"
},
"max-text-length-vertical": {
"description": "Максимальна кількість символів для відображення в вертикальній панелі (0 щоб приховати текст)",
"label": "Макс. довжина тексту (вертикально)"
},
"middle-click": { "middle-click": {
"description": "Команда для виконання при середньому кліку на кнопку.", "description": "Команда для виконання при середньому кліку на кнопку.",
"label": "Середній клік", "label": "Середній клік",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "Потокові рядки з команди відображатимуться як текст на кнопці.", "description": "Потокові рядки з команди відображатимуться як текст на кнопці.",
"label": "Потік" "label": "Потік"
},
"wheel": {
"description": "Команда для виконання при використанні колеса прокрутки.\nВикористовуйте $delta для дельти колеса прокрутки в команді",
"label": "Колесо прокрутки",
"update-text": "Оновити відображуваний текст при прокрутці"
},
"wheel-down": {
"description": "Команда для виконання при прокрутці колеса вниз.",
"label": "Команда прокрутки колеса вниз"
},
"wheel-mode-separate": {
"description": "Увімкнути окремі команди для колеса прокрутки вгору та вниз",
"label": "Окремі команди колеса прокрутки"
},
"wheel-up": {
"description": "Команда для виконання при прокрутці колеса вгору.",
"label": "Команда прокрутки колеса вгору"
} }
}, },
"dialog": { "dialog": {
@@ -396,29 +417,29 @@
} }
}, },
"changelog": { "changelog": {
"error": {
"fetch-failed": "Не вдалося завантажити дані журналу змін. Будь ласка, спробуйте пізніше.",
"rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин."
},
"panel": { "panel": {
"title": "Що нового у {version}", "buttons": {
"discord": "Приєднатися до нашого Discord",
"dismiss": "Ок"
},
"empty": "Примітки до релізу ще недоступні.",
"highlight-title": "Основні зміни",
"section": {
"released": "Випущено {date}",
"version": "Версія {version}"
},
"subtitle": { "subtitle": {
"fresh": "Дякуємо, що встановили Noctalia! Ось що містить цей білд.", "fresh": "Дякуємо, що встановили Noctalia! Ось що містить цей білд.",
"updated": "Оновлено з {previousVersion}" "updated": "Оновлено з {previousVersion}"
}, },
"title": "Що нового у {version}",
"version": { "version": {
"new-user": "Нове встановлення" "new-user": "Нове встановлення"
},
"highlight-title": "Основні зміни",
"empty": "Примітки до релізу ще недоступні.",
"section": {
"version": "Версія {version}",
"released": "Випущено {date}"
},
"buttons": {
"discord": "Приєднатися до нашого Discord",
"feedback": "Надіслати відгук",
"dismiss": "Ок"
} }
},
"error": {
"rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин."
} }
}, },
"clock": { "clock": {
@@ -428,10 +449,12 @@
"activate-app": "Активувати {app}", "activate-app": "Активувати {app}",
"clear-history": "Очистити історію", "clear-history": "Очистити історію",
"close-app": "Закрити {app}", "close-app": "Закрити {app}",
"connect-vpn": "Підключитися до {name}",
"cycle-visualizer": "Візуалізатор циклів", "cycle-visualizer": "Візуалізатор циклів",
"disable-bluetooth": "Вимкнути Bluetooth", "disable-bluetooth": "Вимкнути Bluetooth",
"disable-dnd": "Вимкнути режим \"Не турбувати\"", "disable-dnd": "Вимкнути режим \"Не турбувати\"",
"disable-wifi": "Вимкнути Wi-Fi", "disable-wifi": "Вимкнути Wi-Fi",
"disconnect-vpn": "Відключити {name}",
"enable-bluetooth": "Увімкнути Bluetooth", "enable-bluetooth": "Увімкнути Bluetooth",
"enable-dnd": "Увімкнути режим \"Не турбувати\"", "enable-dnd": "Увімкнути режим \"Не турбувати\"",
"enable-wifi": "Увімкнути Wi-Fi", "enable-wifi": "Увімкнути Wi-Fi",
@@ -1021,6 +1044,10 @@
"description": "Записати {filepath}. Тему Comfy потрібно встановити та активувати вручну.", "description": "Записати {filepath}. Тему Comfy потрібно встановити та активувати вручну.",
"description-missing": "Потрібна установка {app}" "description-missing": "Потрібна установка {app}"
}, },
"telegram": {
"description": "Записати {filepath}.",
"description-missing": "Потрібна установка {app}"
},
"vicinae": { "vicinae": {
"description": "Записати {filepath} та перезавантажити", "description": "Записати {filepath} та перезавантажити",
"description-missing": "Потрібна установка {app}" "description-missing": "Потрібна установка {app}"
@@ -1217,6 +1244,10 @@
"description": "Налаштуйте непрозорість фону дока.", "description": "Налаштуйте непрозорість фону дока.",
"label": "Непрозорість фону" "label": "Непрозорість фону"
}, },
"border-radius": {
"description": "Налаштуйте радіус заокруглення країв док-панелі.",
"label": "Радіус заокруглення"
},
"colorize-icons": { "colorize-icons": {
"description": "Застосувати кольори теми до значків програм у доці (тільки неактивні програми).", "description": "Застосувати кольори теми до значків програм у доці (тільки неактивні програми).",
"label": "Розфарбувати значки" "label": "Розфарбувати значки"
@@ -1368,6 +1399,10 @@
"description": "Налаштуйте непрозорість фону запускача.", "description": "Налаштуйте непрозорість фону запускача.",
"label": "Непрозорість фону" "label": "Непрозорість фону"
}, },
"clip-preview": {
"description": "Показувати попередній перегляд вмісту буфера обміну при використанні команди >clip.",
"label": "Увімкнути попередній перегляд буфера обміну"
},
"clipboard-history": { "clipboard-history": {
"description": "Отримати доступ до раніше скопійованих елементів із запускача.", "description": "Отримати доступ до раніше скопійованих елементів із запускача.",
"label": "Увімкнути історію буфера обміну" "label": "Увімкнути історію буфера обміну"
@@ -1475,6 +1510,10 @@
"description": "Автоматично блокувати екран при призупиненні системи.", "description": "Автоматично блокувати екран при призупиненні системи.",
"label": "Блокувати при призупиненні" "label": "Блокувати при призупиненні"
}, },
"show-hibernate": {
"description": "Показувати опцію 'сплячий режим' у діях живлення.",
"label": "Показувати сплячий режим"
},
"title": "Екран блокування" "title": "Екран блокування"
}, },
"network": { "network": {
@@ -2038,6 +2077,10 @@
"title-matugen": "Помилка обробки шаблонів Matugen", "title-matugen": "Помилка обробки шаблонів Matugen",
"title-predefined": "Помилка обробки попередньо визначеної колірної схеми" "title-predefined": "Помилка обробки попередньо визначеної колірної схеми"
}, },
"vpn": {
"connected": "Підключено до '{name}'",
"disconnected": "Відключено від '{name}'"
},
"wallpaper-colors": { "wallpaper-colors": {
"disabled": "Кольори шпалер вимкнено", "disabled": "Кольори шпалер вимкнено",
"enabled": "Кольори шпалер увімкнено", "enabled": "Кольори шпалер увімкнено",
@@ -2071,6 +2114,7 @@
"input-muted": "Перемкнути вимкнення входу", "input-muted": "Перемкнути вимкнення входу",
"keep-awake": "Не спати", "keep-awake": "Не спати",
"keyboard-layout": "Розкладка клавіатури {layout}", "keyboard-layout": "Розкладка клавіатури {layout}",
"manage-vpn": "Керувати підключеннями VPN",
"manage-wifi": "Керувати Wi-Fi", "manage-wifi": "Керувати Wi-Fi",
"microphone-volume-at": "Гучність мікрофона на {volume}%\nЛівий клік для налаштувань. Правий клік для вимкнення звуку.\nПрокрутка для зміни гучності.", "microphone-volume-at": "Гучність мікрофона на {volume}%\nЛівий клік для налаштувань. Правий клік для вимкнення звуку.\nПрокрутка для зміни гучності.",
"move-to-center-section": "Перемістити в центральну секцію", "move-to-center-section": "Перемістити в центральну секцію",
@@ -2297,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "Доступні мережі",
"connect": "Підключити", "connect": "Підключити",
"connected": "Підключено", "connected": "Підключено",
"disabled": "Wi-Fi вимкнено", "disabled": "Wi-Fi вимкнено",
@@ -2307,6 +2352,7 @@
"forget": "Забути", "forget": "Забути",
"forget-network": "Забути цю мережу?", "forget-network": "Забути цю мережу?",
"forgetting": "Забування...", "forgetting": "Забування...",
"known-networks": "Відомі мережі",
"no-networks": "Мереж не знайдено", "no-networks": "Мереж не знайдено",
"password": "Пароль", "password": "Пароль",
"saved": "Збережено", "saved": "Збережено",

View File

@@ -123,10 +123,6 @@
"stream-description": "输入一个要持续运行的命令。" "stream-description": "输入一个要持续运行的命令。"
}, },
"dynamic-text": "动态文本", "dynamic-text": "动态文本",
"hide-vertical": {
"description": "如果启用,当栏处于垂直布局(左或右)时,将不显示命令输出的文本。",
"label": "在垂直栏中隐藏文本"
},
"icon": { "icon": {
"description": "从库中选择图标。", "description": "从库中选择图标。",
"label": "图标" "label": "图标"
@@ -136,6 +132,14 @@
"label": "左键点击", "label": "左键点击",
"update-text": "左键单击时更新显示的文本" "update-text": "左键单击时更新显示的文本"
}, },
"max-text-length-horizontal": {
"description": "在水平栏中显示的最大字符数0 为隐藏文本)",
"label": "最大文本长度(水平)"
},
"max-text-length-vertical": {
"description": "在垂直栏中显示的最大字符数0 为隐藏文本)",
"label": "最大文本长度(垂直)"
},
"middle-click": { "middle-click": {
"description": "中键点击按钮时执行的命令。", "description": "中键点击按钮时执行的命令。",
"label": "中键点击", "label": "中键点击",
@@ -157,6 +161,23 @@
"text-stream": { "text-stream": {
"description": "来自命令的流式输出行将作为文本显示在按钮上。", "description": "来自命令的流式输出行将作为文本显示在按钮上。",
"label": "流" "label": "流"
},
"wheel": {
"description": "使用滚轮时执行的命令。\n在命令中使用 $delta 表示滚轮增量",
"label": "滚轮",
"update-text": "滚轮滚动时更新显示的文本"
},
"wheel-down": {
"description": "滚轮向下滚动时执行的命令。",
"label": "滚轮向下命令"
},
"wheel-mode-separate": {
"description": "为滚轮向上和向下启用单独的命令",
"label": "分开滚轮命令"
},
"wheel-up": {
"description": "滚轮向上滚动时执行的命令。",
"label": "滚轮向上命令"
} }
}, },
"dialog": { "dialog": {
@@ -228,10 +249,10 @@
"notification-history": { "notification-history": {
"hide-badge-when-zero": { "hide-badge-when-zero": {
"description": "当没有未读通知时隐藏通知徽章。", "description": "当没有未读通知时隐藏通知徽章。",
"label": "时隐藏徽章" "label": "时隐藏徽章"
}, },
"show-unread-badge": { "show-unread-badge": {
"description": "显示示未读通知数量的徽章。", "description": "显示一个用于展示未读通知数量的徽章。",
"label": "显示未读徽章" "label": "显示未读徽章"
} }
}, },
@@ -396,29 +417,29 @@
} }
}, },
"changelog": { "changelog": {
"error": {
"fetch-failed": "无法加载更新日志数据,请稍后再试。",
"rate-limit": "已达到 GitHub 速率限制,请稍后再试。"
},
"panel": { "panel": {
"title": "{version} 有哪些更新", "buttons": {
"discord": "加入我们的 Discord",
"dismiss": "确定"
},
"empty": "暂时没有可用的发行说明。",
"highlight-title": "重点更新",
"section": {
"released": "{date} 发布",
"version": "版本 {version}"
},
"subtitle": { "subtitle": {
"fresh": "感谢安装 Noctalia以下是本次构建包含的内容。", "fresh": "感谢安装 Noctalia以下是本次构建包含的内容。",
"updated": "已从 {previousVersion} 更新" "updated": "已从 {previousVersion} 更新"
}, },
"title": "{version} 有哪些更新",
"version": { "version": {
"new-user": "全新安装" "new-user": "全新安装"
},
"highlight-title": "重点更新",
"empty": "暂时没有可用的发行说明。",
"section": {
"version": "版本 {version}",
"released": "{date} 发布"
},
"buttons": {
"discord": "加入我们的 Discord",
"feedback": "发送反馈",
"dismiss": "确定"
} }
},
"error": {
"rate-limit": "已达到 GitHub 速率限制,请稍后再试。"
} }
}, },
"clock": { "clock": {
@@ -428,22 +449,24 @@
"activate-app": "激活 {app}", "activate-app": "激活 {app}",
"clear-history": "清除历史记录", "clear-history": "清除历史记录",
"close-app": "关闭 {app}", "close-app": "关闭 {app}",
"cycle-visualizer": "周期可视化工具", "connect-vpn": "连接 {name}",
"cycle-visualizer": "切换可视化器样式",
"disable-bluetooth": "禁用蓝牙", "disable-bluetooth": "禁用蓝牙",
"disable-dnd": "关闭勿扰模式", "disable-dnd": "关闭勿扰模式",
"disable-wifi": "禁用Wi-Fi", "disable-wifi": "禁用Wi-Fi",
"disconnect-vpn": "断开 {name}",
"enable-bluetooth": "启用蓝牙", "enable-bluetooth": "启用蓝牙",
"enable-dnd": "启用勿扰模式", "enable-dnd": "启用勿扰模式",
"enable-wifi": "启用 Wi-Fi", "enable-wifi": "启用 Wi-Fi",
"next": "下一", "next": "下一",
"open-calendar": "打开日历", "open-calendar": "打开日历",
"open-display-settings": "显示设置", "open-display-settings": "显示设置",
"open-launcher": "打开启动器", "open-launcher": "打开启动器",
"open-mixer": "音频调音台", "open-mixer": "音频混音器",
"open-settings": "打开设置", "open-settings": "打开设置",
"pause": "暂停", "pause": "暂停",
"play": "", "play": "播放",
"previous": "上一", "previous": "上一",
"random-wallpaper": "随机壁纸", "random-wallpaper": "随机壁纸",
"toggle-mute": "切换静音", "toggle-mute": "切换静音",
"widget-settings": "小部件设置" "widget-settings": "小部件设置"
@@ -1021,6 +1044,10 @@
"description": "写入 {filepath}。Comfy 主题需要手动安装和激活。", "description": "写入 {filepath}。Comfy 主题需要手动安装和激活。",
"description-missing": "需要安装 {app}" "description-missing": "需要安装 {app}"
}, },
"telegram": {
"description": "写入 {filepath}。",
"description-missing": "需要安装 {app}"
},
"vicinae": { "vicinae": {
"description": "写入 {filepath} 并重新加载", "description": "写入 {filepath} 并重新加载",
"description-missing": "需要安装 {app}" "description-missing": "需要安装 {app}"
@@ -1217,6 +1244,10 @@
"description": "调整 Dock 的背景不透明度。", "description": "调整 Dock 的背景不透明度。",
"label": "背景不透明度" "label": "背景不透明度"
}, },
"border-radius": {
"description": "调整程序坞的边框半径。",
"label": "边框半径"
},
"colorize-icons": { "colorize-icons": {
"description": "将主题颜色应用到 Dock 应用图标(仅限非聚焦应用)。", "description": "将主题颜色应用到 Dock 应用图标(仅限非聚焦应用)。",
"label": "着色图标" "label": "着色图标"
@@ -1368,6 +1399,10 @@
"description": "调整启动器的背景不透明度。", "description": "调整启动器的背景不透明度。",
"label": "背景不透明度" "label": "背景不透明度"
}, },
"clip-preview": {
"description": "在使用 >clip 命令时显示剪贴板内容的预览。",
"label": "启用剪贴板预览"
},
"clipboard-history": { "clipboard-history": {
"description": "从启动器访问之前复制的项目。", "description": "从启动器访问之前复制的项目。",
"label": "启用剪贴板历史记录" "label": "启用剪贴板历史记录"
@@ -1475,6 +1510,10 @@
"description": "系统挂起时自动锁定屏幕。", "description": "系统挂起时自动锁定屏幕。",
"label": "挂起时锁定" "label": "挂起时锁定"
}, },
"show-hibernate": {
"description": "在电源操作中显示'休眠'选项。",
"label": "显示休眠"
},
"title": "锁屏" "title": "锁屏"
}, },
"network": { "network": {
@@ -2038,6 +2077,10 @@
"title-matugen": "Matugen模板处理失败", "title-matugen": "Matugen模板处理失败",
"title-predefined": "预定义的颜色方案处理失败" "title-predefined": "预定义的颜色方案处理失败"
}, },
"vpn": {
"connected": "已连接到“{name}”",
"disconnected": "已断开与“{name}”的连接"
},
"wallpaper-colors": { "wallpaper-colors": {
"disabled": "壁纸颜色已禁用", "disabled": "壁纸颜色已禁用",
"enabled": "壁纸颜色已启用", "enabled": "壁纸颜色已启用",
@@ -2071,6 +2114,7 @@
"input-muted": "静音输入设备", "input-muted": "静音输入设备",
"keep-awake": "保持唤醒", "keep-awake": "保持唤醒",
"keyboard-layout": "{layout} 键盘布局", "keyboard-layout": "{layout} 键盘布局",
"manage-vpn": "管理 VPN 连接",
"manage-wifi": "管理 Wi-Fi", "manage-wifi": "管理 Wi-Fi",
"microphone-volume-at": "麦克风音量 {volume}%\n左键点击进入设置。右键点击切换静音。\n滚动滚轮调节音量。", "microphone-volume-at": "麦克风音量 {volume}%\n左键点击进入设置。右键点击切换静音。\n滚动滚轮调节音量。",
"move-to-center-section": "移动到中央部分", "move-to-center-section": "移动到中央部分",
@@ -2297,6 +2341,7 @@
}, },
"wifi": { "wifi": {
"panel": { "panel": {
"available-networks": "可用网络",
"connect": "连接", "connect": "连接",
"connected": "已连接", "connected": "已连接",
"disabled": "Wi-Fi 已禁用", "disabled": "Wi-Fi 已禁用",
@@ -2307,6 +2352,7 @@
"forget": "忘记", "forget": "忘记",
"forget-network": "忘记此网络?", "forget-network": "忘记此网络?",
"forgetting": "正在忘记...", "forgetting": "正在忘记...",
"known-networks": "已知网络",
"no-networks": "未找到网络", "no-networks": "未找到网络",
"password": "密码", "password": "密码",
"saved": "已保存", "saved": "已保存",

View File

@@ -87,9 +87,6 @@
"panelsAttachedToBar": true, "panelsAttachedToBar": true,
"settingsPanelAttachToBar": false "settingsPanelAttachToBar": false
}, },
"changelog": {
"lastSeenVersion": ""
},
"location": { "location": {
"name": "Tokyo", "name": "Tokyo",
"weatherEnabled": true, "weatherEnabled": true,
@@ -143,6 +140,7 @@
}, },
"appLauncher": { "appLauncher": {
"enableClipboardHistory": false, "enableClipboardHistory": false,
"enableClipPreview": true,
"position": "center", "position": "center",
"pinnedExecs": [], "pinnedExecs": [],
"useApp2Unit": false, "useApp2Unit": false,
@@ -223,6 +221,7 @@
"enabled": true, "enabled": true,
"displayMode": "always_visible", "displayMode": "always_visible",
"backgroundOpacity": 1, "backgroundOpacity": 1,
"radiusRatio": 0.1,
"floatingRatio": 1, "floatingRatio": 1,
"size": 1, "size": 1,
"onlySameOutput": true, "onlySameOutput": true,
@@ -292,7 +291,8 @@
"visualizerType": "linear", "visualizerType": "linear",
"visualizerQuality": "high", "visualizerQuality": "high",
"mprisBlacklist": [], "mprisBlacklist": [],
"preferredPlayer": "" "preferredPlayer": "",
"externalMixer": "pwvucontrol || pavucontrol"
}, },
"brightness": { "brightness": {
"brightnessStep": 5, "brightnessStep": 5,
@@ -336,6 +336,9 @@
"manualSunrise": "06:30", "manualSunrise": "06:30",
"manualSunset": "18:30" "manualSunset": "18:30"
}, },
"changelog": {
"lastSeenVersion": ""
},
"hooks": { "hooks": {
"enabled": false, "enabled": false,
"wallpaperChange": "", "wallpaperChange": "",

View File

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

View File

@@ -296,6 +296,7 @@ Singleton {
// applauncher // applauncher
property JsonObject appLauncher: JsonObject { property JsonObject appLauncher: JsonObject {
property bool enableClipboardHistory: false property bool enableClipboardHistory: false
property bool enableClipPreview: true
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center // Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
property string position: "center" property string position: "center"
property list<string> pinnedExecs: [] property list<string> pinnedExecs: []
@@ -385,6 +386,7 @@ Singleton {
property bool enabled: true property bool enabled: true
property string displayMode: "always_visible" // "always_visible", "auto_hide", "exclusive" property string displayMode: "always_visible" // "always_visible", "auto_hide", "exclusive"
property real backgroundOpacity: 1.0 property real backgroundOpacity: 1.0
property real radiusRatio: 0.1
property real floatingRatio: 1.0 property real floatingRatio: 1.0
property real size: 1 property real size: 1
property bool onlySameOutput: true property bool onlySameOutput: true

183
Commons/ShellState.qml Normal file
View File

@@ -0,0 +1,183 @@
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 || {};
}
}

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

View File

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

View File

@@ -21,6 +21,8 @@ Item {
property bool oppositeDirection: false property bool oppositeDirection: false
property bool hovered: false property bool hovered: false
property bool rotateText: 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 // Bar position detection for pill direction
readonly property string barPosition: Settings.data.bar.position readonly property string barPosition: Settings.data.bar.position
@@ -49,10 +51,9 @@ Item {
// Sizing logic for vertical bars // Sizing logic for vertical bars
readonly property int buttonSize: Style.capsuleHeight readonly property int buttonSize: Style.capsuleHeight
readonly property int pillHeight: buttonSize 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 pillOverlap: Math.round(buttonSize * 0.5)
readonly property int maxPillWidth: rotateText ? Math.max(buttonSize, Math.round(textItem.implicitHeight + pillPaddingVertical * 2)) : buttonSize 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 + pillPaddingVertical * 2 + Math.round(iconCircle.height / 4))) : Math.max(1, Math.round(textItem.implicitHeight + pillPaddingVertical * 4)) 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: { readonly property real iconSize: {
switch (root.density) { switch (root.density) {
@@ -91,7 +92,7 @@ Item {
width: buttonSize width: buttonSize
height: revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize height: revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize
radius: halfButtonSize 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) readonly property int halfButtonSize: Math.round(buttonSize * 0.5)
@@ -130,7 +131,7 @@ Item {
id: textItem id: textItem
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: rotateText ? Math.round(iconCircle.height / 4) : getVerticalCenterOffset() anchors.verticalCenterOffset: openDownward ? Style.marginXXS : -Style.marginXXS
rotation: rotateText ? -90 : 0 rotation: rotateText ? -90 : 0
text: root.text + root.suffix text: root.text + root.suffix
family: Settings.data.ui.fontFixed family: Settings.data.ui.fontFixed
@@ -139,15 +140,12 @@ Item {
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter 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 visible: revealed
function getVerticalCenterOffset() { function getVerticalCenterOffset() {
var offset = openDownward ? Math.round(pillPaddingVertical * 0.75) : -Math.round(pillPaddingVertical * 0.75); // A small, symmetrical offset to push the text slightly away from the icon's edge.
if (forceOpen) { return openDownward ? Style.marginXS : -Style.marginXS;
offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS;
}
return offset;
} }
} }
Behavior on width { Behavior on width {
@@ -189,7 +187,7 @@ Item {
icon: root.icon icon: root.icon
pointSize: iconSize pointSize: iconSize
applyUiScale: false applyUiScale: false
color: hovered ? Color.mOnHover : Color.mOnSurface color: hovered ? (customTextIconColor.a > 0 ? customTextIconColor : Color.mOnHover) : (customTextIconColor.a > 0 ? customTextIconColor : Color.mOnSurface)
// Center horizontally // Center horizontally
x: (iconCircle.width - width) / 2 x: (iconCircle.width - width) / 2
// Center vertically accounting for font metrics // 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 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 string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold
readonly property bool isLowBattery: !charging && percent <= warningThreshold
// Test mode // Test mode
readonly property bool testMode: false readonly property bool testMode: false
readonly property int testPercent: 100 readonly property int testPercent: 15
readonly property bool testCharging: false readonly property bool testCharging: false
// Main properties // Main properties
@@ -120,6 +121,8 @@ Item {
autoHide: false autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow" forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery) forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery)
customBackgroundColor: isLowBattery ? Color.mError : Qt.rgba(0, 0, 0, 0)
customTextIconColor: isLowBattery ? Color.mOnError : Qt.rgba(0, 0, 0, 0)
tooltipText: { tooltipText: {
let lines = []; let lines = [];
if (testMode) { if (testMode) {

View File

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

View File

@@ -39,15 +39,19 @@ Item {
readonly property bool rightClickUpdateText: widgetSettings.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText readonly property bool rightClickUpdateText: widgetSettings.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
readonly property bool middleClickUpdateText: widgetSettings.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText 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 string textCommand: widgetSettings.textCommand !== undefined ? widgetSettings.textCommand : (widgetMetadata.textCommand || "")
readonly property bool textStream: widgetSettings.textStream !== undefined ? widgetSettings.textStream : (widgetMetadata.textStream || false) 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 int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000)
readonly property string textCollapse: widgetSettings.textCollapse !== undefined ? widgetSettings.textCollapse : (widgetMetadata.textCollapse || "") 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 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 || (wheelMode === "unified" && wheelExec) || (wheelMode === "separate" && (wheelUpExec || wheelDownExec)))
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
readonly property bool shouldShowText: !isVerticalBar || !hideTextInVerticalBar
implicitWidth: pill.width implicitWidth: pill.width
implicitHeight: pill.height implicitHeight: pill.height
@@ -58,9 +62,9 @@ Item {
screen: root.screen screen: root.screen
oppositeDirection: BarService.getPillDirection(root) oppositeDirection: BarService.getPillDirection(root)
icon: _dynamicIcon !== "" ? _dynamicIcon : customIcon icon: _dynamicIcon !== "" ? _dynamicIcon : customIcon
text: shouldShowText ? _dynamicText : "" text: (!isVerticalBar || currentMaxTextLength > 0) ? _dynamicText : ""
density: Settings.data.bar.density density: Settings.data.bar.density
rotateText: isVerticalBar && !hideTextInVerticalBar rotateText: isVerticalBar && currentMaxTextLength > 0
autoHide: false autoHide: false
forceOpen: _dynamicText !== "" forceOpen: _dynamicText !== ""
tooltipText: { tooltipText: {
@@ -76,6 +80,16 @@ Item {
if (middleClickExec !== "") { if (middleClickExec !== "") {
tooltipLines.push(`Middle click: ${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 !== "") { if (_dynamicTooltip !== "") {
@@ -95,6 +109,7 @@ Item {
onClicked: root.onClicked() onClicked: root.onClicked()
onRightClicked: root.onRightClicked() onRightClicked: root.onRightClicked()
onMiddleClicked: root.onMiddleClicked() onMiddleClicked: root.onMiddleClicked()
onWheel: delta => root.onWheel(delta)
} }
// Internal state for dynamic text // Internal state for dynamic text
@@ -102,12 +117,32 @@ Item {
property string _dynamicIcon: "" property string _dynamicIcon: ""
property string _dynamicTooltip: "" 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) // Periodically run the text command (if set)
Timer { Timer {
id: refreshTimer id: refreshTimer
interval: Math.max(250, textIntervalMs) interval: Math.max(250, textIntervalMs)
repeat: true repeat: true
running: shouldShowText && !textStream && textCommand && textCommand.length > 0 running: (!isVerticalBar || currentMaxTextLength > 0) && !textStream && textCommand && textCommand.length > 0
triggeredOnStart: true triggeredOnStart: true
onTriggered: root.runTextCommand() onTriggered: root.runTextCommand()
} }
@@ -116,10 +151,61 @@ Item {
Timer { Timer {
id: restartTimer id: restartTimer
interval: 1000 interval: 1000
running: shouldShowText && textStream && !textProc.running running: (!isVerticalBar || currentMaxTextLength > 0) && textStream && !textProc.running
onTriggered: root.runTextCommand() 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 { SplitParser {
id: textStdoutSplit id: textStdoutSplit
onRead: line => root.parseDynamicContent(line) onRead: line => root.parseDynamicContent(line)
@@ -162,16 +248,33 @@ Item {
let tooltip = parsed.tooltip || ""; let tooltip = parsed.tooltip || "";
if (checkCollapse(text)) { if (checkCollapse(text)) {
_scrollState.originalText = "";
_dynamicText = ""; _dynamicText = "";
_dynamicIcon = ""; _dynamicIcon = "";
_dynamicTooltip = ""; _dynamicTooltip = "";
_scrollState.needsScrolling = false;
_scrollState.phase = 0;
_scrollState.phaseCounter = 0;
return; 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; _dynamicIcon = icon;
_dynamicTooltip = toHtml(tooltip); _dynamicTooltip = toHtml(tooltip);
_scrollState.offset = 0;
return; return;
} catch (e) { } catch (e) {
Logger.w("CustomButton", `Failed to parse JSON. Content: "${lineToParse}"`); Logger.w("CustomButton", `Failed to parse JSON. Content: "${lineToParse}"`);
@@ -179,15 +282,32 @@ Item {
} }
if (checkCollapse(contentStr)) { if (checkCollapse(contentStr)) {
_scrollState.originalText = "";
_dynamicText = ""; _dynamicText = "";
_dynamicIcon = ""; _dynamicIcon = "";
_dynamicTooltip = ""; _dynamicTooltip = "";
_scrollState.needsScrolling = false;
_scrollState.phase = 0;
_scrollState.phaseCounter = 0;
return; 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 = ""; _dynamicIcon = "";
_dynamicTooltip = toHtml(contentStr); _dynamicTooltip = toHtml(contentStr);
_scrollState.offset = 0;
} }
function checkCollapse(text) { function checkCollapse(text) {
@@ -215,8 +335,8 @@ Item {
if (leftClickExec) { if (leftClickExec) {
Quickshell.execDetached(["sh", "-c", leftClickExec]); Quickshell.execDetached(["sh", "-c", leftClickExec]);
Logger.i("CustomButton", `Executing command: ${leftClickExec}`); Logger.i("CustomButton", `Executing command: ${leftClickExec}`);
} else if (!hasExec && !leftClickUpdateText) { } else if (!leftClickUpdateText) {
// No script was defined, open settings // No left click script was defined, open settings
var settingsPanel = PanelService.getPanel("settingsPanel", screen); var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel.requestedTab = SettingsPanel.Tab.Bar; settingsPanel.requestedTab = SettingsPanel.Tab.Bar;
settingsPanel.open(); settingsPanel.open();
@@ -247,17 +367,19 @@ Item {
} }
function toHtml(str) { 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)) { 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/>");
return str;
}
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 escaped;
return withBreaks;
} }
function runTextCommand() { function runTextCommand() {
@@ -268,4 +390,99 @@ Item {
textProc.command = ["sh", "-lc", textCommand]; textProc.command = ["sh", "-lc", textCommand];
textProc.running = true; 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

@@ -144,10 +144,10 @@ Item {
forceClose: displayMode === "alwaysHide" forceClose: displayMode === "alwaysHide"
tooltipText: I18n.tr("tooltips.microphone-volume-at", { tooltipText: I18n.tr("tooltips.microphone-volume-at", {
"volume": (() => { "volume": (() => {
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
const displayVolume = Math.min(maxVolume, AudioService.inputVolume); const displayVolume = Math.min(maxVolume, AudioService.inputVolume);
return Math.round(displayVolume * 100); return Math.round(displayVolume * 100);
})() })()
}) })
onWheel: function (delta) { onWheel: function (delta) {

View File

@@ -1,3 +1,4 @@
import QtQuick
import Quickshell import Quickshell
import qs.Commons import qs.Commons
import qs.Services.Media import qs.Services.Media
@@ -11,7 +12,7 @@ NIconButton {
property ShellScreen screen 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") tooltipText: ScreenRecorderService.isRecording ? I18n.tr("tooltips.click-to-stop-recording") : I18n.tr("tooltips.click-to-start-recording")
tooltipDirection: BarService.getTooltipDirection() tooltipDirection: BarService.getTooltipDirection()
density: Settings.data.bar.density density: Settings.data.bar.density
@@ -31,4 +32,32 @@ NIconButton {
} }
onClicked: handleClick() 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

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

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

@@ -0,0 +1,140 @@
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
onClicked: PanelService.getPanel("vpnPanel", screen)?.toggle(this)
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" forceClose: displayMode === "alwaysHide"
tooltipText: I18n.tr("tooltips.volume-at", { tooltipText: I18n.tr("tooltips.volume-at", {
"volume": (() => { "volume": (() => {
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
const displayVolume = Math.min(maxVolume, AudioService.volume); const displayVolume = Math.min(maxVolume, AudioService.volume);
return Math.round(displayVolume * 100); return Math.round(displayVolume * 100);
})() })()
}) })
onWheel: function (delta) { onWheel: function (delta) {

View File

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

View File

@@ -20,21 +20,19 @@ import qs.Widgets
import qs.Widgets.AudioSpectrum import qs.Widgets.AudioSpectrum
Loader { Loader {
id: lockScreen id: root
active: false active: false
// Track if triggered via deprecated IPC call Component.onCompleted: {
property bool triggeredViaDeprecatedCall: false // Register with panel service
PanelService.lockScreen = this;
}
Timer { Timer {
id: unloadAfterUnlockTimer id: unloadAfterUnlockTimer
interval: 250 interval: 250
repeat: false repeat: false
onTriggered: { onTriggered: root.active = false
lockScreen.active = false;
// Reset the deprecation flag when unlocking
lockScreen.triggeredViaDeprecatedCall = false;
}
} }
function scheduleUnloadAfterUnlock() { function scheduleUnloadAfterUnlock() {
@@ -49,7 +47,7 @@ Loader {
id: lockContext id: lockContext
onUnlocked: { onUnlocked: {
lockSession.locked = false; lockSession.locked = false;
lockScreen.scheduleUnloadAfterUnlock(); root.scheduleUnloadAfterUnlock();
lockContext.currentText = ""; lockContext.currentText = "";
} }
onFailed: { onFailed: {
@@ -59,7 +57,7 @@ Loader {
WlSessionLock { WlSessionLock {
id: lockSession id: lockSession
locked: lockScreen.active locked: root.active
WlSessionLockSurface { WlSessionLockSurface {
readonly property var now: Time.now readonly property var now: Time.now
@@ -377,64 +375,8 @@ Loader {
backgroundColor: Color.mSurface backgroundColor: Color.mSurface
clockColor: Color.mOnSurface clockColor: Color.mOnSurface
secondHandColor: Color.mPrimary secondHandColor: Color.mPrimary
} hoursFontSize: Style.fontSizeL
} minutesFontSize: Style.fontSizeL
}
// 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
} }
} }
} }
@@ -550,7 +492,7 @@ Loader {
// Bottom container with weather, password input and controls // Bottom container with weather, password input and controls
Rectangle { Rectangle {
width: 750 id: bottomContainer
height: Settings.data.general.compactLockScreen ? 120 : 220 height: Settings.data.general.compactLockScreen ? 120 : 220
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -558,6 +500,59 @@ Loader {
radius: Style.radiusL radius: Style.radiusL
color: Color.mSurface 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 + 5 buttons + 4 spacings + margins
// Plus ColumnLayout margins (14 on each side = 28 total)
// Add extra buffer to ensure password input has proper padding
property real minButtonRowWidth: buttonRowTextMeasurer.minButtonWidth > 0 ? (5 * buttonRowTextMeasurer.minButtonWidth) + 40 + (2 * Style.marginM) + 28 + (2 * Style.marginM) : 750
width: Math.max(750, minButtonRowWidth)
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 14 anchors.margins: 14
@@ -1091,6 +1086,7 @@ Loader {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24 radius: Settings.data.general.compactLockScreen ? 18 : 24
color: logoutButtonArea.containsMouse ? Color.mHover : "transparent" color: logoutButtonArea.containsMouse ? Color.mHover : "transparent"
@@ -1139,6 +1135,7 @@ Loader {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24 radius: Settings.data.general.compactLockScreen ? 18 : 24
color: suspendButtonArea.containsMouse ? Color.mHover : "transparent" color: suspendButtonArea.containsMouse ? Color.mHover : "transparent"
@@ -1187,6 +1184,7 @@ Loader {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24 radius: Settings.data.general.compactLockScreen ? 18 : 24
color: hibernateButtonArea.containsMouse ? Color.mHover : "transparent" color: hibernateButtonArea.containsMouse ? Color.mHover : "transparent"
@@ -1235,6 +1233,7 @@ Loader {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24 radius: Settings.data.general.compactLockScreen ? 18 : 24
color: rebootButtonArea.containsMouse ? Color.mHover : "transparent" color: rebootButtonArea.containsMouse ? Color.mHover : "transparent"
@@ -1283,6 +1282,7 @@ Loader {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
radius: Settings.data.general.compactLockScreen ? 18 : 24 radius: Settings.data.general.compactLockScreen ? 18 : 24
color: shutdownButtonArea.containsMouse ? Color.mError : "transparent" color: shutdownButtonArea.containsMouse ? Color.mError : "transparent"

View File

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

View File

@@ -11,6 +11,7 @@ import qs.Modules.Bar
import qs.Modules.Bar.Extras import qs.Modules.Bar.Extras
import qs.Modules.Panels.Audio import qs.Modules.Panels.Audio
import qs.Modules.Panels.Bluetooth import qs.Modules.Panels.Bluetooth
import qs.Modules.Panels.Brightness
import qs.Modules.Panels.Calendar import qs.Modules.Panels.Calendar
import qs.Modules.Panels.Changelog import qs.Modules.Panels.Changelog
import qs.Modules.Panels.ControlCenter import qs.Modules.Panels.ControlCenter
@@ -33,6 +34,7 @@ PanelWindow {
// Expose panels as readonly property aliases // Expose panels as readonly property aliases
readonly property alias audioPanel: audioPanel readonly property alias audioPanel: audioPanel
readonly property alias bluetoothPanel: bluetoothPanel readonly property alias bluetoothPanel: bluetoothPanel
readonly property alias brightnessPanel: brightnessPanel
readonly property alias calendarPanel: calendarPanel readonly property alias calendarPanel: calendarPanel
readonly property alias changelogPanel: changelogPanel readonly property alias changelogPanel: changelogPanel
readonly property alias controlCenterPanel: controlCenterPanel readonly property alias controlCenterPanel: controlCenterPanel
@@ -48,6 +50,7 @@ PanelWindow {
// Expose panel placeholders for AllBackgrounds // Expose panel placeholders for AllBackgrounds
readonly property var audioPanelPlaceholder: audioPanel.panelPlaceholder readonly property var audioPanelPlaceholder: audioPanel.panelPlaceholder
readonly property var bluetoothPanelPlaceholder: bluetoothPanel.panelPlaceholder readonly property var bluetoothPanelPlaceholder: bluetoothPanel.panelPlaceholder
readonly property var brightnessPanelPlaceholder: brightnessPanel.panelPlaceholder
readonly property var calendarPanelPlaceholder: calendarPanel.panelPlaceholder readonly property var calendarPanelPlaceholder: calendarPanel.panelPlaceholder
readonly property var changelogPanelPlaceholder: changelogPanel.panelPlaceholder readonly property var changelogPanelPlaceholder: changelogPanel.panelPlaceholder
readonly property var controlCenterPanelPlaceholder: controlCenterPanel.panelPlaceholder readonly property var controlCenterPanelPlaceholder: controlCenterPanel.panelPlaceholder
@@ -171,6 +174,12 @@ PanelWindow {
screen: root.screen screen: root.screen
} }
BrightnessPanel {
id: brightnessPanel
objectName: "brightnessPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
ControlCenterPanel { ControlCenterPanel {
id: controlCenterPanel id: controlCenterPanel
objectName: "controlCenterPanel-" + (root.screen?.name || "unknown") objectName: "controlCenterPanel-" + (root.screen?.name || "unknown")

View File

@@ -615,11 +615,7 @@ Item {
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: { duration: root.isClosing ? Style.animationFast : Style.animationNormal
if (!panelBackground.shouldAnimateHeight)
return 0;
return root.isClosing ? Style.animationFast : Style.animationNormal;
}
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: panelBackground.bezierCurve easing.bezierCurve: panelBackground.bezierCurve
} }

View File

@@ -29,10 +29,13 @@ PanelWindow {
// Use Top layer (same as MainScreen) for proper event handling // Use Top layer (same as MainScreen) for proper event handling
WlrLayershell.layer: WlrLayer.Top WlrLayershell.layer: WlrLayer.Top
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: hasDialog ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.namespace: "noctalia-" + windowType + "-" + (screen?.name || "unknown") WlrLayershell.namespace: "noctalia-" + windowType + "-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: ExclusionMode.Ignore 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 // Register with PanelService so widgets can find this window
Component.onCompleted: { Component.onCompleted: {
objectName = "popupMenuWindow-" + (screen?.name || "unknown"); objectName = "popupMenuWindow-" + (screen?.name || "unknown");

View File

@@ -21,8 +21,8 @@ Variants {
property ListModel notificationModel: NotificationService.activeList property ListModel notificationModel: NotificationService.activeList
// Loader is active when there are notifications // Always create window (but with 0x0 dimensions when no notifications)
active: notificationModel.count > 0 || delayTimer.running active: true
// Keep loader active briefly after last notification to allow animations to complete // Keep loader active briefly after last notification to allow animations to complete
Timer { Timer {
@@ -104,8 +104,8 @@ Variants {
margins.left: isLeft ? barOffsetLeft : 0 margins.left: isLeft ? barOffsetLeft : 0
margins.right: isRight ? barOffsetRight : 0 margins.right: isRight ? barOffsetRight : 0
implicitWidth: notifWidth implicitWidth: (notificationModel.count > 0 || delayTimer.running) ? notifWidth : 0
implicitHeight: notificationStack.implicitHeight + Style.marginL implicitHeight: (notificationModel.count > 0 || delayTimer.running) ? (notificationStack.implicitHeight + Style.marginL) : 0
property var animateConnection: null property var animateConnection: null

View File

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

View File

@@ -18,7 +18,11 @@ SmartPanel {
panelContent: Rectangle { panelContent: Rectangle {
color: Color.transparent 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 { ColumnLayout {
id: mainColumn id: mainColumn
@@ -83,6 +87,7 @@ SmartPanel {
// Adapter not available of disabled // Adapter not available of disabled
NBox { NBox {
id: disabledBox
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled) visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@@ -135,6 +140,7 @@ SmartPanel {
contentWidth: availableWidth contentWidth: availableWidth
ColumnLayout { ColumnLayout {
id: devicesList
width: parent.width width: parent.width
spacing: Style.marginM 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 bool hasPreviousVersion: previousVersion && previousVersion.length > 0
readonly property var releaseHighlights: UpdateService.releaseHighlights || [] readonly property var releaseHighlights: UpdateService.releaseHighlights || []
readonly property string subtitleText: hasPreviousVersion ? I18n.tr("changelog.panel.subtitle.updated", { readonly property string subtitleText: hasPreviousVersion ? I18n.tr("changelog.panel.subtitle.updated", {
"previousVersion": previousVersion "previousVersion": previousVersion
}) : I18n.tr("changelog.panel.subtitle.fresh") }) : I18n.tr("changelog.panel.subtitle.fresh")
panelContent: Rectangle { panelContent: Rectangle {
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
@@ -51,8 +51,8 @@ SmartPanel {
NText { NText {
text: I18n.tr("changelog.panel.title", { text: I18n.tr("changelog.panel.title", {
"version": currentVersion || UpdateService.currentVersion "version": currentVersion || UpdateService.currentVersion
}) })
pointSize: Style.fontSizeXL pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mPrimary color: Color.mPrimary
@@ -128,12 +128,6 @@ SmartPanel {
width: parent.width width: parent.width
spacing: Style.marginM spacing: Style.marginM
NText {
text: I18n.tr("changelog.panel.highlight-title")
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText { NText {
visible: UpdateService.fetchError !== "" visible: UpdateService.fetchError !== ""
text: UpdateService.fetchError text: UpdateService.fetchError
@@ -141,62 +135,37 @@ SmartPanel {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
Repeater { ColumnLayout {
model: releaseHighlights Layout.fillWidth: true
delegate: ColumnLayout { spacing: Style.marginS
width: parent.width
spacing: Style.marginS
NText { Repeater {
text: I18n.tr("changelog.panel.section.version", { model: releaseHighlights
"version": modelData.version || I18n.tr("system.unknown-version") delegate: ColumnLayout {
}) width: parent.width
font.weight: Style.fontWeightBold spacing: Style.marginXS
color: Color.mOnSurface
}
NText { Repeater {
visible: modelData.date && modelData.date.length > 0 model: modelData.entries
text: I18n.tr("changelog.panel.section.released", { delegate: NText {
"date": root.formatReleaseDate(modelData.date) readonly property int headingLevel: root.headingLevel(modelData)
}) text: {
color: Color.mOnSurfaceVariant if (modelData.length === 0)
pointSize: Style.fontSizeXS return "\u00A0";
} if (headingLevel > 0)
return modelData.replace(/^#+\s+/, "");
Repeater { return modelData;
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
} }
}
NText {
text: modelData
color: Color.mOnSurface
wrapMode: Text.WordWrap 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 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: { onClosed: {
if (GitHubService && GitHubService.clearReleaseCache) {
GitHubService.clearReleaseCache();
}
if (UpdateService && UpdateService.changelogCurrentVersion) { if (UpdateService && UpdateService.changelogCurrentVersion) {
UpdateService.markChangelogSeen(UpdateService.changelogCurrentVersion); UpdateService.markChangelogSeen(UpdateService.changelogCurrentVersion);
} }
@@ -262,4 +240,3 @@ SmartPanel {
} }
} }
} }

View File

@@ -117,6 +117,7 @@ NBox {
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
elide: Text.ElideRight elide: Text.ElideRight
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: 0
} }
} }
@@ -165,6 +166,7 @@ NBox {
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
elide: Text.ElideRight elide: Text.ElideRight
Layout.fillWidth: true 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 { SmartPanel {
id: root id: root
readonly property bool previewActive: searchText.startsWith(">clip") && Settings.data.appLauncher.enableClipPreview
// Panel configuration // 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) preferredHeight: Math.round(600 * Style.uiScaleRatio)
preferredWidthRatio: 0.3 preferredWidthRatio: 0.3
preferredHeightRatio: 0.5 preferredHeightRatio: 0.5
@@ -262,13 +268,50 @@ SmartPanel {
} }
} }
// UI
panelContent: Rectangle { panelContent: Rectangle {
id: ui id: ui
color: Color.transparent color: Color.transparent
opacity: resultsReady ? 1.0 : 0.0 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 { MouseArea {
id: mouseMovementDetector id: mouseMovementDetector
anchors.fill: parent anchors.fill: parent
@@ -282,7 +325,6 @@ SmartPanel {
property bool initialized: false property bool initialized: false
onPositionChanged: mouse => { onPositionChanged: mouse => {
// Store initial position
if (!initialized) { if (!initialized) {
lastX = mouse.x; lastX = mouse.x;
lastY = mouse.y; lastY = mouse.y;
@@ -290,7 +332,6 @@ SmartPanel {
return; return;
} }
// Check if mouse actually moved
const deltaX = Math.abs(mouse.x - lastX); const deltaX = Math.abs(mouse.x - lastX);
const deltaY = Math.abs(mouse.y - lastY); const deltaY = Math.abs(mouse.y - lastY);
if (deltaX > 1 || deltaY > 1) { if (deltaX > 1 || deltaY > 1) {
@@ -300,7 +341,6 @@ SmartPanel {
} }
} }
// Reset when launcher opens
Connections { Connections {
target: root target: root
function onOpened() { function onOpened() {
@@ -329,298 +369,302 @@ SmartPanel {
} }
} }
ColumnLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginL anchors.margins: Style.marginL // Apply overall margins here
spacing: Style.marginM spacing: Style.marginM // Apply spacing between elements here
NTextInput { // Left Pane
id: searchInput ColumnLayout {
Layout.fillWidth: true id: leftPane
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
Layout.fillHeight: true Layout.fillHeight: true
spacing: Style.marginXXS Layout.preferredWidth: root.listPanelWidth
model: results spacing: Style.marginM
currentIndex: selectedIndex
cacheBuffer: resultsList.height * 2 NTextInput {
onCurrentIndexChanged: { id: searchInput
cancelFlick(); Layout.fillWidth: true
if (currentIndex >= 0) {
positionViewAtIndex(currentIndex, ListView.Contain); 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 { NListView {
id: entry id: resultsList
property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex) horizontalPolicy: ScrollBar.AlwaysOff
// Accessor for app id verticalPolicy: ScrollBar.AsNeeded
property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
// Pin helpers Layout.fillWidth: true
function togglePin(appId) { Layout.fillHeight: true
if (!appId) spacing: Style.marginXXS
return; model: results
let arr = (Settings.data.dock.pinnedApps || []).slice(); currentIndex: selectedIndex
const idx = arr.indexOf(appId); cacheBuffer: resultsList.height * 2
if (idx >= 0) onCurrentIndexChanged: {
arr.splice(idx, 1); cancelFlick();
else if (currentIndex >= 0) {
arr.push(appId); positionViewAtIndex(currentIndex, ListView.Contain);
Settings.data.dock.pinnedApps = arr; }
} if (clipboardPreviewLoader.item) {
clipboardPreviewLoader.item.currentItem = results[currentIndex] || null;
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);
} }
} }
onModelChanged: {}
width: resultsList.width - Style.marginS delegate: Rectangle {
implicitHeight: entryHeight id: entry
radius: Style.radiusM
color: entry.isSelected ? Color.mHover : Color.mSurface
Behavior on color { property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex)
ColorAnimation { property string appId: (modelData && modelData.appId) ? String(modelData.appId) : ""
duration: Style.animationFast
easing.type: Easing.OutCirc // 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 { function isPinned(appId) {
id: contentLayout const arr = Settings.data.dock.pinnedApps || [];
anchors.fill: parent return appId && arr.indexOf(appId) >= 0;
anchors.margins: Style.marginM }
spacing: Style.marginM
// Top row - Main entry content with pin button // Property to reliably track the current item's ID.
RowLayout { // This changes whenever the delegate is recycled for a new item.
Layout.fillWidth: true 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 spacing: Style.marginM
// Icon badge or Image preview // Top row - Main entry content with pin button
Rectangle { RowLayout {
Layout.preferredWidth: badgeSize Layout.fillWidth: true
Layout.preferredHeight: badgeSize spacing: Style.marginM
radius: Style.radiusM
color: Color.mSurfaceVariant
// Image preview for clipboard images // Icon badge or Image preview
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
Rectangle { Rectangle {
visible: modelData.isImage && imagePreview.visible Layout.preferredWidth: badgeSize
anchors.bottom: parent.bottom Layout.preferredHeight: badgeSize
anchors.right: parent.right
anchors.margins: 2
width: formatLabel.width + 6
height: formatLabel.height + 2
radius: Style.radiusM radius: Style.radiusM
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
NText { // Image preview for clipboard images
id: formatLabel NImageRounded {
anchors.centerIn: parent id: imagePreview
text: { anchors.fill: parent
if (!modelData.isImage) visible: modelData.isImage
return ""; imageRadius: Style.radiusM
const desc = modelData.description || "";
const parts = desc.split(" • "); // This property creates a dependency on the service's revision counter
return parts[0] || "IMG"; 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 || 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
}
}
}
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
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 // Text content
ColumnLayout { 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 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 { // Pin/Unpin action icon button
text: modelData.description || "" NIconButton {
pointSize: Style.fontSizeS visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0)
color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
elide: Text.ElideRight icon: entry.isPinned(entry.appId) ? "unpin" : "pin"
Layout.fillWidth: true tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin")
visible: text !== "" 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 { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
z: -1 z: -1
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onEntered: { onEntered: {
if (!root.ignoreMouseHover) { if (!root.ignoreMouseHover) {
selectedIndex = index; selectedIndex = index;
}
} }
} onClicked: mouse => {
onClicked: mouse => { if (mouse.button === Qt.LeftButton) {
if (mouse.button === Qt.LeftButton) { selectedIndex = index;
selectedIndex = index; root.activate();
root.activate(); mouse.accepted = true;
mouse.accepted = true; }
} }
} acceptedButtons: Qt.LeftButton
acceptedButtons: Qt.LeftButton }
} }
} }
}
NDivider { NDivider {
Layout.fillWidth: true Layout.fillWidth: true
} }
// Status NText {
NText { Layout.fillWidth: true
Layout.fillWidth: true text: {
text: { if (results.length === 0)
if (results.length === 0) return searchText ? "No results" : "";
return searchText ? "No results" : ""; const prefix = activePlugin?.name ? `${activePlugin.name}: ` : "";
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : ""; return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`;
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; return results;
} }
// Helper: Format image clipboard entry
function formatImageEntry(item) { function formatImageEntry(item) {
const meta = parseImageMeta(item.preview); 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 { return {
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image", "name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
"description": meta ? `${meta.fmt} ${meta.size}` : item.mime || "Image data", "description": meta ? `${meta.fmt} ${meta.size}` : item.mime || "Image data",
@@ -218,22 +215,20 @@ Item {
"imageWidth": meta ? meta.w : 0, "imageWidth": meta ? meta.w : 0,
"imageHeight": meta ? meta.h : 0, "imageHeight": meta ? meta.h : 0,
"clipboardId": item.id, "clipboardId": item.id,
"mime": item.mime "mime": item.mime,
"preview": item.preview
}; };
} }
// Helper: Format text clipboard entry with preview
function formatTextEntry(item) { function formatTextEntry(item) {
const preview = (item.preview || "").trim(); const preview = (item.preview || "").trim();
const lines = preview.split('\n').filter(l => l.trim()); const lines = preview.split('\n').filter(l => l.trim());
// Use first line as title, limit length
let title = lines[0] || "Empty text"; let title = lines[0] || "Empty text";
if (title.length > 60) { if (title.length > 60) {
title = title.substring(0, 57) + "..."; title = title.substring(0, 57) + "...";
} }
// Use second line or character count as description
let description = ""; let description = "";
if (lines.length > 1) { if (lines.length > 1) {
description = lines[1]; description = lines[1];
@@ -250,11 +245,12 @@ Item {
"name": title, "name": title,
"description": description, "description": description,
"icon": "text-x-generic", "icon": "text-x-generic",
"isImage": false "isImage": false,
"clipboardId": item.id,
"preview": preview
}; };
} }
// Helper: Parse image metadata from preview string
function parseImageMeta(preview) { function parseImageMeta(preview) {
const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i; 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); 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) { function getImageForItem(clipboardId) {
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null; return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null;
} }

View File

@@ -29,6 +29,8 @@ Popup {
if (widgetData && widgetId) { if (widgetData && widgetId) {
loadWidgetSettings(); loadWidgetSettings();
} }
// Request focus to ensure keyboard input works
forceActiveFocus();
} }
background: Rectangle { background: Rectangle {
@@ -40,71 +42,108 @@ Popup {
border.width: Style.borderM border.width: Style.borderM
} }
contentItem: ColumnLayout { contentItem: FocusScope {
id: content id: focusScope
focus: true
width: parent.width ColumnLayout {
spacing: Style.marginM id: content
anchors.fill: parent
// 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
spacing: Style.marginM spacing: Style.marginM
Item { // Title
RowLayout {
Layout.fillWidth: true 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 { // Separator
text: I18n.tr("bar.widget-settings.dialog.cancel") Rectangle {
outlined: true Layout.fillWidth: true
onClicked: root.close() Layout.preferredHeight: 1
color: Color.mOutline
} }
NButton { // Settings based on widget type
text: I18n.tr("bar.widget-settings.dialog.apply") // Will be triggered via settingsLoader.setSource()
icon: "check" Loader {
onClicked: { id: settingsLoader
if (settingsLoader.item && settingsLoader.item.saveSettings) { Layout.fillWidth: true
var newSettings = settingsLoader.item.saveSettings(); onLoaded: {
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings); // Try to focus the first focusable item in the loaded settings
root.close(); 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 string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
property bool valueTextStream: widgetData.textStream !== undefined ? widgetData.textStream : widgetMetadata.textStream property bool valueTextStream: widgetData.textStream !== undefined ? widgetData.textStream : widgetMetadata.textStream
property bool valueParseJson: widgetData.parseJson !== undefined ? widgetData.parseJson : widgetMetadata.parseJson 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() { function saveSettings() {
var settings = Object.assign({}, widgetData || {}); var settings = Object.assign({}, widgetData || {});
@@ -27,11 +28,21 @@ ColumnLayout {
settings.rightClickUpdateText = rightClickUpdateText.checked; settings.rightClickUpdateText = rightClickUpdateText.checked;
settings.middleClickExec = middleClickExecInput.text; settings.middleClickExec = middleClickExecInput.text;
settings.middleClickUpdateText = middleClickUpdateText.checked; 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.textCommand = textCommandInput.text;
settings.textCollapse = textCollapseInput.text; settings.textCollapse = textCollapseInput.text;
settings.textStream = valueTextStream; settings.textStream = valueTextStream;
settings.parseJson = valueParseJson; settings.parseJson = valueParseJson;
settings.hideTextInVerticalBar = valueHideTextInVerticalBar; settings.maxTextLength = {
"horizontal": valueMaxTextLengthHorizontal,
"vertical": valueMaxTextLengthVertical
};
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10); settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10);
return settings; return settings;
} }
@@ -137,6 +148,104 @@ ColumnLayout {
} }
} }
// 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 { NDivider {
Layout.fillWidth: true Layout.fillWidth: true
} }
@@ -145,11 +254,22 @@ ColumnLayout {
label: I18n.tr("bar.widget-settings.custom-button.dynamic-text") label: I18n.tr("bar.widget-settings.custom-button.dynamic-text")
} }
NToggle { NSpinBox {
label: I18n.tr("bar.widget-settings.custom-button.hide-vertical.label", "Hide text in vertical bar") 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.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).") 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)")
checked: valueHideTextInVerticalBar from: 0
onToggled: checked => valueHideTextInVerticalBar = checked 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 { NToggle {

View File

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

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

View File

@@ -26,8 +26,7 @@ Popup {
property var schemeColorsCache: ({}) property var schemeColorsCache: ({})
property int cacheVersion: 0 property int cacheVersion: 0
// Cache for available schemes list // Cache for available schemes list (uses ShellState singleton)
property string schemesCacheFile: Settings.cacheDir + "color-schemes-list.json"
property int schemesCacheUpdateFrequency: 2 * 60 * 60 // 2 hours in seconds property int schemesCacheUpdateFrequency: 2 * 60 * 60 // 2 hours in seconds
// Cache for repo branch info (to reduce API calls during downloads) // Cache for repo branch info (to reduce API calls during downloads)
@@ -99,33 +98,6 @@ Popup {
xhr.send(); 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 { background: Rectangle {
color: Color.mSurface color: Color.mSurface
@@ -135,58 +107,96 @@ Popup {
} }
function loadSchemesFromCache() { 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 // Check if cache is expired or missing
if (!schemesCacheAdapter.timestamp || (now >= schemesCacheAdapter.timestamp + schemesCacheUpdateFrequency)) { if (!cachedTimestamp || (now >= cachedTimestamp + schemesCacheUpdateFrequency)) {
// Only fetch from API if we haven't fetched recently (prevent rapid repeated calls) // Try migration first if cache is empty
const timeSinceLastFetch = now - lastApiFetchTime; if (cachedSchemes.length === 0) {
if (timeSinceLastFetch >= minApiFetchInterval) { migrateFromOldSchemesList();
Logger.d("ColorSchemeDownload", "Cache expired or missing, fetching new schemes"); }
fetchAvailableSchemesFromAPI();
return; // Only fetch from API if we haven't fetched recently (prevent rapid repeated calls)
} else { const timeSinceLastFetch = now - lastApiFetchTime;
// Use cached data even if expired, to avoid rate limits if (timeSinceLastFetch >= minApiFetchInterval) {
Logger.d("ColorSchemeDownload", "Cache expired but recent API call detected, using cached data"); Logger.d("ColorSchemeDownload", "Cache expired or missing, fetching new schemes");
if (schemesCacheAdapter.schemes && schemesCacheAdapter.schemes.length > 0) { fetchAvailableSchemesFromAPI();
availableSchemes = schemesCacheAdapter.schemes;
hasInitialData = true;
fetching = false;
return; 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); const ageMinutes = Math.round((now - cachedTimestamp) / 60);
Logger.d("ColorSchemeDownload", "Loading cached schemes (age:", ageMinutes, "minutes)"); Logger.d("ColorSchemeDownload", "Loading cached schemes from ShellState (age:", ageMinutes, "minutes)");
if (schemesCacheAdapter.schemes && schemesCacheAdapter.schemes.length > 0) { if (cachedSchemes.length > 0) {
availableSchemes = schemesCacheAdapter.schemes; availableSchemes = cachedSchemes;
hasInitialData = true; 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; 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 migrateFromOldSchemesList() {
const oldSchemesPath = Settings.cacheDir + "color-schemes-list.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
FileView {
id: migrationView
path: "${oldSchemesPath}"
printErrors: false
adapter: JsonAdapter {
property var schemes: []
property real timestamp: 0
}
onLoaded: {
root.availableSchemes = adapter.schemes || [];
root.saveSchemesToCache();
Logger.i("ColorSchemeDownload", "Migrated color-schemes-list.json to ShellState");
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "schemesMigrationView");
}
function saveSchemesToCache() { function saveSchemesToCache() {
schemesCacheAdapter.schemes = availableSchemes; try {
schemesCacheAdapter.timestamp = Time.timestamp; ShellState.setColorSchemesList({
schemes: availableSchemes,
// Ensure cache directory exists timestamp: Time.timestamp
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); });
Logger.d("ColorSchemeDownload", "Schemes list saved to ShellState");
Qt.callLater(() => { } catch (error) {
schemesCacheFileView.writeAdapter(); Logger.e("ColorSchemeDownload", "Failed to save schemes to cache:", error);
Logger.d("ColorSchemeDownload", "Schemes list saved to cache"); }
});
} }
function fetchAvailableSchemes() { function fetchAvailableSchemes() {
@@ -194,19 +204,11 @@ Popup {
return; return;
} }
// Path is set when popup becomes visible, so FileView will start loading // Try to load from ShellState cache first
// Try to load from cache first if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
if (schemesCacheFileView.loaded) {
loadSchemesFromCache(); 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 { } else {
// No cache file path, fetch directly from API // ShellState not ready, fetch directly from API
fetchAvailableSchemesFromAPI(); fetchAvailableSchemesFromAPI();
} }
} }
@@ -521,7 +523,7 @@ Popup {
return; return;
} }
var targetDir = ColorSchemeService.schemesDirectory + "/" + schemeName; var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName;
var downloadScript = "mkdir -p '" + targetDir + "'\n"; var downloadScript = "mkdir -p '" + targetDir + "'\n";
// Build download script for all files // Build download script for all files
@@ -552,6 +554,7 @@ Popup {
var downloadProcess = Qt.createQmlObject(` var downloadProcess = Qt.createQmlObject(`
import QtQuick import QtQuick
import Quickshell.Io import Quickshell.Io
import qs.Commons
Process { Process {
id: downloadProcess id: downloadProcess
command: ["sh", "-c", ` + JSON.stringify(downloadScript) + `] command: ["sh", "-c", ` + JSON.stringify(downloadScript) + `]
@@ -611,6 +614,17 @@ Popup {
return false; 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) { function deleteScheme(schemeName) {
if (downloading) { if (downloading) {
return; return;
@@ -623,7 +637,8 @@ Popup {
var deletedSchemeDisplayName = ColorSchemeService.getBasename(schemeName); var deletedSchemeDisplayName = ColorSchemeService.getBasename(schemeName);
var needsReset = (currentScheme === deletedSchemeDisplayName); 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 deleteScript = "rm -rf '" + targetDir + "'";
var deleteProcess = Qt.createQmlObject(` var deleteProcess = Qt.createQmlObject(`
@@ -712,7 +727,25 @@ Popup {
} }
onAvailableSchemesChanged: preFetchSchemeColors() 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 { contentItem: ColumnLayout {
id: contentColumn id: contentColumn
@@ -896,12 +929,14 @@ Popup {
NIconButton { NIconButton {
property bool isDownloading: downloading && downloadingScheme === schemeRow.schemeName property bool isDownloading: downloading && downloadingScheme === schemeRow.schemeName
property bool isInstalled: root.isSchemeInstalled(schemeRow.schemeName) property bool isInstalled: root.isSchemeInstalled(schemeRow.schemeName)
property bool isDownloaded: root.isSchemeDownloaded(schemeRow.schemeName)
icon: isDownloading ? "" : (isInstalled ? "trash" : "download") icon: isDownloading ? "" : (isDownloaded ? "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")) 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 enabled: !downloading
Layout.alignment: Qt.AlignVCenter 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 { NBusyIndicator {
anchors.centerIn: parent anchors.centerIn: parent

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 { ColumnLayout {
visible: Settings.data.dock.enabled visible: Settings.data.dock.enabled
spacing: Style.marginXXS spacing: Style.marginXXS

View File

@@ -65,6 +65,13 @@ ColumnLayout {
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked 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 { NToggle {
label: I18n.tr("settings.launcher.settings.sort-by-usage.label") label: I18n.tr("settings.launcher.settings.sort-by-usage.label")
description: I18n.tr("settings.launcher.settings.sort-by-usage.description") description: I18n.tr("settings.launcher.settings.sort-by-usage.description")

View File

@@ -145,17 +145,17 @@ ColumnLayout {
}, },
{ {
"key": "6", "key": "6",
"name": I18n.locale.dayName(6, Locale.LongFormat) "name": I18n.locale.dayName(6, Locale.LongFormat).trim()
} // Saturday } // Saturday
, ,
{ {
"key": "0", "key": "0",
"name": I18n.locale.dayName(0, Locale.LongFormat) "name": I18n.locale.dayName(0, Locale.LongFormat).trim()
} // Sunday } // Sunday
, ,
{ {
"key": "1", "key": "1",
"name": I18n.locale.dayName(1, Locale.LongFormat) "name": I18n.locale.dayName(1, Locale.LongFormat).trim()
} // Monday } // Monday
] ]
onSelected: key => Settings.data.location.firstDayOfWeek = parseInt(key) onSelected: key => Settings.data.location.firstDayOfWeek = parseInt(key)

View File

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

View File

@@ -184,9 +184,15 @@ SmartPanel {
if (!modelData.onlyMenu) { if (!modelData.onlyMenu) {
modelData.activate(); modelData.activate();
} }
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
PanelService.openedPanel.close();
}
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
// Middle click: activate with middle button // Middle click: activate with middle button
modelData.secondaryActivate && modelData.secondaryActivate(); modelData.secondaryActivate && modelData.secondaryActivate();
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
PanelService.openedPanel.close();
}
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
// Right click: open context menu // Right click: open context menu
TooltipService.hideImmediately(); 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) preferredHeight: Math.round(500 * Style.uiScaleRatio)
property string passwordSsid: "" property string passwordSsid: ""
property string passwordInput: ""
property string expandedSsid: "" 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 { panelContent: Rectangle {
color: Color.transparent 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 { ColumnLayout {
id: mainColumn id: mainColumn
@@ -116,14 +172,13 @@ SmartPanel {
} }
} }
// Main content area // WiFi disabled state
NBox { NBox {
visible: !Settings.data.network.wifiEnabled
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
// WiFi disabled state
ColumnLayout { ColumnLayout {
visible: !Settings.data.network.wifiEnabled
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginM anchors.margins: Style.marginM
@@ -158,10 +213,15 @@ SmartPanel {
Layout.fillHeight: true 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 { ColumnLayout {
visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginM anchors.margins: Style.marginM
spacing: Style.marginL spacing: Style.marginL
@@ -188,374 +248,15 @@ SmartPanel {
Layout.fillHeight: true Layout.fillHeight: true
} }
} }
}
// Networks list container // Empty state when no networks (only show after we've had networks before, meaning a real empty result)
NScrollView { NBox {
visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0) visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 && root.hasHadNetworks
anchors.fill: parent Layout.fillWidth: true
anchors.margins: Style.marginM Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: 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 { ColumnLayout {
visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0
anchors.fill: parent anchors.fill: parent
spacing: Style.marginL 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

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

View File

@@ -32,21 +32,26 @@ Singleton {
// Backend service loader // Backend service loader
property var backend: null property var backend: null
// Cache file path
property string displayCachePath: ""
Component.onCompleted: { Component.onCompleted: {
// Setup cache path (needs Settings to be available) // Load display scales from ShellState
Qt.callLater(() => { Qt.callLater(() => {
if (typeof Settings !== 'undefined' && Settings.cacheDir) { if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
displayCachePath = Settings.cacheDir + "display.json"; loadDisplayScalesFromState();
displayCacheFileView.path = displayCachePath;
} }
}); });
detectCompositor(); detectCompositor();
} }
Connections {
target: typeof ShellState !== 'undefined' ? ShellState : null
function onIsLoadedChanged() {
if (ShellState.isLoaded) {
loadDisplayScalesFromState();
}
}
}
function detectCompositor() { function detectCompositor() {
const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE"); const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE");
const niriSocket = Quickshell.env("NIRI_SOCKET"); const niriSocket = Quickshell.env("NIRI_SOCKET");
@@ -99,29 +104,50 @@ Singleton {
} }
} }
// Cache FileView for display scales // Load display scales from ShellState
FileView { function loadDisplayScalesFromState() {
id: displayCacheFileView try {
printErrors: false const cached = ShellState.getDisplay();
watchChanges: false if (cached && Object.keys(cached).length > 0) {
displayScales = cached;
adapter: JsonAdapter { displayScalesLoaded = true;
id: displayCacheAdapter Logger.d("CompositorService", "Loaded display scales from ShellState");
property var displays: ({}) } else {
} // Try to migrate from old display.json if it exists
migrateFromOldDisplayFile();
onLoaded: { }
// Load cached display scales } catch (error) {
displayScales = displayCacheAdapter.displays || {}; Logger.e("CompositorService", "Failed to load display scales:", error);
displayScalesLoaded = true; displayScalesLoaded = true;
// Logger.i("CompositorService", "Loaded display scales from cache:", JSON.stringify(displayScales))
} }
}
onLoadFailed: { // Migration from old display.json file
// Cache doesn't exist yet, will be created on first update function migrateFromOldDisplayFile() {
displayScalesLoaded = true; const oldDisplayPath = Settings.cacheDir + "display.json";
// Logger.i("CompositorService", "No display cache found, will create on first update") const migrationFileView = Qt.createQmlObject(`
} import QtQuick
import Quickshell.Io
FileView {
id: migrationView
path: "${oldDisplayPath}"
printErrors: false
adapter: JsonAdapter {
property var displays: ({})
}
onLoaded: {
parent.displayScales = adapter.displays || {};
parent.displayScalesLoaded = true;
parent.saveDisplayScalesToCache();
Logger.i("CompositorService", "Migrated display.json to ShellState");
migrationView.destroy();
}
onLoadFailed: {
parent.displayScalesLoaded = true;
migrationView.destroy();
}
}
`, root, "migrationFileView");
} }
// Hyprland backend component // Hyprland backend component
@@ -234,12 +260,12 @@ Singleton {
// Save display scales to cache // Save display scales to cache
function saveDisplayScalesToCache() { function saveDisplayScalesToCache() {
if (!displayCachePath) { try {
return; 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 // Public function to get scale for a specific display

View File

@@ -120,24 +120,8 @@ Item {
// New preferred method - lock the screen // New preferred method - lock the screen
function lock() { function lock() {
// Only lock if not already locked (prevents the red screen issue) // Only lock if not already locked (prevents the red screen issue)
// Note: No unlock via IPC for security reasons if (!PanelService.lockScreen.active) {
if (!lockScreen.active) { PanelService.lockScreen.active = true;
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;
} }
} }
} }

View File

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

View File

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

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") {
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 Quickshell.Io
import qs.Commons import qs.Commons
// GitHub API logic and caching // GitHub API logic for contributors
Singleton { Singleton {
id: root id: root
property string githubDataFile: Quickshell.env("NOCTALIA_GITHUB_FILE") || (Settings.cacheDir + "github.json") property string githubDataFile: Quickshell.env("NOCTALIA_GITHUB_FILE") || (Settings.cacheDir + "github.json")
property int githubUpdateFrequency: 60 * 60 // 1 hour expressed in seconds property int githubUpdateFrequency: 60 * 60 // 1 hour expressed in seconds
property bool isFetchingData: false property bool isFetchingData: false
property bool isReleasesFetching: false
readonly property alias data: adapter // Used to access via GitHubService.data.xxx.yyy readonly property alias data: adapter // Used to access via GitHubService.data.xxx.yyy
// Public properties for easy access // Public properties for easy access
property string latestVersion: I18n.tr("system.unknown-version") property string latestVersion: I18n.tr("system.unknown-version")
property var contributors: [] property var contributors: []
property string releaseNotes: ""
property var releases: []
property string releaseFetchError: ""
FileView { FileView {
id: githubDataFileView id: githubDataFileView
@@ -48,8 +44,6 @@ Singleton {
property string version: I18n.tr("system.unknown-version") property string version: I18n.tr("system.unknown-version")
property var contributors: [] property var contributors: []
property string releaseNotes: ""
property var releases: []
property real timestamp: 0 property real timestamp: 0
} }
} }
@@ -71,15 +65,6 @@ Singleton {
if (data.contributors) { if (data.contributors) {
root.contributors = 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) { if (needsRefetch) {
fetchFromGitHub(); fetchFromGitHub();
@@ -96,14 +81,13 @@ Singleton {
isFetchingData = true; isFetchingData = true;
versionProcess.running = true; versionProcess.running = true;
contributorsProcess.running = true; contributorsProcess.running = true;
fetchAllReleases();
} }
// -------------------------------- // --------------------------------
function saveData() { function saveData() {
data.timestamp = Time.timestamp; data.timestamp = Time.timestamp;
Logger.d("GitHub", "Saving data to cache file:", githubDataFile); 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 // Ensure cache directory exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); 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() { function resetCache() {
data.version = I18n.tr("system.unknown-version"); data.version = I18n.tr("system.unknown-version");
data.contributors = []; data.contributors = [];
data.releaseNotes = "";
data.releases = [];
data.timestamp = 0; data.timestamp = 0;
// Try to fetch immediately // Try to fetch immediately
fetchFromGitHub(); fetchFromGitHub();
} }
function clearReleaseCache() {
Logger.d("GitHub", "Clearing cached release data");
data.releases = [];
root.releases = [];
githubDataFileView.writeAdapter();
}
Process { Process {
id: versionProcess id: versionProcess
@@ -152,15 +136,9 @@ Singleton {
Logger.d("GitHub", "Latest version fetched from GitHub:", version); Logger.d("GitHub", "Latest version fetched from GitHub:", version);
} else if (data.message) { } else if (data.message) {
Logger.w("GitHub", "Latest release fetch warning:", data.message); Logger.w("GitHub", "Latest release fetch warning:", data.message);
handleRateLimitError(data.message);
} else { } else {
Logger.w("GitHub", "No tag_name in GitHub response"); Logger.w("GitHub", "No tag_name in GitHub response");
} }
if (data.body) {
root.data.releaseNotes = data.body;
root.releaseNotes = root.data.releaseNotes;
}
} else { } else {
Logger.w("GitHub", "Empty response from GitHub API"); 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 id: root
// Version properties // Version properties
property string baseVersion: "3.2.0" readonly property string baseVersion: "3.2.0"
property bool isDevelopment: false readonly property bool isDevelopment: true
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` readonly property string developmentSuffix: "-git"
property string changelogStateFile: Quickshell.env("NOCTALIA_CHANGELOG_STATE_FILE") || (Settings.cacheDir + "changelog-state.json") 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 // Changelog properties
property bool initialized: false property bool initialized: false
@@ -24,11 +29,8 @@ Singleton {
property string previousVersion: "" property string previousVersion: ""
property string changelogCurrentVersion: "" property string changelogCurrentVersion: ""
property var releaseHighlights: [] property var releaseHighlights: []
property string releaseNotesUrl: ""
property string discordUrl: "https://discord.noctalia.dev"
property string lastShownVersion: "" property string lastShownVersion: ""
property bool popupScheduled: false property bool popupScheduled: false
property string feedbackUrl: Quickshell.env("NOCTALIA_CHANGELOG_FEEDBACK_URL") || ""
property string fetchError: "" property string fetchError: ""
property string changelogLastSeenVersion: "" property string changelogLastSeenVersion: ""
property bool changelogStateLoaded: false property bool changelogStateLoaded: false
@@ -41,59 +43,27 @@ Singleton {
signal popupQueued(string fromVersion, string toVersion) 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() { function init() {
if (initialized) if (initialized)
return; return;
initialized = true; initialized = true;
Logger.i("UpdateService", "Version:", root.currentVersion); Logger.i("UpdateService", "Version:", root.currentVersion);
// Load changelog state from ShellState
Qt.callLater(() => {
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
loadChangelogState();
}
});
} }
Connections { Connections {
target: GitHubService ? GitHubService : null target: typeof ShellState !== 'undefined' ? ShellState : null
function onReleaseNotesChanged() { function onIsLoadedChanged() {
rebuildHighlights(); if (ShellState.isLoaded) {
} loadChangelogState();
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);
} }
changelogStateLoaded = true;
if (pendingShowRequest) {
pendingShowRequest = false;
Qt.callLater(root.showLatestChangelog);
}
}
JsonAdapter {
id: changelogStateAdapter
property string lastSeenVersion: ""
} }
} }
@@ -120,71 +90,72 @@ Singleton {
previousVersion = fromVersion; previousVersion = fromVersion;
changelogCurrentVersion = toVersion; changelogCurrentVersion = toVersion;
fetchError = GitHubService ? GitHubService.releaseFetchError : "";
releaseHighlights = buildReleaseHighlights(previousVersion, changelogCurrentVersion); // Fetch the upgrade log from the server
releaseNotesUrl = buildReleaseNotesUrl(toVersion); fetchUpgradeLog(fromVersion, toVersion);
popupScheduled = true; popupScheduled = true;
root.popupQueued(previousVersion, changelogCurrentVersion); root.popupQueued(previousVersion, changelogCurrentVersion);
clearChangelogRequest(); clearChangelogRequest();
openWhenReady();
} }
function rebuildHighlights() { function fetchUpgradeLog(fromVersion, toVersion) {
if (!changelogCurrentVersion) // Use the last seen version, or default to v3.0.0 if this is a fresh install
return; let from = fromVersion || changelogLastSeenVersion || "v3.0.0";
fetchError = GitHubService ? GitHubService.releaseFetchError : ""; let to = toVersion;
releaseHighlights = buildReleaseHighlights(previousVersion, changelogCurrentVersion);
}
function buildReleaseHighlights(fromVersion, toVersion) { // Remove potential legacy -dev stuff
const releases = GitHubService && GitHubService.releases ? GitHubService.releases : []; // TODO: remove in 2026!
const selected = []; from = from.replace("-dev", "");
const fromNorm = normalizeVersion(fromVersion); to = to.replace("-dev", "");
const toNorm = normalizeVersion(toVersion);
if (releases.length > 0) { // Strip suffix from versions
for (var i = 0; i < releases.length; i++) { from = from.replace(root.developmentSuffix, "");
const rel = releases[i]; to = to.replace(root.developmentSuffix, "");
const tag = rel.version || "";
const tagNorm = normalizeVersion(tag);
if (!tagNorm)
continue;
if (toNorm && compareVersions(tagNorm, toNorm) > 0) { // 'from' always need to be before 'to'
continue; // handle edge case that will show up as we changed -dev to -git
} if (from === to) {
from = "v3.0.0";
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
});
}
} }
if (selected.length === 0 && toVersion) { Logger.d("UpdateService", "Fetching upgrade log", "from:", from, "to:", to);
const fallback = parseReleaseNotes(GitHubService ? GitHubService.releaseNotes : "");
if (fallback.length > 0) {
selected.push({
"version": toVersion,
"date": "",
"entries": fallback
});
fetchError = "";
}
}
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) { function normalizeVersion(version) {
@@ -217,13 +188,6 @@ Singleton {
return 0; 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) { function parseReleaseNotes(body) {
if (!body) if (!body)
return []; return [];
@@ -232,32 +196,16 @@ Singleton {
var entries = []; var entries = [];
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim(); const line = lines[i];
if (!line) entries.push(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;
} }
var uniqueEntries = []; // Remove trailing blank lines
var seen = {}; while (entries.length > 0 && entries[entries.length - 1].trim().length === 0) {
for (var j = 0; j < entries.length; j++) { entries.pop();
const key = entries[j].toLowerCase();
if (seen[key])
continue;
seen[key] = true;
uniqueEntries.push(entries[j]);
} }
return uniqueEntries; return entries;
} }
function isVersionLine(text) { function isVersionLine(text) {
@@ -322,12 +270,6 @@ Singleton {
lastShownVersion = changelogCurrentVersion; lastShownVersion = changelogCurrentVersion;
} }
function openReleaseNotes() {
if (!releaseNotesUrl)
return;
Quickshell.execDetached(["xdg-open", releaseNotesUrl]);
}
function openDiscord() { function openDiscord() {
if (!discordUrl) if (!discordUrl)
return; return;
@@ -374,12 +316,21 @@ Singleton {
function loadChangelogState() { function loadChangelogState() {
try { try {
changelogLastSeenVersion = changelogStateAdapter.lastSeenVersion || ""; const changelog = ShellState.getChangelogState();
if (!changelogLastSeenVersion && Settings.data && Settings.data.changelog && Settings.data.changelog.lastSeenVersion) { changelogLastSeenVersion = changelog.lastSeenVersion || "";
changelogLastSeenVersion = Settings.data.changelog.lastSeenVersion;
debouncedSaveChangelogState(); if (!changelogLastSeenVersion) {
Logger.i("UpdateService", "Migrated changelog lastSeenVersion from settings to cache"); // Try to migrate from old changelog-state.json
migrateFromOldChangelogFile();
// Also try settings migration
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 ShellState");
}
} }
Logger.d("UpdateService", "Loaded changelog state from ShellState");
} catch (error) { } catch (error) {
Logger.e("UpdateService", "Failed to load changelog state:", error); Logger.e("UpdateService", "Failed to load changelog state:", error);
} }
@@ -390,6 +341,31 @@ Singleton {
} }
} }
function migrateFromOldChangelogFile() {
const oldChangelogPath = Settings.cacheDir + "changelog-state.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
FileView {
id: migrationView
path: "${oldChangelogPath}"
printErrors: false
adapter: JsonAdapter {
property string lastSeenVersion: ""
}
onLoaded: {
parent.changelogLastSeenVersion = adapter.lastSeenVersion || "";
parent.debouncedSaveChangelogState();
Logger.i("UpdateService", "Migrated changelog-state.json to ShellState");
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "changelogMigrationView");
}
function debouncedSaveChangelogState() { function debouncedSaveChangelogState() {
// Queue a save and restart the debounce timer // Queue a save and restart the debounce timer
pendingSave = true; pendingSave = true;
@@ -411,26 +387,16 @@ Singleton {
saveInProgress = true; saveInProgress = true;
try { try {
changelogStateAdapter.lastSeenVersion = changelogLastSeenVersion || ""; ShellState.setChangelogState({
lastSeenVersion: changelogLastSeenVersion || ""
});
Logger.d("UpdateService", "Saved changelog state to ShellState");
saveInProgress = false;
// Ensure cache directory exists // Check if another save was queued while we were saving
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); if (pendingSave) {
Qt.callLater(executeSave);
// 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;
}
});
} catch (error) { } catch (error) {
Logger.e("UpdateService", "Failed to save changelog state:", error); Logger.e("UpdateService", "Failed to save changelog state:", error);
saveInProgress = false; saveInProgress = false;

View File

@@ -17,7 +17,6 @@ Singleton {
property int maxVisible: 5 property int maxVisible: 5
property int maxHistory: 100 property int maxHistory: 100
property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json")
property string stateFile: Settings.cacheDir + "notifications-state.json"
// State // State
property real lastSeenTs: 0 property real lastSeenTs: 0
@@ -138,6 +137,22 @@ Singleton {
if (Settings.isLoaded) { if (Settings.isLoaded) {
updateNotificationServer(); 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 { Connections {
@@ -471,22 +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 { Timer {
id: saveTimer id: saveTimer
@@ -546,22 +545,57 @@ Singleton {
function loadState() { function loadState() {
try { 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) { if (root.lastSeenTs === 0) {
root.lastSeenTs = Settings.data.notifications.lastSeenTs; // Try to migrate from old notifications-state.json
saveState(); migrateFromOldStateFile();
Logger.i("Notifications", "Migrated lastSeenTs from settings to state file"); // Also try settings migration
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 ShellState");
}
} }
Logger.d("Notifications", "Loaded state from ShellState");
} catch (e) { } catch (e) {
Logger.e("Notifications", "Load state failed:", e); Logger.e("Notifications", "Load state failed:", e);
} }
} }
function migrateFromOldStateFile() {
const oldStatePath = Settings.cacheDir + "notifications-state.json";
const migrationFileView = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
FileView {
id: migrationView
path: "${oldStatePath}"
printErrors: false
adapter: JsonAdapter {
property real lastSeenTs: 0
}
onLoaded: {
parent.lastSeenTs = adapter.lastSeenTs || 0;
parent.saveState();
Logger.i("Notifications", "Migrated notifications-state.json to ShellState");
migrationView.destroy();
}
onLoadFailed: {
migrationView.destroy();
}
}
`, root, "notificationMigrationView");
}
function saveState() { function saveState() {
try { try {
stateAdapter.lastSeenTs = root.lastSeenTs; ShellState.setNotificationsState({
stateFileView.writeAdapter(); lastSeenTs: root.lastSeenTs
});
Logger.d("Notifications", "Saved state to ShellState");
} catch (e) { } catch (e) {
Logger.e("Notifications", "Save state failed:", e); Logger.e("Notifications", "Save state failed:", e);
} }

View File

@@ -13,6 +13,7 @@ Singleton {
property var schemes: [] property var schemes: []
property bool scanning: false property bool scanning: false
property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorScheme" property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorScheme"
property string downloadedSchemesDirectory: Settings.configDir + "colorschemes"
property string colorsJsonFilePath: Settings.configDir + "colors.json" property string colorsJsonFilePath: Settings.configDir + "colors.json"
Connections { Connections {
@@ -43,8 +44,11 @@ Singleton {
Logger.d("ColorScheme", "Load colorScheme"); Logger.d("ColorScheme", "Load colorScheme");
scanning = true; scanning = true;
schemes = []; schemes = [];
// Use find command to locate all scheme.json files // Use find command to locate all scheme.json files in both directories
findProcess.command = ["find", schemesDirectory, "-name", "*.json", "-type", "f"]; // 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; findProcess.running = true;
} }
@@ -81,7 +85,17 @@ Singleton {
} else if (schemeName === "Tokyo Night") { } else if (schemeName === "Tokyo Night") {
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) { function applyScheme(nameOrPath) {

View File

@@ -5,6 +5,7 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Commons import qs.Commons
import qs.Services.System import qs.Services.System
import qs.Services.Theming
import qs.Services.UI import qs.Services.UI
Singleton { Singleton {
@@ -329,7 +330,26 @@ Singleton {
extension = ".toml"; 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

@@ -330,8 +330,11 @@ Singleton {
Settings.saveImmediate(); 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 // Close the popup menu window when dialog closes
dialog.closed.connect(() => { dialog.closed.connect(() => {
popupMenuWindow.hasDialog = false;
popupMenuWindow.close(); popupMenuWindow.close();
dialog.destroy(); dialog.destroy();
}); });

View File

@@ -36,6 +36,7 @@ Singleton {
"TaskbarGrouped": taskbarGroupedComponent, "TaskbarGrouped": taskbarGroupedComponent,
"Tray": trayComponent, "Tray": trayComponent,
"Volume": volumeComponent, "Volume": volumeComponent,
"VPN": vpnComponent,
"WiFi": wiFiComponent, "WiFi": wiFiComponent,
"WallpaperSelector": wallpaperSelectorComponent, "WallpaperSelector": wallpaperSelectorComponent,
"Workspace": workspaceComponent "Workspace": workspaceComponent
@@ -58,12 +59,13 @@ Singleton {
"SessionMenu": "WidgetSettings/SessionMenuSettings.qml", "SessionMenu": "WidgetSettings/SessionMenuSettings.qml",
"Spacer": "WidgetSettings/SpacerSettings.qml", "Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.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", "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: ({ property var widgetMetadata: ({
@@ -124,7 +126,17 @@ Singleton {
"textIntervalMs": 3000, "textIntervalMs": 3000,
"textCollapse": "", "textCollapse": "",
"parseJson": false, "parseJson": false,
"hideTextInVerticalBar": false "wheelExec": "",
"wheelUpExec": "",
"wheelDownExec": "",
"wheelMode": "unified",
"wheelUpdateText": false,
"wheelUpUpdateText": false,
"wheelDownUpdateText": false,
"maxTextLength": {
"horizontal": 10,
"vertical": 10
}
}, },
"KeyboardLayout": { "KeyboardLayout": {
"allowUserSettings": true, "allowUserSettings": true,
@@ -188,11 +200,9 @@ Singleton {
}, },
"TaskbarGrouped": { "TaskbarGrouped": {
"allowUserSettings": true, "allowUserSettings": true,
"showWorkspaceNumbers": true,
"showNumbersOnlyWhenOccupied": true,
"labelMode": "index",
"hideUnoccupied": false, "hideUnoccupied": false,
"characterCount": 2, "labelMode": "index",
"showLabelsOnlyWhenOccupied": true,
"colorizeIcons": false "colorizeIcons": false
}, },
"Tray": { "Tray": {
@@ -202,6 +212,10 @@ Singleton {
"pinned": [], "pinned": [],
"drawerEnabled": true "drawerEnabled": true
}, },
"VPN": {
"allowUserSettings": true,
"displayMode": "onhover"
},
"WiFi": { "WiFi": {
"allowUserSettings": true, "allowUserSettings": true,
"displayMode": "onhover" "displayMode": "onhover"
@@ -291,6 +305,9 @@ Singleton {
property Component volumeComponent: Component { property Component volumeComponent: Component {
Volume {} Volume {}
} }
property Component vpnComponent: Component {
VPN {}
}
property Component wiFiComponent: Component { property Component wiFiComponent: Component {
WiFi {} WiFi {}
} }

View File

@@ -6,8 +6,7 @@ import qs.Commons
Singleton { Singleton {
id: root id: root
// A ref. to the lockScreen, so it's accessible from anywhere // A ref. to the lockScreen, so it's accessible from anywhere.
// This is not a panel...
property var lockScreen: null property var lockScreen: null
// Panels // Panels

View File

@@ -83,18 +83,42 @@ Singleton {
translateModels(); translateModels();
// Rebuild cache from settings // Load wallpapers from ShellState first (faster), then fall back to Settings
currentWallpapers = ({}); currentWallpapers = ({});
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
var cachedWallpapers = ShellState.getWallpapers();
if (cachedWallpapers && Object.keys(cachedWallpapers).length > 0) {
currentWallpapers = cachedWallpapers;
Logger.d("Wallpaper", "Loaded wallpapers from ShellState");
} else {
// Fall back to Settings if ShellState is empty
loadFromSettings();
}
} else {
// ShellState not ready yet, load from Settings
loadFromSettings();
}
isInitialized = true;
Logger.d("Wallpaper", "Triggering initial wallpaper scan");
Qt.callLater(refreshWallpapersList);
}
function loadFromSettings() {
var monitors = Settings.data.wallpaper.monitors || []; var monitors = Settings.data.wallpaper.monitors || [];
for (var i = 0; i < monitors.length; i++) { for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name && monitors[i].wallpaper) { if (monitors[i].name && monitors[i].wallpaper) {
currentWallpapers[monitors[i].name] = monitors[i].wallpaper; currentWallpapers[monitors[i].name] = monitors[i].wallpaper;
} }
} }
Logger.d("Wallpaper", "Loaded wallpapers from Settings");
isInitialized = true; // Migrate to ShellState if we loaded from Settings
Logger.d("Wallpaper", "Triggering initial wallpaper scan"); if (typeof ShellState !== 'undefined' && ShellState.isLoaded && Object.keys(currentWallpapers).length > 0) {
Qt.callLater(refreshWallpapersList); ShellState.setWallpapers(currentWallpapers);
Logger.i("Wallpaper", "Migrated wallpaper paths from Settings to ShellState");
}
} }
// ------------------------------------------------- // -------------------------------------------------
@@ -270,33 +294,11 @@ Singleton {
// Update cache directly // Update cache directly
currentWallpapers[screenName] = path; currentWallpapers[screenName] = path;
// Update Settings - still need immutable update for Settings persistence // Save to ShellState (wallpaper paths now only stored here, not in Settings)
// The slice() ensures Settings detects the change and saves properly if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
var monitors = Settings.data.wallpaper.monitors || []; ShellState.setWallpapers(currentWallpapers);
var found = false;
var newMonitors = monitors.map(function (monitor) {
if (monitor.name === screenName) {
found = true;
return {
"name": screenName,
"directory": Settings.preprocessPath(monitor.directory) || getMonitorDirectory(screenName),
"wallpaper": path
};
}
return monitor;
});
if (!found) {
newMonitors.push({
"name": screenName,
"directory": getMonitorDirectory(screenName),
"wallpaper": path
});
} }
Settings.data.wallpaper.monitors = newMonitors.slice();
// Emit signal for this specific wallpaper change // Emit signal for this specific wallpaper change
root.wallpaperChanged(screenName, path); root.wallpaperChanged(screenName, path);

View File

@@ -49,6 +49,10 @@ Item {
property color progressColor: root.secondHandColor property color progressColor: root.secondHandColor
// Font size properties for digital clock
property real hoursFontSize: Style.fontSizeXS
property real minutesFontSize: Style.fontSizeXXS
height: Math.round((Style.fontSizeXXXL * 1.9) / 2 * Style.uiScaleRatio) * 2 height: Math.round((Style.fontSizeXXXL * 1.9) / 2 * Style.uiScaleRatio) * 2
width: root.height width: root.height
@@ -78,6 +82,16 @@ Item {
return root.progressColor; return root.progressColor;
}); });
} }
if (item.hasOwnProperty("hoursFontSize")) {
item.hoursFontSize = Qt.binding(function () {
return root.hoursFontSize;
});
}
if (item.hasOwnProperty("minutesFontSize")) {
item.minutesFontSize = Qt.binding(function () {
return root.minutesFontSize;
});
}
} }
} }
@@ -187,6 +201,8 @@ Item {
property color backgroundColor: Color.mPrimary property color backgroundColor: Color.mPrimary
property color clockColor: Color.mOnPrimary property color clockColor: Color.mOnPrimary
property color progressColor: Color.mError property color progressColor: Color.mError
property real hoursFontSize: Style.fontSizeXS
property real minutesFontSize: Style.fontSizeXXS
anchors.fill: parent anchors.fill: parent
@@ -238,7 +254,7 @@ Item {
return t.split(" ")[0]; return t.split(" ")[0];
} }
pointSize: Style.fontSizeXS pointSize: hoursFontSize
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: clockColor color: clockColor
family: Settings.data.ui.fontFixed family: Settings.data.ui.fontFixed
@@ -247,7 +263,7 @@ Item {
NText { NText {
text: Qt.formatTime(now, "mm") text: Qt.formatTime(now, "mm")
pointSize: Style.fontSizeXXS pointSize: minutesFontSize
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: clockColor color: clockColor
family: Settings.data.ui.fontFixed family: Settings.data.ui.fontFixed

View File

@@ -4,10 +4,12 @@ import Quickshell.Widgets
import qs.Commons import qs.Commons
Rectangle { Rectangle {
width: parent.width property bool vertical: false
height: Style.borderS
width: vertical ? Style.borderS : parent.width
height: vertical ? parent.height : Style.borderS
gradient: Gradient { gradient: Gradient {
orientation: Gradient.Horizontal orientation: vertical ? Gradient.Vertical : Gradient.Horizontal
GradientStop { GradientStop {
position: 0.0 position: 0.0
color: Color.transparent color: Color.transparent

View File

@@ -7,6 +7,7 @@ ColumnLayout {
property string label: "" property string label: ""
property string description: "" property string description: ""
property bool enableDescriptionRichText: false
spacing: Style.marginXXS spacing: Style.marginXXS
Layout.fillWidth: true Layout.fillWidth: true
@@ -27,5 +28,6 @@ ColumnLayout {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
Layout.fillWidth: true Layout.fillWidth: true
visible: root.description !== "" visible: root.description !== ""
richTextEnabled: root.enableDescriptionRichText
} }
} }

View File

@@ -11,6 +11,7 @@ Rectangle {
property color borderColor: Color.transparent property color borderColor: Color.transparent
property real borderWidth: 0 property real borderWidth: 0
property real imageRadius: width * 0.5 property real imageRadius: width * 0.5
property int imageFillMode: Image.PreserveAspectCrop
property string fallbackIcon: "" property string fallbackIcon: ""
property real fallbackIconSize: Style.fontSizeXXL property real fallbackIconSize: Style.fontSizeXXL
@@ -30,12 +31,14 @@ Rectangle {
id: img id: img
anchors.fill: parent anchors.fill: parent
source: imagePath source: imagePath
visible: false // Hide since we're using it as shader source visible: false
mipmap: true mipmap: true
smooth: true smooth: true
asynchronous: true asynchronous: true
antialiasing: true antialiasing: true
fillMode: Image.PreserveAspectCrop fillMode: root.imageFillMode
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
onStatusChanged: root.statusChanged(status) onStatusChanged: root.statusChanged(status)
} }
@@ -51,17 +54,14 @@ Rectangle {
format: ShaderEffectSource.RGBA format: ShaderEffectSource.RGBA
} }
// Use custom property names to avoid conflicts with final properties
property real itemWidth: root.width property real itemWidth: root.width
property real itemHeight: root.height property real itemHeight: root.height
property real cornerRadius: root.radius property real cornerRadius: root.radius
property real imageOpacity: root.opacity property real imageOpacity: root.opacity
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb") fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
// Qt6 specific properties - ensure proper blending
supportsAtlasTextures: false supportsAtlasTextures: false
blending: true blending: true
// Make sure the background is transparent
Rectangle { Rectangle {
id: background id: background
anchors.fill: parent anchors.fill: parent
@@ -70,7 +70,6 @@ Rectangle {
} }
} }
// Fallback icon
Loader { Loader {
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "") active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
anchors.centerIn: parent anchors.centerIn: parent
@@ -83,7 +82,6 @@ Rectangle {
} }
} }
// Border
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius

View File

@@ -26,5 +26,5 @@ Text {
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
textFormat: richTextEnabled ? Text.RichText : Text.StyledText textFormat: richTextEnabled ? Text.RichText : Text.PlainText
} }

18
flake.lock generated
View File

@@ -18,23 +18,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs"
"systems": "systems"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
} }
} }
}, },

View File

@@ -3,24 +3,21 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
systems.url = "github:nix-systems/default";
}; };
outputs = { outputs = {
self, self,
nixpkgs, nixpkgs,
systems,
... ...
}: let }: let
eachSystem = nixpkgs.lib.genAttrs (import systems); eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
pkgsFor = eachSystem (system: nixpkgs.legacyPackages.${system}.appendOverlays [self.overlays.default]);
in { in {
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra); formatter = eachSystem (system: pkgsFor.${system}.alejandra);
packages = eachSystem ( packages = eachSystem (
system: let system: {
pkgs = nixpkgs.legacyPackages.${system}.appendOverlays [self.overlays.default]; default = pkgsFor.${system}.noctalia-shell;
in {
default = pkgs.noctalia-shell;
} }
); );
@@ -42,10 +39,8 @@
}; };
devShells = eachSystem ( devShells = eachSystem (
system: let system: {
pkgs = nixpkgs.legacyPackages.${system}; default = pkgsFor.${system}.callPackage ./nix/shell.nix {};
in {
default = pkgs.callPackage ./nix/shell.nix {};
} }
); );
@@ -58,9 +53,6 @@
programs.noctalia-shell.package = programs.noctalia-shell.package =
lib.mkDefault lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.default; self.packages.${pkgs.stdenv.hostPlatform.system}.default;
programs.noctalia-shell.app2unit.package =
lib.mkDefault
nixpkgs.legacyPackages.${pkgs.stdenv.hostPlatform.system}.app2unit;
}; };
nixosModules.default = { nixosModules.default = {

View File

@@ -89,6 +89,7 @@ in {
app2unit.package = lib.mkOption { app2unit.package = lib.mkOption {
type = lib.types.package; type = lib.types.package;
default = pkgs.app2unit;
description = '' description = ''
The app2unit package to use when appLauncher.useApp2Unit is enabled. The app2unit package to use when appLauncher.useApp2Unit is enabled.
''; '';
@@ -96,9 +97,7 @@ in {
}; };
config = let config = let
restart = '' restart = "${pkgs.systemd}/bin/systemctl --user try-restart noctalia-shell.service 2>/dev/null || true";
${pkgs.systemd}/bin/systemctl --user try-restart noctalia-shell.service 2>/dev/null || true
'';
useApp2Unit = cfg.settings.appLauncher.useApp2Unit or false; useApp2Unit = cfg.settings.appLauncher.useApp2Unit or false;
in in
lib.mkIf cfg.enable { lib.mkIf cfg.enable {

View File

@@ -24,7 +24,7 @@ in {
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
systemd.user.services.noctalia-shell = { systemd.user.services.noctalia-shell = {
description = "Noctalia Shell - Wayland desktop shell"; description = "Noctalia Shell - Wayland desktop shell";
documentation = ["https://github.com/noctalia-dev/noctalia-shell"]; documentation = ["https://docs.noctalia.dev/docs"];
after = [cfg.target]; after = [cfg.target];
partOf = [cfg.target]; partOf = [cfg.target];
wantedBy = [cfg.target]; wantedBy = [cfg.target];
@@ -34,17 +34,9 @@ in {
PATH = lib.mkForce null; PATH = lib.mkForce null;
}; };
unitConfig = {
StartLimitIntervalSec = 60;
StartLimitBurst = 3;
};
serviceConfig = { serviceConfig = {
ExecStart = "${cfg.package}/bin/noctalia-shell"; ExecStart = lib.getExe cfg.package;
Restart = "on-failure"; Restart = "on-failure";
RestartSec = 3;
TimeoutStartSec = 10;
TimeoutStopSec = 5;
Environment = [ Environment = [
"NOCTALIA_SETTINGS_FALLBACK=%h/.config/noctalia/gui-settings.json" "NOCTALIA_SETTINGS_FALLBACK=%h/.config/noctalia/gui-settings.json"
]; ];

View File

@@ -26,7 +26,6 @@
/.github /.github
/.gitignore /.gitignore
/Assets/Screenshots /Assets/Screenshots
/Assets/Wallpaper
/Bin/dev /Bin/dev
/nix /nix
/LICENSE /LICENSE

View File

@@ -26,17 +26,18 @@ import qs.Services.Control
import qs.Services.Hardware import qs.Services.Hardware
import qs.Services.Location import qs.Services.Location
import qs.Services.Networking import qs.Services.Networking
import qs.Services.Noctalia
import qs.Services.Power import qs.Services.Power
import qs.Services.System import qs.Services.System
import qs.Services.Theming import qs.Services.Theming
import qs.Services.UI import qs.Services.UI
import qs.Services.Noctalia
ShellRoot { ShellRoot {
id: shellRoot id: shellRoot
property bool i18nLoaded: false property bool i18nLoaded: false
property bool settingsLoaded: false property bool settingsLoaded: false
property bool shellStateLoaded: false
Component.onCompleted: { Component.onCompleted: {
Logger.i("Shell", "---------------------------"); Logger.i("Shell", "---------------------------");
@@ -63,8 +64,18 @@ ShellRoot {
settingsLoaded = true; settingsLoaded = true;
} }
} }
Connections {
target: typeof ShellState !== 'undefined' ? ShellState : null
function onIsLoadedChanged() {
if (ShellState.isLoaded) {
shellStateLoaded = true;
}
}
}
Loader { Loader {
active: i18nLoaded && settingsLoaded active: i18nLoaded && settingsLoaded && shellStateLoaded
sourceComponent: Item { sourceComponent: Item {
Component.onCompleted: { Component.onCompleted: {
@@ -92,25 +103,16 @@ ShellRoot {
Overview {} Overview {}
Background {} Background {}
AllScreens {}
Dock {} Dock {}
Notification {}
ToastOverlay {} ToastOverlay {}
OSD {} OSD {}
Notification {}
LockScreen { LockScreen {}
id: lockScreen
Component.onCompleted: {
// Save a ref. to our lockScreen so we can access it easily
PanelService.lockScreen = lockScreen;
}
}
// IPCService is treated as a service but it's actually an // IPCService is treated as a service but it's actually an Item that needs to exists in the shell.
// Item that needs to exists in the shell.
IPCService {} IPCService {}
// MainScreen for each screen
AllScreens {}
} }
} }
@@ -143,6 +145,7 @@ ShellRoot {
setupWizardTimer.start(); setupWizardTimer.start();
} else { } else {
Settings.data.setupCompleted = true; Settings.data.setupCompleted = true;
Settings.saveImmediate();
} }
} }