1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-16 16:15:23 -04:00

Compare commits

..

19 Commits

Author SHA1 Message Date
bbedward 53cea7023f calendar: rename dcal binary 2026-06-15 15:26:06 -04:00
jbwfu a098088f03 refactor(settings): split network settings into tabs (#2633) 2026-06-15 15:21:02 -04:00
bbedward 59998e9fd2 calendar(dank): Add support for DankCalendar backend
- Add keyboard navigation to overview
- Add edit events to overview
- Add create events to overview
- Add setting for auto/khal/dankcalendar backend selection
2026-06-15 14:02:35 -04:00
purian23 1df7e478df fix(FileBrowser): Improve save-to-file handling w/safety override diags
Fixes #2641
2026-06-15 01:11:32 -04:00
Artrix 1fc4890857 tray: add automatic overflow popup (#2629) 2026-06-15 00:10:02 -04:00
purian23 f5d52f1506 update(ipc): docs & dms ipc list 2026-06-14 23:35:22 -04:00
purian23 2026ba5bd2 fix(Notepad): clean up edge cases & updated popout handling 2026-06-14 22:15:34 -04:00
purian23 db56c8d74d feat(Notepad): Complete refactor - New popout mode & settings
- Add a full popout Notepad experience w/new layout settings
- New ability to choose the left or right side of the screen
- Add more safegaurds to preserve user data throughout
- New banner to show file reload/conflict handling
- New extensionless file support
- Polish settings with gap controls, compact buttons, and shortcuts help
2026-06-14 20:20:36 -04:00
Huỳnh Thiện Lộc 9d1a81c93c feat(media-control): support scroll and right-click on audio output devices in media popout (#2615)
* feat(media-control): support scroll and right-click on audio output devices in media popout

* feat(media-control): make device list volume scrolling optional
2026-06-13 17:51:03 -04:00
bbedward 3701b3d7a3 wallpaper: re-introduce updatesEnabled 2026-06-12 20:00:53 -04:00
purian23 bae98daa5c fix(wallpaper): simplify wallpaper rendering logic & reliability
- Keep wallpaper surfaces persistent and remove `updatesEnabled` throttling that could leave wallpapers grey or frozen after DPMS, suspend, fullscreen, or output changes

Fixes #2612
Fixes #2299
Fixes #2272
Fixes #2028
2026-06-12 17:30:54 -04:00
jbwfu b34a04f723 fix(clipboard): hide pin action while keeping saved indicator (#2626) 2026-06-12 15:39:23 -04:00
purian23 1c0245f2db fix(translations): add newline at end of JSON file and output file 2026-06-12 15:06:36 -04:00
purian23 7777e87dc8 refactor(settings): reorg to break out sections and verbiage 2026-06-12 14:57:25 -04:00
jbwfu 820fa07846 feat(settings): add clipboard entry action visibility controls (#2621)
* feat(settings): add clipboard entry action visibility controls

* fix(clipboard): show pinned indicator for saved entries when pin action is hidden
2026-06-12 14:08:05 -04:00
purian23 66794582c9 fix(fullscreen): retain user dbar standalone configs while in framemode fullscreen 2026-06-12 12:39:38 -04:00
jbwfu 73eb471ae3 fix(clipboard): keep first item selected when navigating upward (#2622) 2026-06-12 11:35:07 -04:00
jbwfu 0f2f4b96c4 Fix/clipboard confirmation keyboard safety (#2623)
* fix(clipboard): improve confirmation dialog keyboard focus

* fix(clipboard): require confirmation for clear-all shortcut
2026-06-12 11:34:16 -04:00
purian23 d53809cf2b refactor(framemode): unify connected surface chrome via SDF pipeline
- Shadow system rewrite with SDF quads
- Replace ConnectedShape/layer FBOs w/frame & chrome SDF shaders
- Improve frame blur performance
- Plugin performance gate
2026-06-12 11:03:39 -04:00
71 changed files with 6880 additions and 3340 deletions
+9 -1
View File
@@ -19,7 +19,12 @@ var (
var colorCmd = &cobra.Command{ var colorCmd = &cobra.Command{
Use: "color", Use: "color",
Short: "Color utilities", Short: "Color utilities",
Long: "Color utilities including picking colors from the screen", Long: `Color utilities including picking colors from the screen.
This is the screen eyedropper CLI. To open the in-shell color modal, use:
dms ipc call color-picker toggle
See: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
} }
var colorPickCmd = &cobra.Command{ var colorPickCmd = &cobra.Command{
@@ -29,6 +34,9 @@ var colorPickCmd = &cobra.Command{
Click on any pixel to capture its color, or press Escape to cancel. Click on any pixel to capture its color, or press Escape to cancel.
This is the screen eyedropper CLI. To open the in-shell color modal, use:
dms ipc call color-picker toggle
Output format flags (mutually exclusive, default: --hex): Output format flags (mutually exclusive, default: --hex):
--hex - Hexadecimal (#RRGGBB) --hex - Hexadecimal (#RRGGBB)
--rgb - RGB values (R G B) --rgb - RGB values (R G B)
+16 -3
View File
@@ -77,10 +77,15 @@ var killCmd = &cobra.Command{
} }
var ipcCmd = &cobra.Command{ var ipcCmd = &cobra.Command{
Use: "ipc [target] [function] [args...]", Use: "ipc",
Short: "Send IPC commands to running DMS shell", Short: "Send IPC commands to running DMS shell",
Long: `Send IPC commands to the running DMS shell.
dms ipc call <target> <function> [args...] invoke a command
dms ipc list list all targets and functions
Full reference: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = findConfig(cmd, args)
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@@ -88,9 +93,17 @@ var ipcCmd = &cobra.Command{
}, },
} }
var ipcListCmd = &cobra.Command{
Use: "list",
Short: "List all IPC targets and functions",
Run: func(cmd *cobra.Command, args []string) {
printIPCHelp()
},
}
func init() { func init() {
ipcCmd.AddCommand(ipcListCmd)
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
_ = findConfig(cmd, args)
printIPCHelp() printIPCHelp()
}) })
} }
+41 -24
View File
@@ -601,12 +601,30 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
return targets return targets
} }
func getShellIPCCompletions(args []string, _ string) []string { func buildQsIPCBaseArgs() ([]string, error) {
cmdArgs := []string{"ipc"} cmdArgs := []string{"ipc"}
switch pid, ok := getFirstDMSPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
return nil, err
}
if qsHasAnyDisplay() { if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display") cmdArgs = append(cmdArgs, "--any-display")
} }
cmdArgs = append(cmdArgs, "-p", configPath, "show") cmdArgs = append(cmdArgs, "-p", configPath)
}
return cmdArgs, nil
}
func getShellIPCCompletions(args []string, _ string) []string {
baseArgs, err := buildQsIPCBaseArgs()
if err != nil {
log.Debugf("Error building IPC args for completions: %v", err)
return nil
}
cmdArgs := append(baseArgs, "show")
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
var targets ipcTargets var targets ipcTargets
@@ -623,7 +641,7 @@ func getShellIPCCompletions(args []string, _ string) []string {
if len(args) == 0 { if len(args) == 0 {
targetNames := make([]string, 0) targetNames := make([]string, 0)
targetNames = append(targetNames, "call") targetNames = append(targetNames, "call", "list")
for k := range targets { for k := range targets {
targetNames = append(targetNames, k) targetNames = append(targetNames, k)
} }
@@ -696,23 +714,11 @@ func runShellIPCCommand(args []string) {
args = append([]string{"call"}, args...) args = append([]string{"call"}, args...)
} }
cmdArgs := []string{"ipc"} baseArgs, err := buildQsIPCBaseArgs()
if err != nil {
switch pid, ok := getFirstDMSPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
log.Fatalf("Error finding config: %v", err) log.Fatalf("Error finding config: %v", err)
} }
// ! TODO - remove check when QS 0.3 is released cmdArgs := append(baseArgs, args...)
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
}
cmdArgs = append(cmdArgs, args...)
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -724,19 +730,20 @@ func runShellIPCCommand(args []string) {
} }
func printIPCHelp() { func printIPCHelp() {
fmt.Println("Usage: dms ipc <target> <function> [args...]") fmt.Println("Usage: dms ipc call <target> <function> [args...]")
fmt.Println() fmt.Println()
cmdArgs := []string{"ipc"} baseArgs, err := buildQsIPCBaseArgs()
if qsHasAnyDisplay() { if err != nil {
cmdArgs = append(cmdArgs, "--any-display") printIPCHelpFailure(err)
return
} }
cmdArgs = append(cmdArgs, "-p", configPath, "show") cmdArgs := append(baseArgs, "show")
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
fmt.Println("Could not retrieve available IPC targets (is DMS running?)") printIPCHelpFailure(err)
return return
} }
@@ -765,6 +772,16 @@ func printIPCHelp() {
} }
} }
func printIPCHelpFailure(err error) {
fmt.Println("Could not retrieve IPC targets.")
if err != nil {
fmt.Printf(" %v\n", err)
}
fmt.Println()
fmt.Println(" Full docs: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc")
fmt.Println(" Try: dms ipc call <target> <function>")
}
// ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults // ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults
func ensureFontCache() { func ensureFontCache() {
if _, err := exec.LookPath("fc-list"); err != nil { if _, err := exec.LookPath("fc-list"); err != nil {
+21 -1
View File
@@ -6,6 +6,18 @@ DankMaterialShell provides comprehensive IPC (Inter-Process Communication) funct
dms ipc call <target> <function> [parameters...] dms ipc call <target> <function> [parameters...]
``` ```
## Discovering IPC commands
List all available targets and functions while DMS is running:
```bash
dms ipc list
dms ipc # same
dms ipc --help # same, plus usage text
```
Live listing requires DMS to be running. If listing fails, use this document or the [Keybinds & IPC docs](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc) as an offline reference.
## Target: `audio` ## Target: `audio`
Audio system control and information. Audio system control and information.
@@ -707,7 +719,7 @@ File browser controls for selecting wallpapers and profile images.
- Both browsers support common image formats (jpg, jpeg, png, bmp, gif, webp) - Both browsers support common image formats (jpg, jpeg, png, bmp, gif, webp)
### Target: `color-picker` ### Target: `color-picker`
Color picker modal control. In-shell color picker modal for theme and settings color selection.
**Functions:** **Functions:**
- `open` - Show color picker modal - `open` - Show color picker modal
@@ -718,6 +730,14 @@ Color picker modal control.
- `toggle` - Toggle color picker modal visibility - `toggle` - Toggle color picker modal visibility
- `toggleInstant` - Toggle color picker modal visibility without animation on hide - `toggleInstant` - Toggle color picker modal visibility without animation on hide
**Note:** This controls the in-shell modal. To pick a pixel from the screen via CLI, use `dms color pick` instead (see [Color Picker CLI](https://danklinux.com/docs/dankmaterialshell/cli-color-picker)).
**Examples:**
```bash
dms ipc call color-picker toggle
dms ipc call color-picker openColor "#3f51b5"
```
### Target: `hypr` ### Target: `hypr`
Hyprland-specific controls including keybinds cheatsheet and workspace overview (Hyprland only). Hyprland-specific controls including keybinds cheatsheet and workspace overview (Hyprland only).
+20 -18
View File
@@ -7,29 +7,31 @@ Item {
property alias path: socket.path property alias path: socket.path
property alias parser: socket.parser property alias parser: socket.parser
property bool connected: false property bool connected: false
property bool linkUp: false
property int reconnectBaseMs: 400 property int reconnectBaseMs: 400
property int reconnectMaxMs: 15000 property int reconnectMaxMs: 15000
property int _reconnectAttempt: 0 property int _reconnectAttempt: 0
signal connectionStateChanged() signal connectionStateChanged
onConnectedChanged: { onConnectedChanged: {
socket.connected = connected socket.connected = connected;
} }
Socket { Socket {
id: socket id: socket
onConnectionStateChanged: { onConnectionStateChanged: {
root.connectionStateChanged() root.linkUp = connected;
root.connectionStateChanged();
if (connected) { if (connected) {
root._reconnectAttempt = 0 root._reconnectAttempt = 0;
return return;
} }
if (root.connected) { if (root.connected) {
root._scheduleReconnect() root._scheduleReconnect();
} }
} }
} }
@@ -39,24 +41,24 @@ Item {
interval: 0 interval: 0
repeat: false repeat: false
onTriggered: { onTriggered: {
socket.connected = false socket.connected = false;
Qt.callLater(() => socket.connected = true) Qt.callLater(() => socket.connected = true);
} }
} }
function send(data) { function send(data) {
const json = typeof data === "string" ? data : JSON.stringify(data) const json = typeof data === "string" ? data : JSON.stringify(data);
const message = json.endsWith("\n") ? json : json + "\n" const message = json.endsWith("\n") ? json : json + "\n";
socket.write(message) socket.write(message);
socket.flush() socket.flush();
} }
function _scheduleReconnect() { function _scheduleReconnect() {
const pow = Math.min(_reconnectAttempt, 10) const pow = Math.min(_reconnectAttempt, 10);
const base = Math.min(reconnectBaseMs * Math.pow(2, pow), reconnectMaxMs) const base = Math.min(reconnectBaseMs * Math.pow(2, pow), reconnectMaxMs);
const jitter = Math.floor(Math.random() * Math.floor(base / 4)) const jitter = Math.floor(Math.random() * Math.floor(base / 4));
reconnectTimer.interval = base + jitter reconnectTimer.interval = base + jitter;
reconnectTimer.restart() reconnectTimer.restart();
_reconnectAttempt++ _reconnectAttempt++;
} }
} }
+3
View File
@@ -56,6 +56,9 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call dankdash wallpaper", label: "Wallpaper Browser" }, { id: "spawn dms ipc call dankdash wallpaper", label: "Wallpaper Browser" },
{ id: "spawn dms ipc call file browse wallpaper", label: "File: Browse Wallpaper" }, { id: "spawn dms ipc call file browse wallpaper", label: "File: Browse Wallpaper" },
{ id: "spawn dms ipc call file browse profile", label: "File: Browse Profile" }, { id: "spawn dms ipc call file browse profile", label: "File: Browse Profile" },
{ id: "spawn dms ipc call color-picker toggle", label: "Color Picker: Toggle" },
{ id: "spawn dms ipc call color-picker open", label: "Color Picker: Open" },
{ id: "spawn dms ipc call color-picker close", label: "Color Picker: Close" },
{ id: "spawn dms ipc call keybinds toggle niri", label: "Keybinds Cheatsheet: Toggle", compositor: "niri" }, { id: "spawn dms ipc call keybinds toggle niri", label: "Keybinds Cheatsheet: Toggle", compositor: "niri" },
{ id: "spawn dms ipc call keybinds open niri", label: "Keybinds Cheatsheet: Open", compositor: "niri" }, { id: "spawn dms ipc call keybinds open niri", label: "Keybinds Cheatsheet: Open", compositor: "niri" },
{ id: "spawn dms ipc call keybinds close", label: "Keybinds Cheatsheet: Close" }, { id: "spawn dms ipc call keybinds close", label: "Keybinds Cheatsheet: Close" },
+41
View File
@@ -108,6 +108,7 @@ Singleton {
} }
property bool clipboardEnterToPaste: false property bool clipboardEnterToPaste: false
property var clipboardVisibleEntryActions: ["pin", "edit", "delete"]
property var launcherPluginVisibility: ({}) property var launcherPluginVisibility: ({})
@@ -181,6 +182,7 @@ Singleton {
property int firstDayOfWeek: -1 property int firstDayOfWeek: -1
property bool showWeekNumber: false property bool showWeekNumber: false
property string calendarBackend: "auto"
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
property bool padHours12Hour: false property bool padHours12Hour: false
@@ -396,6 +398,7 @@ Singleton {
property bool audioVisualizerEnabled: true property bool audioVisualizerEnabled: true
property string audioScrollMode: "volume" property string audioScrollMode: "volume"
property int audioWheelScrollAmount: 5 property int audioWheelScrollAmount: 5
property bool audioDeviceScrollVolumeEnabled: false
property bool clockCompactMode: false property bool clockCompactMode: false
property int focusedWindowSize: 1 property int focusedWindowSize: 1
property bool focusedWindowCompactMode: false property bool focusedWindowCompactMode: false
@@ -403,6 +406,9 @@ Singleton {
property int barMaxVisibleApps: 0 property int barMaxVisibleApps: 0
property int barMaxVisibleRunningApps: 0 property int barMaxVisibleRunningApps: 0
property bool barShowOverflowBadge: true property bool barShowOverflowBadge: true
property bool trayAutoOverflow: true
property bool trayPopupSingleLine: true
property int trayMaxVisibleItems: 0
property bool appsDockHideIndicators: false property bool appsDockHideIndicators: false
property bool appsDockColorizeActive: false property bool appsDockColorizeActive: false
property string appsDockActiveColorMode: "primary" property string appsDockActiveColorMode: "primary"
@@ -518,13 +524,39 @@ Singleton {
property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def
property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def
property bool notepadShowLineNumbers: false property bool notepadShowLineNumbers: false
property bool notepadAutoSave: false
property string notepadSlideoutSide: "right"
property string notepadDefaultMode: "slideout"
property real notepadTransparencyOverride: -1 property real notepadTransparencyOverride: -1
property real notepadLastCustomTransparency: 0.7 property real notepadLastCustomTransparency: 0.7
property bool notepadUseCompositorGap: false
property int notepadEdgeGap: 0
// Compositor layout gap when enabled and available, else the manual value.
readonly property int notepadEffectiveEdgeGap: {
if (notepadUseCompositorGap) {
var g = -1;
if (CompositorService.isNiri)
g = niriLayoutGapsOverride;
else if (CompositorService.isHyprland)
g = hyprlandLayoutGapsOverride;
else if (CompositorService.isMango)
g = mangoLayoutGapsOverride;
if (g >= 0)
return g;
}
return Math.max(0, notepadEdgeGap);
}
onNotepadUseMonospaceChanged: saveSettings() onNotepadUseMonospaceChanged: saveSettings()
onNotepadFontFamilyChanged: saveSettings() onNotepadFontFamilyChanged: saveSettings()
onNotepadFontSizeChanged: saveSettings() onNotepadFontSizeChanged: saveSettings()
onNotepadShowLineNumbersChanged: saveSettings() onNotepadShowLineNumbersChanged: saveSettings()
onNotepadAutoSaveChanged: saveSettings()
onNotepadSlideoutSideChanged: saveSettings()
onNotepadDefaultModeChanged: saveSettings()
onNotepadUseCompositorGapChanged: saveSettings()
onNotepadEdgeGapChanged: saveSettings()
// onCenteringModeChanged: saveSettings() // onCenteringModeChanged: saveSettings()
onNotepadTransparencyOverrideChanged: { onNotepadTransparencyOverrideChanged: {
if (notepadTransparencyOverride > 0) { if (notepadTransparencyOverride > 0) {
@@ -1650,6 +1682,15 @@ Singleton {
}; };
} }
function effectiveBarConfigForRender(config, usesFrameBarChrome) {
if (!config || !connectedFrameModeActive || usesFrameBarChrome)
return config;
const backup = connectedFrameBarStyleBackups[config.id];
if (!backup)
return config;
return Object.assign({}, config, backup);
}
// Single entry point for connected-mode settings state. // Single entry point for connected-mode settings state.
// !active → restore backups // !active → restore backups
function _reconcileConnectedFrameBarStyles() { function _reconcileConnectedFrameBarStyles() {
@@ -37,6 +37,7 @@ var SPEC = {
firstDayOfWeek: { def: -1 }, firstDayOfWeek: { def: -1 },
showWeekNumber: { def: false }, showWeekNumber: { def: false },
calendarBackend: { def: "auto" },
use24HourClock: { def: true }, use24HourClock: { def: true },
showSeconds: { def: false }, showSeconds: { def: false },
padHours12Hour: { def: false }, padHours12Hour: { def: false },
@@ -156,6 +157,7 @@ var SPEC = {
audioVisualizerEnabled: { def: true }, audioVisualizerEnabled: { def: true },
audioScrollMode: { def: "volume" }, audioScrollMode: { def: "volume" },
audioWheelScrollAmount: { def: 5 }, audioWheelScrollAmount: { def: 5 },
audioDeviceScrollVolumeEnabled: { def: false },
clockCompactMode: { def: false }, clockCompactMode: { def: false },
focusedWindowCompactMode: { def: false }, focusedWindowCompactMode: { def: false },
focusedWindowSize: { def: 1 }, focusedWindowSize: { def: 1 },
@@ -163,6 +165,9 @@ var SPEC = {
barMaxVisibleApps: { def: 0 }, barMaxVisibleApps: { def: 0 },
barMaxVisibleRunningApps: { def: 0 }, barMaxVisibleRunningApps: { def: 0 },
barShowOverflowBadge: { def: true }, barShowOverflowBadge: { def: true },
trayAutoOverflow: { def: true },
trayPopupSingleLine: { def: true },
trayMaxVisibleItems: { def: 0 },
appsDockHideIndicators: { def: false }, appsDockHideIndicators: { def: false },
appsDockColorizeActive: { def: false }, appsDockColorizeActive: { def: false },
appsDockActiveColorMode: { def: "primary" }, appsDockActiveColorMode: { def: "primary" },
@@ -263,8 +268,13 @@ var SPEC = {
notificationSummaryFontSize: { def: 0 }, notificationSummaryFontSize: { def: 0 },
notificationBodyFontSize: { def: 0 }, notificationBodyFontSize: { def: 0 },
notepadShowLineNumbers: { def: false }, notepadShowLineNumbers: { def: false },
notepadAutoSave: { def: false },
notepadSlideoutSide: { def: "right" },
notepadDefaultMode: { def: "slideout" },
notepadTransparencyOverride: { def: -1 }, notepadTransparencyOverride: { def: -1 },
notepadLastCustomTransparency: { def: 0.7 }, notepadLastCustomTransparency: { def: 0.7 },
notepadUseCompositorGap: { def: false },
notepadEdgeGap: { def: 0 },
soundsEnabled: { def: true }, soundsEnabled: { def: true },
useSystemSoundTheme: { def: false }, useSystemSoundTheme: { def: false },
@@ -572,6 +582,7 @@ var SPEC = {
builtInPluginSettings: { def: {} }, builtInPluginSettings: { def: {} },
clipboardEnterToPaste: { def: false }, clipboardEnterToPaste: { def: false },
clipboardVisibleEntryActions: { def: ["pin", "edit", "delete"] },
launcherPluginVisibility: { def: {} }, launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] }, launcherPluginOrder: { def: [] },
+31 -19
View File
@@ -64,27 +64,15 @@ Item {
} }
} }
property bool wallpaperSurfacesLoaded: true
Loader { Loader {
id: blurredWallpaperBackgroundLoader id: blurredWallpaperBackgroundLoader
active: root.wallpaperSurfacesLoaded && SettingsData.blurredWallpaperLayer && CompositorService.isNiri active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
asynchronous: false asynchronous: false
sourceComponent: BlurredWallpaperBackground {} sourceComponent: BlurredWallpaperBackground {}
} }
DeferredAction { WallpaperBackground {}
id: wallpaperSurfaceReloadAction
onTriggered: root.wallpaperSurfacesLoaded = true
}
Loader {
id: wallpaperBackgroundLoader
active: root.wallpaperSurfacesLoaded
asynchronous: false
sourceComponent: WallpaperBackground {}
}
DesktopWidgetLayer {} DesktopWidgetLayer {}
@@ -398,11 +386,6 @@ Item {
frameSurfaceReloadAction.schedule(); frameSurfaceReloadAction.schedule();
} }
if (root.wallpaperSurfacesLoaded) {
root.wallpaperSurfacesLoaded = false;
wallpaperSurfaceReloadAction.schedule();
}
root.dockEnabled = false; root.dockEnabled = false;
Qt.callLater(() => { Qt.callLater(() => {
root.dockEnabled = true; root.dockEnabled = true;
@@ -1110,11 +1093,22 @@ Item {
slideoutWidth: 480 slideoutWidth: 480
expandable: true expandable: true
expandedWidthValue: 960 expandedWidthValue: 960
edgeGap: SettingsData.notepadEffectiveEdgeGap
slideEdge: SettingsData.notepadSlideoutSide
onIsVisibleChanged: {
if (isVisible)
PopoutService.notepadPopout?.hide();
}
content: Component { content: Component {
Notepad { Notepad {
slideout: notepadSlideout slideout: notepadSlideout
onHideRequested: notepadSlideout.hide() onHideRequested: notepadSlideout.hide()
onPopoutRequested: {
notepadSlideout.hide();
PopoutService.openNotepadPopout();
}
} }
} }
@@ -1131,6 +1125,24 @@ Item {
Component.onCompleted: PopoutService.notepadSlideouts = instances Component.onCompleted: PopoutService.notepadSlideouts = instances
} }
LazyLoader {
id: notepadPopoutLoader
active: false
Component.onCompleted: {
PopoutService.notepadPopoutLoader = notepadPopoutLoader;
}
onActiveChanged: {
if (active && item) {
PopoutService.notepadPopout = item;
PopoutService._onNotepadPopoutLoaded();
}
}
NotepadPopoutWindow {}
}
LazyLoader { LazyLoader {
id: powerMenuModalLoader id: powerMenuModalLoader
+13 -1
View File
@@ -373,6 +373,10 @@ Item {
} }
function open(): string { function open(): string {
if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.openNotepadPopout();
return "NOTEPAD_OPEN_SUCCESS";
}
var instance = getActiveNotepadInstance(); var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.show(); instance.show();
@@ -382,6 +386,10 @@ Item {
} }
function close(): string { function close(): string {
if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.notepadPopout?.hide();
return "NOTEPAD_CLOSE_SUCCESS";
}
var instance = getActiveNotepadInstance(); var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.hide(); instance.hide();
@@ -391,6 +399,10 @@ Item {
} }
function toggle(): string { function toggle(): string {
if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.toggleNotepadPopout();
return "NOTEPAD_TOGGLE_SUCCESS";
}
var instance = getActiveNotepadInstance(); var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.toggle(); instance.toggle();
@@ -944,7 +956,7 @@ Item {
function tabs(): string { function tabs(): string {
if (!PopoutService.settingsModal) if (!PopoutService.settingsModal)
return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_appearance\ndankbar_widgets\nframe\nworkspaces\ncompositor\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nprinters\nlock_screen\npower_sleep\nplugins\nabout"; return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_appearance\ndankbar_widgets\nframe\nworkspaces\ncompositor\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nnetwork_status\nnetwork_ethernet\nnetwork_wifi\nnetwork_vpn\nprinters\nlock_screen\npower_sleep\nplugins\nabout";
var modal = PopoutService.settingsModal; var modal = PopoutService.settingsModal;
var ids = []; var ids = [];
var structure = modal.sidebar?.categoryStructure ?? []; var structure = modal.sidebar?.categoryStructure ?? [];
@@ -7,7 +7,6 @@ Item {
id: clipboardContent id: clipboardContent
required property var modal required property var modal
required property var clearConfirmDialog
property alias searchField: searchField property alias searchField: searchField
property alias clipboardListView: clipboardListView property alias clipboardListView: clipboardListView
@@ -33,14 +32,7 @@ Item {
pinnedCount: modal.pinnedCount pinnedCount: modal.pinnedCount
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
onTabChanged: tabName => modal.activeTab = tabName onTabChanged: tabName => modal.activeTab = tabName
onClearAllClicked: { onClearAllClicked: modal.confirmClearAll()
const hasPinned = modal.pinnedCount > 0;
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(modal.pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
modal.clearAll();
modal.hide();
}, function () {});
}
onCloseClicked: modal.hide() onCloseClicked: modal.hide()
} }
+32 -7
View File
@@ -22,7 +22,14 @@ Rectangle {
readonly property string entryType: modal ? modal.getEntryType(entry) : "text" readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : "" readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
readonly property var pinnedDuplicateEntry: !entry.pinned ? ClipboardService.getPinnedEntryByHash(entry.hash) : null readonly property var pinnedDuplicateEntry: !entry.pinned ? ClipboardService.getPinnedEntryByHash(entry.hash) : null
readonly property bool effectivePinned: entry.pinned || pinnedDuplicateEntry !== null readonly property bool hasPinnedDuplicate: pinnedDuplicateEntry !== null
readonly property bool effectivePinned: entry.pinned || hasPinnedDuplicate
readonly property var visibleEntryActions: SettingsData.clipboardVisibleEntryActions || ["pin", "edit", "delete"]
readonly property bool showPinAction: visibleEntryActions.includes("pin")
readonly property bool showEditAction: visibleEntryActions.includes("edit")
readonly property bool showDeleteAction: visibleEntryActions.includes("delete")
readonly property bool showPinnedIndicator: hasPinnedDuplicate && !showPinAction
readonly property bool showAnyAction: showPinAction || showEditAction || showDeleteAction || showPinnedIndicator
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
@@ -63,12 +70,28 @@ Rectangle {
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: root.showAnyAction
Item {
width: 40
height: 40
visible: root.showPinnedIndicator
// Status indicator only; the Pin action remains hidden.
DankIcon {
anchors.centerIn: parent
name: "push_pin"
size: Theme.iconSize - 6
color: Theme.primary
}
}
DankActionButton { DankActionButton {
iconName: "push_pin" iconName: "push_pin"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
iconColor: effectivePinned ? Theme.primary : Theme.surfaceText iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText
backgroundColor: effectivePinned ? Theme.primarySelected : "transparent" backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent"
visible: root.showPinAction
onClicked: { onClicked: {
if (entry.pinned) { if (entry.pinned) {
unpinRequested(entry); unpinRequested(entry);
@@ -86,6 +109,7 @@ Rectangle {
iconName: "edit" iconName: "edit"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
visible: root.showEditAction
onClicked: { onClicked: {
if (entryType === "image") { if (entryType === "image") {
@@ -99,6 +123,7 @@ Rectangle {
iconName: "close" iconName: "close"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
visible: root.showDeleteAction
onClicked: deleteRequested() onClicked: deleteRequested()
} }
} }
@@ -106,8 +131,8 @@ Rectangle {
Item { Item {
anchors.left: indexBadge.right anchors.left: indexBadge.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: actionButtons.left anchors.right: root.showAnyAction ? actionButtons.left : parent.right
anchors.rightMargin: Theme.spacingM anchors.rightMargin: root.showAnyAction ? Theme.spacingM : Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// height: contentColumn.implicitHeight // height: contentColumn.implicitHeight
height: ClipboardConstants.itemHeight height: ClipboardConstants.itemHeight
@@ -168,8 +193,8 @@ Rectangle {
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.left: parent.left anchors.left: parent.left
anchors.right: actionButtons.left anchors.right: root.showAnyAction ? actionButtons.left : parent.right
anchors.rightMargin: Theme.spacingS anchors.rightMargin: root.showAnyAction ? Theme.spacingS : 0
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
hoverEnabled: true hoverEnabled: true
@@ -82,6 +82,15 @@ FocusScope {
ClipboardService.clearAll(); ClipboardService.clearAll();
} }
function confirmClearAll() {
const hasPinned = pinnedCount > 0;
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
clearAll();
hide();
}, function () {});
}
function getEntryPreview(entry) { function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry); return ClipboardService.getEntryPreview(entry);
} }
@@ -135,7 +144,6 @@ FocusScope {
id: historyContent id: historyContent
anchors.fill: parent anchors.fill: parent
modal: root modal: root
clearConfirmDialog: root.clearConfirmDialog
} }
} }
@@ -77,22 +77,35 @@ DankModal {
id: clearConfirmDialog id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All") confirmButtonText: I18n.tr("Clear All")
confirmButtonColor: Theme.primary confirmButtonColor: Theme.primary
onVisibleChanged: { onShouldBeVisibleChanged: {
if (visible) { if (shouldBeVisible) {
clipboardHistoryModal.shouldHaveFocus = false; clipboardHistoryModal.shouldHaveFocus = false;
selectedButton = 0;
keyboardNavigation = true;
return; return;
} }
Qt.callLater(function () { Qt.callLater(function () {
if (!clipboardHistoryModal.shouldBeVisible) { if (!clipboardHistoryModal.shouldBeVisible) {
return; return;
} }
clipboardHistoryModal.shouldHaveFocus = true; clipboardHistoryModal.shouldHaveFocus = Qt.binding(() => clipboardHistoryModal.shouldBeVisible);
clipboardHistoryModal.modalFocusScope.forceActiveFocus(); clipboardHistoryModal.modalFocusScope.forceActiveFocus();
if (clipboardHistoryModal.contentLoader.item?.searchField) { if (clipboardHistoryModal.contentLoader.item?.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus(); clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
} }
}); });
} }
Connections {
target: clearConfirmDialog.modalFocusScope.Keys
function onPressed(event) {
if (!clearConfirmDialog.shouldBeVisible || event.key !== Qt.Key_Backtab) {
return;
}
clearConfirmDialog.selectedButton = clearConfirmDialog.selectedButton === -1 ? 1 : (clearConfirmDialog.selectedButton - 1 + 2) % 2;
clearConfirmDialog.keyboardNavigation = true;
event.accepted = true;
}
}
} }
content: Component { content: Component {
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Modals.Clipboard import qs.Modals.Clipboard
import qs.Modals.Common import qs.Modals.Common
@@ -95,6 +96,35 @@ DankPopout {
id: clearConfirmDialog id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All") confirmButtonText: I18n.tr("Clear All")
confirmButtonColor: Theme.primary confirmButtonColor: Theme.primary
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
root.customKeyboardFocus = WlrKeyboardFocus.None;
selectedButton = 0;
keyboardNavigation = true;
return;
}
root.customKeyboardFocus = null;
Qt.callLater(function () {
if (!root.shouldBeVisible || !root.contentLoader.item) {
return;
}
root.contentLoader.item.forceActiveFocus();
if (root.contentLoader.item.searchField) {
root.contentLoader.item.searchField.forceActiveFocus();
}
});
}
Connections {
target: clearConfirmDialog.modalFocusScope.Keys
function onPressed(event) {
if (!clearConfirmDialog.shouldBeVisible || event.key !== Qt.Key_Backtab) {
return;
}
clearConfirmDialog.selectedButton = clearConfirmDialog.selectedButton === -1 ? 1 : (clearConfirmDialog.selectedButton - 1 + 2) % 2;
clearConfirmDialog.keyboardNavigation = true;
event.accepted = true;
}
}
} }
content: Component { content: Component {
@@ -125,8 +125,6 @@ QtObject {
if (!ClipboardService.keyboardNavigationActive) { if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0; ClipboardService.selectedIndex = 0;
} else if (ClipboardService.selectedIndex === 0) {
ClipboardService.keyboardNavigationActive = false;
} else { } else {
selectPrevious(); selectPrevious();
} }
@@ -155,8 +153,6 @@ QtObject {
if (!ClipboardService.keyboardNavigationActive) { if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0; ClipboardService.selectedIndex = 0;
} else if (ClipboardService.selectedIndex === 0) {
ClipboardService.keyboardNavigationActive = false;
} else { } else {
selectPrevious(); selectPrevious();
} }
@@ -184,8 +180,7 @@ QtObject {
if (event.modifiers & Qt.ShiftModifier) { if (event.modifiers & Qt.ShiftModifier) {
switch (event.key) { switch (event.key) {
case Qt.Key_Delete: case Qt.Key_Delete:
modal.clearAll(); modal.confirmClearAll();
modal.hide();
event.accepted = true; event.accepted = true;
return; return;
case Qt.Key_Return: case Qt.Key_Return:
@@ -201,6 +201,21 @@ FocusScope {
keyboardSelectionRequested = true; keyboardSelectionRequested = true;
} }
function activateFile(path, name, isDir) {
if (isDir) {
navigateTo(path);
return;
}
if (saveMode) {
saveRow.fileName = name;
pendingFilePath = path;
showOverwriteConfirmation = true;
} else {
fileSelected(path);
closeRequested();
}
}
function handleSaveFile(filePath) { function handleSaveFile(filePath) {
var normalizedPath = filePath; var normalizedPath = filePath;
if (!normalizedPath.startsWith("file://")) { if (!normalizedPath.startsWith("file://")) {
@@ -652,6 +667,7 @@ FocusScope {
Row { Row {
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: root.saveMode ? 40 + Theme.spacingL * 2 : 0
spacing: 0 spacing: 0
Row { Row {
@@ -756,12 +772,7 @@ FocusScope {
onItemClicked: (index, path, name, isDir) => { onItemClicked: (index, path, name, isDir) => {
selectedIndex = index; selectedIndex = index;
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
if (isDir) { root.activateFile(path, name, isDir);
navigateTo(path);
} else {
fileSelected(path);
root.closeRequested();
}
} }
onItemSelected: (index, path, name, isDir) => { onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
@@ -776,12 +787,7 @@ FocusScope {
root.keyboardSelectionRequested = false; root.keyboardSelectionRequested = false;
selectedIndex = index; selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir); setSelectedFileData(filePath, fileName, fileIsDir);
if (fileIsDir) { root.activateFile(filePath, fileName, fileIsDir);
navigateTo(filePath);
} else {
fileSelected(filePath);
root.closeRequested();
}
} }
} }
@@ -817,12 +823,7 @@ FocusScope {
onItemClicked: (index, path, name, isDir) => { onItemClicked: (index, path, name, isDir) => {
selectedIndex = index; selectedIndex = index;
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
if (isDir) { root.activateFile(path, name, isDir);
navigateTo(path);
} else {
fileSelected(path);
root.closeRequested();
}
} }
onItemSelected: (index, path, name, isDir) => { onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
@@ -837,12 +838,7 @@ FocusScope {
root.keyboardSelectionRequested = false; root.keyboardSelectionRequested = false;
selectedIndex = index; selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir); setSelectedFileData(filePath, fileName, fileIsDir);
if (fileIsDir) { root.activateFile(filePath, fileName, fileIsDir);
navigateTo(filePath);
} else {
fileSelected(filePath);
root.closeRequested();
}
} }
} }
@@ -855,6 +851,7 @@ FocusScope {
} }
FileBrowserSaveRow { FileBrowserSaveRow {
id: saveRow
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@@ -913,6 +910,7 @@ FocusScope {
} }
} }
} }
}
FileBrowserOverwriteDialog { FileBrowserOverwriteDialog {
anchors.fill: parent anchors.fill: parent
@@ -929,7 +927,6 @@ FocusScope {
pendingFilePath = ""; pendingFilePath = "";
} }
} }
}
FileBrowserItemContextMenu { FileBrowserItemContextMenu {
id: itemContextMenu id: itemContextMenu
@@ -74,7 +74,7 @@ Item {
width: 80 width: 80
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant color: cancelArea.containsMouse ? Qt.lighter(Theme.surfaceVariant, 1.2) : Theme.surfaceVariant
border.color: Theme.outline border.color: Theme.outline
border.width: 1 border.width: 1
@@ -8,6 +8,7 @@ Row {
property bool saveMode: false property bool saveMode: false
property string defaultFileName: "" property string defaultFileName: ""
property string currentPath: "" property string currentPath: ""
property alias fileName: fileNameInput.text
signal saveRequested(string filePath) signal saveRequested(string filePath)
+47 -1
View File
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Modules.Settings import qs.Modules.Settings
import qs.Services
import qs.Widgets import qs.Widgets
FocusScope { FocusScope {
@@ -232,7 +233,52 @@ FocusScope {
visible: active visible: active
focus: active focus: active
sourceComponent: NetworkTab {} sourceComponent: NetworkStatusTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: networkEthernetLoader
anchors.fill: parent
active: root.currentIndex === 39
visible: active
focus: active
sourceComponent: NetworkEthernetTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: networkWifiLoader
anchors.fill: parent
active: root.currentIndex === 40
visible: active
focus: active
sourceComponent: NetworkWifiTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: networkVpnLoader
anchors.fill: parent
active: root.currentIndex === 41
visible: active
focus: active
sourceComponent: NetworkVpnTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
+7 -6
View File
@@ -53,20 +53,21 @@ FloatingWindow {
visible = !visible; visible = !visible;
} }
function showWithTab(tabIndex: int) { function setTabIndex(tabIndex: int) {
if (tabIndex >= 0) { if (tabIndex < 0)
return;
currentTabIndex = tabIndex; currentTabIndex = tabIndex;
sidebar.autoExpandForTab(tabIndex); sidebar.autoExpandForTab(tabIndex);
} }
function showWithTab(tabIndex: int) {
setTabIndex(tabIndex);
visible = true; visible = true;
} }
function showWithTabName(tabName: string) { function showWithTabName(tabName: string) {
var idx = sidebar.resolveTabIndex(tabName); var idx = sidebar.resolveTabIndex(tabName);
if (idx >= 0) { setTabIndex(idx);
currentTabIndex = idx;
sidebar.autoExpandForTab(idx);
}
visible = true; visible = true;
} }
+35 -10
View File
@@ -105,8 +105,8 @@ Rectangle {
}, },
{ {
"id": "compositor_layout", "id": "compositor_layout",
"text": CompositorService.isNiri ? "niri" : (CompositorService.isHyprland ? "Hyprland" : "MangoWC"), "text": CompositorService.isNiri ? "Niri" : (CompositorService.isHyprland ? "Hyprland" : "MangoWC"),
"icon": "crop_square", "icon": "layers",
"tabIndex": 37, "tabIndex": 37,
"layoutCapable": true "layoutCapable": true
} }
@@ -117,18 +117,18 @@ Rectangle {
"text": I18n.tr("Dank Bar"), "text": I18n.tr("Dank Bar"),
"icon": "toolbar", "icon": "toolbar",
"children": [ "children": [
{
"id": "dankbar_settings",
"text": I18n.tr("Settings"),
"icon": "tune",
"tabIndex": 3
},
{ {
"id": "dankbar_appearance", "id": "dankbar_appearance",
"text": I18n.tr("Appearance"), "text": I18n.tr("Appearance"),
"icon": "palette", "icon": "palette",
"tabIndex": 6 "tabIndex": 6
}, },
{
"id": "dankbar_settings",
"text": I18n.tr("Settings"),
"icon": "tune",
"tabIndex": 3
},
{ {
"id": "dankbar_widgets", "id": "dankbar_widgets",
"text": I18n.tr("Widgets"), "text": I18n.tr("Widgets"),
@@ -238,8 +238,33 @@ Rectangle {
"id": "network", "id": "network",
"text": I18n.tr("Network"), "text": I18n.tr("Network"),
"icon": "wifi", "icon": "wifi",
"tabIndex": 7, "dmsOnly": true,
"dmsOnly": true "children": [
{
"id": "network_status",
"text": I18n.tr("Status"),
"icon": "lan",
"tabIndex": 7
},
{
"id": "network_ethernet",
"text": I18n.tr("Ethernet"),
"icon": "settings_ethernet",
"tabIndex": 39
},
{
"id": "network_wifi",
"text": I18n.tr("WiFi"),
"icon": "wifi",
"tabIndex": 40
},
{
"id": "network_vpn",
"text": I18n.tr("VPN"),
"icon": "vpn_key",
"tabIndex": 41
}
]
}, },
{ {
"id": "applications", "id": "applications",
@@ -7,6 +7,7 @@ import qs.Widgets
import qs.Services import qs.Services
Variants { Variants {
readonly property var log: Log.scoped("BlurredWallpaperBackground")
model: { model: {
if (SessionData.isGreeterMode) { if (SessionData.isGreeterMode) {
return Quickshell.screens; return Quickshell.screens;
@@ -32,6 +33,8 @@ Variants {
color: "transparent" color: "transparent"
updatesEnabled: root.renderActive || root._settleFrames > 0
mask: Region { mask: Region {
item: Item {} item: Item {}
} }
@@ -85,7 +88,6 @@ Variants {
} }
Component.onCompleted: { Component.onCompleted: {
blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
isInitialized = true; isInitialized = true;
} }
@@ -93,51 +95,67 @@ Variants {
property real transitionProgress: 0 property real transitionProgress: 0
readonly property bool transitioning: transitionAnimation.running readonly property bool transitioning: transitionAnimation.running
property bool effectActive: false property bool effectActive: false
property bool _renderSettling: true
property bool useNextForEffect: false property bool useNextForEffect: false
readonly property var backingWindow: Window.window
readonly property bool renderActive: !source || effectActive || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
property int _settleFrames: 3
Connections { function invalidate() {
target: currentWallpaper _settleFrames = 3;
function onStatusChanged() { backingWindow?.update();
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
return;
root._renderSettling = true;
renderSettleTimer.restart();
}
} }
onRenderActiveChanged: invalidate()
onBackingWindowChanged: invalidate()
Connections { Connections {
target: blurWallpaperWindow target: root.backingWindow
function onFrameSwapped() {
if (root._settleFrames > 0)
root._settleFrames--;
}
function onVisibleChanged() {
root.invalidate();
}
function onWidthChanged() { function onWidthChanged() {
root._renderSettling = true; root.invalidate();
renderSettleTimer.restart();
} }
function onHeightChanged() { function onHeightChanged() {
root._renderSettling = true; root.invalidate();
renderSettleTimer.restart();
} }
} }
Connections { Connections {
target: Quickshell target: Quickshell
function onScreensChanged() { function onScreensChanged() {
root._renderSettling = true; root.invalidate();
renderSettleTimer.restart();
} }
} }
Connections { Connections {
target: SettingsData target: SettingsData
function onWallpaperFillModeChanged() { function onWallpaperFillModeChanged() {
root._renderSettling = true; root.invalidate();
renderSettleTimer.restart();
} }
} }
Timer { Connections {
id: renderSettleTimer target: IdleService
interval: 1000 function onIsShellLockedChanged() {
onTriggered: root._renderSettling = false if (IdleService.isShellLocked)
return;
root.invalidate();
}
}
function handleTransitionLoadError(failedSource) {
log.warn("failed to load candidate wallpaper for", modelData.name + ":", failedSource);
transitionDelayTimer.stop();
transitionAnimation.stop();
root.useNextForEffect = false;
root.effectActive = false;
root.transitionProgress = 0.0;
nextWallpaper.source = "";
} }
onSourceChanged: { onSourceChanged: {
@@ -164,8 +182,6 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root.effectActive = false; root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = newSource; currentWallpaper.source = newSource;
nextWallpaper.source = ""; nextWallpaper.source = "";
} }
@@ -194,8 +210,6 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0; root.transitionProgress = 0;
root.effectActive = false; root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = nextWallpaper.source; currentWallpaper.source = nextWallpaper.source;
nextWallpaper.source = ""; nextWallpaper.source = "";
} }
@@ -204,9 +218,6 @@ Variants {
return; return;
} }
root._renderSettling = true;
renderSettleTimer.restart();
nextWallpaper.source = newPath; nextWallpaper.source = newPath;
if (nextWallpaper.status === Image.Ready) if (nextWallpaper.status === Image.Ready)
@@ -215,7 +226,7 @@ Variants {
Loader { Loader {
anchors.fill: parent anchors.fill: parent
active: !root.source || root.isColorSource active: !root.source || root.isColorSource || currentWallpaper.status === Image.Error
asynchronous: true asynchronous: true
sourceComponent: DankBackdrop { sourceComponent: DankBackdrop {
@@ -238,6 +249,12 @@ Variants {
cache: true cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight) sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name)) fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status === Image.Error) {
log.warn("failed to load active wallpaper for", modelData.name + ":", source);
}
}
} }
Image { Image {
@@ -253,6 +270,10 @@ Variants {
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name)) fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: { onStatusChanged: {
if (status === Image.Error) {
root.handleTransitionLoadError(source);
return;
}
if (status !== Image.Ready) if (status !== Image.Ready)
return; return;
if (!root.transitioning) { if (!root.transitioning) {
@@ -329,8 +350,6 @@ Variants {
root.useNextForEffect = false; root.useNextForEffect = false;
nextWallpaper.source = ""; nextWallpaper.source = "";
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root._renderSettling = true;
renderSettleTimer.restart();
root.effectActive = false; root.effectActive = false;
} }
} }
@@ -151,7 +151,7 @@ Rectangle {
iconColor: Theme.surfaceVariantText iconColor: Theme.surfaceVariantText
onClicked: { onClicked: {
PopoutService.closeControlCenter(); PopoutService.closeControlCenter();
PopoutService.openSettingsWithTab("network"); PopoutService.openSettingsWithTab(currentPreferenceIndex === 0 ? "network_ethernet" : "network_wifi");
} }
} }
} }
@@ -15,6 +15,7 @@ Item {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -359,6 +360,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === centerRepeater.count - 1 isLast: index === centerRepeater.count - 1
sectionSpacing: parent.itemSpacing sectionSpacing: parent.itemSpacing
@@ -497,6 +497,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, hCenterSection.x > 0 ? hCenterSection.x : parent.width / 3)
} }
Binding { Binding {
@@ -529,6 +530,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, hCenterSection.x > 0 ? parent.width - (hCenterSection.x + hCenterSection.width) : parent.width / 3)
} }
Binding { Binding {
@@ -561,6 +563,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, hRightSection.x > 0 ? hRightSection.x - (hLeftSection.x + hLeftSection.width) : parent.width / 3)
} }
Binding { Binding {
@@ -600,6 +603,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, vCenterSection.y > 0 ? vCenterSection.y : parent.height / 3)
} }
Binding { Binding {
@@ -633,6 +637,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, vRightSection.y > 0 ? vRightSection.y - (vLeftSection.y + vLeftSection.height) : parent.height / 3)
} }
Binding { Binding {
@@ -667,6 +672,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, vCenterSection.y > 0 ? parent.height - (vCenterSection.y + vCenterSection.height) : parent.height / 3)
} }
Binding { Binding {
+16 -14
View File
@@ -286,9 +286,6 @@ PanelWindow {
readonly property bool isVertical: axis.isVertical readonly property bool isVertical: axis.isVertical
property bool gothCornersEnabled: barConfig?.gothCornersEnabled ?? false
property real wingtipsRadius: barConfig?.gothCornerRadiusOverride ? (barConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius
readonly property real _wingR: Math.max(0, wingtipsRadius)
readonly property color _surfaceContainer: Theme.surfaceContainer readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default" readonly property string _barId: barConfig?.id ?? "default"
property real _backgroundAlpha: barConfig?.transparency ?? 1.0 property real _backgroundAlpha: barConfig?.transparency ?? 1.0
@@ -300,25 +297,30 @@ PanelWindow {
} }
readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen) readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen)
property string screenName: modelData.name
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName)
readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName)
readonly property var renderBarConfig: SettingsData.effectiveBarConfigForRender(barConfig, usesFrameBarChrome)
property bool gothCornersEnabled: renderBarConfig?.gothCornersEnabled ?? false
property real wingtipsRadius: renderBarConfig?.gothCornerRadiusOverride ? (renderBarConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius
readonly property real _wingR: Math.max(0, wingtipsRadius)
// Shadow buffer: extra window space for shadow to render beyond bar bounds // Shadow buffer: extra window space for shadow to render beyond bar bounds
readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (barConfig?.shadowIntensity ?? 0) > 0 readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (renderBarConfig?.shadowIntensity ?? 0) > 0
readonly property real _shadowBuffer: { readonly property real _shadowBuffer: {
if (!_shadowActive) if (!_shadowActive)
return 0; return 0;
const hasOverride = (barConfig?.shadowIntensity ?? 0) > 0; const hasOverride = (renderBarConfig?.shadowIntensity ?? 0) > 0;
if (hasOverride) { if (hasOverride) {
const blur = (barConfig.shadowIntensity ?? 0) * 0.2; const blur = (renderBarConfig.shadowIntensity ?? 0) * 0.2;
const offset = blur * 0.5; const offset = blur * 0.5;
return Theme.snap(Math.max(16, blur + offset + 8), _dpr); return Theme.snap(Math.max(16, blur + offset + 8), _dpr);
} }
return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr); return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr);
} }
property string screenName: modelData.name
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName)
readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName)
// Flatten/spacing collapse for maximized windows is only for frame-integrated layout. // Flatten/spacing collapse for maximized windows is only for frame-integrated layout.
// When the bar draws its own pill, keep rounded corners and spacing like the dock. // When the bar draws its own pill, keep rounded corners and spacing like the dock.
readonly property bool flattenForMaximizedWindow: !SettingsData.frameEnabled || usesFrameBarChrome readonly property bool flattenForMaximizedWindow: !SettingsData.frameEnabled || usesFrameBarChrome
@@ -554,8 +556,8 @@ PanelWindow {
} }
screen: modelData screen: modelData
implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
color: "transparent" color: "transparent"
Component.onCompleted: { Component.onCompleted: {
@@ -952,7 +954,7 @@ PanelWindow {
id: barBackground id: barBackground
barWindow: barWindow barWindow: barWindow
axis: axis axis: axis
barConfig: barWindow.barConfig barConfig: barWindow.renderBarConfig
} }
MouseArea { MouseArea {
@@ -14,6 +14,7 @@ Item {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -61,6 +62,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -106,6 +108,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -14,6 +14,7 @@ Item {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -63,6 +64,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -108,6 +110,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -17,6 +17,7 @@ Loader {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool isFirst: false property bool isFirst: false
property bool isLast: false property bool isLast: false
property real sectionSpacing: 0 property real sectionSpacing: 0
@@ -141,6 +142,14 @@ Loader {
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: root.item
when: root.item && "sectionAvailablePrimarySize" in root.item
property: "sectionAvailablePrimarySize"
value: root.sectionAvailablePrimarySize
restoreMode: Binding.RestoreNone
}
Binding { Binding {
target: root.item target: root.item
when: root.item && "isLeftBarEdge" in root.item when: root.item && "isLeftBarEdge" in root.item
@@ -32,9 +32,20 @@ BasePill {
} }
readonly property var notepadInstance: resolveNotepadInstance() readonly property var notepadInstance: resolveNotepadInstance()
readonly property bool isActive: notepadInstance?.isVisible ?? false readonly property bool popoutDefault: SettingsData.notepadDefaultMode === "popout"
readonly property bool isActive: popoutDefault ? (PopoutService.notepadPopout?.visible ?? false) : (notepadInstance?.isVisible ?? false)
property bool isAutoHideBar: false property bool isAutoHideBar: false
function showActiveSurface() {
if (root.popoutDefault) {
PopoutService.openNotepadPopout();
return;
}
const instance = prepareNotepadInstance(root.notepadInstance);
if (instance && typeof instance.show === "function")
instance.show();
}
function prepareNotepadInstance(instance) { function prepareNotepadInstance(instance) {
if (instance) if (instance)
instance.triggerUsesOverlayLayer = root.barUsesOverlayLayer; instance.triggerUsesOverlayLayer = root.barUsesOverlayLayer;
@@ -75,20 +86,14 @@ BasePill {
function openTabByIndex(tabIndex) { function openTabByIndex(tabIndex) {
if (tabIndex < 0) if (tabIndex < 0)
return; return;
const instance = prepareNotepadInstance(root.notepadInstance); showActiveSurface();
if (instance && typeof instance.show === "function") {
instance.show();
}
Qt.callLater(() => { Qt.callLater(() => {
NotepadStorageService.switchToTab(tabIndex); NotepadStorageService.switchToTab(tabIndex);
}); });
} }
function openNewNote() { function openNewNote() {
const instance = prepareNotepadInstance(root.notepadInstance); showActiveSurface();
if (instance && typeof instance.show === "function") {
instance.show();
}
Qt.callLater(() => { Qt.callLater(() => {
NotepadStorageService.createNewTab(); NotepadStorageService.createNewTab();
}); });
@@ -147,6 +152,10 @@ BasePill {
openContextMenu(); openContextMenu();
return; return;
} }
if (root.popoutDefault) {
PopoutService.toggleNotepadPopout();
return;
}
const inst = prepareNotepadInstance(root.notepadInstance); const inst = prepareNotepadInstance(root.notepadInstance);
if (inst) { if (inst) {
inst.toggle(); inst.toggle();
@@ -22,6 +22,10 @@ BasePill {
property bool isAtBottom: false property bool isAtBottom: false
property bool isAutoHideBar: false property bool isAutoHideBar: false
property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
property bool useSingleLineOverflowPopup: widgetData?.trayPopupSingleLine ?? SettingsData.trayPopupSingleLine
property bool useAutomaticOverflow: widgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow
property int configuredMaxVisibleItems: widgetData?.trayMaxVisibleItems ?? SettingsData.trayMaxVisibleItems
property real sectionAvailablePrimarySize: 0
readonly property var hiddenTrayIds: { readonly property var hiddenTrayIds: {
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || ""; const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : []; return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
@@ -146,12 +150,32 @@ BasePill {
readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger) readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger)
readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item)) readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item))
readonly property var mainBarItemsRaw: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item))) readonly property var visibleSortedTrayItems: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property int automaticVisibleItemLimit: {
if (!root.useAutomaticOverflow)
return root.visibleSortedTrayItems.length;
const explicitLimit = Number(root.configuredMaxVisibleItems || 0);
if (explicitLimit > 0)
return Math.max(1, Math.min(root.visibleSortedTrayItems.length, explicitLimit));
const scale = (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? Math.max(1, CompositorService.getScreenScale(root.parentScreen)) : 1;
const sectionPrimary = root.sectionAvailablePrimarySize > 0 ? root.sectionAvailablePrimarySize : (root.isVerticalOrientation ? (root.parentScreen?.height || 0) : (root.parentScreen?.width || 0));
const logicalPrimary = sectionPrimary > 0 ? (sectionPrimary / scale) : 640;
const maxTrayShare = root.isVerticalOrientation ? 0.55 : 0.50;
const itemSize = Math.max(1, root.trayItemSize);
const slots = Math.floor((logicalPrimary * maxTrayShare) / itemSize);
return Math.max(2, Math.min(10, Math.min(root.visibleSortedTrayItems.length, slots)));
}
readonly property var mainBarItemsRaw: visibleSortedTrayItems.slice(0, automaticVisibleItemLimit)
readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({ readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({
key: getTrayItemKey(item), key: getTrayItemKey(item),
item: item item: item
})) }))
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item))) readonly property var autoOverflowBarItems: visibleSortedTrayItems.slice(automaticVisibleItemLimit)
readonly property var manualHiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property var hiddenBarItemKeys: manualHiddenBarItems.concat(autoOverflowBarItems).map(item => root.getTrayItemKey(item))
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => hiddenBarItemKeys.indexOf(root.getTrayItemKey(item)) !== -1)
readonly property string trayIconTintMode: { readonly property string trayIconTintMode: {
const configuredMode = SettingsData.systemTrayIconTintMode || "none"; const configuredMode = SettingsData.systemTrayIconTintMode || "none";
switch (configuredMode) { switch (configuredMode) {
@@ -219,6 +243,10 @@ BasePill {
const fromKey = mainBarItems[visibleFromIndex]?.key ?? null; const fromKey = mainBarItems[visibleFromIndex]?.key ?? null;
const toKey = mainBarItems[visibleToIndex]?.key ?? null; const toKey = mainBarItems[visibleToIndex]?.key ?? null;
moveTrayItemKeyInFullOrder(fromKey, toKey);
}
function moveTrayItemKeyInFullOrder(fromKey, toKey) {
if (!fromKey || !toKey) if (!fromKey || !toKey)
return; return;
@@ -233,10 +261,103 @@ BasePill {
SessionData.setTrayItemOrder(fullOrder); SessionData.setTrayItemOrder(fullOrder);
} }
function promoteTrayItemToBar(item) {
const itemKey = getTrayItemKey(item);
if (!itemKey)
return;
if (SessionData.isHiddenTrayId(itemKey)) {
SessionData.showTrayId(itemKey);
return;
}
const fullOrder = [...allSortedTrayItemKeys];
const fromIndex = fullOrder.indexOf(itemKey);
if (fromIndex < 0)
return;
const movedKey = fullOrder.splice(fromIndex, 1)[0];
const targetIndex = Math.max(0, Math.min(root.automaticVisibleItemLimit - 1, fullOrder.length));
fullOrder.splice(targetIndex, 0, movedKey);
SessionData.setTrayItemOrder(fullOrder);
}
function isManualHiddenTrayItem(item) {
return SessionData.isHiddenTrayId(getTrayItemKey(item));
}
function isAutoOverflowTrayItem(item) {
const key = getTrayItemKey(item);
return key && !isManualHiddenTrayItem(item) && root.autoOverflowBarItems.some(overflowItem => getTrayItemKey(overflowItem) === key);
}
function dragShiftOffset(index, draggedIndex, dropTargetIndex, shiftAmount) {
if (draggedIndex < 0 || index === draggedIndex || dropTargetIndex < 0)
return 0;
if (draggedIndex < dropTargetIndex && index > draggedIndex && index <= dropTargetIndex)
return -shiftAmount;
if (draggedIndex > dropTargetIndex && index >= dropTargetIndex && index < draggedIndex)
return shiftAmount;
return 0;
}
function beginMainDrag(visualIndex, reversed) {
root.draggedIndex = reversed ? (root.mainBarItems.length - 1 - visualIndex) : visualIndex;
root.dropTargetIndex = root.draggedIndex;
}
function updateMainDrag(axisOffset, visualIndex, reversed) {
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, visualIndex + slotOffset));
const newTargetIndex = reversed ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex)
root.dropTargetIndex = newTargetIndex;
}
function finishMainDrag() {
const didReorder = root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
root.draggedIndex = -1;
root.dropTargetIndex = -1;
return didReorder;
}
function beginPopupDrag(index) {
root.popupDraggedIndex = index;
root.popupDropTargetIndex = index;
}
function updatePopupDrag(axisOffset, index) {
const itemSize = root.trayItemSize + 6;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.hiddenBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.popupDropTargetIndex)
root.popupDropTargetIndex = newTargetIndex;
}
function finishPopupDrag() {
const didReorder = root.popupDropTargetIndex >= 0 && root.popupDropTargetIndex !== root.popupDraggedIndex;
if (didReorder) {
const fromItem = root.hiddenBarItems[root.popupDraggedIndex];
const toItem = root.hiddenBarItems[root.popupDropTargetIndex];
root.suppressShiftAnimation = true;
root.moveTrayItemKeyInFullOrder(root.getTrayItemKey(fromItem), root.getTrayItemKey(toItem));
Qt.callLater(() => root.suppressShiftAnimation = false);
}
root.popupDraggedIndex = -1;
root.popupDropTargetIndex = -1;
return didReorder;
}
property int draggedIndex: -1 property int draggedIndex: -1
property int dropTargetIndex: -1 property int dropTargetIndex: -1
property int popupDraggedIndex: -1
property int popupDropTargetIndex: -1
property bool suppressShiftAnimation: false property bool suppressShiftAnimation: false
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length readonly property bool hasHiddenItems: hiddenBarItems.length > 0
readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
visible: allTrayItems.length > 0 visible: allTrayItems.length > 0
opacity: allTrayItems.length > 0 ? 1 : 0 opacity: allTrayItems.length > 0 ? 1 : 0
@@ -351,22 +472,7 @@ BasePill {
height: root.barThickness height: root.barThickness
z: dragHandler.dragging ? 100 : 0 z: dragHandler.dragging ? 100 : 0
property real shiftOffset: { property real shiftOffset: root.dragShiftOffset(index, root.draggedIndex, root.dropTargetIndex, root.trayItemSize)
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate { transform: Translate {
x: delegateRoot.shiftOffset x: delegateRoot.shiftOffset
@@ -466,19 +572,12 @@ BasePill {
onReleased: mouse => { onReleased: mouse => {
longPressTimer.stop(); longPressTimer.stop();
const wasDragging = dragHandler.dragging; const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex; if (wasDragging)
root.finishMainDrag();
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false; dragHandler.longPressing = false;
dragHandler.dragging = false; dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0; dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton) if (wasDragging || mouse.button !== Qt.LeftButton)
return; return;
@@ -501,8 +600,7 @@ BasePill {
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x); const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) { if (distance > 5) {
dragHandler.dragging = true; dragHandler.dragging = true;
root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index; root.beginMainDrag(index, root.reverseInlineHorizontal);
root.dropTargetIndex = root.draggedIndex;
} }
} }
if (!dragHandler.dragging) if (!dragHandler.dragging)
@@ -510,13 +608,7 @@ BasePill {
const axisOffset = mouse.x - dragHandler.dragStartPos.x; const axisOffset = mouse.x - dragHandler.dragStartPos.x;
dragHandler.dragAxisOffset = axisOffset; dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize; root.updateMainDrag(axisOffset, index, root.reverseInlineHorizontal);
const slotOffset = Math.round(axisOffset / itemSize);
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
} }
onClicked: mouse => { onClicked: mouse => {
@@ -706,22 +798,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0 z: dragHandler.dragging ? 100 : 0
property real shiftOffset: { property real shiftOffset: root.dragShiftOffset(index, root.draggedIndex, root.dropTargetIndex, root.trayItemSize)
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate { transform: Translate {
y: shiftOffset y: shiftOffset
@@ -821,19 +898,12 @@ BasePill {
onReleased: mouse => { onReleased: mouse => {
longPressTimer.stop(); longPressTimer.stop();
const wasDragging = dragHandler.dragging; const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex; if (wasDragging)
root.finishMainDrag();
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false; dragHandler.longPressing = false;
dragHandler.dragging = false; dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0; dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton) if (wasDragging || mouse.button !== Qt.LeftButton)
return; return;
@@ -856,8 +926,7 @@ BasePill {
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y); const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
if (distance > 5) { if (distance > 5) {
dragHandler.dragging = true; dragHandler.dragging = true;
root.draggedIndex = index; root.beginMainDrag(index, false);
root.dropTargetIndex = root.draggedIndex;
} }
} }
if (!dragHandler.dragging) if (!dragHandler.dragging)
@@ -865,12 +934,7 @@ BasePill {
const axisOffset = mouse.y - dragHandler.dragStartPos.y; const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset; dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize; root.updateMainDrag(axisOffset, index, false);
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
} }
onClicked: mouse => { onClicked: mouse => {
@@ -1115,11 +1179,12 @@ BasePill {
} }
function updatePosition() { function updatePosition() {
const globalPos = root.mapToGlobal(0, 0); // Window-local maps directly to screen-local because the bar window spans the
const screenX = screen.x || 0; // full screen edge; this avoids mixing mapToGlobal with a separately-tracked
const screenY = screen.y || 0; // screen.x/.y origin, which desync on non-primary monitors and after DPMS/hotplug.
const relativeX = globalPos.x - screenX; const localPos = root.mapToItem(null, 0, 0);
const relativeY = globalPos.y - screenY; const relativeX = localPos.x;
const relativeY = localPos.y;
if (root.isVerticalOrientation) { if (root.isVerticalOrientation) {
const edge = root.axis?.edge; const edge = root.axis?.edge;
@@ -1136,20 +1201,38 @@ BasePill {
id: menuContainer id: menuContainer
objectName: "overflowMenuContainer" objectName: "overflowMenuContainer"
readonly property bool popupUsesVerticalLine: root.useSingleLineOverflowPopup && root.isVerticalOrientation
readonly property real popupPadding: Theme.spacingS + (popupUsesVerticalLine ? 3 : 0)
readonly property real rawWidth: { readonly property real rawWidth: {
const itemCount = root.hiddenBarItems.length; const itemCount = root.hiddenBarItems.length;
const cols = Math.min(5, itemCount); if (itemCount === 0)
return 0;
if (popupUsesVerticalLine)
return root.trayItemSize + 4 + popupPadding * 2;
const cols = root.useSingleLineOverflowPopup ? itemCount : Math.min(5, itemCount);
const itemSize = root.trayItemSize + 4; const itemSize = root.trayItemSize + 4;
const spacing = 2; const spacing = 2;
return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2; const desiredWidth = cols * itemSize + (cols - 1) * spacing + popupPadding * 2;
if (!root.useSingleLineOverflowPopup)
return desiredWidth;
const maxWidth = Math.max(itemSize + popupPadding * 2, overflowMenu.maskWidth - 20);
return Math.min(desiredWidth, maxWidth);
} }
readonly property real rawHeight: { readonly property real rawHeight: {
const itemCount = root.hiddenBarItems.length; const itemCount = root.hiddenBarItems.length;
const cols = Math.min(5, itemCount); if (itemCount === 0)
const rows = Math.ceil(itemCount / cols); return 0;
const itemSize = root.trayItemSize + 4; const itemSize = root.trayItemSize + 4;
const spacing = 2; const spacing = 2;
return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2; if (popupUsesVerticalLine) {
const desiredHeight = itemCount * itemSize + (itemCount - 1) * spacing + popupPadding * 2;
const maxHeight = Math.max(itemSize + popupPadding * 2, overflowMenu.maskHeight - 20);
return Math.min(desiredHeight, maxHeight);
}
const cols = root.useSingleLineOverflowPopup ? itemCount : Math.min(5, itemCount);
const rows = Math.ceil(itemCount / cols);
return rows * itemSize + (rows - 1) * spacing + popupPadding * 2;
} }
readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr) readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr)
@@ -1230,10 +1313,21 @@ BasePill {
z: 100 z: 100
} }
Flickable {
anchors.centerIn: parent
width: parent.width - menuContainer.popupPadding * 2
height: parent.height - menuContainer.popupPadding * 2
contentWidth: menuGrid.implicitWidth
contentHeight: menuGrid.implicitHeight
boundsBehavior: Flickable.StopAtBounds
clip: true
interactive: root.useSingleLineOverflowPopup && (menuContainer.popupUsesVerticalLine ? contentHeight > height : contentWidth > width)
Grid { Grid {
id: menuGrid id: menuGrid
anchors.centerIn: parent anchors.verticalCenter: menuContainer.popupUsesVerticalLine ? undefined : parent.verticalCenter
columns: Math.min(5, root.hiddenBarItems.length) anchors.horizontalCenter: menuContainer.popupUsesVerticalLine ? parent.horizontalCenter : undefined
columns: menuContainer.popupUsesVerticalLine ? 1 : (root.useSingleLineOverflowPopup ? root.hiddenBarItems.length : Math.min(5, root.hiddenBarItems.length))
spacing: 2 spacing: 2
rowSpacing: 2 rowSpacing: 2
@@ -1241,13 +1335,56 @@ BasePill {
model: root.hiddenBarItems model: root.hiddenBarItems
delegate: Rectangle { delegate: Rectangle {
id: overflowItemRoot
property var trayItem: modelData property var trayItem: modelData
property string itemKey: root.getTrayItemKey(trayItem)
property string iconSource: root.trayIconSourceFor(trayItem) property string iconSource: root.trayIconSourceFor(trayItem)
width: root.trayItemSize + 4 width: root.trayItemSize + 4
height: root.trayItemSize + 4 height: root.trayItemSize + 4
z: popupDragHandler.dragging ? 100 : 0
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0) color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
border.width: popupDragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: popupDragHandler.dragging ? 0.8 : 1.0
property real shiftOffset: root.dragShiftOffset(index, root.popupDraggedIndex, root.popupDropTargetIndex, root.trayItemSize + 6)
transform: Translate {
x: !menuContainer.popupUsesVerticalLine ? overflowItemRoot.shiftOffset + (popupDragHandler.dragging ? popupDragHandler.dragAxisOffset : 0) : 0
y: menuContainer.popupUsesVerticalLine ? overflowItemRoot.shiftOffset + (popupDragHandler.dragging ? popupDragHandler.dragAxisOffset : 0) : 0
Behavior on x {
enabled: !root.suppressShiftAnimation && !menuContainer.popupUsesVerticalLine
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
Behavior on y {
enabled: !root.suppressShiftAnimation && menuContainer.popupUsesVerticalLine
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
Item {
id: popupDragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
property bool longPressing: false
Timer {
id: popupLongPressTimer
interval: 400
repeat: false
onTriggered: popupDragHandler.longPressing = true
}
}
IconImage { IconImage {
id: menuIconImg id: menuIconImg
@@ -1285,8 +1422,38 @@ BasePill {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor cursorShape: popupDragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
onPressed: mouse => {
if (mouse.button === Qt.LeftButton) {
popupDragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
popupLongPressTimer.start();
}
}
onReleased: mouse => {
popupLongPressTimer.stop();
const wasDragging = popupDragHandler.dragging;
if (wasDragging)
root.finishPopupDrag();
popupDragHandler.longPressing = false;
popupDragHandler.dragging = false;
popupDragHandler.dragAxisOffset = 0;
}
onPositionChanged: mouse => {
const axisDelta = menuContainer.popupUsesVerticalLine ? (mouse.y - popupDragHandler.dragStartPos.y) : (mouse.x - popupDragHandler.dragStartPos.x);
if (popupDragHandler.longPressing && !popupDragHandler.dragging && Math.abs(axisDelta) > 5) {
popupDragHandler.dragging = true;
root.beginPopupDrag(index);
}
if (!popupDragHandler.dragging)
return;
popupDragHandler.dragAxisOffset = axisDelta;
root.updatePopupDrag(axisDelta, index);
}
onClicked: mouse => { onClicked: mouse => {
if (popupDragHandler.dragging)
return;
if (!trayItem) if (!trayItem)
return; return;
if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) { if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
@@ -1307,6 +1474,7 @@ BasePill {
} }
} }
} }
}
Component { Component {
id: trayMenuComponent id: trayMenuComponent
@@ -1555,11 +1723,13 @@ BasePill {
anchorPos = Qt.point(targetX, targetY); anchorPos = Qt.point(targetX, targetY);
} }
} else { } else {
const globalPos = targetItem.mapToGlobal(0, 0); // Window-local maps directly to screen-local because the bar window spans
const screenX = screen.x || 0; // the full screen edge; this avoids mixing mapToGlobal with a separately-
const screenY = screen.y || 0; // tracked screen.x/.y origin, which desync on non-primary monitors and after
const relativeX = globalPos.x - screenX; // DPMS/hotplug.
const relativeY = globalPos.y - screenY; const localPos = targetItem.mapToItem(null, 0, 0);
const relativeX = localPos.x;
const relativeY = localPos.y;
if (menuRoot.isVertical) { if (menuRoot.isVertical) {
const edge = menuRoot.axis?.edge; const edge = menuRoot.axis?.edge;
@@ -1695,7 +1865,12 @@ BasePill {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: menuRoot.trayItem?.id || "Unknown" text: {
const itemId = menuRoot.trayItem?.id || "Unknown";
if (root.isAutoOverflowTrayItem(menuRoot.trayItem))
return itemId + " · " + I18n.tr("Keep in Bar");
return itemId;
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
elide: Text.ElideMiddle elide: Text.ElideMiddle
@@ -1706,7 +1881,11 @@ BasePill {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: SessionData.isHiddenTrayId(root.getTrayItemKey(menuRoot.trayItem)) ? "visibility" : "visibility_off" name: {
if (root.isAutoOverflowTrayItem(menuRoot.trayItem))
return "push_pin";
return root.isManualHiddenTrayItem(menuRoot.trayItem) ? "visibility" : "visibility_off";
}
size: 16 size: 16
color: Theme.widgetTextColor color: Theme.widgetTextColor
} }
@@ -1720,7 +1899,9 @@ BasePill {
const itemKey = root.getTrayItemKey(menuRoot.trayItem); const itemKey = root.getTrayItemKey(menuRoot.trayItem);
if (!itemKey) if (!itemKey)
return; return;
if (SessionData.isHiddenTrayId(itemKey)) { if (root.isAutoOverflowTrayItem(menuRoot.trayItem)) {
root.promoteTrayItemToBar(menuRoot.trayItem);
} else if (root.isManualHiddenTrayItem(menuRoot.trayItem)) {
SessionData.showTrayId(itemKey); SessionData.showTrayId(itemKey);
} else { } else {
SessionData.hideTrayId(itemKey); SessionData.hideTrayId(itemKey);
@@ -108,9 +108,6 @@ DankPopout {
MprisController.setActivePlayer(player); MprisController.setActivePlayer(player);
root.__hideDropdowns(); root.__hideDropdowns();
} }
onDeviceSelected: device => {
root.__hideDropdowns();
}
} }
} }
@@ -230,6 +227,13 @@ DankPopout {
return; return;
} }
if (root.currentTabIndex === 0 && overviewLoader.item?.handleKeyEvent) {
if (overviewLoader.item.handleKeyEvent(event)) {
event.accepted = true;
return;
}
}
if (root.currentTabIndex === 1 && mediaLoader.item?.handleKeyEvent) { if (root.currentTabIndex === 1 && mediaLoader.item?.handleKeyEvent) {
if (mediaLoader.item.handleKeyEvent(event)) { if (mediaLoader.item.handleKeyEvent(event)) {
event.accepted = true; event.accepted = true;
@@ -359,6 +363,7 @@ DankPopout {
sourceComponent: Component { sourceComponent: Component {
OverviewTab { OverviewTab {
onCloseDash: root.dashVisible = false onCloseDash: root.dashVisible = false
onNavFocusRequested: mainContainer.forceActiveFocus()
onSwitchToWeatherTab: { onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) { if (SettingsData.weatherEnabled) {
root.currentTabIndex = 3; root.currentTabIndex = 3;
@@ -383,7 +383,27 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: mouse => {
if (mouse.button === Qt.RightButton) {
mouse.accepted = true;
}
}
onWheel: wheelEvent => {
if (SettingsData.audioDeviceScrollVolumeEnabled && wheelEvent.x >= deviceMouseArea.width / 2) {
AudioService.handleNodeVolumeWheel(modelData, wheelEvent);
} else {
wheelEvent.accepted = false;
}
}
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (modelData && modelData.audio) {
SessionData.suppressOSDTemporarily();
modelData.audio.muted = !modelData.audio.muted;
}
return;
}
if (modelData && modelData.name) { if (modelData && modelData.name) {
AudioService.setDefaultSinkByName(modelData.name); AudioService.setDefaultSinkByName(modelData.name);
root.deviceSelected(modelData); root.deviceSelected(modelData);
+21 -1
View File
@@ -866,7 +866,27 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: mouse => {
if (mouse.button === Qt.RightButton) {
mouse.accepted = true;
}
}
onWheel: wheelEvent => {
const delta = wheelEvent.angleDelta.y;
if (delta !== 0) {
AudioService.cycleAudioOutputDirection(delta < 0);
wheelEvent.accepted = true;
}
}
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (AudioService.sink?.audio) {
SessionData.suppressOSDTemporarily();
AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
}
return;
}
if (devicesExpanded) { if (devicesExpanded) {
const sinks = AudioService.getAvailableSinks(); const sinks = AudioService.getAvailableSinks();
if (sinks && sinks.length > 1) { if (sinks && sinks.length > 1) {
@@ -0,0 +1,311 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var eventData: null
property bool canEdit: false
signal editRequested
signal deleteRequested
signal closeRequested
readonly property bool _descriptionIsHtml: /<[a-z][^>]*>/i.test((eventData && eventData.description) || "")
function _styleAnchors(html) {
return html.replace(/<a\s([^>]*)>/gi, (m, attrs) => {
const cleaned = attrs.replace(/style="[^"]*"/gi, "");
return "<a style=\"text-decoration:none; color:" + Theme.primary + ";\" " + cleaned + ">";
});
}
function _inlineMarkdown(line) {
let out = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
out = out.replace(/\\([\\`*_{}[\]()#+\-.!~>])/g, "$1");
out = out.replace(/(?:https?:\/\/|www\.)[^\s<>)\]]*[^\s<>)\].,;:!?"']/g, (m, offset, s) => {
const prev = offset > 0 ? s[offset - 1] : "";
if (prev === "(" || prev === "[" || prev === "\"" || prev === "'")
return m;
const href = m.startsWith("www.") ? "https://" + m : m;
return "<a href=\"" + href + "\">" + m + "</a>";
});
out = out.replace(/\[([^\]]+)\]\(([^()\s]+)\)/g, "<a href=\"$2\">$1</a>");
out = out.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
out = out.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, "$1<i>$2</i>");
return out;
}
// Descriptions arrive as HTML (Google) or markdown/plain text; both render
// as RichText so links become clickable anchors recolored to the theme.
function _descriptionRichText() {
const raw = ((eventData && eventData.description) || "").trim();
if (raw === "")
return "";
if (_descriptionIsHtml)
return _styleAnchors(raw);
const parts = [];
let list = "";
const closeList = () => {
if (list === "")
return;
parts.push("</" + list + ">");
list = "";
};
const lines = raw.split("\n");
for (let i = 0; i < lines.length; i++) {
const ul = lines[i].match(/^\s*[-*+]\s+(.+)$/);
const ol = lines[i].match(/^\s*\d+[.)]\s+(.+)$/);
if (ul || ol) {
const tag = ul ? "ul" : "ol";
if (list !== tag) {
closeList();
parts.push("<" + tag + ">");
list = tag;
}
parts.push("<li>" + _inlineMarkdown((ul || ol)[1]) + "</li>");
continue;
}
closeList();
parts.push(_inlineMarkdown(lines[i]) + "<br/>");
}
closeList();
return _styleAnchors(parts.join("").replace(/<br\/>$/, ""));
}
function _timeText() {
if (!eventData)
return "";
const dateStr = Qt.formatDate(eventData.start, "ddd, MMM d");
if (eventData.allDay)
return I18n.tr("All day") + " · " + dateStr;
const fmt = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
const startStr = Qt.formatTime(eventData.start, fmt);
if (eventData.start.getTime() === eventData.end.getTime())
return dateStr + " · " + startStr;
return dateStr + " · " + startStr + " " + Qt.formatTime(eventData.end, fmt);
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(0, 0, 0, 0.45)
MouseArea {
anchors.fill: parent
onClicked: root.closeRequested()
}
}
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - Theme.spacingL * 2, 380)
height: Math.min(parent.height - Theme.spacingM * 2, body.implicitHeight + Theme.spacingL * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Theme.outlineMedium
border.width: 1
clip: true
MouseArea {
anchors.fill: parent
}
DankActionButton {
id: closeButton
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingXS
circular: false
iconName: "close"
iconSize: 16
z: 1
onClicked: root.closeRequested()
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingL
contentWidth: width
contentHeight: body.implicitHeight
clip: true
Column {
id: body
width: parent.width
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: 4
height: titleText.implicitHeight
radius: 2
anchors.top: parent.top
color: (root.eventData && root.eventData.color) ? root.eventData.color : Theme.primary
}
StyledText {
id: titleText
width: parent.width - 4 - Theme.spacingS - closeButton.width
text: root.eventData ? root.eventData.title : ""
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
maximumLineCount: 3
elide: Text.ElideRight
}
}
StyledText {
width: parent.width
text: root._timeText()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
}
Row {
width: parent.width
spacing: Theme.spacingXS
visible: root.eventData && root.eventData.calendar
DankIcon {
name: "calendar_month"
size: 14
color: Theme.surfaceVariantText
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
width: parent.width - 14 - Theme.spacingXS
text: {
if (!root.eventData)
return "";
const acc = root.eventData.account || "";
return root.eventData.calendar + (acc ? " · " + acc : "");
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
}
}
Row {
width: parent.width
spacing: Theme.spacingXS
visible: root.eventData && root.eventData.location
DankIcon {
name: "place"
size: 14
color: Theme.surfaceVariantText
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
width: parent.width - 14 - Theme.spacingXS
text: root.eventData ? root.eventData.location : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
}
}
Row {
width: parent.width
spacing: Theme.spacingXS
visible: root.eventData && root.eventData.url
DankIcon {
name: "link"
size: 14
color: Theme.primary
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
width: parent.width - 14 - Theme.spacingXS
text: root.eventData ? root.eventData.url : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
wrapMode: Text.WrapAnywhere
maximumLineCount: 2
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.eventData && root.eventData.url)
Qt.openUrlExternally(root.eventData.url);
}
}
}
}
StyledText {
id: descriptionText
width: parent.width
text: root._descriptionRichText()
visible: root.eventData && root.eventData.description
textFormat: Text.RichText
linkColor: Theme.primary
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
onLinkActivated: link => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: descriptionText.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: root.canEdit
topPadding: Theme.spacingXS
DankButton {
text: I18n.tr("Edit")
iconName: "edit"
buttonHeight: 32
onClicked: root.editRequested()
}
DankButton {
text: I18n.tr("Delete")
iconName: "delete"
buttonHeight: 32
backgroundColor: Theme.withAlpha(Theme.error, 0.15)
textColor: Theme.error
onClicked: root.deleteRequested()
}
}
}
}
}
}
@@ -0,0 +1,350 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var eventData: null
property date initialDate: new Date()
signal saved
signal closeRequested
property string fTitle: ""
property bool fAllDay: false
property date fDate: initialDate
property string fStart: "10:00"
property string fEnd: "11:00"
property string fLocation: ""
property string fDescription: ""
property string fCalendarId: ""
property int fReminder: -1
property string errorText: ""
property bool saving: false
readonly property var _cals: CalendarService.writableCalendars()
readonly property var _remLabels: [I18n.tr("No reminder"), I18n.tr("At start"), I18n.tr("5 min before"), I18n.tr("10 min before"), I18n.tr("15 min before"), I18n.tr("30 min before"), I18n.tr("1 hour before"), I18n.tr("1 day before")]
readonly property var _remMins: [-1, 0, 5, 10, 15, 30, 60, 1440]
function _parseTime(value) {
const m = value.trim().match(/^(\d{1,2}):(\d{2})$/);
if (!m)
return null;
const h = parseInt(m[1]);
const min = parseInt(m[2]);
if (h > 23 || min > 59)
return null;
return {
"h": h,
"m": min
};
}
function _isoFromDateTime(dateObj, h, m) {
const d = new Date(dateObj);
d.setHours(h, m, 0, 0);
return d.toISOString();
}
function _allDayIso(dateObj, dayOffset) {
return new Date(Date.UTC(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate() + dayOffset)).toISOString();
}
function _calendarName(id) {
for (let i = 0; i < _cals.length; i++) {
if (_cals[i].id === id)
return _cals[i].name;
}
return _cals.length > 0 ? _cals[0].name : "";
}
function save() {
const title = fTitle.trim();
if (!title) {
errorText = I18n.tr("Title is required");
return;
}
let calId = fCalendarId;
if (!calId) {
const def = CalendarService.defaultCalendar();
calId = def ? def.id : "";
}
if (!calId) {
errorText = I18n.tr("No writable calendar available");
return;
}
let startIso, endIso;
if (fAllDay) {
startIso = _allDayIso(fDate, 0);
endIso = _allDayIso(fDate, 1);
} else {
const s = _parseTime(fStart);
const e = _parseTime(fEnd);
if (!s || !e) {
errorText = I18n.tr("Use HH:MM time format");
return;
}
startIso = _isoFromDateTime(fDate, s.h, s.m);
endIso = _isoFromDateTime(fDate, e.h, e.m);
if (new Date(endIso).getTime() <= new Date(startIso).getTime()) {
errorText = I18n.tr("End must be after start");
return;
}
}
const fields = {
"calendarId": calId,
"summary": title,
"description": fDescription,
"location": fLocation,
"start": startIso,
"end": endIso,
"allDay": fAllDay,
"reminders": fReminder >= 0 ? [
{
"method": "popup",
"minutes": fReminder
}
] : []
};
saving = true;
errorText = "";
const cb = response => {
saving = false;
if (response.error) {
errorText = response.error;
return;
}
root.saved();
};
if (eventData && eventData.id)
CalendarService.updateEvent(eventData.id, fields, cb);
else
CalendarService.createEvent(fields, cb);
}
Component.onCompleted: {
if (!eventData) {
fCalendarId = CalendarService.defaultCalendar() ? CalendarService.defaultCalendar().id : "";
return;
}
fTitle = eventData.title || "";
fAllDay = !!eventData.allDay;
fDate = eventData.start;
const fmt = "HH:mm";
fStart = Qt.formatTime(eventData.start, fmt);
fEnd = Qt.formatTime(eventData.end, fmt);
fLocation = eventData.location || "";
fDescription = eventData.description || "";
fCalendarId = eventData.calendarId || "";
if (eventData.reminders && eventData.reminders.length > 0)
fReminder = eventData.reminders[0].minutes;
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(0, 0, 0, 0.45)
MouseArea {
anchors.fill: parent
onClicked: root.closeRequested()
}
}
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - Theme.spacingL * 2, 400)
height: Math.min(parent.height - Theme.spacingM, 300)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Theme.outlineMedium
border.width: 1
MouseArea {
anchors.fill: parent
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingM
contentWidth: width
contentHeight: form.implicitHeight
clip: true
Column {
id: form
width: parent.width
spacing: Theme.spacingS
StyledText {
width: parent.width
text: root.eventData ? I18n.tr("Edit event") : I18n.tr("New event")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
}
DankTextField {
width: parent.width
labelText: I18n.tr("Title")
leftIconName: "title"
leftIconSize: Theme.iconSize - 6
placeholderText: I18n.tr("Event title")
text: root.fTitle
onTextChanged: root.fTitle = text
}
DankToggle {
width: parent.width
text: I18n.tr("All day")
checked: root.fAllDay
onToggled: checked => root.fAllDay = checked
}
Row {
width: parent.width
spacing: Theme.spacingXS
DankActionButton {
circular: false
iconName: "chevron_left"
iconSize: 16
onClicked: {
let d = new Date(root.fDate);
d.setDate(d.getDate() - 1);
root.fDate = d;
}
}
StyledText {
width: parent.width - 72
text: Qt.formatDate(root.fDate, "ddd, MMM d yyyy")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
height: 32
}
DankActionButton {
circular: false
iconName: "chevron_right"
iconSize: 16
onClicked: {
let d = new Date(root.fDate);
d.setDate(d.getDate() + 1);
root.fDate = d;
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: !root.fAllDay
DankTextField {
width: (parent.width - Theme.spacingS) / 2
labelText: I18n.tr("Start")
leftIconName: "schedule"
leftIconSize: Theme.iconSize - 6
placeholderText: "HH:MM"
text: root.fStart
onTextChanged: root.fStart = text
}
DankTextField {
width: (parent.width - Theme.spacingS) / 2
labelText: I18n.tr("End")
placeholderText: "HH:MM"
text: root.fEnd
onTextChanged: root.fEnd = text
}
}
DankDropdown {
width: parent.width
text: I18n.tr("Calendar")
options: root._cals.map(c => c.name)
currentValue: root._calendarName(root.fCalendarId)
onValueChanged: value => {
for (let i = 0; i < root._cals.length; i++) {
if (root._cals[i].name === value) {
root.fCalendarId = root._cals[i].id;
return;
}
}
}
}
DankDropdown {
width: parent.width
text: I18n.tr("Reminder")
options: root._remLabels
currentValue: root._remLabels[Math.max(0, root._remMins.indexOf(root.fReminder))]
onValueChanged: value => {
const idx = root._remLabels.indexOf(value);
if (idx >= 0)
root.fReminder = root._remMins[idx];
}
}
DankTextField {
width: parent.width
labelText: I18n.tr("Location")
leftIconName: "place"
leftIconSize: Theme.iconSize - 6
placeholderText: I18n.tr("Add location")
text: root.fLocation
onTextChanged: root.fLocation = text
}
DankTextField {
width: parent.width
labelText: I18n.tr("Notes")
leftIconName: "notes"
leftIconSize: Theme.iconSize - 6
placeholderText: I18n.tr("Add notes")
text: root.fDescription
onTextChanged: root.fDescription = text
}
StyledText {
width: parent.width
text: root.errorText
visible: root.errorText !== ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
wrapMode: Text.WordWrap
}
Row {
width: parent.width
spacing: Theme.spacingS
DankButton {
text: root.saving ? I18n.tr("Saving…") : I18n.tr("Save")
iconName: "check"
buttonHeight: 32
backgroundColor: Theme.primary
textColor: Theme.primaryText
enabled: !root.saving
onClicked: root.save()
}
DankButton {
text: I18n.tr("Cancel")
buttonHeight: 32
onClicked: root.closeRequested()
}
}
}
}
}
}
@@ -8,14 +8,21 @@ Rectangle {
id: root id: root
readonly property var log: Log.scoped("CalendarOverviewCard") readonly property var log: Log.scoped("CalendarOverviewCard")
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
implicitWidth: SettingsData.showWeekNumber ? 736 : 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
property bool showEventDetails: false property bool showEventDetails: false
property date selectedDate: systemClock.date property date selectedDate: systemClock.date
property var selectedDateEvents: [] property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0 property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
property var detailEvent: null
property bool showEditor: false
property var editorEvent: null
signal closeDash signal closeDash
signal navFocusRequested
function weekStartQt() { function weekStartQt() {
if (SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0) { if (SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0) {
@@ -79,7 +86,7 @@ Rectangle {
} }
function updateSelectedDateEvents() { function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) { if (CalendarService && CalendarService.calendarAvailable) {
const events = CalendarService.getEventsForDate(selectedDate); const events = CalendarService.getEventsForDate(selectedDate);
selectedDateEvents = events; selectedDateEvents = events;
} else { } else {
@@ -88,7 +95,7 @@ Rectangle {
} }
function loadEventsForMonth() { function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) { if (!CalendarService || !CalendarService.calendarAvailable) {
return; return;
} }
@@ -104,11 +111,83 @@ Rectangle {
CalendarService.loadEvents(startDate, endDate); CalendarService.loadEvents(startDate, endDate);
} }
function goToToday() {
const now = systemClock.date;
calendarGrid.selectedDate = now;
calendarGrid.displayDate = now;
root.selectedDate = now;
loadEventsForMonth();
}
function moveSelection(days) {
let d = new Date(calendarGrid.selectedDate);
d.setDate(d.getDate() + days);
calendarGrid.selectedDate = d;
root.selectedDate = d;
if (d.getMonth() !== calendarGrid.displayDate.getMonth() || d.getFullYear() !== calendarGrid.displayDate.getFullYear()) {
calendarGrid.displayDate = d;
loadEventsForMonth();
}
}
function shiftMonth(delta) {
let d = new Date(calendarGrid.displayDate);
d.setMonth(d.getMonth() + delta);
calendarGrid.displayDate = d;
loadEventsForMonth();
}
function handleKeyEvent(event) {
if (showEventDetails) {
if (event.key === Qt.Key_Escape) {
showEventDetails = false;
return true;
}
return false;
}
switch (event.key) {
case Qt.Key_Left:
case Qt.Key_H:
moveSelection(I18n.isRtl ? 1 : -1);
return true;
case Qt.Key_Right:
case Qt.Key_L:
moveSelection(I18n.isRtl ? -1 : 1);
return true;
case Qt.Key_Up:
case Qt.Key_K:
moveSelection(-7);
return true;
case Qt.Key_Down:
case Qt.Key_J:
moveSelection(7);
return true;
case Qt.Key_PageUp:
shiftMonth(-1);
return true;
case Qt.Key_PageDown:
shiftMonth(1);
return true;
case Qt.Key_T:
goToToday();
return true;
case Qt.Key_Return:
case Qt.Key_Enter:
case Qt.Key_Space:
root.selectedDate = calendarGrid.selectedDate;
showEventDetails = true;
return true;
}
return false;
}
onSelectedDateChanged: updateSelectedDateEvents() onSelectedDateChanged: updateSelectedDateEvents()
onShowEventDetailsChanged: { onShowEventDetailsChanged: {
if (showEventDetails) { if (showEventDetails) {
taskInput.forceActiveFocus(); taskInput.forceActiveFocus();
} else {
navFocusRequested();
} }
} }
@@ -122,8 +201,8 @@ Rectangle {
updateSelectedDateEvents(); updateSelectedDateEvents();
} }
function onKhalAvailableChanged() { function onCalendarAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable) { if (CalendarService && CalendarService.calendarAvailable) {
loadEventsForMonth(); loadEventsForMonth();
} }
updateSelectedDateEvents(); updateSelectedDateEvents();
@@ -143,6 +222,55 @@ Rectangle {
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
spacing: Theme.spacingS spacing: Theme.spacingS
Rectangle {
id: dankWarning
width: parent.width
visible: CalendarService && CalendarService.dankNeedsLaunch
height: visible ? Math.max(28, warningRow.implicitHeight) + Theme.spacingS : 0
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.35)
border.width: 1
Row {
id: warningRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: "warning"
size: 16
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
width: parent.width - 16 - Theme.spacingS - (launchButton.visible ? launchButton.width + Theme.spacingS : 0)
anchors.verticalCenter: parent.verticalCenter
text: (CalendarService && CalendarService.dankBinaryExists) ? I18n.tr("DankCalendar isn't running") : I18n.tr("DankCalendar isn't installed")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
}
DankButton {
id: launchButton
anchors.verticalCenter: parent.verticalCenter
visible: CalendarService && CalendarService.dankBinaryExists
text: I18n.tr("Launch")
buttonHeight: 26
backgroundColor: Theme.primary
textColor: Theme.primaryText
onClicked: CalendarService.launchDankCalendar()
}
}
}
Item { Item {
width: parent.width width: parent.width
height: 40 height: 40
@@ -173,11 +301,40 @@ Rectangle {
} }
} }
Rectangle {
width: 32
height: 32
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.canCreateEvents
color: addEventArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "event"
size: 16
color: Theme.primary
}
MouseArea {
id: addEventArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.editorEvent = null;
root.showEditor = true;
}
}
}
StyledText { StyledText {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.leftMargin: 32 + Theme.spacingS * 2 anchors.leftMargin: 32 + Theme.spacingS * 2
anchors.rightMargin: Theme.spacingS anchors.rightMargin: (CalendarService && CalendarService.canCreateEvents) ? 32 + Theme.spacingS * 2 : Theme.spacingS
height: 40 height: 40
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
@@ -229,7 +386,7 @@ Rectangle {
} }
StyledText { StyledText {
width: parent.width - 56 width: parent.width - 84
height: 28 height: 28
text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy") text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -239,6 +396,28 @@ Rectangle {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: todayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "today"
size: 14
color: Theme.primary
}
MouseArea {
id: todayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.goToToday()
}
}
Rectangle { Rectangle {
width: 28 width: 28
height: 28 height: 28
@@ -388,6 +567,8 @@ Rectangle {
height: width height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: (isSelected && !isToday) ? Theme.primary : "transparent"
border.width: (isSelected && !isToday) ? 1 : 0
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
@@ -397,21 +578,31 @@ Rectangle {
font.weight: isToday ? Font.Medium : Font.Normal font.weight: isToday ? Font.Medium : Font.Normal
} }
Rectangle { Row {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 4 anchors.bottomMargin: 3
width: 12 spacing: 2
height: 2 visible: CalendarService && CalendarService.calendarAvailable && CalendarService.hasEventsForDate(dayDate)
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
Behavior on opacity { Repeater {
NumberAnimation { model: {
duration: Theme.shortDuration const evs = CalendarService.getEventsForDate(dayDate);
easing.type: Theme.standardEasing const seen = [];
for (let i = 0; i < evs.length && seen.length < 3; i++) {
const c = (evs[i].color && evs[i].color.length) ? evs[i].color : "primary";
if (seen.indexOf(c) === -1)
seen.push(c);
}
return seen;
}
Rectangle {
width: 5
height: 5
radius: 2.5
color: modelData === "primary" ? (isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary) : modelData
opacity: isToday ? 0.95 : 0.8
} }
} }
} }
@@ -423,6 +614,7 @@ Rectangle {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
calendarGrid.selectedDate = dayDate;
root.selectedDate = dayDate; root.selectedDate = dayDate;
root.showEventDetails = true; root.showEventDetails = true;
} }
@@ -622,7 +814,15 @@ Rectangle {
} }
} }
color: isDragging ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : Theme.nestedSurface) readonly property bool isTask: modelData && modelData.id && modelData.id.startsWith("task_")
readonly property color accentColor: {
if (isTask)
return modelData.completed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Theme.primary;
return (modelData && modelData.color && modelData.color.length) ? modelData.color : Theme.primary;
}
readonly property color surfaceColor: isDragging ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : Theme.nestedSurface)
color: surfaceColor
border.color: isDragging ? Theme.primary : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : Theme.outlineMedium) border.color: isDragging ? Theme.primary : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : Theme.outlineMedium)
border.width: (isDragging || eventMouseArea.containsMouse) ? 1 : Theme.layerOutlineWidth border.width: (isDragging || eventMouseArea.containsMouse) ? 1 : Theme.layerOutlineWidth
@@ -660,15 +860,22 @@ Rectangle {
} }
} }
Rectangle { Item {
width: 3 id: accentClip
height: parent.height - 6 width: 4
clip: true
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 3
anchors.verticalCenter: parent.verticalCenter Rectangle {
radius: Theme.cornerRadius width: taskItem.width
color: (modelData && modelData.id && modelData.id.startsWith("task_")) ? (modelData.completed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Theme.primary) : Theme.primary height: taskItem.height
opacity: 0.8 radius: taskItem.radius
color: taskItem.accentColor
anchors.top: parent.top
anchors.left: parent.left
}
} }
// Drag Handle // Drag Handle
@@ -767,6 +974,7 @@ Rectangle {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: (modelData && modelData.id && modelData.id.startsWith("task_") && modelData.completed) ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) : Theme.surfaceText color: (modelData && modelData.id && modelData.id.startsWith("task_") && modelData.completed) ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) : Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
} }
@@ -774,21 +982,24 @@ Rectangle {
StyledText { StyledText {
width: parent.width width: parent.width
text: { text: {
if (!modelData || modelData.allDay) { if (!modelData)
return I18n.tr("All day", "calendar task with no specific time"); return "";
} else if (modelData.start && modelData.end) { const cal = (modelData.calendar && modelData.calendar.length) ? " · " + modelData.calendar : "";
if (modelData.allDay)
return I18n.tr("All day", "calendar task with no specific time") + cal;
if (modelData.start && modelData.end) {
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"; const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
const startTime = Qt.formatTime(modelData.start, timeFormat); const startTime = Qt.formatTime(modelData.start, timeFormat);
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) { if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime())
return startTime + " " + Qt.formatTime(modelData.end, timeFormat); return startTime + " " + Qt.formatTime(modelData.end, timeFormat) + cal;
} return startTime + cal;
return startTime;
} }
return ""; return "";
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal font.weight: Font.Normal
horizontalAlignment: Text.AlignLeft
visible: text !== "" && modelData && modelData.id && !modelData.id.startsWith("task_") visible: text !== "" && modelData && modelData.id && !modelData.id.startsWith("task_")
} }
} }
@@ -824,8 +1035,9 @@ Rectangle {
taskItem.isEditing = false; taskItem.isEditing = false;
} }
Keys.onEscapePressed: { Keys.onEscapePressed: event => {
taskItem.isEditing = false; taskItem.isEditing = false;
event.accepted = true;
} }
} }
} }
@@ -838,18 +1050,15 @@ Rectangle {
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 32 : 6 anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 32 : 6
anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : 0 anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : 0
hoverEnabled: true hoverEnabled: true
cursorShape: (modelData && (modelData.url || (modelData.id && modelData.id.startsWith("task_")))) ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: modelData ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData && (modelData.url !== "" || (modelData.id && modelData.id.startsWith("task_"))) && !taskItem.isEditing enabled: modelData && !taskItem.isEditing
onClicked: { onClicked: {
if (modelData && modelData.id && modelData.id.startsWith("task_")) { if (modelData && modelData.id && modelData.id.startsWith("task_")) {
CalendarService.toggleTask(modelData.id); CalendarService.toggleTask(modelData.id);
} else if (modelData && modelData.url && modelData.url !== "") { return;
if (Qt.openUrlExternally(modelData.url) === false) {
log.warn("Failed to open URL: " + modelData.url);
} else {
root.closeDash();
}
} }
if (modelData)
root.detailEvent = modelData;
} }
} }
@@ -953,7 +1162,7 @@ Rectangle {
Text { Text {
text: I18n.tr("Add a task...", "placeholder in the new-task input field") text: I18n.tr("Add a task...", "placeholder in the new-task input field")
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
visible: !taskInput.text && !taskInput.activeFocus visible: taskInput.text.length === 0
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -965,6 +1174,52 @@ Rectangle {
text = ""; text = "";
} }
} }
Keys.onEscapePressed: event => {
root.showEventDetails = false;
event.accepted = true;
}
}
}
}
Loader {
anchors.fill: parent
z: 1000
active: root.detailEvent !== null
sourceComponent: CalendarEventDetail {
eventData: root.detailEvent
canEdit: CalendarService && CalendarService.canCreateEvents && root.detailEvent && !root.detailEvent.readOnly && !(root.detailEvent.id && root.detailEvent.id.startsWith("task_"))
onCloseRequested: root.detailEvent = null
onEditRequested: {
root.editorEvent = root.detailEvent;
root.detailEvent = null;
root.showEditor = true;
}
onDeleteRequested: {
if (root.detailEvent && root.detailEvent.id)
CalendarService.deleteEvent(root.detailEvent.id, null);
root.detailEvent = null;
}
}
}
Loader {
anchors.fill: parent
z: 1000
active: root.showEditor
sourceComponent: CalendarEventEditor {
eventData: root.editorEvent
initialDate: root.selectedDate
onCloseRequested: {
root.showEditor = false;
root.editorEvent = null;
}
onSaved: {
root.showEditor = false;
root.editorEvent = null;
} }
} }
} }
@@ -14,6 +14,11 @@ Item {
signal switchToWeatherTab signal switchToWeatherTab
signal switchToMediaTab signal switchToMediaTab
signal closeDash signal closeDash
signal navFocusRequested
function handleKeyEvent(event) {
return calendarCard.handleKeyEvent(event);
}
Item { Item {
anchors.fill: parent anchors.fill: parent
@@ -54,12 +59,14 @@ Item {
// Calendar - bottom middle (wider and taller) // Calendar - bottom middle (wider and taller)
CalendarOverviewCard { CalendarOverviewCard {
id: calendarCard
x: parent.width * 0.2 - Theme.spacingM x: parent.width * 0.2 - Theme.spacingM
y: 100 + Theme.spacingM y: 100 + Theme.spacingM
width: parent.width * 0.6 width: parent.width * 0.6
height: 300 height: 300
onCloseDash: root.closeDash() onCloseDash: root.closeDash()
onNavFocusRequested: root.navFocusRequested()
} }
// Media - bottom right (narrow and taller) // Media - bottom right (narrow and taller)
+277 -8
View File
@@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
@@ -21,21 +22,71 @@ Item {
property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null
property bool showSettingsMenu: false property bool showSettingsMenu: false
property string pendingSaveContent: "" property string pendingSaveContent: ""
readonly property bool conflictBannerVisible: currentTab !== null && NotepadStorageService.conflictTabId === currentTab.id
property var slideout: null property var slideout: null
property bool inPopout: false
property bool surfaceVisible: slideout ? slideout.isVisible : true
signal hideRequested signal hideRequested
signal popoutRequested
signal dockRequested
signal previewRequested(string content) signal previewRequested(string content)
function externalSync() {
textEditor.syncFromDisk();
}
function flushAutoSave() {
textEditor.autoSaveToSession();
}
Ref { Ref {
service: NotepadStorageService service: NotepadStorageService
} }
// In connected frame mode the slideout sits on the Overlay layer
onFileDialogOpenChanged: {
if (slideout)
slideout.suppressOverlayLayer = fileDialogOpen;
}
Connections { Connections {
target: slideout target: slideout
enabled: slideout !== null enabled: slideout !== null
function onAboutToHide() { function onAboutToHide() {
textEditor.autoSaveToSession(); textEditor.autoSaveToSession();
} }
function onRevealed() {
textEditor.syncFromDisk();
}
}
function showConflictBanner(diskContent) {
if (!currentTab)
return;
NotepadStorageService.flagConflict(currentTab.id, diskContent);
}
function resolveConflictKeepEdits() {
if (!root.conflictBannerVisible)
return;
NotepadStorageService.clearConflict();
if (currentTab && currentTab.filePath && !currentTab.isTemporary) {
root.saveToFile("file://" + currentTab.filePath);
}
}
function resolveConflictReload() {
if (!root.conflictBannerVisible)
return;
const diskContent = NotepadStorageService.conflictDiskContent;
NotepadStorageService.clearConflict();
textEditor.reloadFromDisk(diskContent);
}
function dismissConflictBanner() {
if (root.conflictBannerVisible)
NotepadStorageService.clearConflict();
} }
function hasUnsavedChanges() { function hasUnsavedChanges() {
@@ -51,10 +102,14 @@ Item {
} }
function performCreateNewTab() { function performCreateNewTab() {
textEditor.commitLiveBuffer();
NotepadStorageService.createNewTab(); NotepadStorageService.createNewTab();
textEditor.applyingShared = true;
textEditor.text = ""; textEditor.text = "";
textEditor.lastSavedContent = ""; textEditor.lastSavedContent = "";
textEditor.loadedTabId = -1;
textEditor.contentLoaded = true; textEditor.contentLoaded = true;
textEditor.applyingShared = false;
textEditor.textArea.forceActiveFocus(); textEditor.textArea.forceActiveFocus();
} }
@@ -86,7 +141,6 @@ Item {
NotepadStorageService.switchToTab(tabIndex); NotepadStorageService.switchToTab(tabIndex);
Qt.callLater(() => { Qt.callLater(() => {
textEditor.loadCurrentTabContent();
if (currentTab) { if (currentTab) {
root.currentFileName = currentTab.fileName || ""; root.currentFileName = currentTab.fileName || "";
root.currentFileUrl = currentTab.fileUrl || ""; root.currentFileUrl = currentTab.fileUrl || "";
@@ -100,6 +154,7 @@ Item {
var content = textEditor.text; var content = textEditor.text;
var filePath = fileUrl.toString().replace(/^file:\/\//, ''); var filePath = fileUrl.toString().replace(/^file:\/\//, '');
textEditor.externalWatchPaused = true;
saveFileView.path = ""; saveFileView.path = "";
pendingSaveContent = content; pendingSaveContent = content;
saveFileView.path = filePath; saveFileView.path = filePath;
@@ -109,6 +164,53 @@ Item {
}); });
} }
function saveExternalWithFreshnessCheck() {
if (!currentTab || currentTab.isTemporary || !currentTab.filePath)
return;
const filePath = currentTab.filePath;
loadFileView.path = "";
loadFileView.path = filePath;
if (!loadFileView.waitForJob()) {
saveToFile("file://" + filePath);
return;
}
Qt.callLater(() => {
if (!currentTab || currentTab.isTemporary || currentTab.filePath !== filePath)
return;
const diskContent = loadFileView.text();
if (diskContent !== undefined && diskContent !== null && diskContent !== textEditor.text && diskContent !== textEditor.lastSavedContent) {
root.showConflictBanner(diskContent);
return;
}
saveToFile("file://" + filePath);
});
}
function autoSaveExternal() {
if (!SettingsData.notepadAutoSave)
return;
if (!currentTab || currentTab.isTemporary || !currentTab.filePath)
return;
if (!textEditor.hasUnsavedChanges())
return;
const filePath = currentTab.filePath;
loadFileView.path = "";
loadFileView.path = filePath;
if (!loadFileView.waitForJob())
return;
Qt.callLater(() => {
if (!currentTab || currentTab.isTemporary || currentTab.filePath !== filePath)
return;
const diskContent = loadFileView.text();
if (diskContent === undefined || diskContent === null)
return;
if (diskContent !== textEditor.lastSavedContent)
return;
saveToFile("file://" + filePath);
});
}
function loadFromFile(fileUrl) { function loadFromFile(fileUrl) {
if (hasUnsavedTemporaryContent()) { if (hasUnsavedTemporaryContent()) {
root.pendingFileUrl = fileUrl; root.pendingFileUrl = fileUrl;
@@ -146,14 +248,155 @@ Item {
root.currentFileName = fileName; root.currentFileName = fileName;
root.currentFileUrl = fileUrl; root.currentFileUrl = fileUrl;
textEditor.saveCurrentTabContent(); textEditor.loadedTabId = currentTab.id;
NotepadStorageService.clearSessionBuffer(currentTab.id);
if (root.conflictBannerVisible)
NotepadStorageService.clearConflict();
} }
}); });
} }
} }
Column { Item {
id: conflictBanner
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: root.conflictBannerVisible ? bannerRect.implicitHeight : 0
visible: height > 0
clip: true
z: 5
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
StyledRect {
id: bannerRect
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: bannerLayout.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.warning, 0.12)
border.color: Theme.withAlpha(Theme.warning, 0.5)
border.width: 1
ColumnLayout {
id: bannerLayout
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingM
DankIcon {
Layout.alignment: Qt.AlignVCenter
name: "sync_problem"
size: Theme.iconSize - 2
color: Theme.warning
}
StyledText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: I18n.tr("File changed on disk")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
wrapMode: Text.NoWrap
elide: Text.ElideRight
}
DankActionButton {
Layout.alignment: Qt.AlignVCenter
iconName: "close"
iconSize: Theme.iconSizeSmall
iconColor: Theme.surfaceText
buttonSize: 28
onClicked: root.dismissConflictBanner()
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 32
Row {
id: bannerActions
anchors.right: parent.right
spacing: Theme.spacingS
readonly property real available: parent.width
StyledRect {
width: Math.min(keepText.implicitWidth + Theme.spacingM * 2, Math.max(104, (bannerActions.available - bannerActions.spacing) / 2))
height: 32
radius: Theme.cornerRadius
color: "transparent"
border.color: Theme.outlineMedium
border.width: 1
StateLayer {
anchors.fill: parent
cornerRadius: parent.radius
stateColor: Theme.surfaceText
onClicked: root.resolveConflictKeepEdits()
}
StyledText {
id: keepText
anchors.centerIn: parent
width: parent.width - Theme.spacingM
text: I18n.tr("Keep My Edits")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
StyledRect {
width: Math.min(reloadText.implicitWidth + Theme.spacingM * 2, Math.max(116, (bannerActions.available - bannerActions.spacing) / 2))
height: 32
radius: Theme.cornerRadius
color: Theme.primary
StateLayer {
anchors.fill: parent
cornerRadius: parent.radius
stateColor: Theme.background
onClicked: root.resolveConflictReload()
}
StyledText {
id: reloadText
anchors.centerIn: parent
width: parent.width - Theme.spacingM
text: I18n.tr("Reload From Disk")
font.pixelSize: Theme.fontSizeSmall
color: Theme.background
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
}
}
}
}
}
Column {
anchors.top: conflictBanner.bottom
anchors.topMargin: root.conflictBannerVisible ? Theme.spacingM : 0
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: Theme.spacingM spacing: Theme.spacingM
NotepadTabs { NotepadTabs {
@@ -178,11 +421,12 @@ Item {
id: textEditor id: textEditor
width: parent.width width: parent.width
height: parent.height - tabBar.height - Theme.spacingM * 2 height: parent.height - tabBar.height - Theme.spacingM * 2
inPopout: root.inPopout
surfaceVisible: root.surfaceVisible
onSaveRequested: { onSaveRequested: {
if (currentTab && !currentTab.isTemporary && currentTab.filePath) { if (currentTab && !currentTab.isTemporary && currentTab.filePath) {
var fileUrl = "file://" + currentTab.filePath; root.saveExternalWithFreshnessCheck();
saveToFile(fileUrl);
} else { } else {
root.fileDialogOpen = true; root.fileDialogOpen = true;
saveBrowserLoader.active = true; saveBrowserLoader.active = true;
@@ -214,12 +458,28 @@ Item {
onEscapePressed: { onEscapePressed: {
textEditor.autoSaveToSession(); textEditor.autoSaveToSession();
if (showSettingsMenu) {
showSettingsMenu = false;
return;
}
if (!root.inPopout) {
root.hideRequested(); root.hideRequested();
} }
}
onSettingsRequested: { onSettingsRequested: {
showSettingsMenu = !showSettingsMenu; showSettingsMenu = !showSettingsMenu;
} }
onPopoutRequested: root.popoutRequested()
onDockRequested: root.dockRequested()
onConflictDetected: diskContent => {
root.showConflictBanner(diskContent);
}
onAutoSaveRequested: root.autoSaveExternal()
} }
} }
@@ -242,17 +502,24 @@ Item {
printErrors: true printErrors: true
onSaved: { onSaved: {
if (currentTab && saveFileView.path && pendingSaveContent) { if (currentTab && saveFileView.path) {
NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, { NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, {
hasUnsavedChanges: false, hasUnsavedChanges: false,
lastSavedContent: pendingSaveContent lastSavedContent: pendingSaveContent
}); });
root.lastSavedFileContent = pendingSaveContent; root.lastSavedFileContent = pendingSaveContent;
pendingSaveContent = ""; textEditor.lastSavedContent = pendingSaveContent;
textEditor.ignoreNextExternalChange = true;
textEditor.commitLiveBuffer();
if (root.conflictBannerVisible)
NotepadStorageService.clearConflict();
} }
textEditor.externalWatchPaused = false;
pendingSaveContent = "";
} }
onSaveFailed: error => { onSaveFailed: error => {
textEditor.externalWatchPaused = false;
pendingSaveContent = ""; pendingSaveContent = "";
} }
} }
@@ -298,6 +565,7 @@ Item {
root.currentFileName = fileName; root.currentFileName = fileName;
root.currentFileUrl = fileUrl; root.currentFileUrl = fileUrl;
textEditor.externalWatchPaused = true;
if (currentTab) { if (currentTab) {
NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath); NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath);
@@ -343,7 +611,7 @@ Item {
browserTitle: I18n.tr("Open Notepad File") browserTitle: I18n.tr("Open Notepad File")
browserIcon: "folder_open" browserIcon: "folder_open"
browserType: "notepad_load" browserType: "notepad_load"
fileExtensions: ["*.txt", "*.md", "*.*"] fileExtensions: ["*"]
allowStacking: true allowStacking: true
onFileSelected: path => { onFileSelected: path => {
@@ -376,6 +644,7 @@ Item {
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 180 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 180
shouldBeVisible: false shouldBeVisible: false
allowStacking: true allowStacking: true
useOverlayLayer: true
onBackgroundClicked: { onBackgroundClicked: {
close(); close();
@@ -0,0 +1,137 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Notepad
FloatingWindow {
id: win
property alias shouldBeVisible: win.visible
function show() {
visible = true;
}
function hide() {
visible = false;
}
function toggle() {
visible = !visible;
}
title: I18n.tr("Notepad")
minimumSize: Qt.size(360, 320)
implicitWidth: 640
implicitHeight: 760
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
if (visible) {
Qt.callLater(notepad.externalSync);
} else {
notepad.flushAutoSave();
}
}
// A compositor close (e.g. niri close-window)
onClosed: win.visible = false
Item {
anchors.fill: parent
Item {
id: titleBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 44
z: 10
MouseArea {
anchors.fill: parent
onPressed: windowControls.tryStartMove()
onDoubleClicked: windowControls.tryToggleMaximize()
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainerHigh
opacity: 0.5
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "edit_note"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Notepad")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankActionButton {
visible: windowControls.canMaximize
circular: false
iconName: win.maximized ? "fullscreen_exit" : "fullscreen"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: windowControls.tryToggleMaximize()
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: win.hide()
}
}
}
Notepad {
id: notepad
anchors.top: titleBar.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Theme.spacingM
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingM
inPopout: true
surfaceVisible: win.visible
onHideRequested: win.hide()
onDockRequested: {
win.hide();
PopoutService.openNotepadSlideout();
}
}
}
FloatingWindowControls {
id: windowControls
targetWindow: win
}
}
+207 -10
View File
@@ -10,6 +10,7 @@ Item {
property var cachedFontFamilies: [] property var cachedFontFamilies: []
property var cachedMonoFamilies: [] property var cachedMonoFamilies: []
property bool fontsEnumerated: false property bool fontsEnumerated: false
property bool shortcutsExpanded: false
signal settingsRequested signal settingsRequested
signal findRequested signal findRequested
@@ -62,11 +63,23 @@ Item {
} }
} }
MouseArea { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: root.isVisible visible: root.isVisible
onClicked: root.settingsRequested()
z: 50 z: 50
color: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, 0.85)
WheelHandler {
// Hold scroll so the editor beneath doesn't move while settings are open.
onWheel: event => {
event.accepted = true;
}
}
MouseArea {
anchors.fill: parent
onClicked: root.settingsRequested()
}
} }
Rectangle { Rectangle {
@@ -74,8 +87,8 @@ Item {
visible: root.isVisible visible: root.isVisible
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 360 width: Math.min(360, root.width - Theme.spacingL * 2)
height: settingsColumn.implicitHeight + Theme.spacingXL * 2 height: Math.min(settingsColumn.implicitHeight + Theme.spacingXL * 2, root.height - Theme.spacingL * 2)
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, Theme.notepadTransparency) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, Theme.notepadTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
@@ -93,12 +106,18 @@ Item {
z: parent.z - 1 z: parent.z - 1
} }
DankFlickable {
id: settingsFlickable
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: settingsColumn.implicitHeight + Theme.spacingXL * 2
Column { Column {
id: settingsColumn id: settingsColumn
width: parent.width - Theme.spacingXL * 2 x: Theme.spacingXL
anchors.horizontalCenter: parent.horizontalCenter y: Theme.spacingXL
anchors.top: parent.top width: settingsFlickable.width - Theme.spacingXL * 2
anchors.topMargin: Theme.spacingXL
spacing: Theme.spacingS spacing: Theme.spacingS
Rectangle { Rectangle {
@@ -110,7 +129,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: -Theme.spacingXS anchors.leftMargin: -Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Notepad Font Settings") text: I18n.tr("Notepad Settings")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -147,6 +166,18 @@ Item {
} }
} }
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Auto-save to disk")
description: I18n.tr("Automatically save changes to opened files as you type")
checked: SettingsData.notepadAutoSave
onToggled: checked => {
SettingsData.notepadAutoSave = checked;
}
}
StyledRect { StyledRect {
width: parent.width width: parent.width
height: 60 height: 60
@@ -321,7 +352,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM width: parent.width + Theme.spacingM
text: I18n.tr("Custom Transparency") text: I18n.tr("Surface Opacity")
description: I18n.tr("Override global transparency for Notepad") description: I18n.tr("Override global transparency for Notepad")
checked: SettingsData.notepadTransparencyOverride >= 0 checked: SettingsData.notepadTransparencyOverride >= 0
onToggled: checked => { onToggled: checked => {
@@ -354,6 +385,102 @@ Item {
} }
} }
Rectangle {
width: parent.width
height: gapColumn.height + Theme.spacingS
color: "transparent"
Column {
id: gapColumn
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Default Mode")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
DankButtonGroup {
model: [I18n.tr("Slideout"), I18n.tr("Popout")]
size: "small"
currentIndex: SettingsData.notepadDefaultMode === "popout" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.notepadDefaultMode = index === 1 ? "popout" : "slideout";
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: SettingsData.notepadDefaultMode !== "popout"
StyledText {
text: I18n.tr("Open From")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
DankButtonGroup {
model: [I18n.tr("Right"), I18n.tr("Left")]
size: "small"
currentIndex: SettingsData.notepadSlideoutSide === "left" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.notepadSlideoutSide = index === 1 ? "left" : "right";
}
}
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Auto Compositor Gaps")
description: I18n.tr("Inset the Notepad from screen edges using the compositor's configured gaps")
checked: SettingsData.notepadUseCompositorGap
onToggled: checked => {
SettingsData.notepadUseCompositorGap = checked;
}
}
StyledText {
visible: !SettingsData.notepadUseCompositorGap
text: I18n.tr("Manual Gaps")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
DankSlider {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingXS
width: parent.width - Theme.spacingXS * 2
height: 24
visible: !SettingsData.notepadUseCompositorGap
value: SettingsData.notepadEdgeGap
minimum: 0
maximum: 64
unit: "px"
showValue: true
wheelEnabled: false
onSliderValueChanged: newValue => {
SettingsData.notepadEdgeGap = newValue;
}
}
}
}
StyledText { StyledText {
width: parent.width width: parent.width
text: SettingsData.notepadUseMonospace ? I18n.tr("Using global monospace font from Settings → Personalization") : I18n.tr("Global fonts can be configured in Settings → Personalization") text: SettingsData.notepadUseMonospace ? I18n.tr("Using global monospace font from Settings → Personalization") : I18n.tr("Global fonts can be configured in Settings → Personalization")
@@ -362,6 +489,76 @@ Item {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
opacity: 0.8 opacity: 0.8
} }
StyledRect {
width: parent.width
implicitHeight: shortcutsHeader.height + (root.shortcutsExpanded ? shortcutsColumn.implicitHeight + Theme.spacingM : 0)
radius: Theme.cornerRadius
color: root.shortcutsExpanded ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : "transparent"
border.color: root.shortcutsExpanded ? Theme.primary : Theme.outlineMedium
border.width: root.shortcutsExpanded ? 2 : 1
StateLayer {
anchors.fill: parent
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: root.shortcutsExpanded = !root.shortcutsExpanded
}
Row {
id: shortcutsHeader
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingS
height: 36
spacing: Theme.spacingS
DankIcon {
name: root.shortcutsExpanded ? "expand_less" : "expand_more"
size: Theme.iconSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Keyboard Shortcuts")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
id: shortcutsColumn
visible: root.shortcutsExpanded
width: parent.width - Theme.spacingL * 2
anchors.top: shortcutsHeader.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
width: parent.width
text: I18n.tr("Ctrl+S: Save • Ctrl+O: Open • Ctrl+N: New • Ctrl+F: Find")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: parent.width
text: I18n.tr("Ctrl+A: Select All • Ctrl+P: Preview • Enter/Shift+Enter: Find Next/Previous • Esc: Close")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
}
}
} }
} }
} }
+252 -17
View File
@@ -32,6 +32,23 @@ Column {
property string pluginHighlightedHtml: "" property string pluginHighlightedHtml: ""
property string lastPluginContent: "" property string lastPluginContent: ""
property int loadRequestId: 0 property int loadRequestId: 0
property bool ignoreNextExternalChange: false
property bool watcherReloadPending: false
property bool externalWatchPaused: false
property bool inPopout: false
property bool surfaceVisible: true
// Tab ids are Date.now() timestamps (~1.78e12) which overflow a 32-bit `int`,
// corrupting the value (e.g. -946062153) and breaking buffer keying. `var`
// holds the full JS-safe integer.
property var loadedTabId: -1
property bool applyingShared: false
property bool showPathInfo: false
function currentFilePath() {
if (!currentTab)
return "";
return currentTab.isTemporary ? (NotepadStorageService.baseDir + "/" + currentTab.filePath) : currentTab.filePath;
}
signal saveRequested signal saveRequested
signal openRequested signal openRequested
@@ -40,6 +57,10 @@ Column {
signal escapePressed signal escapePressed
signal contentChanged signal contentChanged
signal settingsRequested signal settingsRequested
signal popoutRequested
signal dockRequested
signal conflictDetected(string diskContent)
signal autoSaveRequested
function hasUnsavedChanges() { function hasUnsavedChanges() {
if (!currentTab || !contentLoaded) { if (!currentTab || !contentLoaded) {
@@ -52,6 +73,12 @@ Column {
return textArea.text !== lastSavedContent; return textArea.text !== lastSavedContent;
} }
function commitLiveBuffer() {
if (loadedTabId < 0 || !contentLoaded)
return;
NotepadStorageService.setSessionBuffer(loadedTabId, textArea.text, lastSavedContent);
}
function loadCurrentTabContent() { function loadCurrentTabContent() {
if (!currentTab) if (!currentTab)
return; return;
@@ -62,8 +89,25 @@ Column {
const activeTab = NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null; const activeTab = NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null;
if (requestId !== loadRequestId || !activeTab || activeTab.id !== requestedTabId) if (requestId !== loadRequestId || !activeTab || activeTab.id !== requestedTabId)
return; return;
const buffer = NotepadStorageService.getSessionBuffer(requestedTabId);
if (buffer !== undefined) {
applyingShared = true;
lastSavedContent = buffer.baseline;
textArea.text = buffer.content;
applyingShared = false;
loadedTabId = requestedTabId;
contentLoaded = true;
syncContentToPlugin();
applyDiskContent(content);
return;
}
applyingShared = true;
lastSavedContent = content; lastSavedContent = content;
textArea.text = content; textArea.text = content;
applyingShared = false;
loadedTabId = requestedTabId;
contentLoaded = true; contentLoaded = true;
syncContentToPlugin(); syncContentToPlugin();
}); });
@@ -72,14 +116,56 @@ Column {
function saveCurrentTabContent() { function saveCurrentTabContent() {
if (!currentTab || !contentLoaded) if (!currentTab || !contentLoaded)
return; return;
if (!currentTab.isTemporary)
return;
NotepadStorageService.saveTabContent(NotepadStorageService.currentTabIndex, textArea.text); NotepadStorageService.saveTabContent(NotepadStorageService.currentTabIndex, textArea.text);
lastSavedContent = textArea.text; lastSavedContent = textArea.text;
NotepadStorageService.clearSessionBuffer(loadedTabId);
} }
function autoSaveToSession() { function autoSaveToSession() {
commitLiveBuffer();
if (!currentTab || !contentLoaded) if (!currentTab || !contentLoaded)
return; return;
if (currentTab.isTemporary) {
saveCurrentTabContent(); saveCurrentTabContent();
} else if (SettingsData.notepadAutoSave) {
root.autoSaveRequested();
}
}
function syncFromDisk() {
if (!currentTab)
return;
loadCurrentTabContent();
}
function applyDiskContent(diskContent) {
if (diskContent === undefined || diskContent === null)
return;
if (diskContent === textArea.text) {
lastSavedContent = diskContent;
return;
}
if (diskContent === lastSavedContent) {
return;
}
if (textArea.text === lastSavedContent) {
reloadFromDisk(diskContent);
} else if (surfaceVisible) {
conflictDetected(diskContent);
}
}
function reloadFromDisk(diskContent) {
applyingShared = true;
contentLoaded = false;
textArea.text = diskContent;
lastSavedContent = diskContent;
contentLoaded = true;
applyingShared = false;
NotepadStorageService.clearSessionBuffer(loadedTabId);
syncContentToPlugin();
} }
function setTextDocumentLineHeight() { function setTextDocumentLineHeight() {
@@ -202,7 +288,8 @@ Column {
if (!currentTab) if (!currentTab)
return; return;
const filePath = currentTab?.filePath || ""; const filePath = currentTab?.filePath || "";
const ext = filePath.split('.').pop().toLowerCase(); const baseName = filePath.split('/').pop();
const ext = baseName.includes('.') ? baseName.split('.').pop().toLowerCase() : "";
const content = textArea.text; const content = textArea.text;
if (content === lastPluginContent && SettingsData.getBuiltInPluginSetting("dankNotepadModule", "previewActive", false) === inlinePreviewVisible) { if (content === lastPluginContent && SettingsData.getBuiltInPluginSetting("dankNotepadModule", "previewActive", false) === inlinePreviewVisible) {
@@ -550,6 +637,7 @@ Column {
Connections { Connections {
target: NotepadStorageService target: NotepadStorageService
function onCurrentTabIndexChanged() { function onCurrentTabIndexChanged() {
root.commitLiveBuffer();
loadCurrentTabContent(); loadCurrentTabContent();
Qt.callLater(() => { Qt.callLater(() => {
textArea.forceActiveFocus(); textArea.forceActiveFocus();
@@ -570,7 +658,9 @@ Column {
} }
onTextChanged: { onTextChanged: {
if (contentLoaded && text !== lastSavedContent) { // Debounced flush to the shared buffer (+ optional disk
// autosave) for every loaded tab, not just scratch notes.
if (contentLoaded && !applyingShared) {
autoSaveTimer.restart(); autoSaveTimer.restart();
} }
root.contentChanged(); root.contentChanged();
@@ -744,6 +834,7 @@ Column {
spacing: Theme.spacingS spacing: Theme.spacingS
Item { Item {
id: buttonBarItem
width: parent.width width: parent.width
height: 32 height: 32
@@ -820,9 +911,29 @@ Column {
} }
} }
DankActionButton { Row {
id: rightButtonRow
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
visible: !root.inPopout
iconName: "open_in_new"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
onClicked: root.popoutRequested()
}
DankActionButton {
visible: root.inPopout
iconName: "dock_to_right"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
onClicked: root.dockRequested()
}
DankActionButton {
iconName: "more_horiz" iconName: "more_horiz"
iconSize: Theme.iconSize - 2 iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
@@ -830,7 +941,68 @@ Column {
} }
} }
StyledRect {
id: pathInfoPopup
visible: root.showPathInfo
anchors.right: parent.right
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
width: Math.min(root.width, 360)
height: pathInfoRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: 1
z: 10
Row { Row {
id: pathInfoRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: currentTab && currentTab.isTemporary ? "draft" : "description"
size: Theme.iconSize - 4
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
width: pathInfoRow.width - (Theme.iconSize - 4) - copyPathButton.width - Theme.spacingS * 2
text: root.currentFilePath()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
elide: Text.ElideMiddle
anchors.verticalCenter: parent.verticalCenter
}
DankActionButton {
id: copyPathButton
iconName: "content_copy"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceTextMedium
anchors.verticalCenter: parent.verticalCenter
onClicked: {
const proc = clipboardCopyProcComp.createObject(root, {
content: root.currentFilePath(),
running: true
});
proc.exited.connect(() => {
ToastService.showInfo(I18n.tr("Path copied to clipboard"));
proc.destroy();
});
}
}
}
}
}
Row {
id: statusRow
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
@@ -853,35 +1025,46 @@ Column {
opacity: 1.0 opacity: 1.0
} }
Row {
visible: textArea.text.length > 0
spacing: Theme.spacingXS
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter
readonly property bool savingToDisk: autoSaveTimer.running && currentTab && (currentTab.isTemporary || SettingsData.notepadAutoSave)
text: { text: {
if (autoSaveTimer.running) { if (savingToDisk) {
return I18n.tr("Auto-saving..."); return I18n.tr("Saving...");
} }
if (hasUnsavedChanges()) {
if (currentTab && currentTab.isTemporary) { if (currentTab && currentTab.isTemporary) {
return I18n.tr("Unsaved note..."); return I18n.tr("Auto saved");
} else {
return I18n.tr("Unsaved changes");
}
} else {
return I18n.tr("Saved");
} }
return hasUnsavedChanges() ? I18n.tr("Unsaved changes") : I18n.tr("Saved");
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
if (autoSaveTimer.running) { if (savingToDisk) {
return Theme.primary; return Theme.primary;
} }
if (hasUnsavedChanges()) { if (currentTab && currentTab.isTemporary) {
return Theme.warning;
} else {
return Theme.success; return Theme.success;
} }
return hasUnsavedChanges() ? Theme.warning : Theme.success;
}
}
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "info"
iconSize: Theme.iconSizeSmall
iconColor: root.showPathInfo ? Theme.primary : Theme.surfaceTextMedium
buttonSize: 20
onClicked: root.showPathInfo = !root.showPathInfo
} }
opacity: textArea.text.length > 0 ? 1.0 : 0.0
} }
} }
} }
@@ -902,6 +1085,38 @@ Column {
onTriggered: syncContentToPlugin() onTriggered: syncContentToPlugin()
} }
FileView {
id: externalWatch
path: (!root.externalWatchPaused && currentTab && !currentTab.isTemporary && currentTab.filePath) ? currentTab.filePath : ""
blockLoading: true
preload: true
watchChanges: true
onFileChanged: {
root.watcherReloadPending = true;
reload();
}
onLoaded: {
if (root.ignoreNextExternalChange) {
root.ignoreNextExternalChange = false;
root.lastSavedContent = externalWatch.text();
root.watcherReloadPending = false;
return;
}
if (!root.watcherReloadPending)
return;
root.watcherReloadPending = false;
if (!root.contentLoaded || !root.currentTab || root.currentTab.isTemporary)
return;
if (!root.surfaceVisible)
return;
root.applyDiskContent(externalWatch.text());
}
onLoadFailed: error => {}
}
Connections { Connections {
target: SettingsData target: SettingsData
function onBuiltInPluginSettingsChanged() { function onBuiltInPluginSettingsChanged() {
@@ -910,4 +1125,24 @@ Column {
} }
} }
} }
Connections {
target: NotepadStorageService
function onSessionBufferRevisionChanged() {
if (applyingShared || !contentLoaded || loadedTabId < 0)
return;
if (textArea.activeFocus)
return;
var buffer = NotepadStorageService.getSessionBuffer(loadedTabId);
if (buffer === undefined || buffer.content === textArea.text)
return;
if (textArea.text === lastSavedContent) {
applyingShared = true;
lastSavedContent = buffer.baseline;
textArea.text = buffer.content;
applyingShared = false;
syncContentToPlugin();
}
}
}
} }
@@ -152,6 +152,9 @@ Item {
} }
] ]
readonly property var entryActionKeys: ["pin", "edit", "delete"]
readonly property var entryActionLabels: [I18n.tr("Pin"), I18n.tr("Edit"), I18n.tr("Delete")]
function getMaxHistoryText(value) { function getMaxHistoryText(value) {
if (value <= 0) if (value <= 0)
return "∞"; return "∞";
@@ -187,6 +190,29 @@ Item {
return value.toString(); return value.toString();
} }
function visibleEntryActionKeys() {
return SettingsData.clipboardVisibleEntryActions || ["pin", "edit", "delete"];
}
function visibleEntryActionLabels() {
const visibleKeys = visibleEntryActionKeys();
return entryActionKeys.map((key, index) => visibleKeys.includes(key) ? entryActionLabels[index] : null).filter(label => label !== null);
}
function setVisibleEntryAction(index, selected) {
const actionKey = entryActionKeys[index];
if (!actionKey)
return;
let actions = visibleEntryActionKeys().slice();
if (selected && !actions.includes(actionKey)) {
actions.push(actionKey);
} else if (!selected && actions.includes(actionKey)) {
actions = actions.filter(action => action !== actionKey);
}
SettingsData.set("clipboardVisibleEntryActions", actions);
}
function loadConfig() { function loadConfig() {
configLoaded = false; configLoaded = false;
configError = false; configError = false;
@@ -437,6 +463,24 @@ Item {
checked: SettingsData.clipboardEnterToPaste checked: SettingsData.clipboardEnterToPaste
onToggled: checked => SettingsData.set("clipboardEnterToPaste", checked) onToggled: checked => SettingsData.set("clipboardEnterToPaste", checked)
} }
SettingsButtonGroupRow {
tab: "clipboard"
tags: ["clipboard", "actions", "buttons", "hide", "density", "pin", "edit", "delete"]
settingKey: "clipboardVisibleEntryActions"
text: I18n.tr("Visible Entry Actions")
description: I18n.tr("Choose which action buttons appear on clipboard entries")
selectionMode: "multi"
model: root.entryActionLabels
currentSelection: root.visibleEntryActionLabels()
checkEnabled: false
buttonHeight: 28
minButtonWidth: 56
buttonPadding: Theme.spacingS
textSize: Theme.fontSizeSmall
spacing: 1
onSelectionChanged: (index, selected) => root.setVisibleEntryAction(index, selected)
}
} }
SettingsCard { SettingsCard {
@@ -23,9 +23,9 @@ Item {
SettingsCard { SettingsCard {
width: parent.width width: parent.width
tags: ["niri", "layout", "gaps", "radius", "window", "border"] tags: ["niri", "layout", "gaps", "radius", "window", "border"]
title: I18n.tr("Niri Layout Overrides").replace("Niri", "niri") title: I18n.tr("Niri Layout Overrides")
settingKey: "niriLayout" settingKey: "niriLayout"
iconName: "crop_square" iconName: "layers"
visible: CompositorService.isNiri visible: CompositorService.isNiri
SettingsToggleRow { SettingsToggleRow {
+68 -67
View File
@@ -796,18 +796,81 @@ Item {
} }
} }
SettingsCard {
tab: "appearance"
iconName: "opacity"
title: I18n.tr("Opacity")
settingKey: "barTransparency"
visible: dankBarTab.appearanceOnly && selectedBarConfig?.enabled
SettingsSliderRow {
id: barTransparencySlider
visible: !SettingsData.frameEnabled
text: I18n.tr("Bar Opacity")
description: I18n.tr("Controls opacity of the bar background")
value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
transparency: finalValue / 100
});
}
Binding {
target: barTransparencySlider
property: "value"
value: (selectedBarConfig?.transparency ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
SettingsSliderRow {
id: widgetTransparencySlider
text: I18n.tr("Widget Opacity")
description: I18n.tr("Controls opacity of widget backgrounds")
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
widgetTransparency: finalValue / 100
});
}
Binding {
target: widgetTransparencySlider
property: "value"
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
SettingsControlledByFrame { SettingsControlledByFrame {
visible: !dankBarTab.appearanceOnly && SettingsData.frameEnabled visible: SettingsData.frameEnabled
parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("Bar Opacity")
reason: I18n.tr("Managed by Frame")
}
}
SettingsControlledByFrame {
visible: dankBarTab.appearanceOnly && SettingsData.frameEnabled
parentModal: dankBarTab.parentModal parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("Bar spacing and size") settingLabel: I18n.tr("Bar spacing and size")
reason: I18n.tr("Managed by Frame") reason: I18n.tr("Managed by Frame")
} }
SettingsCard { SettingsCard {
tab: "appearance"
iconName: "space_bar" iconName: "space_bar"
title: I18n.tr("Spacing") title: I18n.tr("Spacing")
settingKey: "barSpacing" settingKey: "barSpacing"
visible: !dankBarTab.appearanceOnly && (selectedBarConfig?.enabled ?? false) && !SettingsData.frameEnabled visible: dankBarTab.appearanceOnly && (selectedBarConfig?.enabled ?? false) && !SettingsData.frameEnabled
SettingsSliderRow { SettingsSliderRow {
id: edgeSpacingSlider id: edgeSpacingSlider
@@ -956,68 +1019,6 @@ Item {
} }
} }
SettingsCard {
tab: "appearance"
iconName: "opacity"
title: I18n.tr("Transparency")
settingKey: "barTransparency"
visible: dankBarTab.appearanceOnly && selectedBarConfig?.enabled
SettingsSliderRow {
id: barTransparencySlider
visible: !SettingsData.frameEnabled
text: I18n.tr("Bar Transparency")
description: I18n.tr("Opacity of the bar background")
value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
transparency: finalValue / 100
});
}
Binding {
target: barTransparencySlider
property: "value"
value: (selectedBarConfig?.transparency ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
SettingsSliderRow {
id: widgetTransparencySlider
text: I18n.tr("Widget Transparency")
description: I18n.tr("Opacity of widget backgrounds")
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
widgetTransparency: finalValue / 100
});
}
Binding {
target: widgetTransparencySlider
property: "value"
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
SettingsControlledByFrame {
visible: SettingsData.frameEnabled
parentModal: dankBarTab.parentModal
settingLabel: I18n.tr("Bar Transparency")
reason: I18n.tr("Managed by Frame")
}
}
SettingsSliderCard { SettingsSliderCard {
id: fontScaleSliderCard id: fontScaleSliderCard
tab: "appearance" tab: "appearance"
@@ -1358,7 +1359,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: borderOpacitySlider id: borderOpacitySlider
text: I18n.tr("Opacity") text: I18n.tr("Opacity")
description: I18n.tr("Transparency of the border") description: I18n.tr("Controls opacity of the border")
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100 value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -1453,7 +1454,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: widgetOutlineOpacitySlider id: widgetOutlineOpacitySlider
text: I18n.tr("Opacity") text: I18n.tr("Opacity")
description: I18n.tr("Transparency of the widget outline") description: I18n.tr("Controls opacity of the widget outline")
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100 value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -1562,7 +1563,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
visible: shadowCard.shadowActive visible: shadowCard.shadowActive
text: I18n.tr("Opacity") text: I18n.tr("Opacity")
description: I18n.tr("Transparency of the shadow layer") description: I18n.tr("Controls opacity of the shadow layer")
minimum: 10 minimum: 10
maximum: 100 maximum: 100
unit: "%" unit: "%"
+3 -3
View File
@@ -643,19 +643,19 @@ Item {
SettingsControlledByFrame { SettingsControlledByFrame {
visible: root.connectedFrameModeActive visible: root.connectedFrameModeActive
parentModal: root.parentModal parentModal: root.parentModal
settingLabel: I18n.tr("Dock margin, transparency, and border") settingLabel: I18n.tr("Dock margin, opacity, and border")
reason: I18n.tr("Managed by Frame in Connected Mode") reason: I18n.tr("Managed by Frame in Connected Mode")
} }
SettingsCard { SettingsCard {
width: parent.width width: parent.width
iconName: "opacity" iconName: "opacity"
title: I18n.tr("Transparency") title: I18n.tr("Opacity")
settingKey: "dockTransparency" settingKey: "dockTransparency"
visible: !root.connectedFrameModeActive visible: !root.connectedFrameModeActive
SettingsSliderRow { SettingsSliderRow {
text: I18n.tr("Dock Transparency") text: I18n.tr("Dock Opacity")
value: Math.round(SettingsData.dockTransparency * 100) value: Math.round(SettingsData.dockTransparency * 100)
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -113,6 +113,13 @@ Item {
} }
} }
} }
SettingsToggleRow {
text: I18n.tr("Device list scroll volume")
description: I18n.tr("Allow adjusting device volume by scrolling on the right half of items in the device list")
checked: SettingsData.audioDeviceScrollVolumeEnabled
onToggled: checked => SettingsData.set("audioDeviceScrollVolumeEnabled", checked)
}
} }
} }
} }
@@ -0,0 +1,462 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Services
import qs.Widgets
Item {
id: networkEthernetTab
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Component.onCompleted: {
NetworkService.addRef();
}
Component.onDestruction: {
NetworkService.removeRef();
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(600, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingL
SettingsCard {
id: root
property string expandedEthDevice: ""
title: I18n.tr("Ethernet")
iconName: "settings_ethernet"
settingKey: "networkEthernet"
tags: ["ethernet", "wired", "network", "adapters", "connection"]
width: parent.width
Column {
id: ethernetSection
width: parent.width
spacing: Theme.spacingM
StyledText {
text: {
const devices = NetworkService.ethernetDevices;
const connected = devices.filter(d => d.connected).length;
if (devices.length === 0)
return I18n.tr("No adapters");
if (connected === 0)
return devices.length === 1 ? I18n.tr("%1 adapter, none connected").arg(devices.length) : I18n.tr("%1 adapters, none connected").arg(devices.length);
return I18n.tr("%1 connected").arg(connected);
}
font.pixelSize: Theme.fontSizeSmall
color: NetworkService.ethernetConnected ? Theme.primary : Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
Column {
width: parent.width
spacing: 4
visible: NetworkService.ethernetDevices.length > 0
StyledText {
text: I18n.tr("Adapters")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Repeater {
model: NetworkService.ethernetDevices
delegate: Rectangle {
id: ethDeviceDelegate
required property var modelData
required property int index
readonly property bool isConnected: modelData.connected || false
readonly property bool isExpanded: root.expandedEthDevice === modelData.name
width: parent.width
height: isExpanded ? 56 + ethExpandedContent.height : 56
radius: Theme.cornerRadius
color: ethDeviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.width: isConnected ? 2 : 0
border.color: Theme.primary
clip: true
Behavior on height {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 56
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.right: ethDeviceActions.left
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: "lan"
size: 20
color: isConnected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width - 20 - Theme.spacingS
StyledText {
text: modelData.name || I18n.tr("Unknown")
font.pixelSize: Theme.fontSizeMedium
color: isConnected ? Theme.primary : Theme.surfaceText
font.weight: isConnected ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Row {
anchors.left: parent.left
spacing: Theme.spacingXS
StyledText {
text: {
switch (modelData.state) {
case "activated":
return I18n.tr("Connected");
case "disconnected":
return I18n.tr("Disconnected");
case "unavailable":
return I18n.tr("Unavailable");
default:
return modelData.state || I18n.tr("Unknown");
}
}
font.pixelSize: Theme.fontSizeSmall
color: isConnected ? Theme.primary : Theme.surfaceVariantText
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: (modelData.ip || "").length > 0
}
StyledText {
text: modelData.ip || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: (modelData.ip || "").length > 0
}
}
}
}
Row {
id: ethDeviceActions
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Rectangle {
width: 28
height: 28
radius: 14
color: ethExpandBtn.containsMouse ? Theme.surfacePressed : "transparent"
visible: isConnected
DankIcon {
anchors.centerIn: parent
name: isExpanded ? "expand_less" : "expand_more"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: ethExpandBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (isExpanded) {
root.expandedEthDevice = "";
} else {
root.expandedEthDevice = modelData.name;
NetworkService.fetchWiredNetworkInfo(NetworkService.ethernetConnectionUuid);
}
}
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: ethDisconnectBtn.containsMouse ? Theme.errorHover : "transparent"
visible: isConnected
DankIcon {
anchors.centerIn: parent
name: "link_off"
size: 18
color: ethDisconnectBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: ethDisconnectBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NetworkService.disconnectEthernetDevice(modelData.name)
}
}
}
MouseArea {
id: ethDeviceMouseArea
anchors.fill: parent
anchors.rightMargin: ethDeviceActions.width + Theme.spacingM
hoverEnabled: true
}
}
Column {
id: ethExpandedContent
width: parent.width
visible: isExpanded
Rectangle {
width: parent.width - Theme.spacingM * 2
height: 1
x: Theme.spacingM
color: Theme.outlineLight
}
Item {
width: parent.width
height: ethDetailsColumn.implicitHeight + Theme.spacingM * 2
Column {
id: ethDetailsColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Flow {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: {
const fields = [];
const dev = modelData;
if (!dev)
return fields;
if (dev.ip)
fields.push({
label: I18n.tr("IP"),
value: dev.ip
});
if (dev.speed && dev.speed > 0)
fields.push({
label: I18n.tr("Speed"),
value: dev.speed + " Mbps"
});
if (dev.hwAddress)
fields.push({
label: I18n.tr("MAC"),
value: dev.hwAddress
});
if (dev.driver)
fields.push({
label: I18n.tr("Driver"),
value: dev.driver
});
fields.push({
label: I18n.tr("State"),
value: dev.state || I18n.tr("Unknown")
});
return fields;
}
delegate: Rectangle {
required property var modelData
required property int index
width: ethFieldContent.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineLight
Row {
id: ethFieldContent
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: modelData.label + ":"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.value
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Item {
width: parent.width
height: NetworkService.networkWiredInfoLoading ? 40 : 0
visible: NetworkService.networkWiredInfoLoading
DankSpinner {
anchors.centerIn: parent
size: 20
}
}
}
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: NetworkService.wiredConnections.length > 0
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
StyledText {
text: I18n.tr("Saved Configurations")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Repeater {
model: NetworkService.wiredConnections
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 48
radius: Theme.cornerRadius
color: wiredMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.width: modelData.isActive ? 2 : 0
border.color: Theme.primary
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "lan"
size: 20
color: modelData.isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.id || I18n.tr("Unknown")
font.pixelSize: Theme.fontSizeMedium
color: modelData.isActive ? Theme.primary : Theme.surfaceText
font.weight: modelData.isActive ? Font.Medium : Font.Normal
}
StyledText {
text: modelData.isActive ? I18n.tr("Active") : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
visible: modelData.isActive
}
}
}
MouseArea {
id: wiredMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!modelData.isActive) {
NetworkService.connectToSpecificWiredConfig(modelData.uuid);
}
}
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,202 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Services
import qs.Widgets
Item {
id: networkStatusTab
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Component.onCompleted: {
NetworkService.addRef();
}
Component.onDestruction: {
NetworkService.removeRef();
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(600, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingL
SettingsCard {
id: root
title: I18n.tr("Network Status")
iconName: "lan"
settingKey: "networkStatus"
tags: ["status", "network", "connectivity", "internet"]
width: parent.width
Column {
id: overviewSection
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Overview of your network connections")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
Grid {
columns: 2
columnSpacing: Theme.spacingL
rowSpacing: Theme.spacingS
width: parent.width
StyledText {
text: I18n.tr("Backend")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
StyledText {
text: NetworkService.backend || I18n.tr("Unknown")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Status")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
Row {
spacing: Theme.spacingS
Rectangle {
width: 8
height: 8
radius: 4
anchors.verticalCenter: parent.verticalCenter
color: {
switch (NetworkService.networkStatus) {
case "ethernet":
case "wifi":
return Theme.success;
case "disconnected":
return Theme.error;
default:
return Theme.warning;
}
}
}
StyledText {
text: {
switch (NetworkService.networkStatus) {
case "ethernet":
return I18n.tr("Ethernet");
case "wifi":
return I18n.tr("WiFi");
case "disconnected":
return I18n.tr("Disconnected");
default:
return NetworkService.networkStatus || I18n.tr("Unknown");
}
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
}
StyledText {
text: I18n.tr("Primary")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: NetworkService.primaryConnection.length > 0
}
StyledText {
text: NetworkService.primaryConnection || "-"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
elide: Text.ElideRight
visible: NetworkService.primaryConnection.length > 0
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: NetworkService.backend === "networkmanager" && NetworkService.ethernetConnected && NetworkService.wifiConnected
StyledText {
text: I18n.tr("Preference")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - preferenceLabel.width - preferenceButtons.width - Theme.spacingM * 2
height: 1
}
DankButtonGroup {
id: preferenceButtons
model: [I18n.tr("Auto"), I18n.tr("Ethernet"), I18n.tr("WiFi")]
currentIndex: {
switch (NetworkService.userPreference) {
case "ethernet":
return 1;
case "wifi":
return 2;
default:
return 0;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
switch (index) {
case 0:
NetworkService.setNetworkPreference("auto");
break;
case 1:
NetworkService.setNetworkPreference("ethernet");
break;
case 2:
NetworkService.setNetworkPreference("wifi");
break;
}
}
}
}
StyledText {
id: preferenceLabel
visible: false
text: I18n.tr("Preference")
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,516 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Modals.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
Item {
id: networkVpnTab
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Component.onCompleted: {
NetworkService.addRef();
}
Component.onDestruction: {
NetworkService.removeRef();
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(600, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingL
SettingsCard {
id: root
property string expandedVpnUuid: ""
title: I18n.tr("VPN")
iconName: "vpn_key"
settingKey: "networkVpn"
tags: ["vpn", "network", "profiles", "import", "openvpn", "wireguard"]
function openVpnFileBrowser() {
vpnFileBrowserLoader.active = true;
if (vpnFileBrowserLoader.item)
vpnFileBrowserLoader.item.open();
}
property var vpnFileBrowserLoader: LazyLoader {
active: false
FileBrowserModal {
browserTitle: I18n.tr("Import VPN")
browserIcon: "vpn_key"
browserType: "vpn"
fileExtensions: VPNService.getFileFilter()
onFileSelected: path => {
VPNService.importVpn(path.replace("file://", ""));
}
}
}
property var deleteVpnConfirm: ConfirmModal {}
width: parent.width
Column {
id: vpnSection
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Unavailable")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
visible: !DMSNetworkService.vpnAvailable
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: DMSNetworkService.vpnAvailable
StyledText {
text: {
if (!DMSNetworkService.connected)
return I18n.tr("Disconnected");
const names = DMSNetworkService.activeNames || [];
if (names.length <= 1)
return names[0] || I18n.tr("Connected");
return names[0] + " +" + (names.length - 1);
}
font.pixelSize: Theme.fontSizeSmall
color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceVariantText
width: parent.width - vpnHeaderControls.width - Theme.spacingM
horizontalAlignment: Text.AlignLeft
anchors.verticalCenter: parent.verticalCenter
}
Row {
id: vpnHeaderControls
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Rectangle {
height: 28
radius: 14
width: importVpnRow.width + Theme.spacingM * 2
color: importVpnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
opacity: VPNService.importing ? 0.5 : 1.0
Row {
id: importVpnRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: VPNService.importing ? "sync" : "add"
size: Theme.fontSizeSmall
color: Theme.primary
}
StyledText {
text: I18n.tr("Import")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
}
MouseArea {
id: importVpnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: VPNService.importing ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !VPNService.importing
onClicked: root.openVpnFileBrowser()
}
}
Rectangle {
height: 28
radius: 14
width: disconnectAllRow.width + Theme.spacingM * 2
color: disconnectAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: DMSNetworkService.connected
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
Row {
id: disconnectAllRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "link_off"
size: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Disconnect")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: disconnectAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.disconnectAllActive()
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: DMSNetworkService.vpnAvailable
}
Item {
width: parent.width
height: 100
visible: DMSNetworkService.vpnAvailable && DMSNetworkService.profiles.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "vpn_key_off"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No VPN profiles")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Click Import to add a .ovpn or .conf")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Column {
width: parent.width
spacing: 4
visible: DMSNetworkService.vpnAvailable && DMSNetworkService.profiles.length > 0
Repeater {
model: DMSNetworkService.profiles
delegate: Rectangle {
id: vpnProfileRow
required property var modelData
required property int index
readonly property bool isActive: DMSNetworkService.isActiveUuid(modelData.uuid)
readonly property bool isTransient: !!modelData.transient
readonly property bool canExpand: modelData.canExpand !== false
readonly property bool canDelete: modelData.canDelete !== false
readonly property bool isExpanded: root.expandedVpnUuid === modelData.uuid
readonly property var configData: (!isTransient && isExpanded) ? VPNService.editConfig : null
width: parent.width
height: isExpanded ? 56 + vpnExpandedContent.height : 56
radius: Theme.cornerRadius
color: vpnRowArea.containsMouse ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight)
border.width: isActive ? 2 : 0
border.color: Theme.primary
opacity: DMSNetworkService.isBusy ? 0.6 : 1.0
clip: true
Behavior on height {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
MouseArea {
id: vpnRowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(modelData.uuid)
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
Row {
width: parent.width
height: 56 - Theme.spacingS * 2
spacing: Theme.spacingS
DankIcon {
name: isActive ? "vpn_lock" : "vpn_key_off"
size: 20
color: isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - ((canExpand ? 28 : 0) + (canDelete ? 28 : 0)) - Theme.spacingS * 4
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: VPNService.getVpnTypeFromProfile(modelData)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.left: parent.left
}
}
Item {
width: Theme.spacingXS
height: 1
}
Rectangle {
width: 28
height: 28
radius: 14
color: vpnExpandBtn.containsMouse ? Theme.surfacePressed : "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: canExpand
DankIcon {
anchors.centerIn: parent
name: isExpanded ? "expand_less" : "expand_more"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: vpnExpandBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (isExpanded) {
root.expandedVpnUuid = "";
} else {
root.expandedVpnUuid = modelData.uuid;
VPNService.getConfig(modelData.uuid);
}
}
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: vpnDeleteBtn.containsMouse ? Theme.errorHover : "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: canDelete
DankIcon {
anchors.centerIn: parent
name: "delete"
size: 18
color: vpnDeleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: vpnDeleteBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
deleteVpnConfirm.showWithOptions({
title: I18n.tr("Delete VPN"),
message: I18n.tr("Delete \"%1\"?").arg(modelData.name),
confirmText: I18n.tr("Delete"),
confirmColor: Theme.error,
onConfirm: () => VPNService.deleteVpn(modelData.uuid)
});
}
}
}
}
Column {
id: vpnExpandedContent
width: parent.width
spacing: Theme.spacingXS
visible: !isTransient && isExpanded
Rectangle {
width: parent.width
height: 1
color: Theme.outlineLight
}
Item {
width: parent.width
height: VPNService.configLoading ? 40 : 0
visible: VPNService.configLoading
DankSpinner {
anchors.centerIn: parent
size: 20
}
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: !VPNService.configLoading && configData
Repeater {
model: {
if (!configData)
return [];
const fields = [];
const data = configData.data || {};
if (data.remote)
fields.push({
label: I18n.tr("Server"),
value: data.remote
});
if (configData.username || data.username)
fields.push({
label: I18n.tr("Username"),
value: configData.username || data.username
});
if (data.cipher)
fields.push({
label: I18n.tr("Cipher"),
value: data.cipher
});
if (data.auth)
fields.push({
label: I18n.tr("Auth"),
value: data.auth
});
if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no")
fields.push({
label: I18n.tr("Protocol"),
value: data["proto-tcp"] === "yes" ? "TCP" : "UDP"
});
if (data["tunnel-mtu"])
fields.push({
label: I18n.tr("MTU"),
value: data["tunnel-mtu"]
});
if (data["connection-type"])
fields.push({
label: I18n.tr("Auth Type"),
value: data["connection-type"]
});
return fields;
}
delegate: Rectangle {
required property var modelData
required property int index
width: vpnFieldContent.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineLight
Row {
id: vpnFieldContent
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: modelData.label + ":"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.value
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Autoconnect")
checked: configData ? (configData.autoconnect || false) : false
visible: !VPNService.configLoading && configData !== null
onToggled: checked => {
VPNService.updateConfig(modelData.uuid, {
autoconnect: checked
});
}
}
Item {
width: 1
height: Theme.spacingXS
}
}
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,761 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Modules.Settings.Widgets
import qs.Modals.Common
import qs.Services
import qs.Widgets
Item {
id: networkWifiTab
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Component.onCompleted: {
NetworkService.addRef();
}
Component.onDestruction: {
NetworkService.removeRef();
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(600, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingL
SettingsCard {
id: root
property string expandedWifiSsid: ""
property int maxPinnedWifiNetworks: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v);
if (typeof value === "string" && value.length > 0)
return [value];
return [];
}
function getPinnedWifiNetworks() {
const pins = SettingsData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]);
}
function toggleWifiPin(ssid) {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
let pinnedList = normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(ssid);
if (pinIndex !== -1) {
pinnedList.splice(pinIndex, 1);
} else {
pinnedList.unshift(ssid);
if (pinnedList.length > maxPinnedWifiNetworks)
pinnedList = pinnedList.slice(0, maxPinnedWifiNetworks);
}
if (pinnedList.length > 0)
pins["preferredWifi"] = pinnedList;
else
delete pins["preferredWifi"];
SettingsData.set("wifiNetworkPins", pins);
}
property var forgetNetworkConfirm: ConfirmModal {}
width: parent.width
title: I18n.tr("WiFi")
iconName: "wifi"
settingKey: "networkWifi"
tags: ["wifi", "wi-fi", "wireless", "network", "ssid", "adapter", "radio"]
Column {
id: wifiSection
width: parent.width
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: {
if (NetworkService.wifiToggling)
return I18n.tr("Toggling...");
if (!NetworkService.wifiEnabled)
return I18n.tr("Disabled");
if (NetworkService.wifiConnected)
return NetworkService.currentWifiSSID;
return I18n.tr("Not connected");
}
font.pixelSize: Theme.fontSizeSmall
color: NetworkService.wifiConnected ? Theme.primary : Theme.surfaceVariantText
width: parent.width - wifiControls.width - Theme.spacingM
horizontalAlignment: Text.AlignLeft
anchors.verticalCenter: parent.verticalCenter
}
Row {
id: wifiControls
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
iconName: "wifi_find"
buttonSize: 32
visible: NetworkService.backend === "networkmanager" && NetworkService.wifiEnabled && !NetworkService.wifiToggling
onClicked: PopoutService.showHiddenNetworkModal()
}
DankActionButton {
iconName: "refresh"
buttonSize: 32
visible: NetworkService.wifiEnabled && !NetworkService.wifiToggling && !NetworkService.isScanning
onClicked: NetworkService.scanWifi()
}
DankToggle {
checked: NetworkService.wifiEnabled
enabled: !NetworkService.wifiToggling
onToggled: NetworkService.toggleWifiRadio()
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: NetworkService.wifiEnabled && (NetworkService.wifiDevices?.length ?? 0) > 1
StyledText {
text: I18n.tr("WiFi Device")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - wifiDeviceLabel.width - wifiDeviceDropdown.width - Theme.spacingM * 2
height: 1
}
DankDropdown {
id: wifiDeviceDropdown
dropdownWidth: 150
popupWidth: 180
currentValue: NetworkService.wifiDeviceOverride || I18n.tr("Auto")
options: {
const devices = NetworkService.wifiDevices;
if (!devices || devices.length === 0)
return [I18n.tr("Auto")];
return [I18n.tr("Auto")].concat(devices.map(d => d.name));
}
onValueChanged: value => {
const deviceName = value === I18n.tr("Auto") ? "" : value;
NetworkService.setWifiDeviceOverride(deviceName);
}
}
}
StyledText {
id: wifiDeviceLabel
visible: false
text: I18n.tr("WiFi Device")
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: NetworkService.wifiEnabled
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: NetworkService.wifiEnabled && !NetworkService.wifiToggling
Column {
width: parent.width
spacing: Theme.spacingS
visible: NetworkService.wifiInterface.length > 0
Row {
width: parent.width
height: 24
StyledText {
text: I18n.tr("Interface:")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
width: 100
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: NetworkService.wifiInterface || "-"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
height: 24
visible: NetworkService.wifiIP.length > 0
StyledText {
text: I18n.tr("IP Address:")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
width: 100
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: NetworkService.wifiIP || "-"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
height: 24
visible: NetworkService.wifiConnected
StyledText {
text: I18n.tr("Signal:")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
width: 100
anchors.verticalCenter: parent.verticalCenter
}
Row {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: {
const s = NetworkService.wifiSignalStrength;
if (s >= 50)
return "wifi";
if (s >= 25)
return "wifi_2_bar";
return "wifi_1_bar";
}
size: 18
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: NetworkService.wifiSignalStrength + "%"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
}
}
}
Item {
width: parent.width
height: Theme.spacingS
}
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Available Networks")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: 1
height: 1
Layout.fillWidth: true
}
StyledText {
text: NetworkService.wifiNetworks?.length ?? 0
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: parent.width
height: 80
visible: NetworkService.isScanning && (NetworkService.wifiNetworks?.length ?? 0) === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
id: scanningIcon
name: "wifi_find"
size: 32
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
SequentialAnimation {
running: NetworkService.isScanning
loops: Animation.Infinite
OpacityAnimator {
target: scanningIcon
to: 0.3
duration: 400
easing.type: Easing.InOutQuad
}
OpacityAnimator {
target: scanningIcon
to: 1.0
duration: 400
easing.type: Easing.InOutQuad
}
onRunningChanged: if (!running)
scanningIcon.opacity = 1.0
}
}
StyledText {
text: I18n.tr("Scanning...")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Column {
width: parent.width
spacing: 4
visible: (NetworkService.wifiNetworks?.length ?? 0) > 0
Repeater {
model: {
const ssid = NetworkService.currentWifiSSID;
const networks = NetworkService.wifiNetworks || [];
const pinnedList = root.getPinnedWifiNetworks();
let sorted = [...networks];
sorted.sort((a, b) => {
const aPinnedIndex = pinnedList.indexOf(a.ssid);
const bPinnedIndex = pinnedList.indexOf(b.ssid);
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
if (aPinnedIndex === -1)
return 1;
if (bPinnedIndex === -1)
return -1;
return aPinnedIndex - bPinnedIndex;
}
if (a.ssid === ssid)
return -1;
if (b.ssid === ssid)
return 1;
return b.signal - a.signal;
});
return sorted;
}
delegate: Rectangle {
id: wifiNetworkDelegate
required property var modelData
required property int index
readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID
readonly property bool isPinned: root.getPinnedWifiNetworks().includes(modelData.ssid)
readonly property bool isExpanded: root.expandedWifiSsid === modelData.ssid
width: parent.width
height: isExpanded ? 56 + wifiExpandedContent.height : 56
radius: Theme.cornerRadius
color: wifiNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.width: isConnected ? 2 : 0
border.color: Theme.primary
clip: true
Behavior on height {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 56
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.right: wifiNetworkActions.left
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: {
const s = modelData.signal || 0;
if (s >= 50)
return "wifi";
if (s >= 25)
return "wifi_2_bar";
return "wifi_1_bar";
}
size: 20
color: isConnected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width - 20 - Theme.spacingS
Row {
anchors.left: parent.left
spacing: Theme.spacingXS
StyledText {
text: modelData.ssid || I18n.tr("Unknown")
font.pixelSize: Theme.fontSizeMedium
color: isConnected ? Theme.primary : Theme.surfaceText
font.weight: isConnected ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
DankIcon {
name: "push_pin"
size: 14
color: Theme.primary
visible: isPinned
anchors.verticalCenter: parent.verticalCenter
}
DankIcon {
name: "visibility_off"
size: 14
color: Theme.surfaceVariantText
visible: modelData.hidden || false
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.left: parent.left
spacing: Theme.spacingXS
StyledText {
text: isConnected ? I18n.tr("Connected") : (modelData.secured ? I18n.tr("Secured") : I18n.tr("Open"))
font.pixelSize: Theme.fontSizeSmall
color: isConnected ? Theme.primary : Theme.surfaceVariantText
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: modelData.saved
}
StyledText {
text: I18n.tr("Saved")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
visible: modelData.saved
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: modelData.hidden || false
}
StyledText {
text: I18n.tr("Hidden")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: modelData.hidden || false
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.signal + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
Row {
id: wifiNetworkActions
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Rectangle {
width: 28
height: 28
radius: 14
color: wifiExpandBtn.containsMouse ? Theme.surfacePressed : "transparent"
visible: isConnected || modelData.saved
DankIcon {
anchors.centerIn: parent
name: isExpanded ? "expand_less" : "expand_more"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: wifiExpandBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (isExpanded) {
root.expandedWifiSsid = "";
} else {
root.expandedWifiSsid = modelData.ssid;
NetworkService.fetchNetworkInfo(modelData.ssid);
}
}
}
}
DankActionButton {
iconName: "qr_code"
buttonSize: 28
visible: modelData.secured && modelData.saved
onClicked: {
PopoutService.showWifiQRCodeModal(modelData.ssid);
}
}
DankActionButton {
iconName: isPinned ? "push_pin" : "push_pin"
buttonSize: 28
iconColor: isPinned ? Theme.primary : Theme.surfaceVariantText
onClicked: {
root.toggleWifiPin(modelData.ssid);
}
}
DankActionButton {
iconName: "delete"
buttonSize: 28
iconColor: Theme.error
visible: modelData.saved || isConnected
onClicked: {
forgetNetworkConfirm.showWithOptions({
title: I18n.tr("Forget Network"),
message: I18n.tr("Forget \"%1\"?").arg(modelData.ssid),
confirmText: I18n.tr("Forget"),
confirmColor: Theme.error,
onConfirm: () => NetworkService.forgetWifiNetwork(modelData.ssid)
});
}
}
}
MouseArea {
id: wifiNetworkMouseArea
anchors.fill: parent
anchors.rightMargin: wifiNetworkActions.width + Theme.spacingM
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (isConnected) {
NetworkService.disconnectWifi();
return;
}
if (modelData.secured && !modelData.saved && (DMSService.apiVersion < 7 || modelData.enterprise)) {
PopoutService.showWifiPasswordModal(modelData.ssid);
return;
}
NetworkService.connectToWifi(modelData.ssid);
}
}
}
Column {
id: wifiExpandedContent
width: parent.width
visible: isExpanded
Rectangle {
width: parent.width - Theme.spacingM * 2
height: 1
x: Theme.spacingM
color: Theme.outlineLight
}
Item {
width: parent.width
height: wifiDetailsColumn.implicitHeight + Theme.spacingM * 2
Column {
id: wifiDetailsColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Item {
width: parent.width
height: NetworkService.networkInfoLoading ? 40 : 0
visible: NetworkService.networkInfoLoading
DankSpinner {
anchors.centerIn: parent
size: 20
}
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: !NetworkService.networkInfoLoading
Repeater {
model: {
const fields = [];
const net = modelData;
if (!net)
return fields;
fields.push({
label: I18n.tr("Signal"),
value: net.signal + "%"
});
if (net.frequency)
fields.push({
label: I18n.tr("Frequency"),
value: (net.frequency / 1000).toFixed(1) + " GHz"
});
if (net.channel)
fields.push({
label: I18n.tr("Channel"),
value: String(net.channel)
});
if (net.rate)
fields.push({
label: I18n.tr("Rate"),
value: net.rate + " Mbps"
});
if (net.mode)
fields.push({
label: I18n.tr("Mode"),
value: net.mode
});
if (net.bssid)
fields.push({
label: I18n.tr("BSSID"),
value: net.bssid
});
fields.push({
label: I18n.tr("Security"),
value: net.secured ? (net.enterprise ? I18n.tr("Enterprise") : I18n.tr("WPA/WPA2")) : I18n.tr("Open")
});
return fields;
}
delegate: Rectangle {
required property var modelData
required property int index
width: wifiFieldContent.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineLight
Row {
id: wifiFieldContent
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: modelData.label + ":"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.value
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Row {
spacing: Theme.spacingS
visible: (modelData.saved || isConnected) && DMSService.apiVersion > 13
DankToggle {
id: autoconnectToggle
text: I18n.tr("Autoconnect")
checked: modelData.autoconnect || false
onToggled: checked => {
NetworkService.setWifiAutoconnect(modelData.ssid, checked);
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
+111 -103
View File
@@ -1639,7 +1639,7 @@ Item {
SettingsControlledByFrame { SettingsControlledByFrame {
visible: themeColorsTab.connectedFrameModeActive visible: themeColorsTab.connectedFrameModeActive
parentModal: themeColorsTab.parentModal parentModal: themeColorsTab.parentModal
settingLabel: I18n.tr("Transparency") settingLabel: I18n.tr("Surface Opacity")
reason: I18n.tr("Managed by Frame in Connected Mode") reason: I18n.tr("Managed by Frame in Connected Mode")
} }
@@ -1647,8 +1647,8 @@ Item {
tab: "theme" tab: "theme"
tags: ["surface", "popup", "transparency", "opacity", "modal"] tags: ["surface", "popup", "transparency", "opacity", "modal"]
settingKey: "popupTransparency" settingKey: "popupTransparency"
text: I18n.tr("Transparency") text: I18n.tr("Surface Opacity")
description: I18n.tr("Controls opacity of all popouts, modals, and their content layers") description: I18n.tr("Controls opacity of shell surfaces, popouts, and modals")
visible: !themeColorsTab.connectedFrameModeActive visible: !themeColorsTab.connectedFrameModeActive
value: Math.round(SettingsData.popupTransparency * 100) value: Math.round(SettingsData.popupTransparency * 100)
minimum: 0 minimum: 0
@@ -1671,6 +1671,113 @@ Item {
defaultValue: 12 defaultValue: 12
onSliderValueChanged: newValue => SettingsData.setCornerRadius(newValue) onSliderValueChanged: newValue => SettingsData.setCornerRadius(newValue)
} }
}
SettingsCard {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
title: I18n.tr("Background Blur")
settingKey: "blurEnabled"
iconName: "blur_on"
SettingsToggleRow {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
settingKey: "blurEnabled"
text: I18n.tr("Background Blur")
description: !BlurService.available ? I18n.tr("Your compositor does not support background blur (ext-background-effect-v1)") : I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support. Adjust Opacity accordingly.")
checked: SettingsData.blurEnabled ?? false
enabled: BlurService.available
onToggled: checked => SettingsData.set("blurEnabled", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["blur", "foreground", "layers", "contrast", "glass", "frosted"]
settingKey: "blurForegroundLayers"
text: I18n.tr("Foreground Layers")
description: I18n.tr("Show foreground surfaces on blurred panels for stronger contrast")
checked: SettingsData.blurForegroundLayers ?? true
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
enabled: BlurService.available
onToggled: checked => SettingsData.set("blurForegroundLayers", checked)
}
SettingsSliderRow {
tab: "theme"
tags: ["blur", "foreground", "layers", "outline", "border", "cards", "widgets", "notifications", "control center"]
settingKey: "blurLayerOutlineOpacity"
text: I18n.tr("Layer Outline Opacity")
description: I18n.tr("Controls outlines around blurred foreground cards, pills, and notification cards")
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
value: Math.round((SettingsData.blurLayerOutlineOpacity ?? 0.12) * 100)
minimum: 0
maximum: 40
unit: "%"
defaultValue: 12
onSliderValueChanged: newValue => SettingsData.set("blurLayerOutlineOpacity", newValue / 100)
}
SettingsDropdownRow {
tab: "theme"
tags: ["blur", "border", "outline", "edge"]
settingKey: "blurBorderColor"
text: I18n.tr("Blur Border Color")
description: I18n.tr("Border color around blurred surfaces")
visible: SettingsData.blurEnabled
options: [I18n.tr("Outline", "blur border color"), I18n.tr("Primary", "blur border color"), I18n.tr("Secondary", "blur border color"), I18n.tr("Text Color", "blur border color"), I18n.tr("Custom", "blur border color")]
currentValue: {
switch (SettingsData.blurBorderColor) {
case "primary":
return I18n.tr("Primary", "blur border color");
case "secondary":
return I18n.tr("Secondary", "blur border color");
case "surfaceText":
return I18n.tr("Text Color", "blur border color");
case "custom":
return I18n.tr("Custom", "blur border color");
default:
return I18n.tr("Outline", "blur border color");
}
}
onValueChanged: value => {
if (value === I18n.tr("Primary", "blur border color")) {
SettingsData.set("blurBorderColor", "primary");
} else if (value === I18n.tr("Secondary", "blur border color")) {
SettingsData.set("blurBorderColor", "secondary");
} else if (value === I18n.tr("Text Color", "blur border color")) {
SettingsData.set("blurBorderColor", "surfaceText");
} else if (value === I18n.tr("Custom", "blur border color")) {
SettingsData.set("blurBorderColor", "custom");
openBlurBorderColorPicker();
} else {
SettingsData.set("blurBorderColor", "outline");
}
}
}
SettingsSliderRow {
tab: "theme"
tags: ["blur", "border", "opacity"]
settingKey: "blurBorderOpacity"
text: I18n.tr("Blur Border Opacity")
description: I18n.tr("Controls the outer edge of protocol-blurred windows")
visible: SettingsData.blurEnabled
value: Math.round((SettingsData.blurBorderOpacity ?? 0.35) * 100)
minimum: 0
maximum: 100
unit: "%"
defaultValue: 35
onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100)
}
}
SettingsCard {
tab: "theme"
tags: ["elevation", "shadow", "lift", "m3", "material"]
title: I18n.tr("Shadows")
settingKey: "m3ElevationEnabled"
iconName: "layers"
SettingsToggleRow { SettingsToggleRow {
tab: "theme" tab: "theme"
@@ -1702,7 +1809,7 @@ Item {
tags: ["elevation", "shadow", "opacity", "transparency", "m3"] tags: ["elevation", "shadow", "opacity", "transparency", "m3"]
settingKey: "m3ElevationOpacity" settingKey: "m3ElevationOpacity"
text: I18n.tr("Shadow Opacity") text: I18n.tr("Shadow Opacity")
description: I18n.tr("Controls the transparency of the shadow") description: I18n.tr("Controls the opacity of the shadow")
value: SettingsData.m3ElevationOpacity ?? 30 value: SettingsData.m3ElevationOpacity ?? 30
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -1856,105 +1963,6 @@ Item {
} }
} }
SettingsCard {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
title: I18n.tr("Background Blur")
settingKey: "blurEnabled"
iconName: "blur_on"
SettingsToggleRow {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
settingKey: "blurEnabled"
text: I18n.tr("Background Blur")
description: !BlurService.available ? I18n.tr("Your compositor does not support background blur (ext-background-effect-v1)") : I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.")
checked: SettingsData.blurEnabled ?? false
enabled: BlurService.available
onToggled: checked => SettingsData.set("blurEnabled", checked)
}
SettingsToggleRow {
tab: "theme"
tags: ["blur", "foreground", "layers", "contrast", "glass", "frosted"]
settingKey: "blurForegroundLayers"
text: I18n.tr("Foreground Layers")
description: I18n.tr("Show foreground surfaces on blurred panels for stronger contrast")
checked: SettingsData.blurForegroundLayers ?? true
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
enabled: BlurService.available
onToggled: checked => SettingsData.set("blurForegroundLayers", checked)
}
SettingsSliderRow {
tab: "theme"
tags: ["blur", "foreground", "layers", "outline", "border", "cards", "widgets", "notifications", "control center"]
settingKey: "blurLayerOutlineOpacity"
text: I18n.tr("Layer Outline Opacity")
description: I18n.tr("Controls outlines around blurred foreground cards, pills, and notification cards")
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
value: Math.round((SettingsData.blurLayerOutlineOpacity ?? 0.12) * 100)
minimum: 0
maximum: 40
unit: "%"
defaultValue: 12
onSliderValueChanged: newValue => SettingsData.set("blurLayerOutlineOpacity", newValue / 100)
}
SettingsDropdownRow {
tab: "theme"
tags: ["blur", "border", "outline", "edge"]
settingKey: "blurBorderColor"
text: I18n.tr("Blur Border Color")
description: I18n.tr("Border color around blurred surfaces")
visible: SettingsData.blurEnabled
options: [I18n.tr("Outline", "blur border color"), I18n.tr("Primary", "blur border color"), I18n.tr("Secondary", "blur border color"), I18n.tr("Text Color", "blur border color"), I18n.tr("Custom", "blur border color")]
currentValue: {
switch (SettingsData.blurBorderColor) {
case "primary":
return I18n.tr("Primary", "blur border color");
case "secondary":
return I18n.tr("Secondary", "blur border color");
case "surfaceText":
return I18n.tr("Text Color", "blur border color");
case "custom":
return I18n.tr("Custom", "blur border color");
default:
return I18n.tr("Outline", "blur border color");
}
}
onValueChanged: value => {
if (value === I18n.tr("Primary", "blur border color")) {
SettingsData.set("blurBorderColor", "primary");
} else if (value === I18n.tr("Secondary", "blur border color")) {
SettingsData.set("blurBorderColor", "secondary");
} else if (value === I18n.tr("Text Color", "blur border color")) {
SettingsData.set("blurBorderColor", "surfaceText");
} else if (value === I18n.tr("Custom", "blur border color")) {
SettingsData.set("blurBorderColor", "custom");
openBlurBorderColorPicker();
} else {
SettingsData.set("blurBorderColor", "outline");
}
}
}
SettingsSliderRow {
tab: "theme"
tags: ["blur", "border", "opacity"]
settingKey: "blurBorderOpacity"
text: I18n.tr("Blur Border Opacity")
description: I18n.tr("Controls the outer edge of protocol-blurred windows")
visible: SettingsData.blurEnabled
value: Math.round((SettingsData.blurBorderOpacity ?? 0.35) * 100)
minimum: 0
maximum: 100
unit: "%"
defaultValue: 35
onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100)
}
}
SettingsCard { SettingsCard {
tab: "theme" tab: "theme"
tags: ["modal", "darken", "background", "overlay"] tags: ["modal", "darken", "background", "overlay"]
@@ -115,6 +115,43 @@ Item {
} }
} }
SettingsDropdownRow {
tab: "time"
tags: ["calendar", "backend", "daemon", "khal", "dankcalendar", "events"]
settingKey: "calendarBackend"
text: I18n.tr("Calendar Backend")
description: {
const resolved = CalendarService.activeBackend;
switch (resolved) {
case "dankcal":
return I18n.tr("Using DankCalendar%1", "calendar backend status").arg(CalendarService.isDankActive && CalendarService.calendars.length > 0 ? "" : " (connecting…)");
case "khal":
return I18n.tr("Using khal", "calendar backend status");
default:
return I18n.tr("No calendar source available", "calendar backend status");
}
}
readonly property var _backendValues: ["auto", "khal", "dankcal"]
readonly property var _backendLabels: [I18n.tr("Auto", "calendar backend option"), I18n.tr("khal", "calendar backend option"), I18n.tr("DankCalendar", "calendar backend option")]
options: _backendLabels
currentValue: _backendLabels[Math.max(0, _backendValues.indexOf(SettingsData.calendarBackend))]
onValueChanged: value => {
const idx = _backendLabels.indexOf(value);
if (idx < 0)
return;
SettingsData.set("calendarBackend", _backendValues[idx]);
}
}
DankButton {
text: I18n.tr("Launch DankCalendar")
iconName: "calendar_month"
backgroundColor: Theme.primary
textColor: Theme.primaryText
visible: CalendarService.dankNeedsLaunch && CalendarService.dankBinaryExists
onClicked: CalendarService.launchDankCalendar()
}
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 1 height: 1
@@ -64,6 +64,8 @@ Item {
property alias model: buttonGroup.model property alias model: buttonGroup.model
property alias currentIndex: buttonGroup.currentIndex property alias currentIndex: buttonGroup.currentIndex
property alias initialSelection: buttonGroup.initialSelection
property alias currentSelection: buttonGroup.currentSelection
property alias selectionMode: buttonGroup.selectionMode property alias selectionMode: buttonGroup.selectionMode
property alias buttonHeight: buttonGroup.buttonHeight property alias buttonHeight: buttonGroup.buttonHeight
property alias minButtonWidth: buttonGroup.minButtonWidth property alias minButtonWidth: buttonGroup.minButtonWidth
+7 -1
View File
@@ -460,7 +460,7 @@ Item {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "keyboardLayoutNameShowIcon", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "showIdleInhibitorIcon", "showDoNotDisturbIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "hideWhenIdle"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "keyboardLayoutNameShowIcon", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "showIdleInhibitorIcon", "showDoNotDisturbIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "trayPopupSingleLine", "trayAutoOverflow", "trayMaxVisibleItems", "hideWhenIdle"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -803,6 +803,12 @@ Item {
item.barShowOverflowBadge = widget.barShowOverflowBadge; item.barShowOverflowBadge = widget.barShowOverflowBadge;
if (widget.trayUseInlineExpansion !== undefined) if (widget.trayUseInlineExpansion !== undefined)
item.trayUseInlineExpansion = widget.trayUseInlineExpansion; item.trayUseInlineExpansion = widget.trayUseInlineExpansion;
if (widget.trayPopupSingleLine !== undefined)
item.trayPopupSingleLine = widget.trayPopupSingleLine;
if (widget.trayAutoOverflow !== undefined)
item.trayAutoOverflow = widget.trayAutoOverflow;
if (widget.trayMaxVisibleItems !== undefined)
item.trayMaxVisibleItems = widget.trayMaxVisibleItems;
if (widget.hideWhenIdle !== undefined) if (widget.hideWhenIdle !== undefined)
item.hideWhenIdle = widget.hideWhenIdle; item.hideWhenIdle = widget.hideWhenIdle;
} }
@@ -43,7 +43,7 @@ Column {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "keyboardLayoutNameShowIcon", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "showIdleInhibitorIcon", "showDoNotDisturbIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "keyboardLayoutNameShowIcon", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "showIdleInhibitorIcon", "showDoNotDisturbIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "trayPopupSingleLine", "trayAutoOverflow", "trayMaxVisibleItems"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -1126,6 +1126,188 @@ Column {
} }
} }
} }
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: trayPopupLineArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
opacity: (trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false) ? 0.55 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "view_week"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Single-Line Popup")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: trayPopupLineToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: trayContextMenu.currentWidgetData?.trayPopupSingleLine ?? SettingsData.trayPopupSingleLine
enabled: !(trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false)
}
MouseArea {
id: trayPopupLineArea
anchors.fill: parent
hoverEnabled: true
cursorShape: (trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false) ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: {
if (trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false)
return;
const newValue = !(trayContextMenu.currentWidgetData?.trayPopupSingleLine ?? SettingsData.trayPopupSingleLine);
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayPopupSingleLine", newValue);
}
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: trayAutoOverflowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "responsive_layout"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Auto Overflow")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: trayAutoOverflowToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow
}
MouseArea {
id: trayAutoOverflowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const newValue = !(trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow);
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayAutoOverflow", newValue);
}
}
}
Rectangle {
width: parent.width
height: 36
radius: Theme.cornerRadius
color: trayMaxVisibleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
opacity: (trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow) ? 1 : 0.55
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "low_priority"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Max Visible")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
const value = trayContextMenu.currentWidgetData?.trayMaxVisibleItems ?? SettingsData.trayMaxVisibleItems;
return value > 0 ? String(value) : I18n.tr("Auto");
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankActionButton {
buttonSize: 28
iconName: "remove"
iconSize: 16
iconColor: Theme.surfaceText
enabled: trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow
onClicked: {
const current = trayContextMenu.currentWidgetData?.trayMaxVisibleItems ?? SettingsData.trayMaxVisibleItems;
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayMaxVisibleItems", Math.max(0, current - 1));
}
}
DankActionButton {
buttonSize: 28
iconName: "add"
iconSize: 16
iconColor: Theme.surfaceText
enabled: trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow
onClicked: {
const current = trayContextMenu.currentWidgetData?.trayMaxVisibleItems ?? SettingsData.trayMaxVisibleItems;
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayMaxVisibleItems", Math.min(20, current + 1));
}
}
}
MouseArea {
id: trayMaxVisibleArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
}
} }
} }
} }
+76 -86
View File
@@ -32,6 +32,8 @@ Variants {
color: "transparent" color: "transparent"
updatesEnabled: root.renderActive || root._settleFrames > 0
mask: Region { mask: Region {
item: Item {} item: Item {}
} }
@@ -84,20 +86,59 @@ Variants {
readonly property bool transitioning: transitionAnimation.running readonly property bool transitioning: transitionAnimation.running
property bool effectActive: false property bool effectActive: false
property bool _renderSettling: true
property bool _overviewBlurSettling: false
property bool useNextForEffect: false property bool useNextForEffect: false
property string pendingWallpaper: "" property string pendingWallpaper: ""
property string _deferredSource: "" property string _deferredSource: ""
readonly property bool overviewBlurActive: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && currentWallpaper.source !== "" readonly property bool overviewBlurActive: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && currentWallpaper.source !== ""
readonly property var backingWindow: Window.window
readonly property bool renderActive: !source || effectActive || overviewBlurActive || pendingWallpaper !== "" || _deferredSource !== "" || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
property int _settleFrames: 3
function invalidate() {
_settleFrames = 3;
backingWindow?.update();
}
onRenderActiveChanged: invalidate()
onBackingWindowChanged: invalidate()
Connections { Connections {
target: currentWallpaper target: root.backingWindow
function onStatusChanged() { function onFrameSwapped() {
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error) if (root._settleFrames > 0)
root._settleFrames--;
}
function onVisibleChanged() {
root.invalidate();
}
function onWidthChanged() {
root.invalidate();
}
function onHeightChanged() {
root.invalidate();
}
}
Connections {
target: Quickshell
function onScreensChanged() {
root.invalidate();
}
}
Connections {
target: SettingsData
function onWallpaperFillModeChanged() {
root.invalidate();
}
}
Connections {
target: IdleService
function onIsShellLockedChanged() {
if (IdleService.isShellLocked)
return; return;
root._renderSettling = true; root.invalidate();
renderSettleTimer.restart();
} }
} }
@@ -109,32 +150,11 @@ Variants {
} }
} }
Connections {
target: wallpaperWindow
function onWidthChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
function onHeightChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
}
Connections {
target: Quickshell
function onScreensChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
}
Connections { Connections {
target: NiriService target: NiriService
function onDisplayScalesChanged() { function onDisplayScalesChanged() {
root._recheckScreenScale(); root._recheckScreenScale();
root._renderSettling = true; root.invalidate();
renderSettleTimer.restart();
} }
} }
@@ -142,29 +162,7 @@ Variants {
target: WlrOutputService target: WlrOutputService
function onWlrOutputAvailableChanged() { function onWlrOutputAvailableChanged() {
root._recheckScreenScale(); root._recheckScreenScale();
root._renderSettling = true; root.invalidate();
renderSettleTimer.restart();
}
}
Connections {
target: NiriService
function onInOverviewChanged() {
root._overviewBlurSettling = true;
overviewBlurSettleTimer.restart();
}
}
Connections {
target: SettingsData
function onBlurWallpaperOnOverviewChanged() {
root._overviewBlurSettling = true;
overviewBlurSettleTimer.restart();
}
function onWallpaperFillModeChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
} }
} }
@@ -181,26 +179,22 @@ Variants {
} }
} }
Connections { function handleTransitionLoadError(failedSource) {
target: IdleService log.warn("failed to load candidate wallpaper for", modelData.name + ":", failedSource);
function onIsShellLockedChanged() { transitionDelayTimer.stop();
if (!IdleService.isShellLocked) { transitionAnimation.stop();
root._renderSettling = true; root.useNextForEffect = false;
renderSettleTimer.restart(); root.effectActive = false;
} root.transitionProgress = 0.0;
} currentWallpaper.layer.enabled = false;
} nextWallpaper.layer.enabled = false;
nextWallpaper.source = "";
Timer { if (!root.pendingWallpaper)
id: renderSettleTimer return;
interval: 1000 const pending = root.pendingWallpaper;
onTriggered: root._renderSettling = false root.pendingWallpaper = "";
} Qt.callLater(() => root.changeWallpaper(pending, true));
Timer {
id: overviewBlurSettleTimer
interval: 150
onTriggered: root._overviewBlurSettling = false
} }
function getFillMode(modeName) { function getFillMode(modeName) {
@@ -227,11 +221,6 @@ Variants {
} }
Component.onCompleted: { Component.onCompleted: {
wallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || root.overviewBlurActive || root._overviewBlurSettling || root.pendingWallpaper !== "" || root._deferredSource !== "" || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
if (!source) {
root._renderSettling = false;
}
isInitialized = true; isInitialized = true;
} }
@@ -262,8 +251,6 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root.effectActive = false; root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
root.screenScale = CompositorService.getScreenScale(modelData); root.screenScale = CompositorService.getScreenScale(modelData);
currentWallpaper.source = newSource; currentWallpaper.source = newSource;
nextWallpaper.source = ""; nextWallpaper.source = "";
@@ -328,9 +315,6 @@ Variants {
break; break;
} }
root._renderSettling = true;
renderSettleTimer.restart();
nextWallpaper.source = newPath; nextWallpaper.source = newPath;
if (nextWallpaper.status === Image.Ready) if (nextWallpaper.status === Image.Ready)
@@ -339,7 +323,7 @@ Variants {
Loader { Loader {
anchors.fill: parent anchors.fill: parent
active: !root.source || root.isColorSource active: !root.source || root.isColorSource || currentWallpaper.status === Image.Error
asynchronous: true asynchronous: true
sourceComponent: DankBackdrop { sourceComponent: DankBackdrop {
@@ -364,6 +348,12 @@ Variants {
cache: true cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight) sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(modelData.name)) fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status === Image.Error) {
log.warn("failed to load active wallpaper for", modelData.name + ":", source);
}
}
} }
Image { Image {
@@ -380,11 +370,13 @@ Variants {
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(modelData.name)) fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: { onStatusChanged: {
if (status === Image.Error) {
root.handleTransitionLoadError(source);
return;
}
if (status !== Image.Ready) if (status !== Image.Ready)
return; return;
if (root.actualTransitionType === "none") { if (root.actualTransitionType === "none") {
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = source; currentWallpaper.source = source;
nextWallpaper.source = ""; nextWallpaper.source = "";
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
@@ -632,8 +624,6 @@ Variants {
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
currentWallpaper.layer.enabled = false; currentWallpaper.layer.enabled = false;
nextWallpaper.layer.enabled = false; nextWallpaper.layer.enabled = false;
root._renderSettling = true;
renderSettleTimer.restart();
root.effectActive = false; root.effectActive = false;
if (!root.pendingWallpaper) if (!root.pendingWallpaper)
+35 -2
View File
@@ -58,6 +58,8 @@ Singleton {
return SessionData.deviceMaxVolumes[name] ?? 100; return SessionData.deviceMaxVolumes[name] ?? 100;
} }
readonly property int wheelVolumeStep: SettingsData.audioWheelScrollAmount
signal micMuteChanged signal micMuteChanged
signal audioOutputCycled(string deviceName, string deviceIcon) signal audioOutputCycled(string deviceName, string deviceIcon)
signal deviceAliasChanged(string nodeName, string newAlias) signal deviceAliasChanged(string nodeName, string newAlias)
@@ -156,14 +158,19 @@ Singleton {
return false; return false;
} }
function cycleAudioOutput() { function cycleAudioOutputDirection(forward) {
const sinks = getAvailableSinks(); const sinks = getAvailableSinks();
if (sinks.length < 2) if (sinks.length < 2)
return null; return null;
const currentName = root.sink?.name ?? ""; const currentName = root.sink?.name ?? "";
const currentIndex = sinks.findIndex(s => s.name === currentName); const currentIndex = sinks.findIndex(s => s.name === currentName);
const nextIndex = (currentIndex + 1) % sinks.length; let nextIndex;
if (forward) {
nextIndex = (currentIndex + 1) % sinks.length;
} else {
nextIndex = (currentIndex - 1 + sinks.length) % sinks.length;
}
const nextSink = sinks[nextIndex]; const nextSink = sinks[nextIndex];
setDefaultSinkByName(nextSink.name); setDefaultSinkByName(nextSink.name);
const name = displayName(nextSink); const name = displayName(nextSink);
@@ -171,6 +178,10 @@ Singleton {
return name; return name;
} }
function cycleAudioOutput() {
return cycleAudioOutputDirection(true);
}
function getDeviceAlias(nodeName) { function getDeviceAlias(nodeName) {
if (!nodeName) if (!nodeName)
return null; return null;
@@ -833,6 +844,28 @@ EOFCONFIG
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"; return root.sink.audio.muted ? "Audio muted" : "Audio unmuted";
} }
function handleNodeVolumeWheel(node, wheelEvent) {
if (!node?.audio)
return;
SessionData.suppressOSDTemporarily();
const delta = wheelEvent.angleDelta.y;
if (delta === 0)
return;
const current = Math.round(node.audio.volume * 100);
const maxVol = getMaxVolumePercent(node);
const newVolume = delta > 0 ? Math.min(maxVol, current + root.wheelVolumeStep) : Math.max(0, current - root.wheelVolumeStep);
node.audio.muted = false;
node.audio.volume = newVolume / 100;
if (node === sink) {
playVolumeChangeSoundIfEnabled();
}
wheelEvent.accepted = true;
}
function setMicVolume(percentage) { function setMicVolume(percentage) {
if (!root.source?.audio) { if (!root.source?.audio) {
return "No audio source available"; return "No audio source available";
+478
View File
@@ -0,0 +1,478 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
Item {
id: root
readonly property var log: Log.scoped("CalendarDankBackend")
property bool enabled: false
property string socketPath: ""
readonly property bool socketFound: socketPath.length > 0
property bool connected: false
property bool binaryExists: false
property bool binaryChecked: false
property var calendars: []
property var events: []
property var eventsByDate: ({})
property string lastError: ""
property date focusDate: new Date()
property var _loadedFrom: null
property var _loadedTo: null
property var pendingRequests: ({})
property int requestCounter: 0
readonly property var fallbackPalette: ["#7287fd", "#f38ba8", "#a6e3a1", "#fab387", "#cba6f7", "#94e2d5", "#f9e2af", "#89dceb"]
signal eventsUpdated
onEnabledChanged: {
if (enabled) {
if (!connected)
discoverProcess.running = true;
return;
}
requestSocket.connected = false;
subscribeSocket.connected = false;
socketPath = "";
connected = false;
}
Component.onCompleted: {
binaryCheck.running = true;
discoverProcess.running = true;
}
Process {
id: binaryCheck
command: ["sh", "-c", "command -v dcal"]
running: false
onExited: code => {
root.binaryExists = (code === 0);
root.binaryChecked = true;
}
}
Process {
id: discoverProcess
running: false
command: ["sh", "-c", "s=\"${DANKCAL_SOCKET:-}\"; if [ -S \"$s\" ]; then echo \"$s\"; exit 0; fi; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/dankcal-*.sock /tmp/dankcal-*.sock; do [ -S \"$f\" ] || continue; p=$(basename \"$f\" .sock); p=${p#dankcal-}; if kill -0 \"$p\" 2>/dev/null; then echo \"$f\"; exit 0; fi; done"]
stdout: StdioCollector {
onStreamFinished: {
const path = text.trim().split('\n')[0] || "";
if (path.length > 0) {
root._applySocketPath(path);
return;
}
if (!root.connected) {
if (root.socketPath !== "")
root.log.info("dankcal socket gone, waiting for daemon");
requestSocket.connected = false;
subscribeSocket.connected = false;
root.socketPath = "";
}
}
}
}
Timer {
id: rediscoverTimer
interval: 3000
repeat: true
running: root.enabled && !root.connected
onTriggered: {
if (!discoverProcess.running)
discoverProcess.running = true;
}
}
function launch() {
if (!binaryExists)
return;
Quickshell.execDetached(["dcal", "run", "-d", "--hidden"]);
if (enabled && !connected)
discoverProcess.running = true;
}
function _applySocketPath(path) {
if (path === socketPath) {
if (socketFound && !connected)
requestSocket.connected = true;
return;
}
log.info("dankcal socket discovered:", path);
requestSocket.connected = false;
subscribeSocket.connected = false;
socketPath = path;
Qt.callLater(() => requestSocket.connected = true);
}
DankSocket {
id: requestSocket
path: root.socketPath
connected: false
onConnectionStateChanged: {
if (linkUp) {
root.connected = true;
subscribeSocket.connected = true;
root.log.info("connected to dankcal:", root.socketPath);
root.refreshCalendars();
root.reloadEvents();
return;
}
if (!root.connected && !root.socketFound)
return;
root.connected = false;
root._flushPending();
requestSocket.connected = false;
subscribeSocket.connected = false;
root.log.info("dankcal disconnected, rediscovering");
if (root.enabled)
discoverProcess.running = true;
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0)
return;
let response;
try {
response = JSON.parse(line);
} catch (e) {
return;
}
root._handleResponse(response);
}
}
}
DankSocket {
id: subscribeSocket
path: root.socketPath
connected: false
onConnectionStateChanged: {
if (linkUp)
root._sendSubscribe();
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0)
return;
let event;
try {
event = JSON.parse(line);
} catch (e) {
return;
}
root._handleEvent(event);
}
}
}
Timer {
id: refreshDebounce
interval: 400
repeat: false
onTriggered: {
root.refreshCalendars();
root.reloadEvents();
}
}
function _sendSubscribe() {
subscribeSocket.send({
"id": _nextId(),
"method": "subscribe",
"params": {
"topics": ["accounts", "calendars", "events", "sync"]
}
});
}
function _nextId() {
requestCounter++;
return Date.now() + requestCounter;
}
function _flushPending() {
const ids = Object.keys(pendingRequests);
for (const id of ids) {
const cb = pendingRequests[id];
delete pendingRequests[id];
if (cb)
cb({
"error": "disconnected"
});
}
}
function _handleResponse(response) {
if (response.event) {
_handleEvent(response);
return;
}
const id = response.id;
if (!id)
return;
const cb = pendingRequests[id];
if (cb) {
delete pendingRequests[id];
cb(response);
}
}
function _handleEvent(event) {
switch (event.event) {
case "accounts":
case "calendars":
refreshCalendars();
refreshDebounce.restart();
break;
case "events":
case "sync":
refreshDebounce.restart();
break;
}
}
function sendRequest(method, params, callback) {
if (!connected) {
if (callback)
callback({
"error": "not connected to dankcal socket"
});
return;
}
const id = _nextId();
const req = {
"id": id,
"method": method
};
if (params)
req.params = params;
if (callback)
pendingRequests[id] = callback;
requestSocket.send(req);
}
function refreshCalendars() {
sendRequest("calendars.list", null, response => {
if (response.error) {
lastError = response.error;
return;
}
const list = response.result || [];
for (let i = 0; i < list.length; i++) {
if (!list[i].color)
list[i].color = fallbackPalette[i % fallbackPalette.length];
}
calendars = list;
_rebuildEventsByDate();
});
}
function calendarById(id) {
for (let i = 0; i < calendars.length; i++) {
if (calendars[i].id === id)
return calendars[i];
}
return null;
}
function writableCalendars() {
return calendars.filter(c => !c.readOnly);
}
function defaultCalendar() {
const writable = writableCalendars().filter(c => !c.hidden);
return writable.length > 0 ? writable[0] : null;
}
function loadEvents(startDate, endDate) {
const mid = new Date((startDate.getTime() + endDate.getTime()) / 2);
focusDate = mid;
_ensureWindow();
}
function _ensureWindow() {
if (!connected)
return;
if (!_loadedFrom || !_loadedTo) {
reloadEvents();
return;
}
const margin = 14 * 86400000;
const t = focusDate.getTime();
if (t < _loadedFrom.getTime() + margin || t > _loadedTo.getTime() - margin)
reloadEvents();
else
_rebuildEventsByDate();
}
function reloadEvents() {
if (!connected)
return;
const from = new Date(focusDate.getTime() - 60 * 86400000);
const to = new Date(focusDate.getTime() + 90 * 86400000);
sendRequest("events.list", {
"from": from.toISOString(),
"to": to.toISOString(),
"limit": 5000
}, response => {
if (response.error) {
lastError = response.error;
return;
}
_loadedFrom = from;
_loadedTo = to;
const raw = (response.result || {}).events || [];
events = raw.map(e => _normalizeEvent(e));
_rebuildEventsByDate();
});
}
function _dayBoundary(iso) {
const d = new Date(iso);
return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
}
function _normalizeEvent(e) {
const allDay = !!e.allDay;
const id = e.id || "";
if (id.startsWith("task_"))
log.warn("daemon event id collides with task prefix:", id);
return {
"id": id,
"calendarId": e.calendarId || "",
"title": e.summary || "(untitled)",
"description": e.description || "",
"location": e.location || "",
"url": e.url || "",
"start": allDay ? _dayBoundary(e.start) : new Date(e.start),
"end": allDay ? _dayBoundary(e.end) : new Date(e.end),
"allDay": allDay,
"status": e.status || "confirmed",
"recurringId": e.recurringId || "",
"attendees": e.attendees || [],
"organizer": e.organizer || null,
"reminders": e.reminders || []
};
}
function decorateEvent(ev) {
const cal = calendarById(ev.calendarId);
const out = Object.assign({}, ev);
out.color = cal ? cal.color : fallbackPalette[0];
out.calendar = cal ? cal.name : "";
out.account = cal ? (cal.accountName || cal.accountId || "") : "";
out.readOnly = cal ? !!cal.readOnly : false;
out.isMultiDay = ev.start.toDateString() !== ev.end.toDateString();
return out;
}
function _hiddenCalendarIds() {
const hidden = {};
for (let i = 0; i < calendars.length; i++) {
if (calendars[i].hidden)
hidden[calendars[i].id] = true;
}
return hidden;
}
function _clampForDay(ev, cur, endDay) {
const out = Object.assign({}, ev);
const dayStart = new Date(cur.getFullYear(), cur.getMonth(), cur.getDate());
const startDay = new Date(ev.start.getFullYear(), ev.start.getMonth(), ev.start.getDate());
if (dayStart.getTime() === startDay.getTime()) {
out.start = new Date(ev.start);
} else {
out.start = new Date(dayStart);
if (!ev.allDay)
out.start.setHours(0, 0, 0, 0);
}
if (dayStart.getTime() === endDay.getTime()) {
out.end = new Date(ev.end);
} else {
out.end = new Date(dayStart);
if (!ev.allDay)
out.end.setHours(23, 59, 59, 999);
}
return out;
}
function _rebuildEventsByDate() {
const hidden = _hiddenCalendarIds();
const map = {};
for (const raw of events) {
if (raw.status === "cancelled")
continue;
if (hidden[raw.calendarId])
continue;
const ev = decorateEvent(raw);
const lastInstant = ev.allDay ? new Date(ev.end.getTime() - 1) : ev.end;
let cur = new Date(ev.start.getFullYear(), ev.start.getMonth(), ev.start.getDate());
let endDay = new Date(lastInstant.getFullYear(), lastInstant.getMonth(), lastInstant.getDate());
if (endDay < cur)
endDay = new Date(cur);
while (cur <= endDay) {
const key = Qt.formatDate(cur, "yyyy-MM-dd");
if (!map[key])
map[key] = [];
if (!map[key].some(e => e.id === ev.id))
map[key].push(_clampForDay(ev, cur, endDay));
cur.setDate(cur.getDate() + 1);
}
}
eventsByDate = map;
eventsUpdated();
}
function createEvent(fields, callback) {
sendRequest("events.create", fields, response => {
if (response.error)
lastError = response.error;
else
reloadEvents();
if (callback)
callback(response);
});
}
function updateEvent(id, fields, callback) {
const params = Object.assign({
"id": id
}, fields);
sendRequest("events.update", params, response => {
if (response.error)
lastError = response.error;
else
reloadEvents();
if (callback)
callback(response);
});
}
function deleteEvent(id, callback) {
sendRequest("events.delete", {
"id": id
}, response => {
if (response.error)
lastError = response.error;
else
reloadEvents();
if (callback)
callback(response);
});
}
}
+237
View File
@@ -0,0 +1,237 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Io
import qs.Common
import qs.Services
Item {
id: root
readonly property var log: Log.scoped("CalendarKhalBackend")
property bool installed: false
property var eventsByDate: ({})
property bool isLoading: false
property string lastError: ""
property date lastStartDate
property date lastEndDate
property string dateFormat: "MM/dd/yyyy"
function checkAvailability() {
if (!formatProcess.running)
formatProcess.running = true;
}
function loadCurrentMonth() {
let today = new Date();
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
let startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7);
let endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7);
loadEvents(startDate, endDate);
}
function loadEvents(startDate, endDate) {
if (!installed)
return;
if (eventsProcess.running)
return;
root.lastStartDate = startDate;
root.lastEndDate = endDate;
root.isLoading = true;
let startDateStr = Qt.formatDate(startDate, root.dateFormat);
let endDateStr = Qt.formatDate(endDate, root.dateFormat);
eventsProcess.requestStartDate = startDate;
eventsProcess.requestEndDate = endDate;
eventsProcess.command = ["khal", "list", "--json", "title", "--json", "description", "--json", "start-date", "--json", "start-time", "--json", "end-date", "--json", "end-time", "--json", "all-day", "--json", "location", "--json", "url", startDateStr, endDateStr];
eventsProcess.running = true;
}
function _parseDateFormat(formatExample) {
return formatExample.replace("12", "MM").replace("21", "dd").replace("2013", "yyyy");
}
Component.onCompleted: checkAvailability()
Process {
id: formatProcess
command: ["khal", "printformats"]
running: false
onExited: exitCode => {
if (exitCode !== 0)
checkProcess.running = true;
}
stdout: StdioCollector {
onStreamFinished: {
let lines = text.split('\n');
for (let line of lines) {
if (!line.startsWith('dateformat:'))
continue;
let formatExample = line.substring(line.indexOf(':') + 1).trim();
root.dateFormat = root._parseDateFormat(formatExample);
break;
}
checkProcess.running = true;
}
}
}
Process {
id: checkProcess
command: ["khal", "list", "today"]
running: false
onExited: exitCode => {
root.installed = (exitCode === 0);
if (root.installed)
root.loadCurrentMonth();
}
}
Process {
id: eventsProcess
property date requestStartDate
property date requestEndDate
property string rawOutput: ""
running: false
onExited: exitCode => {
root.isLoading = false;
if (exitCode !== 0) {
root.lastError = "Failed to load events (exit code: " + exitCode + ")";
return;
}
try {
let newEventsByDate = {};
let lines = eventsProcess.rawOutput.split('\n');
for (let line of lines) {
line = line.trim();
if (!line || line === "[]")
continue;
let dayEvents = JSON.parse(line);
for (let event of dayEvents) {
if (!event.title)
continue;
let startDate, endDate;
if (event['start-date'])
startDate = Date.fromLocaleString(I18n.locale(), event['start-date'], root.dateFormat);
else
startDate = new Date();
if (event['end-date'])
endDate = Date.fromLocaleString(I18n.locale(), event['end-date'], root.dateFormat);
else
endDate = new Date(startDate);
let startTime = new Date(startDate);
let endTime = new Date(endDate);
if (event['start-time'] && event['all-day'] !== "True") {
let timeStr = event['start-time'];
if (timeStr) {
let timeParts = timeStr.match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i);
if (timeParts) {
let hours = parseInt(timeParts[1]);
let minutes = parseInt(timeParts[2]);
if (timeParts[3]) {
let period = timeParts[3].toUpperCase();
if (period === 'PM' && hours !== 12)
hours += 12;
else if (period === 'AM' && hours === 12)
hours = 0;
}
startTime.setHours(hours, minutes);
if (event['end-time']) {
let endTimeParts = event['end-time'].match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i);
if (endTimeParts) {
let endHours = parseInt(endTimeParts[1]);
let endMinutes = parseInt(endTimeParts[2]);
if (endTimeParts[3]) {
let endPeriod = endTimeParts[3].toUpperCase();
if (endPeriod === 'PM' && endHours !== 12)
endHours += 12;
else if (endPeriod === 'AM' && endHours === 12)
endHours = 0;
}
endTime.setHours(endHours, endMinutes);
}
} else {
endTime = new Date(startTime);
endTime.setHours(startTime.getHours() + 1);
}
}
}
}
let eventId = event.title + "_" + event['start-date'] + "_" + (event['start-time'] || 'allday');
let extractedUrl = "";
if (!event.url && event.description) {
let urlMatch = event.description.match(/https?:\/\/[^\s]+/);
if (urlMatch)
extractedUrl = urlMatch[0];
}
let eventTemplate = {
"id": eventId,
"title": event.title || "Untitled Event",
"start": startTime,
"end": endTime,
"location": event.location || "",
"description": event.description || "",
"url": event.url || extractedUrl,
"calendar": "",
"color": "",
"allDay": event['all-day'] === "True",
"isMultiDay": startDate.toDateString() !== endDate.toDateString()
};
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
let dateKey = Qt.formatDate(currentDate, "yyyy-MM-dd");
if (!newEventsByDate[dateKey])
newEventsByDate[dateKey] = [];
let existingEvent = newEventsByDate[dateKey].find(e => e.id === eventId);
if (existingEvent) {
currentDate.setDate(currentDate.getDate() + 1);
continue;
}
let dayEvent = Object.assign({}, eventTemplate);
if (currentDate.getTime() === startDate.getTime()) {
dayEvent.start = new Date(startTime);
} else {
dayEvent.start = new Date(currentDate);
if (!dayEvent.allDay)
dayEvent.start.setHours(0, 0, 0, 0);
}
if (currentDate.getTime() === endDate.getTime()) {
dayEvent.end = new Date(endTime);
} else {
dayEvent.end = new Date(currentDate);
if (!dayEvent.allDay)
dayEvent.end.setHours(23, 59, 59, 999);
}
newEventsByDate[dateKey].push(dayEvent);
currentDate.setDate(currentDate.getDate() + 1);
}
}
}
root.eventsByDate = newEventsByDate;
root.lastError = "";
} catch (error) {
root.lastError = "Failed to parse events JSON: " + error.toString();
root.eventsByDate = {};
}
eventsProcess.rawOutput = "";
}
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
eventsProcess.rawOutput += data + "\n";
}
}
}
}
+123 -304
View File
@@ -11,71 +11,87 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("CalendarService") readonly property var log: Log.scoped("CalendarService")
property bool khalAvailable: true // Always true to enable DMS calendar card UI readonly property string backendPref: SettingsData.calendarBackend
property bool khalInstalled: false // Tracks if khal is actually on the system readonly property string activeBackend: {
switch (backendPref) {
case "khal":
return "khal";
case "dankcal":
return "dankcal";
default:
if (dankBackend.connected)
return "dankcal";
if (khalBackend.installed)
return "khal";
return "none";
}
}
readonly property bool calendarAvailable: activeBackend !== "none"
readonly property bool isDankActive: activeBackend === "dankcal"
readonly property bool canCreateEvents: isDankActive && dankBackend.connected
property bool khalAvailable: true // compatibility alias - calendar card UI gate
readonly property bool dankConnected: dankBackend.connected
readonly property bool dankBinaryExists: dankBackend.binaryExists
readonly property bool dankNeedsLaunch: backendPref === "dankcal" && !dankBackend.connected
property var calendars: dankBackend.calendars
property var eventsByDate: ({}) property var eventsByDate: ({})
property var khalEventsByDate: ({})
property var taskEventsByDate: ({}) property var taskEventsByDate: ({})
property var localTasks: ({}) property var localTasks: ({})
property bool isLoading: false property bool isLoading: khalBackend.isLoading
property string lastError: "" property string lastError: ""
property bool _rangeSet: false
property date lastStartDate property date lastStartDate
property date lastEndDate property date lastEndDate
property string khalDateFormat: "MM/dd/yyyy"
onKhalEventsByDateChanged: mergeEvents()
onTaskEventsByDateChanged: mergeEvents() onTaskEventsByDateChanged: mergeEvents()
onActiveBackendChanged: {
function checkKhalAvailability() { mergeEvents();
if (!khalCheckProcess.running) if (_rangeSet)
khalCheckProcess.running = true; loadEvents(lastStartDate, lastEndDate);
} }
function detectKhalDateFormat() { CalendarKhalBackend {
if (!khalFormatProcess.running) id: khalBackend
khalFormatProcess.running = true; onEventsByDateChanged: root.mergeEvents()
} }
function parseKhalDateFormat(formatExample) { CalendarDankBackend {
let qtFormat = formatExample.replace("12", "MM").replace("21", "dd").replace("2013", "yyyy"); id: dankBackend
return { enabled: root.backendPref === "dankcal" || root.backendPref === "auto"
format: qtFormat, onEventsByDateChanged: root.mergeEvents()
parser: null onConnectedChanged: {
}; if (connected && root._rangeSet)
root.loadEvents(root.lastStartDate, root.lastEndDate);
} }
function loadCurrentMonth() {
if (!root.khalAvailable)
return;
let today = new Date();
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
// Add padding
let startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7);
let endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7);
loadEvents(startDate, endDate);
} }
function loadEvents(startDate, endDate) { function loadEvents(startDate, endDate) {
if (!root.khalInstalled) {
return;
}
if (eventsProcess.running) {
return;
}
// Store last requested date range for refresh timer
root.lastStartDate = startDate; root.lastStartDate = startDate;
root.lastEndDate = endDate; root.lastEndDate = endDate;
root.isLoading = true; root._rangeSet = true;
// Format dates for khal using detected format switch (activeBackend) {
let startDateStr = Qt.formatDate(startDate, root.khalDateFormat); case "dankcal":
let endDateStr = Qt.formatDate(endDate, root.khalDateFormat); dankBackend.loadEvents(startDate, endDate);
eventsProcess.requestStartDate = startDate; break;
eventsProcess.requestEndDate = endDate; case "khal":
eventsProcess.command = ["khal", "list", "--json", "title", "--json", "description", "--json", "start-date", "--json", "start-time", "--json", "end-date", "--json", "end-time", "--json", "all-day", "--json", "location", "--json", "url", startDateStr, endDateStr]; khalBackend.loadEvents(startDate, endDate);
eventsProcess.running = true; break;
}
}
function _activeBackendEventsByDate() {
switch (activeBackend) {
case "dankcal":
return dankBackend.eventsByDate;
case "khal":
return khalBackend.eventsByDate;
default:
return {};
}
} }
function getEventsForDate(date) { function getEventsForDate(date) {
@@ -84,11 +100,54 @@ Singleton {
} }
function hasEventsForDate(date) { function hasEventsForDate(date) {
let events = getEventsForDate(date); return getEventsForDate(date).length > 0;
return events.length > 0; }
function writableCalendars() {
return isDankActive ? dankBackend.writableCalendars() : [];
}
function defaultCalendar() {
return isDankActive ? dankBackend.defaultCalendar() : null;
}
function launchDankCalendar() {
dankBackend.launch();
}
function createEvent(fields, callback) {
if (isDankActive) {
dankBackend.createEvent(fields, callback);
return;
}
if (callback)
callback({
"error": "read-only backend"
});
}
function updateEvent(id, fields, callback) {
if (isDankActive) {
dankBackend.updateEvent(id, fields, callback);
return;
}
if (callback)
callback({
"error": "read-only backend"
});
}
function deleteEvent(id, callback) {
if (isDankActive) {
dankBackend.deleteEvent(id, callback);
return;
}
if (callback)
callback({
"error": "read-only backend"
});
} }
// In-memory Task CRUD methods
function loadTasks(text) { function loadTasks(text) {
if (!text || text.trim() === "") { if (!text || text.trim() === "") {
root.localTasks = {}; root.localTasks = {};
@@ -129,8 +188,7 @@ Singleton {
"description": "Task from your Planner", "description": "Task from your Planner",
"url": "", "url": "",
"calendar": "Todo Planner", "calendar": "Todo Planner",
"color": "#10B981" // Pastel Green "color": "#10B981",
,
"allDay": true, "allDay": true,
"isMultiDay": false "isMultiDay": false
}); });
@@ -142,9 +200,8 @@ Singleton {
function addTaskForDate(date, text) { function addTaskForDate(date, text) {
let dateKey = Qt.formatDate(date, "yyyy-MM-dd"); let dateKey = Qt.formatDate(date, "yyyy-MM-dd");
let tasks = Object.assign({}, root.localTasks); let tasks = Object.assign({}, root.localTasks);
if (!tasks[dateKey]) { if (!tasks[dateKey])
tasks[dateKey] = []; tasks[dateKey] = [];
}
let taskId = (new Date().getTime()) + "-dms"; let taskId = (new Date().getTime()) + "-dms";
tasks[dateKey].push({ tasks[dateKey].push({
"id": taskId, "id": taskId,
@@ -187,11 +244,10 @@ Singleton {
let list = tasks[dateKey]; let list = tasks[dateKey];
let filtered = list.filter(item => item.id !== cleanId); let filtered = list.filter(item => item.id !== cleanId);
if (filtered.length !== list.length) { if (filtered.length !== list.length) {
if (filtered.length === 0) { if (filtered.length === 0)
delete tasks[dateKey]; delete tasks[dateKey];
} else { else
tasks[dateKey] = filtered; tasks[dateKey] = filtered;
}
updated = true; updated = true;
break; break;
} }
@@ -208,21 +264,18 @@ Singleton {
let tasks = Object.assign({}, root.localTasks); let tasks = Object.assign({}, root.localTasks);
let v = tasks[dateKey] || []; let v = tasks[dateKey] || [];
let idToItem = {}; let idToItem = {};
for (let item of v) { for (let item of v)
idToItem[item.id] = item; idToItem[item.id] = item;
}
let newV = []; let newV = [];
for (let tid of orderedIds) { for (let tid of orderedIds) {
if (idToItem[tid]) { if (idToItem[tid])
newV.push(idToItem[tid]); newV.push(idToItem[tid]);
} }
}
let orderedSet = new Set(orderedIds); let orderedSet = new Set(orderedIds);
for (let item of v) { for (let item of v) {
if (!orderedSet.has(item.id)) { if (!orderedSet.has(item.id))
newV.push(item); newV.push(item);
} }
}
tasks[dateKey] = newV; tasks[dateKey] = newV;
root.localTasks = tasks; root.localTasks = tasks;
updateTaskEvents(); updateTaskEvents();
@@ -254,30 +307,24 @@ Singleton {
function mergeEvents() { function mergeEvents() {
let merged = {}; let merged = {};
let backendEvents = _activeBackendEventsByDate();
// Merge khal events for (let dateKey in backendEvents)
for (let dateKey in root.khalEventsByDate) { merged[dateKey] = [].concat(backendEvents[dateKey]);
merged[dateKey] = [].concat(root.khalEventsByDate[dateKey]);
}
// Merge task events
for (let dateKey in root.taskEventsByDate) { for (let dateKey in root.taskEventsByDate) {
if (!merged[dateKey]) { if (!merged[dateKey])
merged[dateKey] = []; merged[dateKey] = [];
}
for (let event of root.taskEventsByDate[dateKey]) { for (let event of root.taskEventsByDate[dateKey]) {
if (!merged[dateKey].some(e => e.id === event.id)) { if (!merged[dateKey].some(e => e.id === event.id))
merged[dateKey].push(event); merged[dateKey].push(event);
} }
} }
}
// Sort events within each date
for (let dateKey in merged) { for (let dateKey in merged) {
let list = merged[dateKey]; let list = merged[dateKey];
for (let idx = 0; idx < list.length; idx++) { for (let idx = 0; idx < list.length; idx++)
list[idx]._origIdx = idx; list[idx]._origIdx = idx;
}
list.sort((a, b) => { list.sort((a, b) => {
let diff = a.start.getTime() - b.start.getTime(); let diff = a.start.getTime() - b.start.getTime();
if (diff !== 0) if (diff !== 0)
@@ -289,12 +336,6 @@ Singleton {
root.eventsByDate = merged; root.eventsByDate = merged;
} }
// Initialize on component completion
Component.onCompleted: {
detectKhalDateFormat();
}
// Atomic file view for tasks
FileView { FileView {
id: tasksFileView id: tasksFileView
path: Quickshell.env("HOME") + "/.config/niri-calendar-todo/tasks.json" path: Quickshell.env("HOME") + "/.config/niri-calendar-todo/tasks.json"
@@ -304,233 +345,11 @@ Singleton {
watchChanges: true watchChanges: true
printErrors: false printErrors: false
onLoaded: { onLoaded: loadTasks(tasksFileView.text())
loadTasks(tasksFileView.text());
}
onLoadFailed: { onLoadFailed: {
root.localTasks = {}; root.localTasks = {};
root.taskEventsByDate = {}; root.taskEventsByDate = {};
} }
} }
// Process for detecting khal date format
Process {
id: khalFormatProcess
command: ["khal", "printformats"]
running: false
onExited: exitCode => {
if (exitCode !== 0) {
checkKhalAvailability();
}
}
stdout: StdioCollector {
onStreamFinished: {
let lines = text.split('\n');
for (let line of lines) {
if (line.startsWith('dateformat:')) {
let formatExample = line.substring(line.indexOf(':') + 1).trim();
let formatInfo = parseKhalDateFormat(formatExample);
root.khalDateFormat = formatInfo.format;
break;
}
}
checkKhalAvailability();
}
}
}
// Process for checking khal configuration
Process {
id: khalCheckProcess
command: ["khal", "list", "today"]
running: false
onExited: exitCode => {
root.khalInstalled = (exitCode === 0);
if (root.khalInstalled) {
loadCurrentMonth();
} else {
loadEvents(root.lastStartDate || new Date(), root.lastEndDate || new Date());
}
}
}
// Process for loading events
Process {
id: eventsProcess
property date requestStartDate
property date requestEndDate
property string rawOutput: ""
running: false
onExited: exitCode => {
root.isLoading = false;
if (exitCode !== 0) {
root.lastError = "Failed to load events (exit code: " + exitCode + ")";
return;
}
try {
let newEventsByDate = {};
let lines = eventsProcess.rawOutput.split('\n');
for (let line of lines) {
line = line.trim();
if (!line || line === "[]")
continue;
// Parse JSON line
let dayEvents = JSON.parse(line);
// Process each event in this day's array
for (let event of dayEvents) {
if (!event.title)
continue;
// Parse start and end dates using detected format
let startDate, endDate;
if (event['start-date']) {
startDate = Date.fromLocaleString(I18n.locale(), event['start-date'], root.khalDateFormat);
} else {
startDate = new Date();
}
if (event['end-date']) {
endDate = Date.fromLocaleString(I18n.locale(), event['end-date'], root.khalDateFormat);
} else {
endDate = new Date(startDate);
}
// Create start/end times
let startTime = new Date(startDate);
let endTime = new Date(endDate);
if (event['start-time'] && event['all-day'] !== "True") {
// Parse time if available and not all-day
let timeStr = event['start-time'];
if (timeStr) {
// Match time with optional seconds and AM/PM
let timeParts = timeStr.match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i);
if (timeParts) {
let hours = parseInt(timeParts[1]);
let minutes = parseInt(timeParts[2]);
// Handle AM/PM conversion if present
if (timeParts[3]) {
let period = timeParts[3].toUpperCase();
if (period === 'PM' && hours !== 12) {
hours += 12;
} else if (period === 'AM' && hours === 12) {
hours = 0;
}
}
startTime.setHours(hours, minutes);
if (event['end-time']) {
let endTimeParts = event['end-time'].match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i);
if (endTimeParts) {
let endHours = parseInt(endTimeParts[1]);
let endMinutes = parseInt(endTimeParts[2]);
// Handle AM/PM conversion if present
if (endTimeParts[3]) {
let endPeriod = endTimeParts[3].toUpperCase();
if (endPeriod === 'PM' && endHours !== 12) {
endHours += 12;
} else if (endPeriod === 'AM' && endHours === 12) {
endHours = 0;
}
}
endTime.setHours(endHours, endMinutes);
}
} else {
// Default to 1 hour duration on same day
endTime = new Date(startTime);
endTime.setHours(startTime.getHours() + 1);
}
}
}
}
// Create unique ID for this event (to track multi-day events)
let eventId = event.title + "_" + event['start-date'] + "_" + (event['start-time'] || 'allday');
// Create event object template
let extractedUrl = "";
if (!event.url && event.description) {
let urlMatch = event.description.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
extractedUrl = urlMatch[0];
}
}
let eventTemplate = {
"id": eventId,
"title": event.title || "Untitled Event",
"start": startTime,
"end": endTime,
"location": event.location || "",
"description": event.description || "",
"url": event.url || extractedUrl,
"calendar": "",
"color": "",
"allDay": event['all-day'] === "True",
"isMultiDay": startDate.toDateString() !== endDate.toDateString()
};
// Add event to each day it spans
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
let dateKey = Qt.formatDate(currentDate, "yyyy-MM-dd");
if (!newEventsByDate[dateKey])
newEventsByDate[dateKey] = [];
// Check if this exact event is already added to this date (prevent duplicates)
let existingEvent = newEventsByDate[dateKey].find(e => {
return e.id === eventId;
});
if (existingEvent) {
// Move to next day without adding duplicate
currentDate.setDate(currentDate.getDate() + 1);
continue;
}
// Create a copy of the event for this date
let dayEvent = Object.assign({}, eventTemplate);
// For multi-day events, adjust the display time for this specific day
if (currentDate.getTime() === startDate.getTime()) {
// First day - use original start time
dayEvent.start = new Date(startTime);
} else {
// Subsequent days - start at beginning of day for all-day events
dayEvent.start = new Date(currentDate);
if (!dayEvent.allDay)
dayEvent.start.setHours(0, 0, 0, 0);
}
if (currentDate.getTime() === endDate.getTime()) {
// Last day - use original end time
dayEvent.end = new Date(endTime);
} else {
// Earlier days - end at end of day for all-day events
dayEvent.end = new Date(currentDate);
if (!dayEvent.allDay)
dayEvent.end.setHours(23, 59, 59, 999);
}
newEventsByDate[dateKey].push(dayEvent);
// Move to next day
currentDate.setDate(currentDate.getDate() + 1);
}
}
}
root.khalEventsByDate = newEventsByDate;
root.lastError = "";
} catch (error) {
root.lastError = "Failed to parse events JSON: " + error.toString();
root.khalEventsByDate = {};
}
// Reset for next run
eventsProcess.rawOutput = "";
}
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
eventsProcess.rawOutput += data + "\n";
}
}
}
} }
@@ -23,6 +23,49 @@ Singleton {
property var tabsBeingCreated: ({}) property var tabsBeingCreated: ({})
property bool metadataLoaded: false property bool metadataLoaded: false
// Shared live edit state across slideout and popout surfaces.
property var sessionBuffers: ({})
property int sessionBufferRevision: 0
function setSessionBuffer(tabId, content, baseline) {
if (tabId === undefined || tabId === null || tabId < 0)
return
var next = Object.assign({}, sessionBuffers)
if (content !== baseline) {
next[tabId] = { content: content, baseline: baseline }
} else {
delete next[tabId]
}
sessionBuffers = next
sessionBufferRevision++
}
function getSessionBuffer(tabId) {
return sessionBuffers[tabId]
}
function clearSessionBuffer(tabId) {
if (sessionBuffers[tabId] === undefined)
return
var next = Object.assign({}, sessionBuffers)
delete next[tabId]
sessionBuffers = next
sessionBufferRevision++
}
property var conflictTabId: -1
property string conflictDiskContent: ""
function flagConflict(tabId, diskContent) {
conflictDiskContent = diskContent
conflictTabId = tabId
}
function clearConflict() {
conflictTabId = -1
conflictDiskContent = ""
}
Component.onCompleted: { Component.onCompleted: {
ensureDirectories() ensureDirectories()
} }
@@ -209,6 +252,10 @@ Singleton {
if (tabIndex < 0 || tabIndex >= tabs.length) return if (tabIndex < 0 || tabIndex >= tabs.length) return
var newTabs = tabs.slice() var newTabs = tabs.slice()
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1
clearSessionBuffer(closedTabId)
if (conflictTabId === closedTabId)
clearConflict()
if (newTabs.length <= 1) { if (newTabs.length <= 1) {
var id = Date.now() var id = Date.now()
+81 -8
View File
@@ -392,8 +392,7 @@ Singleton {
function toggleSettingsWithTab(tabName: string) { function toggleSettingsWithTab(tabName: string) {
if (settingsModal) { if (settingsModal) {
var idx = settingsModal.resolveTabIndex(tabName); var idx = settingsModal.resolveTabIndex(tabName);
if (idx >= 0) settingsModal.setTabIndex(idx);
settingsModal.currentTabIndex = idx;
settingsModal.toggle(); settingsModal.toggle();
return; return;
} }
@@ -433,8 +432,7 @@ Singleton {
return; return;
} }
var idx = settingsModal.resolveTabIndex(tabName); var idx = settingsModal.resolveTabIndex(tabName);
if (idx >= 0) settingsModal.setTabIndex(idx);
settingsModal.currentTabIndex = idx;
toplevel.activate(); toplevel.activate();
return; return;
} }
@@ -466,12 +464,11 @@ Singleton {
if (_settingsWantsToggle) { if (_settingsWantsToggle) {
_settingsWantsToggle = false; _settingsWantsToggle = false;
if (_settingsPendingTabIndex >= 0) { if (_settingsPendingTabIndex >= 0) {
settingsModal.currentTabIndex = _settingsPendingTabIndex; settingsModal?.setTabIndex(_settingsPendingTabIndex);
_settingsPendingTabIndex = -1; _settingsPendingTabIndex = -1;
} else if (_settingsPendingTab) { } else if (_settingsPendingTab) {
var idx = settingsModal?.resolveTabIndex(_settingsPendingTab) ?? -1; var idx = settingsModal?.resolveTabIndex(_settingsPendingTab) ?? -1;
if (idx >= 0) settingsModal?.setTabIndex(idx);
settingsModal.currentTabIndex = idx;
_settingsPendingTab = ""; _settingsPendingTab = "";
} }
settingsModal?.toggle(); settingsModal?.toggle();
@@ -789,21 +786,97 @@ Singleton {
networkInfoModal?.close(); networkInfoModal?.close();
} }
function openNotepad() { function closeNotepadSlideouts() {
for (var i = 0; i < notepadSlideouts.length; i++) {
if (notepadSlideouts[i] && notepadSlideouts[i].isVisible)
notepadSlideouts[i].hide();
}
}
function openNotepadSlideout() {
notepadPopout?.hide();
if (notepadSlideouts.length > 0) { if (notepadSlideouts.length > 0) {
notepadSlideouts[0]?.show(); notepadSlideouts[0]?.show();
} }
} }
// Keep the notepad in a single presentation for default modes
Connections {
target: SettingsData
function onNotepadDefaultModeChanged() {
if (SettingsData.notepadDefaultMode === "popout") {
var hadSlideout = false;
for (var i = 0; i < root.notepadSlideouts.length; i++) {
if (root.notepadSlideouts[i] && root.notepadSlideouts[i].isVisible) {
hadSlideout = true;
root.notepadSlideouts[i].hide();
}
}
if (hadSlideout)
root.openNotepadPopout();
} else if (root.notepadPopout && root.notepadPopout.visible) {
root.notepadPopout.hide();
root.openNotepadSlideout();
}
}
}
function openNotepad() {
if (SettingsData.notepadDefaultMode === "popout") {
openNotepadPopout();
return;
}
openNotepadSlideout();
}
function closeNotepad() { function closeNotepad() {
if (SettingsData.notepadDefaultMode === "popout") {
notepadPopout?.hide();
return;
}
if (notepadSlideouts.length > 0) { if (notepadSlideouts.length > 0) {
notepadSlideouts[0]?.hide(); notepadSlideouts[0]?.hide();
} }
} }
function toggleNotepad() { function toggleNotepad() {
if (SettingsData.notepadDefaultMode === "popout") {
toggleNotepadPopout();
return;
}
if (notepadSlideouts.length > 0) { if (notepadSlideouts.length > 0) {
notepadSlideouts[0]?.toggle(); notepadSlideouts[0]?.toggle();
} }
} }
property var notepadPopout: null
property var notepadPopoutLoader: null
property bool _notepadPopoutWantsOpen: false
function openNotepadPopout() {
closeNotepadSlideouts();
if (notepadPopout) {
notepadPopout.show();
} else if (notepadPopoutLoader) {
_notepadPopoutWantsOpen = true;
notepadPopoutLoader.active = true;
}
}
function _onNotepadPopoutLoaded() {
if (_notepadPopoutWantsOpen && notepadPopout) {
_notepadPopoutWantsOpen = false;
notepadPopout.show();
}
}
function toggleNotepadPopout() {
if (notepadPopout) {
if (!notepadPopout.visible)
closeNotepadSlideouts();
notepadPopout.toggle();
} else {
openNotepadPopout();
}
}
} }
+6 -5
View File
@@ -13,11 +13,12 @@ Row {
property var initialSelection: [] property var initialSelection: []
property var currentSelection: initialSelection property var currentSelection: initialSelection
property bool checkEnabled: true property bool checkEnabled: true
property int buttonHeight: 40 property string size: "medium"
property int minButtonWidth: 64 property int buttonHeight: size === "small" ? 32 : 40
property int buttonPadding: Theme.spacingL property int minButtonWidth: size === "small" ? 56 : 64
property int checkIconSize: Theme.iconSizeSmall property int buttonPadding: size === "small" ? Theme.spacingM : Theme.spacingL
property int textSize: Theme.fontSizeMedium property int checkIconSize: size === "small" ? Theme.iconSizeSmall - 2 : Theme.iconSizeSmall
property int textSize: size === "small" ? Theme.fontSizeSmall : Theme.fontSizeMedium
property bool userInteracted: false property bool userInteracted: false
signal selectionChanged(int index, bool selected) signal selectionChanged(int index, bool selected)
+23 -12
View File
@@ -16,21 +16,28 @@ PanelWindow {
property var targetScreen: null property var targetScreen: null
property var modelData: null property var modelData: null
property bool triggerUsesOverlayLayer: false property bool triggerUsesOverlayLayer: false
// Drop off the Overlay layer (back to Top) while an overlay modal
property bool suppressOverlayLayer: false
property real slideoutWidth: 480 property real slideoutWidth: 480
property bool expandable: false property bool expandable: false
property bool expandedWidth: false property bool expandedWidth: false
property real expandedWidthValue: 960 property real expandedWidthValue: 960
property real edgeGap: 0
property string slideEdge: "right"
readonly property bool slideFromLeft: slideEdge === "left"
property Component content: null property Component content: null
property string title: "" property string title: ""
property alias container: contentContainer property alias container: contentContainer
property real customTransparency: -1 property real customTransparency: -1
property bool mappedVisible: false property bool mappedVisible: false
signal aboutToHide signal aboutToHide
signal revealed
function show() { function show() {
mappedVisible = true; mappedVisible = true;
Qt.callLater(() => { Qt.callLater(() => {
isVisible = true; isVisible = true;
revealed();
}); });
} }
@@ -52,9 +59,9 @@ PanelWindow {
anchors.top: true anchors.top: true
anchors.bottom: true anchors.bottom: true
anchors.right: true anchors.right: !root.slideFromLeft
anchors.left: root.slideFromLeft
// Expandable: fixed max surface width; strip width is slideContainer only (keeps blur/mask aligned).
implicitWidth: expandable ? expandedWidthValue : slideoutWidth implicitWidth: expandable ? expandedWidthValue : slideoutWidth
implicitHeight: modelData ? modelData.height : 800 implicitHeight: modelData ? modelData.height : 800
@@ -62,21 +69,22 @@ PanelWindow {
readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
WlrLayershell.layer: (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData)) ? WlrLayershell.Overlay : WlrLayershell.Top WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: 0 WlrLayershell.exclusiveZone: 0
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
readonly property real dpr: CompositorService.getScreenScale(root.screen) readonly property real dpr: CompositorService.getScreenScale(root.screen)
readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr) readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
readonly property real alignedHeight: Theme.px(modelData ? modelData.height : 800, dpr) readonly property real alignedHeight: Theme.px(modelData ? modelData.height : 800, dpr)
readonly property real alignedEdgeGap: Theme.px(edgeGap, dpr)
readonly property real slideoutSlideSnapX: Theme.snap(slideContainer.slideOffset, dpr) readonly property real slideoutSlideSnapX: Theme.snap(slideContainer.slideOffset, dpr)
mask: Region { mask: Region {
item: Rectangle { item: Rectangle {
x: root.width - slideContainer.width x: root.slideFromLeft ? root.alignedEdgeGap : (root.width - slideContainer.width - root.alignedEdgeGap)
y: 0 y: root.alignedEdgeGap
width: slideContainer.width width: slideContainer.width
height: root.height height: root.height - root.alignedEdgeGap * 2
} }
} }
@@ -84,16 +92,21 @@ PanelWindow {
id: slideContainer id: slideContainer
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: root.slideFromLeft ? undefined : parent.right
anchors.left: root.slideFromLeft ? parent.left : undefined
anchors.topMargin: root.alignedEdgeGap
anchors.bottomMargin: root.alignedEdgeGap
anchors.rightMargin: root.alignedEdgeGap
anchors.leftMargin: root.alignedEdgeGap
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight - root.alignedEdgeGap * 2
property real slideOffset: root.alignedWidth property real slideOffset: root.slideFromLeft ? -root.alignedWidth : root.alignedWidth
Connections { Connections {
target: root target: root
function onIsVisibleChanged() { function onIsVisibleChanged() {
slideContainer.slideOffset = root.isVisible ? 0 : slideContainer.width; slideContainer.slideOffset = root.isVisible ? 0 : (root.slideFromLeft ? -slideContainer.width : slideContainer.width);
} }
} }
@@ -111,7 +124,6 @@ PanelWindow {
} }
} }
// Expandable only; mask/blur bind to slideContainer geometry so they track this animation.
Behavior on width { Behavior on width {
enabled: root.expandable enabled: root.expandable
NumberAnimation { NumberAnimation {
@@ -217,7 +229,6 @@ PanelWindow {
} }
} }
// Blur region from slideContainer (not layered contentRect); position uses x + slideoutSlideSnapX, not mapToItem(root).
WindowBlur { WindowBlur {
targetWindow: root targetWindow: root
blurX: root.slideoutBlurActive ? slideContainer.x + root.slideoutSlideSnapX : 0 blurX: root.slideoutBlurActive ? slideContainer.x + root.slideoutSlideSnapX : 0
+20 -3
View File
@@ -23,6 +23,7 @@ StyledRect {
property alias text: textInput.text property alias text: textInput.text
property string placeholderText: "" property string placeholderText: ""
property string labelText: ""
property alias font: textInput.font property alias font: textInput.font
property alias textColor: textInput.color property alias textColor: textInput.color
property alias echoMode: textInput.echoMode property alias echoMode: textInput.echoMode
@@ -85,8 +86,10 @@ StyledRect {
textInput.insert(textInput.cursorPosition, str); textInput.insert(textInput.cursorPosition, str);
} }
readonly property real labelBandHeight: Math.round(Theme.fontSizeSmall * 1.4) + Theme.spacingXS * 2
width: 200 width: 200
height: Math.round(Theme.fontSizeMedium * 3) height: labelText !== "" ? Math.round(Theme.fontSizeMedium * 3) + labelBandHeight : Math.round(Theme.fontSizeMedium * 3)
radius: cornerRadius radius: cornerRadius
color: backgroundColor color: backgroundColor
border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
@@ -97,13 +100,27 @@ StyledRect {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: textInput.verticalCenter
name: leftIconName name: leftIconName
size: leftIconSize size: leftIconSize
color: textInput.activeFocus ? leftIconFocusedColor : leftIconColor color: textInput.activeFocus ? leftIconFocusedColor : leftIconColor
visible: leftIconName !== "" visible: leftIconName !== ""
} }
StyledText {
id: fieldLabel
anchors.left: textInput.left
anchors.right: textInput.right
anchors.top: parent.top
anchors.topMargin: Theme.spacingXS
text: root.labelText
visible: root.labelText !== ""
font.pixelSize: Theme.fontSizeSmall
color: textInput.activeFocus ? Theme.primary : Theme.surfaceVariantText
elide: Text.ElideRight
}
TextInput { TextInput {
id: textInput id: textInput
@@ -112,7 +129,7 @@ StyledRect {
anchors.right: rightButtonsRow.left anchors.right: rightButtonsRow.left
anchors.rightMargin: rightButtonsRow.visible ? Theme.spacingS : Theme.spacingM anchors.rightMargin: rightButtonsRow.visible ? Theme.spacingS : Theme.spacingM
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: root.topPadding anchors.topMargin: root.labelText !== "" ? root.labelBandHeight : root.topPadding
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: root.bottomPadding anchors.bottomMargin: root.bottomPadding
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
+1 -1
View File
@@ -148,7 +148,7 @@ Rectangle {
iconColor: Theme.surfaceVariantText iconColor: Theme.surfaceVariantText
onClicked: { onClicked: {
PopoutService.closeControlCenter(); PopoutService.closeControlCenter();
PopoutService.openSettingsWithTab("network"); PopoutService.openSettingsWithTab("network_vpn");
} }
} }
} }
@@ -102,7 +102,10 @@ TAB_INDEX_MAP = {
"DockTab.qml": 5, "DockTab.qml": 5,
"DankBarAppearanceTab.qml": 6, "DankBarAppearanceTab.qml": 6,
"WorkspaceAppearanceCard.qml": 6, "WorkspaceAppearanceCard.qml": 6,
"NetworkTab.qml": 7, "NetworkStatusTab.qml": 7,
"NetworkEthernetTab.qml": 39,
"NetworkWifiTab.qml": 40,
"NetworkVpnTab.qml": 41,
"PrinterTab.qml": 8, "PrinterTab.qml": 8,
"LauncherTab.qml": 9, "LauncherTab.qml": 9,
"ThemeColorsTab.qml": 10, "ThemeColorsTab.qml": 10,
@@ -172,6 +175,9 @@ TAB_CATEGORY_MAP = {
36: "Autostart", 36: "Autostart",
37: "Personalization", 37: "Personalization",
38: "Applications", 38: "Applications",
39: "Network",
40: "Network",
41: "Network",
} }
SEARCHABLE_COMPONENTS = [ SEARCHABLE_COMPONENTS = [
@@ -546,6 +552,7 @@ def main():
output_path = script_dir / "settings_search_index.json" output_path = script_dir / "settings_search_index.json"
with open(output_path, "w", encoding="utf-8") as f: with open(output_path, "w", encoding="utf-8") as f:
json.dump(all_entries, f, indent=2, ensure_ascii=False) json.dump(all_entries, f, indent=2, ensure_ascii=False)
f.write("\n")
print(f"Found {len(settings_entries)} searchable settings") print(f"Found {len(settings_entries)} searchable settings")
print(f"Found {len(tab_entries)} tab entries") print(f"Found {len(tab_entries)} tab entries")
+341 -150
View File
@@ -58,6 +58,7 @@
"targetable", "targetable",
"wallpaper" "wallpaper"
], ],
"icon": "blur_on",
"description": "Enable compositor-targetable blur layer (namespace: dms:blurwallpaper). Requires manual niri configuration.", "description": "Enable compositor-targetable blur layer (namespace: dms:blurwallpaper). Requires manual niri configuration.",
"conditionKey": "isNiri" "conditionKey": "isNiri"
}, },
@@ -727,21 +728,6 @@
], ],
"icon": "dashboard" "icon": "dashboard"
}, },
{
"section": "_tab_3",
"label": "Dank Bar",
"tabIndex": 3,
"category": "Dank Bar",
"keywords": [
"bar",
"dank",
"panel",
"statusbar",
"taskbar",
"topbar"
],
"icon": "toolbar"
},
{ {
"section": "barDisplay", "section": "barDisplay",
"label": "Display Assignment", "label": "Display Assignment",
@@ -777,30 +763,19 @@
"icon": "vertical_align_center" "icon": "vertical_align_center"
}, },
{ {
"section": "barSpacing", "section": "_tab_3",
"label": "Spacing", "label": "Settings",
"tabIndex": 3, "tabIndex": 3,
"category": "Dank Bar", "category": "Dank Bar",
"keywords": [ "keywords": [
"bar", "bar",
"between",
"dank", "dank",
"edges",
"gap",
"gaps",
"margin",
"margins",
"padding",
"panel", "panel",
"screen", "settings",
"space",
"spacing",
"statusbar", "statusbar",
"taskbar",
"topbar" "topbar"
], ],
"icon": "space_bar", "icon": "tune"
"description": "Space between the bar and screen edges"
}, },
{ {
"section": "barUseOverlayLayer", "section": "barUseOverlayLayer",
@@ -1528,6 +1503,19 @@
"windows" "windows"
] ]
}, },
{
"section": "dockTransparency",
"label": "Opacity",
"tabIndex": 5,
"category": "Dock",
"keywords": [
"dock",
"launcher bar",
"opacity",
"taskbar"
],
"icon": "opacity"
},
{ {
"section": "dockTrashFileManager", "section": "dockTrashFileManager",
"label": "Open Trash With", "label": "Open Trash With",
@@ -1745,23 +1733,6 @@
], ],
"icon": "space_bar" "icon": "space_bar"
}, },
{
"section": "dockTransparency",
"label": "Transparency",
"tabIndex": 5,
"category": "Dock",
"keywords": [
"alpha",
"dock",
"launcher bar",
"opacity",
"taskbar",
"translucent",
"transparency",
"transparent"
],
"icon": "opacity"
},
{ {
"section": "dockTrash", "section": "dockTrash",
"label": "Trash", "label": "Trash",
@@ -1798,21 +1769,6 @@
], ],
"description": "Place the dock on the Wayland overlay layer" "description": "Place the dock on the Wayland overlay layer"
}, },
{
"section": "_tab_6",
"label": "Appearance",
"tabIndex": 6,
"category": "Dank Bar",
"keywords": [
"appearance",
"bar",
"dank",
"panel",
"statusbar",
"topbar"
],
"icon": "palette"
},
{ {
"section": "barBorder", "section": "barBorder",
"label": "Border", "label": "Border",
@@ -1862,6 +1818,21 @@
"icon": "rounded_corner", "icon": "rounded_corner",
"description": "Remove corner rounding from the bar" "description": "Remove corner rounding from the bar"
}, },
{
"section": "_tab_6",
"label": "Dank Bar",
"tabIndex": 6,
"category": "Dank Bar",
"keywords": [
"bar",
"dank",
"panel",
"statusbar",
"taskbar",
"topbar"
],
"icon": "toolbar"
},
{ {
"section": "barAppearance", "section": "barAppearance",
"label": "Dank Bar", "label": "Dank Bar",
@@ -1982,6 +1953,25 @@
], ],
"description": "Use a fixed shadow direction for this bar" "description": "Use a fixed shadow direction for this bar"
}, },
{
"section": "barTransparency",
"label": "Opacity",
"tabIndex": 6,
"category": "Dank Bar",
"keywords": [
"background",
"bar",
"controls",
"dank",
"opacity",
"panel",
"statusbar",
"taskbar",
"topbar"
],
"icon": "opacity",
"description": "Controls opacity of the bar background"
},
{ {
"section": "barShadow", "section": "barShadow",
"label": "Shadow Override", "label": "Shadow Override",
@@ -2002,6 +1992,32 @@
"icon": "layers", "icon": "layers",
"description": "Override the global shadow with per-bar settings" "description": "Override the global shadow with per-bar settings"
}, },
{
"section": "barSpacing",
"label": "Spacing",
"tabIndex": 6,
"category": "Dank Bar",
"keywords": [
"bar",
"between",
"dank",
"edges",
"gap",
"gaps",
"margin",
"margins",
"padding",
"panel",
"screen",
"space",
"spacing",
"statusbar",
"taskbar",
"topbar"
],
"icon": "space_bar",
"description": "Space between the bar and screen edges"
},
{ {
"section": "trayIconTint", "section": "trayIconTint",
"label": "System Tray Icon Tint", "label": "System Tray Icon Tint",
@@ -2030,28 +2046,6 @@
"icon": "filter_b_and_w", "icon": "filter_b_and_w",
"description": "Controls how much original icon color is removed before applying tint" "description": "Controls how much original icon color is removed before applying tint"
}, },
{
"section": "barTransparency",
"label": "Transparency",
"tabIndex": 6,
"category": "Dank Bar",
"keywords": [
"alpha",
"background",
"bar",
"dank",
"opacity",
"panel",
"statusbar",
"taskbar",
"topbar",
"translucent",
"transparency",
"transparent"
],
"icon": "opacity",
"description": "Opacity of the bar background"
},
{ {
"section": "barWidgetOutline", "section": "barWidgetOutline",
"label": "Widget Outline", "label": "Widget Outline",
@@ -2124,6 +2118,25 @@
"icon": "wifi", "icon": "wifi",
"conditionKey": "dmsConnected" "conditionKey": "dmsConnected"
}, },
{
"section": "networkStatus",
"label": "Network Status",
"tabIndex": 7,
"category": "Network",
"keywords": [
"connection",
"connectivity",
"ethernet",
"internet",
"network",
"online",
"status",
"wi-fi",
"wifi",
"wireless"
],
"icon": "lan"
},
{ {
"section": "_tab_8", "section": "_tab_8",
"label": "Printers", "label": "Printers",
@@ -3792,7 +3805,6 @@
"tabIndex": 10, "tabIndex": 10,
"category": "Theme & Colors", "category": "Theme & Colors",
"keywords": [ "keywords": [
"alpha",
"appearance", "appearance",
"colors", "colors",
"controls", "controls",
@@ -3804,11 +3816,9 @@
"shadow", "shadow",
"style", "style",
"theme", "theme",
"translucent", "transparency"
"transparency",
"transparent"
], ],
"description": "Controls the transparency of the shadow" "description": "Controls the opacity of the shadow"
}, },
{ {
"section": "m3ElevationEnabled", "section": "m3ElevationEnabled",
@@ -3833,8 +3843,34 @@
"style", "style",
"theme" "theme"
], ],
"icon": "layers",
"description": "Material inspired shadows and elevation on modals, popouts, and dialogs" "description": "Material inspired shadows and elevation on modals, popouts, and dialogs"
}, },
{
"section": "popupTransparency",
"label": "Surface Opacity",
"tabIndex": 10,
"category": "Theme & Colors",
"keywords": [
"appearance",
"colors",
"controls",
"look",
"modal",
"modals",
"opacity",
"popouts",
"popup",
"scheme",
"shell",
"style",
"surface",
"surfaces",
"theme",
"transparency"
],
"description": "Controls opacity of shell surfaces, popouts, and modals"
},
{ {
"section": "syncModeWithPortal", "section": "syncModeWithPortal",
"label": "Sync Mode with Portal", "label": "Sync Mode with Portal",
@@ -3966,35 +4002,6 @@
"icon": "palette", "icon": "palette",
"description": "Select the palette algorithm used for wallpaper-based colors" "description": "Select the palette algorithm used for wallpaper-based colors"
}, },
{
"section": "popupTransparency",
"label": "Transparency",
"tabIndex": 10,
"category": "Theme & Colors",
"keywords": [
"alpha",
"appearance",
"colors",
"content",
"controls",
"layers",
"look",
"modal",
"modals",
"opacity",
"popouts",
"popup",
"scheme",
"style",
"surface",
"their",
"theme",
"translucent",
"transparency",
"transparent"
],
"description": "Controls opacity of all popouts, modals, and their content layers"
},
{ {
"section": "matugenTemplateVscode", "section": "matugenTemplateVscode",
"label": "VS Code", "label": "VS Code",
@@ -4563,6 +4570,27 @@
], ],
"description": "Automatically lock the screen when DMS starts" "description": "Automatically lock the screen when DMS starts"
}, },
{
"section": "lockBeforeSuspend",
"label": "Lock before suspend",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"automatic",
"automatically",
"before",
"lock",
"login",
"password",
"prepares",
"screen",
"security",
"sleep",
"suspend",
"system"
],
"description": "Automatically lock the screen when the system prepares to suspend"
},
{ {
"section": "lockScreenNotificationMode", "section": "lockScreenNotificationMode",
"label": "Notification Display", "label": "Notification Display",
@@ -5470,6 +5498,26 @@
], ],
"icon": "dashboard" "icon": "dashboard"
}, },
{
"section": "notificationBodyFontSize",
"label": "Body Font Size",
"tabIndex": 17,
"category": "Notifications",
"keywords": [
"alert",
"alerts",
"body",
"font",
"messages",
"notif",
"notification",
"notifications",
"size",
"text",
"toast"
],
"description": "Set the font size for notification body text (htmlBody)"
},
{ {
"section": "notificationCompactMode", "section": "notificationCompactMode",
"label": "Compact", "label": "Compact",
@@ -5867,22 +5915,19 @@
"keywords": [ "keywords": [
"alert", "alert",
"alerts", "alerts",
"appear", "font",
"choose",
"location",
"messages", "messages",
"notif", "notif",
"notification", "notification",
"notifications", "notifications",
"popup",
"popups", "popups",
"position", "size",
"screen", "summary",
"toast", "text",
"where" "toast"
], ],
"icon": "notifications", "icon": "notifications",
"description": "Choose where notification popups appear on screen" "description": "Set the font size for notification summary text"
}, },
{ {
"section": "notificationRules", "section": "notificationRules",
@@ -6032,6 +6077,26 @@
], ],
"description": "Hide notification content until expanded; popups show collapsed by default" "description": "Hide notification content until expanded; popups show collapsed by default"
}, },
{
"section": "notificationSummaryFontSize",
"label": "Summary Font Size",
"tabIndex": 17,
"category": "Notifications",
"keywords": [
"alert",
"alerts",
"font",
"messages",
"notif",
"notification",
"notifications",
"size",
"summary",
"text",
"toast"
],
"description": "Set the font size for notification summary text"
},
{ {
"section": "notificationDedupeEnabled", "section": "notificationDedupeEnabled",
"label": "Suppress Duplicate Notifications", "label": "Suppress Duplicate Notifications",
@@ -6054,6 +6119,32 @@
"toast" "toast"
] ]
}, },
{
"section": "notificationShowTimeoutBar",
"label": "Timeout Progress Bar",
"tabIndex": 17,
"category": "Notifications",
"keywords": [
"alerts",
"bar",
"countdown",
"drains",
"messages",
"notification",
"notifications",
"panel",
"popup",
"progress",
"show",
"statusbar",
"taskbar",
"timeout",
"timer",
"toast",
"topbar"
],
"description": "Show a bar that drains as the popup"
},
{ {
"section": "osdAlwaysShowValue", "section": "osdAlwaysShowValue",
"label": "Always Show Percentage", "label": "Always Show Percentage",
@@ -6697,27 +6788,6 @@
"icon": "schedule", "icon": "schedule",
"description": "Gradually fade the screen before locking with a configurable grace period" "description": "Gradually fade the screen before locking with a configurable grace period"
}, },
{
"section": "lockBeforeSuspend",
"label": "Lock before suspend",
"tabIndex": 21,
"category": "Power & Sleep",
"keywords": [
"automatically",
"before",
"energy",
"lock",
"power",
"prepares",
"screen",
"security",
"shutdown",
"sleep",
"suspend",
"system"
],
"description": "Automatically lock the screen when the system prepares to suspend"
},
{ {
"section": "fadeToLockGracePeriod", "section": "fadeToLockGracePeriod",
"label": "Lock fade grace period", "label": "Lock fade grace period",
@@ -7119,6 +7189,36 @@
], ],
"description": "Maximum number of entries that can be saved" "description": "Maximum number of entries that can be saved"
}, },
{
"section": "clipboardVisibleEntryActions",
"label": "Visible Entry Actions",
"tabIndex": 23,
"category": "System",
"keywords": [
"action",
"actions",
"appear",
"buttons",
"choose",
"clipboard",
"cliphist",
"copy",
"delete",
"density",
"edit",
"entries",
"entry",
"hide",
"history",
"linux",
"os",
"paste",
"pin",
"system",
"visible"
],
"description": "Choose which action buttons appear on clipboard entries"
},
{ {
"section": "_tab_24", "section": "_tab_24",
"label": "Displays", "label": "Displays",
@@ -7221,7 +7321,8 @@
"screen", "screen",
"widgets" "widgets"
], ],
"icon": "widgets" "icon": "widgets",
"conditionKey": "dmsConnected"
}, },
{ {
"section": "_tab_27", "section": "_tab_27",
@@ -8399,7 +8500,7 @@
"topbar", "topbar",
"window" "window"
], ],
"icon": "crop_square", "icon": "layers",
"description": "Use custom gaps instead of bar spacing", "description": "Use custom gaps instead of bar spacing",
"conditionKey": "isNiri" "conditionKey": "isNiri"
}, },
@@ -8820,5 +8921,95 @@
"icon": "select_window", "icon": "select_window",
"description": "Define compositor rules for window behavior", "description": "Define compositor rules for window behavior",
"conditionKey": "windowRulesCapable" "conditionKey": "windowRulesCapable"
},
{
"section": "_tab_39",
"label": "Ethernet",
"tabIndex": 39,
"category": "Network",
"keywords": [
"connectivity",
"ethernet",
"network",
"online"
],
"icon": "settings_ethernet"
},
{
"section": "networkEthernet",
"label": "Ethernet",
"tabIndex": 39,
"category": "Network",
"keywords": [
"adapters",
"connection",
"connectivity",
"ethernet",
"network",
"online",
"wired"
],
"icon": "settings_ethernet"
},
{
"section": "_tab_40",
"label": "WiFi",
"tabIndex": 40,
"category": "Network",
"keywords": [
"connectivity",
"network",
"online",
"wifi"
],
"icon": "wifi"
},
{
"section": "networkWifi",
"label": "WiFi",
"tabIndex": 40,
"category": "Network",
"keywords": [
"adapter",
"connectivity",
"network",
"online",
"radio",
"ssid",
"wi-fi",
"wifi",
"wireless"
],
"icon": "wifi"
},
{
"section": "_tab_41",
"label": "VPN",
"tabIndex": 41,
"category": "Network",
"keywords": [
"connectivity",
"network",
"online",
"vpn"
],
"icon": "vpn_key"
},
{
"section": "networkVpn",
"label": "VPN",
"tabIndex": 41,
"category": "Network",
"keywords": [
"connectivity",
"import",
"network",
"online",
"openvpn",
"profiles",
"vpn",
"wireguard"
],
"icon": "vpn_key"
} }
] ]