mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-28 14:05:21 -04:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aed731efb0 | |||
| cf0632c077 | |||
| e92da4a15f | |||
| 8abdff3220 | |||
| 584d57a8de | |||
| afb5e59c29 | |||
| d9525908f1 | |||
| 6093c37b41 | |||
| bb05cbb6c5 | |||
| 4d4af8f549 | |||
| 0b55fbcb15 | |||
| 7476a220b5 | |||
| aaff1ab61e | |||
| 39622eb62a | |||
| eea039f575 |
@@ -541,5 +541,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
blurCmd,
|
blurCmd,
|
||||||
trashCmd,
|
trashCmd,
|
||||||
systemCmd,
|
systemCmd,
|
||||||
|
switchUserCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var switchUserCmd = &cobra.Command{
|
||||||
|
Use: "switch-user [target]",
|
||||||
|
Short: "Switch to another active session on this seat",
|
||||||
|
Long: `Switch the active VT to another running session.
|
||||||
|
|
||||||
|
With no target, prints the list of switchable sessions. Pass a username or a
|
||||||
|
numeric session ID to switch directly. Requires the target to already be a
|
||||||
|
running session on the same seat (use the greeter for a fresh login).`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: runSwitchUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionInfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Seat string
|
||||||
|
TTY string
|
||||||
|
Type string
|
||||||
|
Class string
|
||||||
|
Active bool
|
||||||
|
State string
|
||||||
|
Current bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSwitchUser(cmd *cobra.Command, args []string) {
|
||||||
|
currentID := os.Getenv("XDG_SESSION_ID")
|
||||||
|
sessions, err := listSessions(currentID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switchable := make([]sessionInfo, 0, len(sessions))
|
||||||
|
for _, s := range sessions {
|
||||||
|
if s.Class != "user" || s.State == "closing" || s.Current {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switchable = append(switchable, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
if len(switchable) == 0 {
|
||||||
|
fmt.Println("No other active sessions on this seat.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printSessions(switchable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
target := args[0]
|
||||||
|
picked, err := pickSession(switchable, target)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
if len(switchable) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "No other active sessions on this seat. Only already-running sessions can be switched to.")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(os.Stderr, "\nSwitchable sessions:")
|
||||||
|
printSessions(switchable)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := activateSession(picked.ID); err != nil {
|
||||||
|
log.Fatalf("loginctl activate %s: %v", picked.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listSessions(currentID string) ([]sessionInfo, error) {
|
||||||
|
listOut, err := exec.Command("loginctl", "list-sessions", "--no-legend").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("loginctl list-sessions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(string(listOut)))
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if len(fields) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids = append(ids, fields[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]sessionInfo, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
s, err := showSession(id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Current = currentID != "" && s.ID == currentID
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
sort.SliceStable(out, func(i, j int) bool {
|
||||||
|
if out[i].Name != out[j].Name {
|
||||||
|
return out[i].Name < out[j].Name
|
||||||
|
}
|
||||||
|
return out[i].ID < out[j].ID
|
||||||
|
})
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func showSession(id string) (sessionInfo, error) {
|
||||||
|
out, err := exec.Command("loginctl", "show-session", id,
|
||||||
|
"-p", "Id", "-p", "Name", "-p", "Seat", "-p", "TTY",
|
||||||
|
"-p", "Type", "-p", "Class", "-p", "Active", "-p", "State").Output()
|
||||||
|
if err != nil {
|
||||||
|
return sessionInfo{}, err
|
||||||
|
}
|
||||||
|
fields := map[string]string{}
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
idx := strings.IndexByte(line, '=')
|
||||||
|
if idx <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields[line[:idx]] = line[idx+1:]
|
||||||
|
}
|
||||||
|
if fields["Id"] == "" {
|
||||||
|
return sessionInfo{}, fmt.Errorf("session %s: no Id", id)
|
||||||
|
}
|
||||||
|
return sessionInfo{
|
||||||
|
ID: fields["Id"],
|
||||||
|
Name: fields["Name"],
|
||||||
|
Seat: fields["Seat"],
|
||||||
|
TTY: fields["TTY"],
|
||||||
|
Type: fields["Type"],
|
||||||
|
Class: fields["Class"],
|
||||||
|
Active: fields["Active"] == "yes",
|
||||||
|
State: fields["State"],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickSession(sessions []sessionInfo, target string) (sessionInfo, error) {
|
||||||
|
for _, s := range sessions {
|
||||||
|
if s.ID == target {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches := make([]sessionInfo, 0, 2)
|
||||||
|
for _, s := range sessions {
|
||||||
|
if s.Name == target {
|
||||||
|
matches = append(matches, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(matches) == 1 {
|
||||||
|
return matches[0], nil
|
||||||
|
}
|
||||||
|
if len(matches) > 1 {
|
||||||
|
ids := make([]string, len(matches))
|
||||||
|
for i, m := range matches {
|
||||||
|
ids[i] = m.ID
|
||||||
|
}
|
||||||
|
return sessionInfo{}, fmt.Errorf("%s has multiple active sessions (%s); pass a session ID instead", target, strings.Join(ids, ", "))
|
||||||
|
}
|
||||||
|
return sessionInfo{}, fmt.Errorf("no switchable session matches %q", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func activateSession(id string) error {
|
||||||
|
return exec.Command("loginctl", "activate", id).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSessions(sessions []sessionInfo) {
|
||||||
|
fmt.Printf("%-6s %-12s %-8s %-8s %-8s\n", "ID", "USER", "TYPE", "SEAT", "TTY")
|
||||||
|
for _, s := range sessions {
|
||||||
|
tty := s.TTY
|
||||||
|
if tty == "" {
|
||||||
|
tty = "-"
|
||||||
|
}
|
||||||
|
seat := s.Seat
|
||||||
|
if seat == "" {
|
||||||
|
seat = "-"
|
||||||
|
}
|
||||||
|
fmt.Printf("%-6s %-12s %-8s %-8s %-8s\n", s.ID, s.Name, s.Type, seat, tty)
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -212,6 +212,52 @@ dms ipc call lock lock
|
|||||||
dms ipc call lock isLocked
|
dms ipc call lock isLocked
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Target: `sessions`
|
||||||
|
|
||||||
|
Logind session enumeration and seat-local session switching. Wraps `loginctl list-sessions` and `loginctl activate`. Only switches between sessions that are *already running* on the current seat — creating a fresh login as another user requires a multi-session greeter setup (greetd-flexiserver / GDM / LightDM) and is out of scope.
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
**`list`**
|
||||||
|
- Print every session DMS knows about as tab-separated columns: `sessionId\tusername\tseat\ttty\ttype\tcurrent-marker`
|
||||||
|
- Returns: Multi-line string. The current session is marked with `*current*`.
|
||||||
|
|
||||||
|
**`refresh`**
|
||||||
|
- Re-enumerate sessions in the background (the picker also refreshes itself on open)
|
||||||
|
- Returns: `"ok"`
|
||||||
|
|
||||||
|
**`open`**
|
||||||
|
- Refresh and open the Switch User picker on the focused screen
|
||||||
|
- Returns: `"ok"`
|
||||||
|
|
||||||
|
**`activate <sessionId>`**
|
||||||
|
- Activate a session by its numeric logind ID (the `Id=` field from `loginctl show-session`). Performs a VT switch
|
||||||
|
- Parameters: `sessionId` - Numeric session ID
|
||||||
|
- Returns: `"ok"` on dispatch, `"ERROR: missing session id"` if blank
|
||||||
|
- Note: Failures from `loginctl activate` surface through the `switchFailed` QML signal and a Log warning — the IPC call returns success once the spawn is queued, not after activation completes
|
||||||
|
|
||||||
|
**`switchTo <target>`**
|
||||||
|
- Switch to another session by username *or* session ID. The first non-current session matching the username wins; if there's no match, the call fails through the same logging path as `activate`
|
||||||
|
- Parameters: `target` - Username (e.g. `testuser2`) or numeric session ID
|
||||||
|
- Returns: `"ok"` on dispatch, `"ERROR: missing target (username or session id)"` if blank
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```bash
|
||||||
|
# Inspect what's switchable
|
||||||
|
dms ipc call sessions list
|
||||||
|
|
||||||
|
# Open the picker (useful for a keybind)
|
||||||
|
dms ipc call sessions open
|
||||||
|
|
||||||
|
# Jump straight to another logged-in user without the picker
|
||||||
|
dms ipc call sessions switchTo testuser2
|
||||||
|
|
||||||
|
# Or by session ID, when the user has multiple sessions
|
||||||
|
dms ipc call sessions activate 4
|
||||||
|
```
|
||||||
|
|
||||||
|
The dedicated `dms switch-user [target]` CLI command wraps the same behavior with a friendlier error path (it prints the switchable list when no target matches).
|
||||||
|
|
||||||
## Target: `inhibit`
|
## Target: `inhibit`
|
||||||
|
|
||||||
Idle inhibitor control to prevent automatic sleep/lock.
|
Idle inhibitor control to prevent automatic sleep/lock.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const DMS_ACTIONS = [
|
|||||||
{ id: "spawn dms ipc call mpris increment 5", label: "Player Volume Up (5%)" },
|
{ id: "spawn dms ipc call mpris increment 5", label: "Player Volume Up (5%)" },
|
||||||
{ id: "spawn dms ipc call mpris decrement 5", label: "Player Volume Down (5%)" },
|
{ id: "spawn dms ipc call mpris decrement 5", label: "Player Volume Down (5%)" },
|
||||||
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
|
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
|
||||||
{ id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" },
|
{ id: "spawn dms ipc call mic mute", label: "Microphone Mute Toggle" },
|
||||||
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
|
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
|
||||||
{ id: "spawn dms ipc call brightness increment 5 \"\"", label: "Brightness Up" },
|
{ id: "spawn dms ipc call brightness increment 5 \"\"", label: "Brightness Up" },
|
||||||
{ id: "spawn dms ipc call brightness increment 1 \"\"", label: "Brightness Up (1%)" },
|
{ id: "spawn dms ipc call brightness increment 1 \"\"", label: "Brightness Up (1%)" },
|
||||||
|
|||||||
@@ -1179,6 +1179,12 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLauncherRestoreMode() {
|
||||||
|
if (!SettingsData.rememberLastMode)
|
||||||
|
return "all";
|
||||||
|
return launcherLastMode || "all";
|
||||||
|
}
|
||||||
|
|
||||||
function setLauncherLastFileSearchType(type) {
|
function setLauncherLastFileSearchType(type) {
|
||||||
launcherLastFileSearchType = type;
|
launcherLastFileSearchType = type;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
|||||||
@@ -435,6 +435,7 @@ Singleton {
|
|||||||
property int appLauncherGridColumns: 4
|
property int appLauncherGridColumns: 4
|
||||||
property bool spotlightCloseNiriOverview: true
|
property bool spotlightCloseNiriOverview: true
|
||||||
property bool rememberLastQuery: false
|
property bool rememberLastQuery: false
|
||||||
|
property bool rememberLastMode: true
|
||||||
property var spotlightSectionViewModes: ({})
|
property var spotlightSectionViewModes: ({})
|
||||||
onSpotlightSectionViewModesChanged: saveSettings()
|
onSpotlightSectionViewModesChanged: saveSettings()
|
||||||
property var appDrawerSectionViewModes: ({})
|
property var appDrawerSectionViewModes: ({})
|
||||||
@@ -687,6 +688,7 @@ Singleton {
|
|||||||
property int notificationTimeoutNormal: 5000
|
property int notificationTimeoutNormal: 5000
|
||||||
property int notificationTimeoutCritical: 0
|
property int notificationTimeoutCritical: 0
|
||||||
property bool notificationCompactMode: false
|
property bool notificationCompactMode: false
|
||||||
|
property bool notificationDedupeEnabled: true
|
||||||
property int notificationPopupPosition: SettingsData.Position.Top
|
property int notificationPopupPosition: SettingsData.Position.Top
|
||||||
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
||||||
property int notificationCustomAnimationDuration: 400
|
property int notificationCustomAnimationDuration: 400
|
||||||
@@ -707,6 +709,7 @@ Singleton {
|
|||||||
property bool osdBrightnessEnabled: true
|
property bool osdBrightnessEnabled: true
|
||||||
property bool osdIdleInhibitorEnabled: true
|
property bool osdIdleInhibitorEnabled: true
|
||||||
property bool osdMicMuteEnabled: true
|
property bool osdMicMuteEnabled: true
|
||||||
|
property bool osdMicVolumeEnabled: true
|
||||||
property bool osdCapsLockEnabled: true
|
property bool osdCapsLockEnabled: true
|
||||||
property bool osdPowerProfileEnabled: true
|
property bool osdPowerProfileEnabled: true
|
||||||
property bool osdAudioOutputEnabled: true
|
property bool osdAudioOutputEnabled: true
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ var SPEC = {
|
|||||||
appLauncherGridColumns: { def: 4 },
|
appLauncherGridColumns: { def: 4 },
|
||||||
spotlightCloseNiriOverview: { def: true },
|
spotlightCloseNiriOverview: { def: true },
|
||||||
rememberLastQuery: { def: false },
|
rememberLastQuery: { def: false },
|
||||||
|
rememberLastMode: { def: true },
|
||||||
spotlightSectionViewModes: { def: {} },
|
spotlightSectionViewModes: { def: {} },
|
||||||
appDrawerSectionViewModes: { def: {} },
|
appDrawerSectionViewModes: { def: {} },
|
||||||
niriOverviewOverlayEnabled: { def: true },
|
niriOverviewOverlayEnabled: { def: true },
|
||||||
@@ -398,6 +399,7 @@ var SPEC = {
|
|||||||
notificationTimeoutNormal: { def: 5000 },
|
notificationTimeoutNormal: { def: 5000 },
|
||||||
notificationTimeoutCritical: { def: 0 },
|
notificationTimeoutCritical: { def: 0 },
|
||||||
notificationCompactMode: { def: false },
|
notificationCompactMode: { def: false },
|
||||||
|
notificationDedupeEnabled: { def: true },
|
||||||
notificationPopupPosition: { def: 0 },
|
notificationPopupPosition: { def: 0 },
|
||||||
notificationAnimationSpeed: { def: 1 },
|
notificationAnimationSpeed: { def: 1 },
|
||||||
notificationCustomAnimationDuration: { def: 400 },
|
notificationCustomAnimationDuration: { def: 400 },
|
||||||
|
|||||||
+131
-4
@@ -30,6 +30,7 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
readonly property var log: Log.scoped("DMSShell")
|
readonly property var log: Log.scoped("DMSShell")
|
||||||
|
readonly property var _sessionsServiceRef: SessionsService
|
||||||
|
|
||||||
property bool osdSurfacesLoaded: true
|
property bool osdSurfacesLoaded: true
|
||||||
property int pendingOsdResumeReloads: 0
|
property int pendingOsdResumeReloads: 0
|
||||||
@@ -63,15 +64,27 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool wallpaperSurfacesLoaded: true
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: blurredWallpaperBackgroundLoader
|
id: blurredWallpaperBackgroundLoader
|
||||||
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
|
active: root.wallpaperSurfacesLoaded && SettingsData.blurredWallpaperLayer && CompositorService.isNiri
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
|
|
||||||
sourceComponent: BlurredWallpaperBackground {}
|
sourceComponent: BlurredWallpaperBackground {}
|
||||||
}
|
}
|
||||||
|
|
||||||
WallpaperBackground {}
|
DeferredAction {
|
||||||
|
id: wallpaperSurfaceReloadAction
|
||||||
|
onTriggered: root.wallpaperSurfacesLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: wallpaperBackgroundLoader
|
||||||
|
active: root.wallpaperSurfacesLoaded
|
||||||
|
asynchronous: false
|
||||||
|
sourceComponent: WallpaperBackground {}
|
||||||
|
}
|
||||||
|
|
||||||
DesktopWidgetLayer {}
|
DesktopWidgetLayer {}
|
||||||
|
|
||||||
@@ -168,6 +181,8 @@ Item {
|
|||||||
property bool barSurfacesLoaded: true
|
property bool barSurfacesLoaded: true
|
||||||
|
|
||||||
function recreateBarSurfaces() {
|
function recreateBarSurfaces() {
|
||||||
|
log.info("Recreating bar surfaces, screens:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => s.name).join(","));
|
||||||
if (barSurfacesLoaded)
|
if (barSurfacesLoaded)
|
||||||
barSurfacesLoaded = false;
|
barSurfacesLoaded = false;
|
||||||
barSurfaceReloadAction.schedule();
|
barSurfaceReloadAction.schedule();
|
||||||
@@ -217,7 +232,18 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Frame {}
|
property bool frameSurfacesLoaded: true
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: root.frameSurfacesLoaded
|
||||||
|
asynchronous: false
|
||||||
|
sourceComponent: Frame {}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeferredAction {
|
||||||
|
id: frameSurfaceReloadAction
|
||||||
|
onTriggered: root.frameSurfacesLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: dankBarRepeater
|
id: dankBarRepeater
|
||||||
@@ -301,6 +327,81 @@ Item {
|
|||||||
onTriggered: root.osdSurfacesLoaded = true
|
onTriggered: root.osdSurfacesLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool hadRealScreen: true
|
||||||
|
|
||||||
|
function _hasRealScreen() {
|
||||||
|
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||||
|
if (Quickshell.screens[i].name.length > 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerSurfaceRecovery(source) {
|
||||||
|
log.info("Surface recovery triggered by:", source,
|
||||||
|
"screens:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => s.name).join(","),
|
||||||
|
"barLoaded:", root.barSurfacesLoaded,
|
||||||
|
"frameLoaded:", root.frameSurfacesLoaded,
|
||||||
|
"dockEnabled:", root.dockEnabled);
|
||||||
|
surfaceResumeRecoveryTimer.pass = 0;
|
||||||
|
surfaceResumeRecoveryTimer.interval = 800;
|
||||||
|
surfaceResumeRecoveryTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Quickshell
|
||||||
|
function onScreensChanged() {
|
||||||
|
const hasReal = root._hasRealScreen();
|
||||||
|
log.info("Screens changed:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => "'" + s.name + "'").join(","),
|
||||||
|
"hasReal:", hasReal, "hadReal:", root.hadRealScreen);
|
||||||
|
if (!root.hadRealScreen && hasReal) {
|
||||||
|
log.info("Real screen reappeared after placeholder state, triggering surface recovery");
|
||||||
|
root.triggerSurfaceRecovery("screen-reconnect");
|
||||||
|
}
|
||||||
|
root.hadRealScreen = hasReal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: surfaceResumeRecoveryTimer
|
||||||
|
interval: 800
|
||||||
|
repeat: false
|
||||||
|
property int pass: 0
|
||||||
|
onTriggered: {
|
||||||
|
pass++;
|
||||||
|
log.info("Surface recovery pass", pass,
|
||||||
|
"screens:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => s.name).join(","));
|
||||||
|
|
||||||
|
root.recreateBarSurfaces();
|
||||||
|
|
||||||
|
if (root.frameSurfacesLoaded) {
|
||||||
|
root.frameSurfacesLoaded = false;
|
||||||
|
frameSurfaceReloadAction.schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.wallpaperSurfacesLoaded) {
|
||||||
|
root.wallpaperSurfacesLoaded = false;
|
||||||
|
wallpaperSurfaceReloadAction.schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
root.dockEnabled = false;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
root.dockEnabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pass < 2) {
|
||||||
|
interval = 2000;
|
||||||
|
restart();
|
||||||
|
} else {
|
||||||
|
pass = 0;
|
||||||
|
interval = 800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
dockRecreateDebounce.start();
|
dockRecreateDebounce.start();
|
||||||
// Force PolkitService singleton to initialize
|
// Force PolkitService singleton to initialize
|
||||||
@@ -887,9 +988,17 @@ Item {
|
|||||||
target: SessionService
|
target: SessionService
|
||||||
|
|
||||||
function onSessionResumed() {
|
function onSessionResumed() {
|
||||||
|
log.info("Session resumed: screens:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => s.name).join(","),
|
||||||
|
"barLoaded:", root.barSurfacesLoaded,
|
||||||
|
"frameLoaded:", root.frameSurfacesLoaded,
|
||||||
|
"dockEnabled:", root.dockEnabled);
|
||||||
|
|
||||||
root.pendingOsdResumeReloads = 2;
|
root.pendingOsdResumeReloads = 2;
|
||||||
osdResumeRecreateTimer.interval = 400;
|
osdResumeRecreateTimer.interval = 400;
|
||||||
osdResumeRecreateTimer.restart();
|
osdResumeRecreateTimer.restart();
|
||||||
|
|
||||||
|
root.triggerSurfaceRecovery("sessionResumed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1038,12 +1147,30 @@ Item {
|
|||||||
lock.activate();
|
lock.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSwitchUserRequested: {
|
||||||
|
switchUserModalLoader.active = true;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (switchUserModalLoader.item)
|
||||||
|
switchUserModalLoader.item.showFromPowerMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
PopoutService.powerMenuModal = powerMenuModal;
|
PopoutService.powerMenuModal = powerMenuModal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: switchUserModalLoader
|
||||||
|
|
||||||
|
active: false
|
||||||
|
|
||||||
|
SwitchUserModal {
|
||||||
|
id: switchUserModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
id: hyprKeybindsModalLoader
|
id: hyprKeybindsModalLoader
|
||||||
|
|
||||||
@@ -1114,7 +1241,7 @@ Item {
|
|||||||
Variants {
|
Variants {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
delegate: MicMuteOSD {
|
delegate: MicVolumeOSD {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1794,6 +1794,36 @@ Item {
|
|||||||
target: "outputs"
|
target: "outputs"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "mic"
|
||||||
|
|
||||||
|
function setvolume(percentage: string): string {
|
||||||
|
return AudioService.setMicVolume(parseInt(percentage));
|
||||||
|
}
|
||||||
|
|
||||||
|
function increment(step: string): string {
|
||||||
|
return AudioService.incrementMicVolume(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrement(step: string): string {
|
||||||
|
return AudioService.decrementMicVolume(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mute(): string {
|
||||||
|
return AudioService.toggleMicMute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
if (!AudioService.source || !AudioService.source.audio) {
|
||||||
|
return "No audio source available";
|
||||||
|
}
|
||||||
|
|
||||||
|
const volume = Math.round(AudioService.source.audio.volume * 100);
|
||||||
|
const muteStatus = AudioService.source.audio.muted ? " (muted)" : "";
|
||||||
|
return `Microphone: ${volume}%${muteStatus}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function findTrayItem(itemId: string): var {
|
function findTrayItem(itemId: string): var {
|
||||||
if (!itemId)
|
if (!itemId)
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ Item {
|
|||||||
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
||||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||||
|
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +205,7 @@ Item {
|
|||||||
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
||||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||||
|
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,519 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var modal
|
||||||
|
property var keyController: null
|
||||||
|
|
||||||
|
property var entry: null
|
||||||
|
property string editorText: ""
|
||||||
|
|
||||||
|
function decodeEntryData(data) {
|
||||||
|
if (!data) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof data !== "string") {
|
||||||
|
return String(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = data.replace(/\s+/g, "");
|
||||||
|
if (sanitized.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chars = new Array(sanitized.length);
|
||||||
|
for (let i = 0; i < sanitized.length; i++) {
|
||||||
|
chars[i] = sanitized.charAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = null;
|
||||||
|
if (typeof Qt !== "undefined" && typeof Qt.atob === "function") {
|
||||||
|
buffer = Qt.atob(chars);
|
||||||
|
} else if (typeof atob === "function") {
|
||||||
|
const binary = atob(sanitized);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
buffer = bytes.buffer;
|
||||||
|
}
|
||||||
|
if (!buffer || buffer.byteLength === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(escape(binary));
|
||||||
|
} catch (e) {
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEntry(newEntry) {
|
||||||
|
entry = newEntry;
|
||||||
|
editorText = newEntry?.text ?? newEntry?.preview ?? "";
|
||||||
|
if (editField) {
|
||||||
|
editField.text = editorText;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (editField) {
|
||||||
|
editField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newEntry || newEntry.isImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedId = newEntry.id;
|
||||||
|
DMSService.sendRequest("clipboard.getEntry", {
|
||||||
|
"id": requestedId
|
||||||
|
}, function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!root.entry || root.entry.id !== requestedId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = response.result;
|
||||||
|
let fullText = "";
|
||||||
|
if (result?.data) {
|
||||||
|
fullText = root.decodeEntryData(result.data);
|
||||||
|
} else {
|
||||||
|
fullText = result?.preview ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fullText || fullText.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.editorText = fullText;
|
||||||
|
if (editField) {
|
||||||
|
editField.text = fullText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEntry(action) {
|
||||||
|
const saveAction = action ?? "history";
|
||||||
|
DMSService.sendRequest("clipboard.copy", {
|
||||||
|
"text": root.editorText
|
||||||
|
}, function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
ToastService.showError(I18n.tr("Failed to update clipboard"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveAction === "history") {
|
||||||
|
modal.mode = "history";
|
||||||
|
Qt.callLater(function () {
|
||||||
|
ClipboardService.reset();
|
||||||
|
ClipboardService.refresh();
|
||||||
|
if (keyController) {
|
||||||
|
keyController.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveAction === "close") {
|
||||||
|
modal.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveAction === "paste") {
|
||||||
|
ClipboardService.pasteClipboard(modal.hide);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionSaveMenu() {
|
||||||
|
saveMenu.width = Math.max(saveMenuColumn.implicitWidth + saveMenu.padding * 2, saveButton.width);
|
||||||
|
const pos = saveButton.mapToItem(Overlay.overlay, 0, 0);
|
||||||
|
const popupW = saveMenu.width;
|
||||||
|
const popupH = saveMenu.height;
|
||||||
|
const overlayW = Overlay.overlay.width;
|
||||||
|
const overlayH = Overlay.overlay.height;
|
||||||
|
|
||||||
|
let x = pos.x + (saveButton.width - popupW) / 2;
|
||||||
|
let y = pos.y + saveButton.height + 4;
|
||||||
|
if (y + popupH > overlayH) {
|
||||||
|
y = pos.y - popupH - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Math.max(8, Math.min(x, overlayW - popupW - 8));
|
||||||
|
y = Math.max(8, y);
|
||||||
|
|
||||||
|
saveMenu.x = x;
|
||||||
|
saveMenu.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSaveMenu() {
|
||||||
|
if (saveMenu.visible) {
|
||||||
|
saveMenu.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveMenu.open();
|
||||||
|
positionSaveMenu();
|
||||||
|
Qt.callLater(positionSaveMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequences: ["Escape"]
|
||||||
|
enabled: modal.mode === "editor"
|
||||||
|
onActivated: modal.mode = "history"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: editorHeader
|
||||||
|
width: parent.width
|
||||||
|
height: ClipboardConstants.headerHeight
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "arrow_back"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: modal.mode = "history"
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Edit Clipboard")
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: modal.mode = "history"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
id: editFieldContainer
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(Theme.fontSizeMedium * 8, parent.height - editorHeader.height - editorActions.height - Theme.spacingM * 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
border.color: editField.activeFocus ? Theme.primary : Theme.outlineMedium
|
||||||
|
border.width: editField.activeFocus ? 2 : 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: editIcon
|
||||||
|
name: "edit"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: editField.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: editScroll
|
||||||
|
anchors.left: editIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
clip: true
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: editField.height
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: editField
|
||||||
|
width: editScroll.width
|
||||||
|
height: Math.max(editScroll.height, contentHeight)
|
||||||
|
text: root.editorText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
selectByMouse: true
|
||||||
|
onTextChanged: root.editorText = text
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
const hasCtrl = (event.modifiers & Qt.ControlModifier) !== 0;
|
||||||
|
const hasShift = (event.modifiers & Qt.ShiftModifier) !== 0;
|
||||||
|
|
||||||
|
if (hasCtrl && event.key === Qt.Key_S) {
|
||||||
|
root.saveEntry(hasShift ? "close" : "history");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasCtrl && hasShift && event.key === Qt.Key_V) {
|
||||||
|
root.saveEntry("paste");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Edit clipboard text")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.outlineButton
|
||||||
|
anchors.left: editScroll.left
|
||||||
|
anchors.right: editScroll.right
|
||||||
|
anchors.top: editScroll.top
|
||||||
|
anchors.bottom: editScroll.bottom
|
||||||
|
visible: editField.text.length === 0 && !editField.activeFocus
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: editorActions
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: buttonSpacer
|
||||||
|
width: Math.max(0, parent.width - cancelButton.width - saveButton.width - Theme.spacingS)
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: cancelButton
|
||||||
|
text: I18n.tr("Cancel")
|
||||||
|
backgroundColor: Theme.surfaceContainerHigh
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
onClicked: modal.mode = "history"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: saveButton
|
||||||
|
|
||||||
|
readonly property int buttonHeight: cancelButton.buttonHeight
|
||||||
|
readonly property int arrowWidth: Theme.iconSizeLarge
|
||||||
|
|
||||||
|
width: cancelButton.width
|
||||||
|
height: buttonHeight
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: saveMainArea
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: saveArrowArea.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.onPrimary
|
||||||
|
anchors.centerIn: saveMainArea
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: saveArrowArea
|
||||||
|
width: saveButton.arrowWidth
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 1
|
||||||
|
height: parent.height - cancelButton.horizontalPadding
|
||||||
|
color: Theme.withAlpha(Theme.onPrimary, 0.2)
|
||||||
|
anchors.right: saveArrowArea.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: saveMenu.visible ? "expand_less" : "expand_more"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.onPrimary
|
||||||
|
anchors.centerIn: saveArrowArea
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
z: 1
|
||||||
|
anchors.fill: saveMainArea
|
||||||
|
stateColor: Theme.onPrimary
|
||||||
|
onClicked: root.saveEntry("history")
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
z: 1
|
||||||
|
anchors.fill: saveArrowArea
|
||||||
|
stateColor: Theme.onPrimary
|
||||||
|
onClicked: root.toggleSaveMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: saveMenu
|
||||||
|
parent: Overlay.overlay
|
||||||
|
padding: Theme.spacingM
|
||||||
|
modal: true
|
||||||
|
dim: false
|
||||||
|
focus: true
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
|
background: StyledRect {
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Column {
|
||||||
|
id: saveMenuColumn
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: saveMenuRow.implicitWidth + Theme.spacingS * 2
|
||||||
|
implicitHeight: saveMenuRow.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: saveMenuSaveArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveMenuRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "save"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: saveMenuSaveArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
saveMenu.close();
|
||||||
|
root.saveEntry("history");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: saveMenuCloseRow.implicitWidth + Theme.spacingS * 2
|
||||||
|
implicitHeight: saveMenuCloseRow.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: saveMenuCloseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveMenuCloseRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "close"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save and close")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: saveMenuCloseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
saveMenu.close();
|
||||||
|
root.saveEntry("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: saveMenuPasteRow.implicitWidth + Theme.spacingS * 2
|
||||||
|
implicitHeight: saveMenuPasteRow.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: saveMenuPasteArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
opacity: modal.wtypeAvailable ? 1 : 0.5
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveMenuPasteRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "content_paste"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save and paste")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: saveMenuPasteArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: modal.wtypeAvailable
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: {
|
||||||
|
saveMenu.close();
|
||||||
|
root.saveEntry("paste");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ Rectangle {
|
|||||||
signal deleteRequested
|
signal deleteRequested
|
||||||
signal pinRequested
|
signal pinRequested
|
||||||
signal unpinRequested
|
signal unpinRequested
|
||||||
|
signal editRequested
|
||||||
|
|
||||||
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) : ""
|
||||||
@@ -70,6 +71,20 @@ Rectangle {
|
|||||||
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "edit"
|
||||||
|
iconSize: Theme.iconSize - 6
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (entryType === "image") {
|
||||||
|
// TODO - forward to editing software
|
||||||
|
} else {
|
||||||
|
editRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
iconName: "close"
|
iconName: "close"
|
||||||
iconSize: Theme.iconSize - 6
|
iconSize: Theme.iconSize - 6
|
||||||
@@ -142,8 +157,11 @@ Rectangle {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
anchors.fill: parent
|
anchors.left: parent.left
|
||||||
anchors.rightMargin: 80
|
anchors.right: actionButtons.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: mouse => {
|
onPressed: mouse => {
|
||||||
|
|||||||
@@ -43,6 +43,18 @@ DankModal {
|
|||||||
service: ClipboardService
|
service: ClipboardService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property string mode: "history"
|
||||||
|
onModeChanged: {
|
||||||
|
if (mode !== "history") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (contentLoader.item?.searchField) {
|
||||||
|
contentLoader.item.searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
ClipboardService.updateFilteredModel();
|
ClipboardService.updateFilteredModel();
|
||||||
}
|
}
|
||||||
@@ -61,6 +73,7 @@ DankModal {
|
|||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
open();
|
open();
|
||||||
|
mode = "history";
|
||||||
activeImageLoads = 0;
|
activeImageLoads = 0;
|
||||||
shouldHaveFocus = true;
|
shouldHaveFocus = true;
|
||||||
ClipboardService.reset();
|
ClipboardService.reset();
|
||||||
@@ -130,6 +143,21 @@ DankModal {
|
|||||||
return ClipboardService.getEntryType(entry);
|
return ClipboardService.getEntryType(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editEntry(entry) {
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entry.isImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const editor = contentLoader.item?.editorView;
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.setEntry(entry);
|
||||||
|
mode = "editor";
|
||||||
|
}
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
modalWidth: ClipboardConstants.modalWidth
|
modalWidth: ClipboardConstants.modalWidth
|
||||||
modalHeight: ClipboardConstants.modalHeight
|
modalHeight: ClipboardConstants.modalHeight
|
||||||
@@ -138,6 +166,7 @@ DankModal {
|
|||||||
borderColor: Theme.outlineMedium
|
borderColor: Theme.outlineMedium
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
enableShadow: true
|
enableShadow: true
|
||||||
|
closeOnEscapeKey: mode !== "editor"
|
||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: hide()
|
||||||
modalFocusScope.Keys.onPressed: function (event) {
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
keyboardController.handleKey(event);
|
keyboardController.handleKey(event);
|
||||||
@@ -174,9 +203,109 @@ DankModal {
|
|||||||
property var confirmDialog: clearConfirmDialog
|
property var confirmDialog: clearConfirmDialog
|
||||||
|
|
||||||
clipboardContent: Component {
|
clipboardContent: Component {
|
||||||
ClipboardContent {
|
Item {
|
||||||
modal: clipboardHistoryModal
|
id: viewContainer
|
||||||
clearConfirmDialog: clipboardHistoryModal.confirmDialog
|
|
||||||
|
property alias editorView: editorView
|
||||||
|
property alias searchField: historyContent.searchField
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: historyView
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
opacity: 1
|
||||||
|
scale: 1
|
||||||
|
visible: opacity > 0.01
|
||||||
|
enabled: clipboardHistoryModal.mode === "history"
|
||||||
|
|
||||||
|
ClipboardContent {
|
||||||
|
id: historyContent
|
||||||
|
anchors.fill: parent
|
||||||
|
modal: clipboardHistoryModal
|
||||||
|
clearConfirmDialog: clipboardHistoryModal.confirmDialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipboardEditor {
|
||||||
|
id: editorView
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
opacity: 0
|
||||||
|
scale: 0.98
|
||||||
|
visible: opacity > 0.01
|
||||||
|
enabled: clipboardHistoryModal.mode === "editor"
|
||||||
|
focus: clipboardHistoryModal.mode === "editor"
|
||||||
|
modal: clipboardHistoryModal
|
||||||
|
keyController: keyboardController
|
||||||
|
}
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "history"
|
||||||
|
when: clipboardHistoryModal.mode === "history"
|
||||||
|
PropertyChanges {
|
||||||
|
target: historyView
|
||||||
|
opacity: 1
|
||||||
|
scale: 1
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: editorView
|
||||||
|
opacity: 0
|
||||||
|
scale: 0.98
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "editor"
|
||||||
|
when: clipboardHistoryModal.mode === "editor"
|
||||||
|
PropertyChanges {
|
||||||
|
target: historyView
|
||||||
|
opacity: 0
|
||||||
|
scale: 0.98
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: editorView
|
||||||
|
opacity: 1
|
||||||
|
scale: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
transitions: [
|
||||||
|
Transition {
|
||||||
|
from: "history"
|
||||||
|
to: "editor"
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
property: "scale"
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "editor"
|
||||||
|
to: "history"
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
property: "scale"
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,24 @@ QtObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editSelected() {
|
||||||
|
const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
|
||||||
|
if (!entries || entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = ClipboardService.selectedIndex >= 0 && ClipboardService.selectedIndex < entries.length ? ClipboardService.selectedIndex : 0;
|
||||||
|
modal.editEntry(entries[index]);
|
||||||
|
}
|
||||||
|
|
||||||
function handleKey(event) {
|
function handleKey(event) {
|
||||||
|
if (modal.mode === "editor") {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
modal.mode = "history";
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case Qt.Key_Escape:
|
case Qt.Key_Escape:
|
||||||
if (ClipboardService.keyboardNavigationActive) {
|
if (ClipboardService.keyboardNavigationActive) {
|
||||||
@@ -152,6 +169,10 @@ QtObject {
|
|||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case Qt.Key_E:
|
||||||
|
editSelected();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Rectangle {
|
|||||||
readonly property string hintsText: {
|
readonly property string hintsText: {
|
||||||
if (!wtypeAvailable)
|
if (!wtypeAvailable)
|
||||||
return I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Del: Clear All • Esc: Close");
|
return I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Del: Clear All • Esc: Close");
|
||||||
return enterToPaste ? I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close");
|
return enterToPaste ? I18n.tr("Ctrl+Tab: Switch Tabs • Ctrl+S: Pin/Unpin • Shift+Enter: Copy • Shift+Del: Clear All • F10: Help • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Ctrl+Tab: Switch Tabs • Ctrl+S: Pin/Unpin • Shift+Enter: Paste • Shift+Del: Clear All • F10: Help • Esc: Close");
|
||||||
}
|
}
|
||||||
|
|
||||||
height: ClipboardConstants.keyboardHintsHeight
|
height: ClipboardConstants.keyboardHintsHeight
|
||||||
@@ -22,13 +22,17 @@ Rectangle {
|
|||||||
z: 100
|
z: 100
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
|
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Ctrl+C: Copy • Del: Delete • Ctrl+E: Edit • Ctrl+S: Pin/Unpin • F10: Help", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • Ctrl+E: Edit • Ctrl+S: Pin/Unpin • F10: Help")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +40,9 @@ Rectangle {
|
|||||||
text: keyboardHints.hintsText
|
text: keyboardHints.hintsText
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ Item {
|
|||||||
|
|
||||||
signal itemExecuted
|
signal itemExecuted
|
||||||
signal searchCompleted
|
signal searchCompleted
|
||||||
signal modeChanged(string mode)
|
signal modeChanged(string mode, bool userInitiated)
|
||||||
signal queryChanged(string query)
|
signal queryChanged(string query)
|
||||||
signal viewModeChanged(string sectionId, string mode)
|
signal viewModeChanged(string sectionId, string mode)
|
||||||
signal searchQueryRequested(string query)
|
signal searchQueryRequested(string query)
|
||||||
@@ -440,7 +440,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMode(mode, isAutoSwitch, fileTypeOverride) {
|
function setMode(mode, isAutoSwitch, fileTypeOverride, notPersist) {
|
||||||
if (searchMode === mode) {
|
if (searchMode === mode) {
|
||||||
if (mode === "files" && fileTypeOverride !== undefined && fileSearchType !== fileTypeOverride) {
|
if (mode === "files" && fileTypeOverride !== undefined && fileSearchType !== fileTypeOverride) {
|
||||||
fileSearchType = fileTypeOverride;
|
fileSearchType = fileTypeOverride;
|
||||||
@@ -458,7 +458,7 @@ Item {
|
|||||||
if (mode === "files") {
|
if (mode === "files") {
|
||||||
fileSearchType = fileTypeOverride !== undefined ? fileTypeOverride : (SessionData.launcherLastFileSearchType || "all");
|
fileSearchType = fileTypeOverride !== undefined ? fileTypeOverride : (SessionData.launcherLastFileSearchType || "all");
|
||||||
}
|
}
|
||||||
modeChanged(mode);
|
modeChanged(mode, !isAutoSwitch && notPersist !== true);
|
||||||
performSearch();
|
performSearch();
|
||||||
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
|
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
|
||||||
if (mode === "files" || filesInAll) {
|
if (mode === "files" || filesInAll) {
|
||||||
@@ -471,7 +471,7 @@ Item {
|
|||||||
return;
|
return;
|
||||||
autoSwitchedToFiles = false;
|
autoSwitchedToFiles = false;
|
||||||
searchMode = previousSearchMode;
|
searchMode = previousSearchMode;
|
||||||
modeChanged(previousSearchMode);
|
modeChanged(previousSearchMode, false);
|
||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1897,7 +1897,7 @@ Item {
|
|||||||
if (browseTrigger && browseTrigger.length > 0) {
|
if (browseTrigger && browseTrigger.length > 0) {
|
||||||
searchQueryRequested(browseTrigger);
|
searchQueryRequested(browseTrigger);
|
||||||
} else {
|
} else {
|
||||||
setMode("plugins");
|
setMode("plugins", false, undefined, true);
|
||||||
pluginFilter = browsePluginId;
|
pluginFilter = browsePluginId;
|
||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ Item {
|
|||||||
spotlightContent.searchField.text = query;
|
spotlightContent.searchField.text = query;
|
||||||
}
|
}
|
||||||
if (spotlightContent.controller) {
|
if (spotlightContent.controller) {
|
||||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
var targetMode = mode || SessionData.getLauncherRestoreMode();
|
||||||
spotlightContent.controller.searchMode = targetMode;
|
spotlightContent.controller.searchMode = targetMode;
|
||||||
spotlightContent.controller.activePluginId = "";
|
spotlightContent.controller.activePluginId = "";
|
||||||
spotlightContent.controller.activePluginName = "";
|
spotlightContent.controller.activePluginName = "";
|
||||||
@@ -539,8 +539,8 @@ Item {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: spotlightContent?.controller ?? null
|
target: spotlightContent?.controller ?? null
|
||||||
function onModeChanged(mode) {
|
function onModeChanged(mode, userInitiated) {
|
||||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
if (!userInitiated || !SettingsData.rememberLastMode)
|
||||||
return;
|
return;
|
||||||
SessionData.setLauncherLastMode(mode);
|
SessionData.setLauncherLastMode(mode);
|
||||||
}
|
}
|
||||||
@@ -928,8 +928,12 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
Keys.onEscapePressed: event => {
|
||||||
root.hide();
|
root.spotlightContent?.activeContextMenu?.handleKey(event);
|
||||||
|
if (!event.accepted)
|
||||||
|
root.hide();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ Item {
|
|||||||
spotlightContent.closeTransientUi?.();
|
spotlightContent.closeTransientUi?.();
|
||||||
|
|
||||||
const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : "");
|
const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : "");
|
||||||
const targetMode = mode || SessionData.launcherLastMode || "all";
|
const targetMode = mode || SessionData.getLauncherRestoreMode();
|
||||||
|
|
||||||
if (spotlightContent.searchField) {
|
if (spotlightContent.searchField) {
|
||||||
spotlightContent.searchField.text = targetQuery;
|
spotlightContent.searchField.text = targetQuery;
|
||||||
@@ -489,8 +489,12 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
Keys.onEscapePressed: event => {
|
||||||
root.hide();
|
root.spotlightContent?.activeContextMenu?.handleKey(event);
|
||||||
|
if (!event.accepted)
|
||||||
|
root.hide();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ Item {
|
|||||||
spotlightContent.searchField.text = targetQuery;
|
spotlightContent.searchField.text = targetQuery;
|
||||||
}
|
}
|
||||||
if (spotlightContent.controller) {
|
if (spotlightContent.controller) {
|
||||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
var targetMode = mode || SessionData.getLauncherRestoreMode();
|
||||||
spotlightContent.controller.searchMode = targetMode;
|
spotlightContent.controller.searchMode = targetMode;
|
||||||
spotlightContent.controller.activePluginId = "";
|
spotlightContent.controller.activePluginId = "";
|
||||||
spotlightContent.controller.activePluginName = "";
|
spotlightContent.controller.activePluginName = "";
|
||||||
@@ -260,8 +260,8 @@ Item {
|
|||||||
Connections {
|
Connections {
|
||||||
target: spotlightContent?.controller ?? null
|
target: spotlightContent?.controller ?? null
|
||||||
|
|
||||||
function onModeChanged(mode) {
|
function onModeChanged(mode, userInitiated) {
|
||||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
if (!userInitiated || !SettingsData.rememberLastMode || (mode !== "all" && mode !== "apps"))
|
||||||
return;
|
return;
|
||||||
SessionData.setLauncherLastMode(mode);
|
SessionData.setLauncherLastMode(mode);
|
||||||
}
|
}
|
||||||
@@ -536,8 +536,12 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
Keys.onEscapePressed: event => {
|
||||||
root.hide();
|
root.spotlightContent?.activeContextMenu?.handleKey(event);
|
||||||
|
if (!event.accepted)
|
||||||
|
root.hide();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ FocusScope {
|
|||||||
property alias controller: controller
|
property alias controller: controller
|
||||||
property alias resultsList: resultsList
|
property alias resultsList: resultsList
|
||||||
property alias actionPanel: actionPanel
|
property alias actionPanel: actionPanel
|
||||||
|
readonly property alias activeContextMenu: contextMenu
|
||||||
|
|
||||||
property bool editMode: false
|
property bool editMode: false
|
||||||
property var editingApp: null
|
property var editingApp: null
|
||||||
|
|||||||
@@ -340,6 +340,31 @@ Item {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKey(event) {
|
||||||
|
if (!openState)
|
||||||
|
return;
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Down:
|
||||||
|
selectNext();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Up:
|
||||||
|
selectPrevious();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
activateSelected();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Left:
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
hide();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectNext() {
|
function selectNext() {
|
||||||
if (visibleItemCount > 0) {
|
if (visibleItemCount > 0) {
|
||||||
keyboardNavigation = true;
|
keyboardNavigation = true;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ FocusScope {
|
|||||||
property var parentModal: null
|
property var parentModal: null
|
||||||
property alias searchField: searchInput
|
property alias searchField: searchInput
|
||||||
property alias controller: searchController
|
property alias controller: searchController
|
||||||
|
readonly property alias activeContextMenu: contextMenu
|
||||||
|
|
||||||
readonly property bool _hasQuery: searchInput.text.length > 0
|
readonly property bool _hasQuery: searchInput.text.length > 0
|
||||||
readonly property real _searchBarH: 56
|
readonly property real _searchBarH: 56
|
||||||
@@ -239,8 +240,8 @@ FocusScope {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: searchController
|
target: searchController
|
||||||
function onModeChanged(mode) {
|
function onModeChanged(mode, userInitiated) {
|
||||||
if (searchController.autoSwitchedToFiles)
|
if (!userInitiated || !SettingsData.rememberLastMode)
|
||||||
return;
|
return;
|
||||||
SessionData.setLauncherLastMode(mode);
|
SessionData.setLauncherLastMode(mode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ DankModal {
|
|||||||
executeAction(action);
|
executeAction(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signal switchUserRequested
|
||||||
|
|
||||||
function executeAction(action) {
|
function executeAction(action) {
|
||||||
if (action === "lock") {
|
if (action === "lock") {
|
||||||
close();
|
close();
|
||||||
@@ -92,6 +94,11 @@ DankModal {
|
|||||||
Quickshell.execDetached(["dms", "restart"]);
|
Quickshell.execDetached(["dms", "restart"]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (action === "switchuser") {
|
||||||
|
close();
|
||||||
|
switchUserRequested();
|
||||||
|
return;
|
||||||
|
}
|
||||||
close();
|
close();
|
||||||
root.powerActionRequested(action, "", "");
|
root.powerActionRequested(action, "", "");
|
||||||
}
|
}
|
||||||
@@ -216,6 +223,12 @@ DankModal {
|
|||||||
"label": I18n.tr("Restart DMS"),
|
"label": I18n.tr("Restart DMS"),
|
||||||
"key": "D"
|
"key": "D"
|
||||||
};
|
};
|
||||||
|
case "switchuser":
|
||||||
|
return {
|
||||||
|
"icon": "switch_account",
|
||||||
|
"label": I18n.tr("Switch User"),
|
||||||
|
"key": "U"
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
"icon": "help",
|
"icon": "help",
|
||||||
|
|||||||
@@ -555,5 +555,20 @@ FocusScope {
|
|||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: usersLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 35
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: UsersTab {}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item)
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,6 +293,12 @@ Rectangle {
|
|||||||
"tabIndex": 20,
|
"tabIndex": 20,
|
||||||
"updaterOnly": true
|
"updaterOnly": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "users",
|
||||||
|
"text": I18n.tr("Users"),
|
||||||
|
"icon": "manage_accounts",
|
||||||
|
"tabIndex": 35
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "window_rules",
|
"id": "window_rules",
|
||||||
"text": I18n.tr("Window Rules"),
|
"text": I18n.tr("Window Rules"),
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool lockOnSwitch: false
|
||||||
|
|
||||||
|
function showFromPowerMenu() {
|
||||||
|
root.lockOnSwitch = false;
|
||||||
|
SessionsService.refresh();
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFromLockScreen() {
|
||||||
|
root.lockOnSwitch = true;
|
||||||
|
SessionsService.refresh();
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatTty(s) {
|
||||||
|
if (s.tty && s.tty.length > 0)
|
||||||
|
return s.tty;
|
||||||
|
if (s.seat && s.seat.length > 0)
|
||||||
|
return s.seat;
|
||||||
|
return I18n.tr("remote");
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatType(s) {
|
||||||
|
if (!s.type || s.type.length === 0)
|
||||||
|
return "";
|
||||||
|
switch (s.type) {
|
||||||
|
case "wayland":
|
||||||
|
return "Wayland";
|
||||||
|
case "x11":
|
||||||
|
return "X11";
|
||||||
|
case "tty":
|
||||||
|
return "TTY";
|
||||||
|
default:
|
||||||
|
return s.type.charAt(0).toUpperCase() + s.type.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _doSwitch(sessionId, username) {
|
||||||
|
if (root.lockOnSwitch && typeof SessionService !== "undefined" && SessionService.loginctlAvailable)
|
||||||
|
SessionService.lock();
|
||||||
|
SessionsService.activate(sessionId, null);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
layerNamespace: "dms:switch-user-modal"
|
||||||
|
shouldBeVisible: false
|
||||||
|
allowStacking: true
|
||||||
|
modalWidth: 420
|
||||||
|
modalHeight: contentLoader.item ? Math.min(540, contentLoader.item.implicitHeight + Theme.spacingM * 2) : 320
|
||||||
|
enableShadow: true
|
||||||
|
shouldHaveFocus: true
|
||||||
|
onBackgroundClicked: close()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionsService
|
||||||
|
function onSwitchRequested() {
|
||||||
|
root.showFromPowerMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
implicitHeight: mainColumn.implicitHeight
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "switch_account"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Switch User")
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Select an active session to switch to. The current session stays running in the background.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: SessionsService.otherSessions().length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: SessionsService.otherSessions().length > 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: SessionsService.otherSessions()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: sessionRow
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
height: 64
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: sessionMouse.containsMouse ? Theme.surfacePressed : Theme.surfaceContainerHighest
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "account_circle"
|
||||||
|
size: Theme.iconSize + 4
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.iconSize - 4 - chevron.width - Theme.spacingM * 2
|
||||||
|
spacing: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: sessionRow.modelData.username
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
const tty = root._formatTty(sessionRow.modelData);
|
||||||
|
const type = root._formatType(sessionRow.modelData);
|
||||||
|
const parts = [];
|
||||||
|
if (type)
|
||||||
|
parts.push(type);
|
||||||
|
parts.push(I18n.tr("session %1").arg(sessionRow.modelData.sessionId));
|
||||||
|
if (tty)
|
||||||
|
parts.push(tty);
|
||||||
|
return parts.join(" · ");
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: chevron
|
||||||
|
name: "chevron_right"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: sessionMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root._doSwitch(sessionRow.modelData.sessionId, sessionRow.modelData.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: SessionsService.otherSessions().length === 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: bodyCol.implicitHeight + Theme.spacingM * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "info"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: bodyCol
|
||||||
|
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("No other active sessions on this seat")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("To sign in as a different user, log out and pick the account from the greeter. Creating a fresh session in parallel needs a multi-session greeter (greetd-flexiserver / GDM / LightDM).")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
layoutDirection: Qt.RightToLeft
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Close")
|
||||||
|
backgroundColor: Theme.surfaceVariantAlpha
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
visible: SessionsService.otherSessions().length === 0 && !root.lockOnSwitch
|
||||||
|
text: I18n.tr("Log out")
|
||||||
|
iconName: "logout"
|
||||||
|
backgroundColor: Theme.primary
|
||||||
|
textColor: Theme.primaryText
|
||||||
|
onClicked: {
|
||||||
|
if (typeof SessionService !== "undefined")
|
||||||
|
SessionService.logout();
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -301,12 +301,22 @@ Column {
|
|||||||
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
|
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 60
|
height: 60
|
||||||
|
iconBlinking: {
|
||||||
|
const id = widgetData.id || "";
|
||||||
|
if (id === "wifi")
|
||||||
|
return NetworkService.isWifiConnecting;
|
||||||
|
if (id === "bluetooth")
|
||||||
|
return BluetoothService.connecting;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
iconName: {
|
iconName: {
|
||||||
switch (widgetData.id || "") {
|
switch (widgetData.id || "") {
|
||||||
case "wifi":
|
case "wifi":
|
||||||
{
|
{
|
||||||
if (NetworkService.wifiToggling)
|
if (NetworkService.wifiToggling)
|
||||||
return "sync";
|
return "sync";
|
||||||
|
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
|
||||||
|
return NetworkService.wifiSignalIcon;
|
||||||
|
|
||||||
const status = NetworkService.networkStatus;
|
const status = NetworkService.networkStatus;
|
||||||
if (status === "ethernet")
|
if (status === "ethernet")
|
||||||
@@ -360,6 +370,8 @@ Column {
|
|||||||
{
|
{
|
||||||
if (NetworkService.wifiToggling)
|
if (NetworkService.wifiToggling)
|
||||||
return NetworkService.wifiEnabled ? I18n.tr("Disabling WiFi...", "network status") : I18n.tr("Enabling WiFi...", "network status");
|
return NetworkService.wifiEnabled ? I18n.tr("Disabling WiFi...", "network status") : I18n.tr("Enabling WiFi...", "network status");
|
||||||
|
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
|
||||||
|
return NetworkService.connectingSSID || I18n.tr("Connecting...", "network status");
|
||||||
|
|
||||||
const status = NetworkService.networkStatus;
|
const status = NetworkService.networkStatus;
|
||||||
if (status === "ethernet")
|
if (status === "ethernet")
|
||||||
@@ -400,6 +412,8 @@ Column {
|
|||||||
{
|
{
|
||||||
if (NetworkService.wifiToggling)
|
if (NetworkService.wifiToggling)
|
||||||
return I18n.tr("Please wait...", "network status");
|
return I18n.tr("Please wait...", "network status");
|
||||||
|
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
|
||||||
|
return I18n.tr("Connecting...", "network status");
|
||||||
|
|
||||||
const status = NetworkService.networkStatus;
|
const status = NetworkService.networkStatus;
|
||||||
if (status === "ethernet")
|
if (status === "ethernet")
|
||||||
@@ -422,6 +436,8 @@ Column {
|
|||||||
return I18n.tr("No adapters", "bluetooth status");
|
return I18n.tr("No adapters", "bluetooth status");
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
||||||
return I18n.tr("Off", "bluetooth status");
|
return I18n.tr("Off", "bluetooth status");
|
||||||
|
if (BluetoothService.connecting)
|
||||||
|
return I18n.tr("Connecting...", "bluetooth status");
|
||||||
const primaryDevice = (() => {
|
const primaryDevice = (() => {
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Rectangle {
|
|||||||
|
|
||||||
property string iconName: ""
|
property string iconName: ""
|
||||||
property color iconColor: Theme.surfaceText
|
property color iconColor: Theme.surfaceText
|
||||||
|
property bool iconBlinking: false
|
||||||
property string primaryText: ""
|
property string primaryText: ""
|
||||||
property string secondaryText: ""
|
property string secondaryText: ""
|
||||||
property bool expanded: false
|
property bool expanded: false
|
||||||
@@ -109,10 +110,16 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
|
id: pillIcon
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: iconName
|
name: iconName
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: isActive ? _tileIconActive : _tileIconInactive
|
color: isActive ? _tileIconActive : _tileIconInactive
|
||||||
|
|
||||||
|
DankBlink {
|
||||||
|
target: pillIcon
|
||||||
|
running: root.iconBlinking
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankRipple {
|
DankRipple {
|
||||||
|
|||||||
@@ -726,7 +726,7 @@ PanelWindow {
|
|||||||
item: clickThroughEnabled ? null : inputMask
|
item: clickThroughEnabled ? null : inputMask
|
||||||
|
|
||||||
Region {
|
Region {
|
||||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress) : {
|
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress + barWindow.width * 0) : {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 0,
|
"w": 0,
|
||||||
@@ -739,7 +739,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Region {
|
Region {
|
||||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress) : {
|
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress + barWindow.width * 0) : {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 0,
|
"w": 0,
|
||||||
@@ -752,7 +752,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Region {
|
Region {
|
||||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress) : {
|
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress + barWindow.width * 0) : {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 0,
|
"w": 0,
|
||||||
|
|||||||
@@ -131,9 +131,19 @@ BasePill {
|
|||||||
function getNetworkIconColor() {
|
function getNetworkIconColor() {
|
||||||
if (NetworkService.wifiToggling)
|
if (NetworkService.wifiToggling)
|
||||||
return Theme.primary;
|
return Theme.primary;
|
||||||
|
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
|
||||||
|
return Theme.primary;
|
||||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.surfaceText;
|
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.surfaceText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getIconBlinking(id) {
|
||||||
|
if (id === "network")
|
||||||
|
return NetworkService.isWifiConnecting;
|
||||||
|
if (id === "bluetooth")
|
||||||
|
return BluetoothService.connecting;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function getVolumeIconName() {
|
function getVolumeIconName() {
|
||||||
if (!AudioService.sink?.audio)
|
if (!AudioService.sink?.audio)
|
||||||
return "volume_up";
|
return "volume_up";
|
||||||
@@ -485,6 +495,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
|
id: vIconOnlyItem
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: !verticalGroupItem.modelData.composite
|
visible: !verticalGroupItem.modelData.composite
|
||||||
name: {
|
name: {
|
||||||
@@ -515,7 +526,7 @@ BasePill {
|
|||||||
case "vpn":
|
case "vpn":
|
||||||
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
||||||
case "bluetooth":
|
case "bluetooth":
|
||||||
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
|
return (BluetoothService.connected || BluetoothService.connecting) ? Theme.primary : Theme.surfaceText;
|
||||||
case "battery":
|
case "battery":
|
||||||
return root.getBatteryIconColor();
|
return root.getBatteryIconColor();
|
||||||
case "printer":
|
case "printer":
|
||||||
@@ -524,6 +535,11 @@ BasePill {
|
|||||||
return Theme.widgetIconColor;
|
return Theme.widgetIconColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankBlink {
|
||||||
|
target: vIconOnlyItem
|
||||||
|
running: root.getIconBlinking(verticalGroupItem.modelData.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -687,7 +703,7 @@ BasePill {
|
|||||||
case "vpn":
|
case "vpn":
|
||||||
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
||||||
case "bluetooth":
|
case "bluetooth":
|
||||||
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
|
return (BluetoothService.connected || BluetoothService.connecting) ? Theme.primary : Theme.surfaceText;
|
||||||
case "battery":
|
case "battery":
|
||||||
return root.getBatteryIconColor();
|
return root.getBatteryIconColor();
|
||||||
case "printer":
|
case "printer":
|
||||||
@@ -696,6 +712,11 @@ BasePill {
|
|||||||
return Theme.widgetIconColor;
|
return Theme.widgetIconColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankBlink {
|
||||||
|
target: iconOnlyItem
|
||||||
|
running: root.getIconBlinking(horizontalGroupItem.modelData.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ Rectangle {
|
|||||||
|
|
||||||
signal closed
|
signal closed
|
||||||
|
|
||||||
|
signal switchUserRequested
|
||||||
|
|
||||||
function updateVisibleActions() {
|
function updateVisibleActions() {
|
||||||
const allActions = powerMenuActionsOverride !== undefined ? powerMenuActionsOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"]);
|
const allActions = powerMenuActionsOverride !== undefined ? powerMenuActionsOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"]);
|
||||||
const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false;
|
const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false;
|
||||||
@@ -128,6 +130,12 @@ Rectangle {
|
|||||||
"label": I18n.tr("Hibernate"),
|
"label": I18n.tr("Hibernate"),
|
||||||
"key": "H"
|
"key": "H"
|
||||||
};
|
};
|
||||||
|
case "switchuser":
|
||||||
|
return {
|
||||||
|
"icon": "switch_account",
|
||||||
|
"label": I18n.tr("Switch User"),
|
||||||
|
"key": "U"
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
"icon": "help",
|
"icon": "help",
|
||||||
@@ -183,6 +191,11 @@ Rectangle {
|
|||||||
function executeAction(action) {
|
function executeAction(action) {
|
||||||
if (!action)
|
if (!action)
|
||||||
return;
|
return;
|
||||||
|
if (action === "switchuser") {
|
||||||
|
hide();
|
||||||
|
switchUserRequested();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof SessionService === "undefined")
|
if (typeof SessionService === "undefined")
|
||||||
return;
|
return;
|
||||||
hide();
|
hide();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Quickshell.Hyprland
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modals
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -73,6 +74,10 @@ Item {
|
|||||||
return pam && (pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending;
|
return pam && (pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canStartSecurityKeyUnlock() {
|
||||||
|
return !demoMode && pam && pam.u2f && pam.u2f.available && SettingsData.enableU2f && SettingsData.u2fMode === "or" && !pam.passwd.active && !pam.u2f.active && !pam.u2fPending && !root.unlocking;
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
WeatherService.addRef();
|
WeatherService.addRef();
|
||||||
UserInfoService.getUserInfo();
|
UserInfoService.getUserInfo();
|
||||||
@@ -761,6 +766,9 @@ Item {
|
|||||||
if (enterButton.visible) {
|
if (enterButton.visible) {
|
||||||
margin += enterButton.width + 2;
|
margin += enterButton.width + 2;
|
||||||
}
|
}
|
||||||
|
if (securityKeyButton.visible) {
|
||||||
|
margin += securityKeyButton.width;
|
||||||
|
}
|
||||||
if (virtualKeyboardButton.visible) {
|
if (virtualKeyboardButton.visible) {
|
||||||
margin += virtualKeyboardButton.width;
|
margin += virtualKeyboardButton.width;
|
||||||
}
|
}
|
||||||
@@ -854,7 +862,7 @@ Item {
|
|||||||
|
|
||||||
anchors.left: lockIconContainer.right
|
anchors.left: lockIconContainer.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
|
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (securityKeyButton.visible ? securityKeyButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
@@ -896,7 +904,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
anchors.left: lockIconContainer.right
|
anchors.left: lockIconContainer.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))))
|
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (securityKeyButton.visible ? securityKeyButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
@@ -926,7 +934,7 @@ Item {
|
|||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: revealButton
|
id: revealButton
|
||||||
|
|
||||||
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))
|
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (securityKeyButton.visible ? securityKeyButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))
|
||||||
anchors.rightMargin: 0
|
anchors.rightMargin: 0
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
||||||
@@ -936,10 +944,26 @@ Item {
|
|||||||
onClicked: parent.showPassword = !parent.showPassword
|
onClicked: parent.showPassword = !parent.showPassword
|
||||||
}
|
}
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: virtualKeyboardButton
|
id: securityKeyButton
|
||||||
|
|
||||||
anchors.right: enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)
|
anchors.right: enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)
|
||||||
anchors.rightMargin: enterButton.visible ? 0 : Theme.spacingS
|
anchors.rightMargin: 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "passkey"
|
||||||
|
buttonSize: 32
|
||||||
|
visible: root.canStartSecurityKeyUnlock()
|
||||||
|
enabled: visible
|
||||||
|
onClicked: {
|
||||||
|
passwordField.text = "";
|
||||||
|
root.passwordBuffer = "";
|
||||||
|
pam.u2f.startForAlternativeAuth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DankActionButton {
|
||||||
|
id: virtualKeyboardButton
|
||||||
|
|
||||||
|
anchors.right: securityKeyButton.visible ? securityKeyButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))
|
||||||
|
anchors.rightMargin: securityKeyButton.visible || enterButton.visible ? 0 : Theme.spacingS
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard"
|
iconName: "keyboard"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
@@ -1438,6 +1462,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
|
id: lockNetworkIcon
|
||||||
name: {
|
name: {
|
||||||
if (NetworkService.wifiToggling)
|
if (NetworkService.wifiToggling)
|
||||||
return "sync";
|
return "sync";
|
||||||
@@ -1451,9 +1476,14 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5)
|
color: (NetworkService.networkStatus !== "disconnected" || NetworkService.isConnecting) ? "white" : Qt.rgba(255, 255, 255, 0.5)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: NetworkService.networkAvailable
|
visible: NetworkService.networkAvailable
|
||||||
|
|
||||||
|
DankBlink {
|
||||||
|
target: lockNetworkIcon
|
||||||
|
running: NetworkService.isWifiConnecting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -1465,11 +1495,17 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
|
id: lockBluetoothIcon
|
||||||
name: "bluetooth"
|
name: "bluetooth"
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
color: "white"
|
color: "white"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: BluetoothService.available && BluetoothService.enabled
|
visible: BluetoothService.available && BluetoothService.enabled
|
||||||
|
|
||||||
|
DankBlink {
|
||||||
|
target: lockBluetoothIcon
|
||||||
|
running: BluetoothService.connecting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -1693,5 +1729,12 @@ Item {
|
|||||||
Qt.callLater(() => passwordField.forceActiveFocus());
|
Qt.callLater(() => passwordField.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onSwitchUserRequested: {
|
||||||
|
switchUserPicker.showFromLockScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchUserModal {
|
||||||
|
id: switchUserPicker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Scope {
|
|||||||
property string fprintState
|
property string fprintState
|
||||||
property string u2fState
|
property string u2fState
|
||||||
property bool u2fPending: false
|
property bool u2fPending: false
|
||||||
|
property string u2fPendingMode
|
||||||
property string buffer
|
property string buffer
|
||||||
|
|
||||||
signal flashMsg
|
signal flashMsg
|
||||||
@@ -35,6 +36,7 @@ Scope {
|
|||||||
passwdActiveTimeout.running = false;
|
passwdActiveTimeout.running = false;
|
||||||
unlockRequestTimeout.running = false;
|
unlockRequestTimeout.running = false;
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
|
root.u2fPendingMode = "";
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
root.unlockInProgress = false;
|
root.unlockInProgress = false;
|
||||||
}
|
}
|
||||||
@@ -58,6 +60,7 @@ Scope {
|
|||||||
u2fErrorRetry.running = false;
|
u2fErrorRetry.running = false;
|
||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
|
root.u2fPendingMode = "";
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
unlockRequestTimeout.restart();
|
unlockRequestTimeout.restart();
|
||||||
unlockRequested();
|
unlockRequested();
|
||||||
@@ -79,6 +82,7 @@ Scope {
|
|||||||
u2fErrorRetry.running = false;
|
u2fErrorRetry.running = false;
|
||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
|
root.u2fPendingMode = "";
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
fprint.checkAvail();
|
fprint.checkAvail();
|
||||||
}
|
}
|
||||||
@@ -142,6 +146,7 @@ Scope {
|
|||||||
unlockRequestTimeout.running = false;
|
unlockRequestTimeout.running = false;
|
||||||
root.unlockInProgress = false;
|
root.unlockInProgress = false;
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
|
root.u2fPendingMode = "";
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
u2f.abort();
|
u2f.abort();
|
||||||
@@ -243,9 +248,8 @@ Scope {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsData.u2fMode === "or") {
|
if (SettingsData.u2fMode === "or")
|
||||||
start();
|
abort();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startForSecondFactor(): void {
|
function startForSecondFactor(): void {
|
||||||
@@ -255,6 +259,18 @@ Scope {
|
|||||||
}
|
}
|
||||||
abort();
|
abort();
|
||||||
root.u2fPending = true;
|
root.u2fPending = true;
|
||||||
|
root.u2fPendingMode = "and";
|
||||||
|
root.u2fState = "";
|
||||||
|
u2fPendingTimeout.restart();
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startForAlternativeAuth(): void {
|
||||||
|
if (!available || !SettingsData.enableU2f || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
|
||||||
|
return;
|
||||||
|
abort();
|
||||||
|
root.u2fPending = true;
|
||||||
|
root.u2fPendingMode = "or";
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
u2fPendingTimeout.restart();
|
u2fPendingTimeout.restart();
|
||||||
start();
|
start();
|
||||||
@@ -281,9 +297,19 @@ Scope {
|
|||||||
abort();
|
abort();
|
||||||
|
|
||||||
if (root.u2fPending) {
|
if (root.u2fPending) {
|
||||||
|
if (root.u2fPendingMode === "or") {
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fPendingMode = "";
|
||||||
|
root.u2fState = root.u2fState === "waiting" ? "" : "insert";
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
fprint.checkAvail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (root.u2fState === "waiting") {
|
if (root.u2fState === "waiting") {
|
||||||
// AND mode: device was found but auth failed → back to password
|
// AND mode: device was found but auth failed → back to password
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
|
root.u2fPendingMode = "";
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
fprint.checkAvail();
|
fprint.checkAvail();
|
||||||
} else {
|
} else {
|
||||||
@@ -292,9 +318,7 @@ Scope {
|
|||||||
u2fErrorRetry.restart();
|
u2fErrorRetry.restart();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// OR mode: prompt to insert key, silently retry
|
|
||||||
root.u2fState = "insert";
|
root.u2fState = "insert";
|
||||||
u2fErrorRetry.restart();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,6 +391,7 @@ Scope {
|
|||||||
root.fprintState = "";
|
root.fprintState = "";
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
|
root.u2fPendingMode = "";
|
||||||
root.lockMessage = "";
|
root.lockMessage = "";
|
||||||
root.resetAuthFlows();
|
root.resetAuthFlows();
|
||||||
fprint.checkAvail();
|
fprint.checkAvail();
|
||||||
@@ -399,6 +424,7 @@ Scope {
|
|||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
unlockRequestTimeout.running = false;
|
unlockRequestTimeout.running = false;
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
|
root.u2fPendingMode = "";
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
u2f.checkAvail();
|
u2f.checkAvail();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,26 +182,30 @@ Rectangle {
|
|||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
readonly property real reservedTrailingWidth: historySeparator.implicitWidth + Math.max(historyTimeText.implicitWidth, 72) + spacing
|
|
||||||
|
|
||||||
StyledText {
|
Item {
|
||||||
id: historyTitleText
|
width: Math.max(0, parent.width - historySeparator.implicitWidth - Math.max(historyTimeText.implicitWidth, 72) - parent.spacing * 2)
|
||||||
width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth))
|
height: historyTitleText.implicitHeight
|
||||||
text: {
|
visible: historyTitleText.text.length > 0
|
||||||
let title = historyItem.summary || "";
|
|
||||||
const appName = historyItem.appName || "";
|
StyledText {
|
||||||
const prefix = appName + " • ";
|
id: historyTitleText
|
||||||
if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) {
|
anchors.fill: parent
|
||||||
title = title.substring(prefix.length);
|
text: {
|
||||||
|
let title = historyItem.summary || "";
|
||||||
|
const appName = historyItem.appName || "";
|
||||||
|
const prefix = appName + " • ";
|
||||||
|
if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||||
|
title = title.substring(prefix.length);
|
||||||
|
}
|
||||||
|
return title;
|
||||||
}
|
}
|
||||||
return title;
|
color: Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
}
|
}
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
}
|
||||||
StyledText {
|
StyledText {
|
||||||
id: historySeparator
|
id: historySeparator
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankOSD {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
osdWidth: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
osdHeight: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
autoHideInterval: 2000
|
|
||||||
enableMouseInteraction: false
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService
|
|
||||||
function onMicMuteChanged() {
|
|
||||||
if (SettingsData.osdMicMuteEnabled) {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? "mic_off" : "mic"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? Theme.error : Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankOSD {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool useVertical: isVerticalLayout
|
||||||
|
property int _displayVolume: 0
|
||||||
|
|
||||||
|
function _syncVolume() {
|
||||||
|
if (!AudioService.source?.audio)
|
||||||
|
return;
|
||||||
|
_displayVolume = Math.round(AudioService.source.audio.volume * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(260, Screen.width - Theme.spacingM * 2)
|
||||||
|
osdHeight: useVertical ? Math.min(260, Screen.height - Theme.spacingM * 2) : (40 + Theme.spacingS * 2)
|
||||||
|
autoHideInterval: 3000
|
||||||
|
enableMouseInteraction: true
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: AudioService.source?.audio ?? null
|
||||||
|
|
||||||
|
function onVolumeChanged() {
|
||||||
|
root._syncVolume();
|
||||||
|
if (SettingsData.osdMicVolumeEnabled)
|
||||||
|
root.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMutedChanged() {
|
||||||
|
if (SettingsData.osdMicMuteEnabled)
|
||||||
|
root.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: AudioService
|
||||||
|
|
||||||
|
function onSourceChanged() {
|
||||||
|
root._syncVolume();
|
||||||
|
if (root.shouldBeVisible && SettingsData.osdMicVolumeEnabled)
|
||||||
|
root.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: useVertical ? verticalContent : horizontalContent
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: horizontalContent
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int gap: Theme.spacingS
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Theme.spacingS * 2
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize
|
||||||
|
height: Theme.iconSize
|
||||||
|
radius: Theme.iconSize / 2
|
||||||
|
color: "transparent"
|
||||||
|
x: parent.gap
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: AudioService.source?.audio?.muted ? "mic_off" : "mic"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: muteButton.containsMouse ? Theme.primary : (AudioService.source?.audio?.muted ? Theme.error : Theme.surfaceText)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: muteButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: AudioService.toggleMicMute()
|
||||||
|
onContainsMouseChanged: setChildHovered(containsMouse || volumeSlider.containsMouse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
id: volumeSlider
|
||||||
|
|
||||||
|
width: parent.width - Theme.iconSize - parent.gap * 3
|
||||||
|
height: 40
|
||||||
|
x: parent.gap * 2 + Theme.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
enabled: AudioService.source?.audio ?? false
|
||||||
|
showValue: true
|
||||||
|
unit: "%"
|
||||||
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
|
valueOverride: root._displayVolume
|
||||||
|
alwaysShowValue: SettingsData.osdAlwaysShowValue
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
root._syncVolume();
|
||||||
|
value = root._displayVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSliderValueChanged: newValue => {
|
||||||
|
if (!AudioService.source?.audio)
|
||||||
|
return;
|
||||||
|
SessionData.suppressOSDTemporarily();
|
||||||
|
AudioService.source.audio.volume = newValue / 100;
|
||||||
|
resetHideTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
onContainsMouseChanged: setChildHovered(containsMouse || muteButton.containsMouse)
|
||||||
|
|
||||||
|
Binding on value {
|
||||||
|
value: root._displayVolume
|
||||||
|
when: !volumeSlider.pressed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: verticalContent
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
property int gap: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize
|
||||||
|
height: Theme.iconSize
|
||||||
|
radius: Theme.iconSize / 2
|
||||||
|
color: "transparent"
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
y: gap
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: AudioService.source?.audio?.muted ? "mic_off" : "mic"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: muteButtonVert.containsMouse ? Theme.primary : (AudioService.source?.audio?.muted ? Theme.error : Theme.surfaceText)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: muteButtonVert
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: AudioService.toggleMicMute()
|
||||||
|
onContainsMouseChanged: setChildHovered(containsMouse || vertSliderArea.containsMouse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: vertSlider
|
||||||
|
width: 12
|
||||||
|
height: parent.height - Theme.iconSize - gap * 3 - 24
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
y: gap * 2 + Theme.iconSize
|
||||||
|
|
||||||
|
property bool dragging: false
|
||||||
|
property int value: root._displayVolume
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: vertTrack
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
anchors.centerIn: parent
|
||||||
|
color: Theme.outline
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: vertFill
|
||||||
|
width: parent.width
|
||||||
|
height: (vertSlider.value / 100) * parent.height
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: AudioService.source?.audio?.muted ? Theme.error : Theme.primary
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: vertHandle
|
||||||
|
width: 24
|
||||||
|
height: 8
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
y: {
|
||||||
|
const ratio = vertSlider.value / 100;
|
||||||
|
const travel = parent.height - height;
|
||||||
|
return Math.max(0, Math.min(travel, travel * (1 - ratio)));
|
||||||
|
}
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: AudioService.source?.audio?.muted ? Theme.error : Theme.primary
|
||||||
|
border.width: 3
|
||||||
|
border.color: Theme.surfaceContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: vertSliderArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -12
|
||||||
|
enabled: AudioService.source?.audio ?? false
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onContainsMouseChanged: setChildHovered(containsMouse || muteButtonVert.containsMouse)
|
||||||
|
|
||||||
|
onPressed: mouse => {
|
||||||
|
vertSlider.dragging = true;
|
||||||
|
updateVolume(mouse);
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleased: vertSlider.dragging = false
|
||||||
|
|
||||||
|
onPositionChanged: mouse => {
|
||||||
|
if (pressed)
|
||||||
|
updateVolume(mouse);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: mouse => updateVolume(mouse)
|
||||||
|
|
||||||
|
function updateVolume(mouse) {
|
||||||
|
if (!AudioService.source?.audio)
|
||||||
|
return;
|
||||||
|
const ratio = 1.0 - (mouse.y / height);
|
||||||
|
const volume = Math.max(0, Math.min(100, Math.round(ratio * 100)));
|
||||||
|
SessionData.suppressOSDTemporarily();
|
||||||
|
AudioService.source.audio.volume = volume / 100;
|
||||||
|
resetHideTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottomMargin: gap
|
||||||
|
text: vertSlider.value + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
visible: SettingsData.osdAlwaysShowValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -597,6 +597,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Auto-hide")
|
text: I18n.tr("Auto-hide")
|
||||||
|
description: I18n.tr("Automatically hide the bar when the pointer moves away")
|
||||||
checked: selectedBarConfig?.autoHide ?? false
|
checked: selectedBarConfig?.autoHide ?? false
|
||||||
onToggled: toggled => {
|
onToggled: toggled => {
|
||||||
SettingsData.updateBarConfig(selectedBarId, {
|
SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -623,6 +624,7 @@ Item {
|
|||||||
id: hideDelaySlider
|
id: hideDelaySlider
|
||||||
width: parent.width - parent.parent.leftPadding
|
width: parent.width - parent.parent.leftPadding
|
||||||
text: I18n.tr("Hide Delay")
|
text: I18n.tr("Hide Delay")
|
||||||
|
description: I18n.tr("Time to wait before hiding after the pointer leaves")
|
||||||
value: selectedBarConfig?.autoHideDelay ?? 250
|
value: selectedBarConfig?.autoHideDelay ?? 250
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 2000
|
maximum: 2000
|
||||||
@@ -645,6 +647,7 @@ Item {
|
|||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
width: parent.width - parent.leftPadding
|
width: parent.width - parent.leftPadding
|
||||||
text: I18n.tr("Strict auto-hide", "Dank bar setting: hide the bar when the pointer leaves even if a menu or bar popover is still open")
|
text: I18n.tr("Strict auto-hide", "Dank bar setting: hide the bar when the pointer leaves even if a menu or bar popover is still open")
|
||||||
|
description: I18n.tr("Hide the bar when the pointer leaves even if a popout is still open")
|
||||||
checked: selectedBarConfig?.autoHideStrict ?? false
|
checked: selectedBarConfig?.autoHideStrict ?? false
|
||||||
onToggled: toggled => {
|
onToggled: toggled => {
|
||||||
SettingsData.updateBarConfig(selectedBarId, {
|
SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -658,6 +661,7 @@ Item {
|
|||||||
width: parent.width - parent.leftPadding
|
width: parent.width - parent.leftPadding
|
||||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||||
text: I18n.tr("Hide When Windows Open")
|
text: I18n.tr("Hide When Windows Open")
|
||||||
|
description: I18n.tr("Show the bar only when no windows are open")
|
||||||
checked: selectedBarConfig?.showOnWindowsOpen ?? false
|
checked: selectedBarConfig?.showOnWindowsOpen ?? false
|
||||||
onToggled: toggled => {
|
onToggled: toggled => {
|
||||||
SettingsData.updateBarConfig(selectedBarId, {
|
SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -676,6 +680,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Manual Show/Hide")
|
text: I18n.tr("Manual Show/Hide")
|
||||||
|
description: I18n.tr("Toggle bar visibility manually via IPC")
|
||||||
checked: selectedBarConfig?.visible ?? true
|
checked: selectedBarConfig?.visible ?? true
|
||||||
onToggled: toggled => {
|
onToggled: toggled => {
|
||||||
SettingsData.updateBarConfig(selectedBarId, {
|
SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -694,6 +699,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Click Through")
|
text: I18n.tr("Click Through")
|
||||||
|
description: I18n.tr("Mouse clicks pass through the bar to windows behind it")
|
||||||
checked: selectedBarConfig?.clickThrough ?? false
|
checked: selectedBarConfig?.clickThrough ?? false
|
||||||
onToggled: toggled => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: toggled => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
clickThrough: toggled
|
clickThrough: toggled
|
||||||
@@ -713,6 +719,7 @@ Item {
|
|||||||
enabled: !SettingsData.frameEnabled
|
enabled: !SettingsData.frameEnabled
|
||||||
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||||
text: I18n.tr("Show on Overview")
|
text: I18n.tr("Show on Overview")
|
||||||
|
description: I18n.tr("Show the bar when niri overview is active")
|
||||||
checked: selectedBarConfig?.openOnOverview ?? false
|
checked: selectedBarConfig?.openOnOverview ?? false
|
||||||
onToggled: toggled => {
|
onToggled: toggled => {
|
||||||
SettingsData.updateBarConfig(selectedBarId, {
|
SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -759,6 +766,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: edgeSpacingSlider
|
id: edgeSpacingSlider
|
||||||
text: I18n.tr("Edge Spacing")
|
text: I18n.tr("Edge Spacing")
|
||||||
|
description: I18n.tr("Space between the bar and screen edges")
|
||||||
value: selectedBarConfig?.spacing ?? 4
|
value: selectedBarConfig?.spacing ?? 4
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 32
|
maximum: 32
|
||||||
@@ -780,6 +788,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: exclusiveZoneSlider
|
id: exclusiveZoneSlider
|
||||||
text: I18n.tr("Exclusive Zone Offset")
|
text: I18n.tr("Exclusive Zone Offset")
|
||||||
|
description: I18n.tr("Fine-tune the space reserved for the bar from the screen edge")
|
||||||
value: selectedBarConfig?.bottomGap ?? 0
|
value: selectedBarConfig?.bottomGap ?? 0
|
||||||
minimum: -50
|
minimum: -50
|
||||||
maximum: 50
|
maximum: 50
|
||||||
@@ -801,6 +810,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: sizeSlider
|
id: sizeSlider
|
||||||
text: I18n.tr("Size")
|
text: I18n.tr("Size")
|
||||||
|
description: I18n.tr("Adjust the bar height via inner padding")
|
||||||
value: selectedBarConfig?.innerPadding ?? 4
|
value: selectedBarConfig?.innerPadding ?? 4
|
||||||
minimum: -8
|
minimum: -8
|
||||||
maximum: 24
|
maximum: 24
|
||||||
@@ -822,6 +832,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: widgetPaddingSlider
|
id: widgetPaddingSlider
|
||||||
text: I18n.tr("Padding")
|
text: I18n.tr("Padding")
|
||||||
|
description: I18n.tr("Inner padding applied to each widget")
|
||||||
value: selectedBarConfig?.widgetPadding ?? 8
|
value: selectedBarConfig?.widgetPadding ?? 8
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 32
|
maximum: 32
|
||||||
@@ -852,6 +863,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Auto Popup Gaps")
|
text: I18n.tr("Auto Popup Gaps")
|
||||||
|
description: I18n.tr("Automatically calculate popup gap based on bar spacing")
|
||||||
checked: selectedBarConfig?.popupGapsAuto ?? true
|
checked: selectedBarConfig?.popupGapsAuto ?? true
|
||||||
onToggled: checked => {
|
onToggled: checked => {
|
||||||
SettingsData.updateBarConfig(selectedBarId, {
|
SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -877,6 +889,7 @@ Item {
|
|||||||
id: popupGapsManualSlider
|
id: popupGapsManualSlider
|
||||||
width: parent.width - parent.parent.leftPadding
|
width: parent.width - parent.parent.leftPadding
|
||||||
text: I18n.tr("Manual Gap Size")
|
text: I18n.tr("Manual Gap Size")
|
||||||
|
description: I18n.tr("Override the popup gap size when auto is disabled")
|
||||||
value: selectedBarConfig?.popupGapsManual ?? 4
|
value: selectedBarConfig?.popupGapsManual ?? 4
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 50
|
maximum: 50
|
||||||
@@ -907,6 +920,7 @@ Item {
|
|||||||
id: barTransparencySlider
|
id: barTransparencySlider
|
||||||
visible: !SettingsData.frameEnabled
|
visible: !SettingsData.frameEnabled
|
||||||
text: I18n.tr("Bar Transparency")
|
text: I18n.tr("Bar Transparency")
|
||||||
|
description: I18n.tr("Opacity of the bar background")
|
||||||
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
@@ -929,6 +943,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: widgetTransparencySlider
|
id: widgetTransparencySlider
|
||||||
text: I18n.tr("Widget Transparency")
|
text: I18n.tr("Widget Transparency")
|
||||||
|
description: I18n.tr("Opacity of widget backgrounds")
|
||||||
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
|
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
@@ -1023,6 +1038,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Square Corners")
|
text: I18n.tr("Square Corners")
|
||||||
|
description: I18n.tr("Remove corner rounding from the bar")
|
||||||
visible: !SettingsData.frameEnabled
|
visible: !SettingsData.frameEnabled
|
||||||
checked: selectedBarConfig?.squareCorners ?? false
|
checked: selectedBarConfig?.squareCorners ?? false
|
||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -1032,6 +1048,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("No Background")
|
text: I18n.tr("No Background")
|
||||||
|
description: I18n.tr("Make the bar background fully transparent")
|
||||||
visible: !SettingsData.frameEnabled
|
visible: !SettingsData.frameEnabled
|
||||||
checked: selectedBarConfig?.noBackground ?? false
|
checked: selectedBarConfig?.noBackground ?? false
|
||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -1041,6 +1058,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Maximize Widget Icons")
|
text: I18n.tr("Maximize Widget Icons")
|
||||||
|
description: I18n.tr("Stretch widget icons to fill the available bar height")
|
||||||
checked: selectedBarConfig?.maximizeWidgetIcons ?? false
|
checked: selectedBarConfig?.maximizeWidgetIcons ?? false
|
||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
maximizeWidgetIcons: checked
|
maximizeWidgetIcons: checked
|
||||||
@@ -1049,6 +1067,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Maximize Widget Text")
|
text: I18n.tr("Maximize Widget Text")
|
||||||
|
description: I18n.tr("Stretch widget text to fill the available bar height")
|
||||||
checked: selectedBarConfig?.maximizeWidgetText ?? false
|
checked: selectedBarConfig?.maximizeWidgetText ?? false
|
||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
maximizeWidgetText: checked
|
maximizeWidgetText: checked
|
||||||
@@ -1057,6 +1076,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Remove Widget Padding")
|
text: I18n.tr("Remove Widget Padding")
|
||||||
|
description: I18n.tr("Remove inner padding from all widgets")
|
||||||
checked: selectedBarConfig?.removeWidgetPadding ?? false
|
checked: selectedBarConfig?.removeWidgetPadding ?? false
|
||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
removeWidgetPadding: checked
|
removeWidgetPadding: checked
|
||||||
@@ -1072,6 +1092,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Goth Corners")
|
text: I18n.tr("Goth Corners")
|
||||||
|
description: I18n.tr("Apply inverse concave corner cutouts to the bar")
|
||||||
visible: !SettingsData.frameEnabled
|
visible: !SettingsData.frameEnabled
|
||||||
checked: selectedBarConfig?.gothCornersEnabled ?? false
|
checked: selectedBarConfig?.gothCornersEnabled ?? false
|
||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -1081,6 +1102,7 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Corner Radius Override")
|
text: I18n.tr("Corner Radius Override")
|
||||||
|
description: I18n.tr("Use a custom radius for goth corner cutouts")
|
||||||
checked: selectedBarConfig?.gothCornerRadiusOverride ?? false
|
checked: selectedBarConfig?.gothCornerRadiusOverride ?? false
|
||||||
visible: selectedBarConfig?.gothCornersEnabled ?? false
|
visible: selectedBarConfig?.gothCornersEnabled ?? false
|
||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
@@ -1239,6 +1261,7 @@ Item {
|
|||||||
|
|
||||||
SettingsButtonGroupRow {
|
SettingsButtonGroupRow {
|
||||||
text: I18n.tr("Color")
|
text: I18n.tr("Color")
|
||||||
|
description: I18n.tr("Theme color used for the border")
|
||||||
model: ["Surface", "Secondary", "Primary"]
|
model: ["Surface", "Secondary", "Primary"]
|
||||||
currentIndex: {
|
currentIndex: {
|
||||||
switch (selectedBarConfig?.borderColor || "surfaceText") {
|
switch (selectedBarConfig?.borderColor || "surfaceText") {
|
||||||
@@ -1276,6 +1299,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: borderOpacitySlider
|
id: borderOpacitySlider
|
||||||
text: I18n.tr("Opacity")
|
text: I18n.tr("Opacity")
|
||||||
|
description: I18n.tr("Transparency of the border")
|
||||||
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
|
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
@@ -1298,6 +1322,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: borderThicknessSlider
|
id: borderThicknessSlider
|
||||||
text: I18n.tr("Thickness")
|
text: I18n.tr("Thickness")
|
||||||
|
description: I18n.tr("Width of the border in pixels")
|
||||||
value: selectedBarConfig?.borderThickness ?? 1
|
value: selectedBarConfig?.borderThickness ?? 1
|
||||||
minimum: 1
|
minimum: 1
|
||||||
maximum: 10
|
maximum: 10
|
||||||
@@ -1329,6 +1354,7 @@ Item {
|
|||||||
|
|
||||||
SettingsButtonGroupRow {
|
SettingsButtonGroupRow {
|
||||||
text: I18n.tr("Color")
|
text: I18n.tr("Color")
|
||||||
|
description: I18n.tr("Theme color used for the widget outline")
|
||||||
model: ["Surface", "Secondary", "Primary"]
|
model: ["Surface", "Secondary", "Primary"]
|
||||||
currentIndex: {
|
currentIndex: {
|
||||||
switch (selectedBarConfig?.widgetOutlineColor || "primary") {
|
switch (selectedBarConfig?.widgetOutlineColor || "primary") {
|
||||||
@@ -1366,6 +1392,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: widgetOutlineOpacitySlider
|
id: widgetOutlineOpacitySlider
|
||||||
text: I18n.tr("Opacity")
|
text: I18n.tr("Opacity")
|
||||||
|
description: I18n.tr("Transparency of the widget outline")
|
||||||
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
|
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
@@ -1388,6 +1415,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: widgetOutlineThicknessSlider
|
id: widgetOutlineThicknessSlider
|
||||||
text: I18n.tr("Thickness")
|
text: I18n.tr("Thickness")
|
||||||
|
description: I18n.tr("Width of the widget outline in pixels")
|
||||||
value: selectedBarConfig?.widgetOutlineThickness ?? 1
|
value: selectedBarConfig?.widgetOutlineThickness ?? 1
|
||||||
minimum: 1
|
minimum: 1
|
||||||
maximum: 10
|
maximum: 10
|
||||||
@@ -1458,6 +1486,7 @@ Item {
|
|||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
visible: shadowCard.shadowActive
|
visible: shadowCard.shadowActive
|
||||||
text: I18n.tr("Intensity", "shadow intensity slider")
|
text: I18n.tr("Intensity", "shadow intensity slider")
|
||||||
|
description: I18n.tr("Shadow blur radius in pixels")
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
unit: "px"
|
unit: "px"
|
||||||
@@ -1471,6 +1500,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")
|
||||||
minimum: 10
|
minimum: 10
|
||||||
maximum: 100
|
maximum: 100
|
||||||
unit: "%"
|
unit: "%"
|
||||||
@@ -1658,6 +1688,7 @@ Item {
|
|||||||
|
|
||||||
SettingsButtonGroupRow {
|
SettingsButtonGroupRow {
|
||||||
text: I18n.tr("Y Axis")
|
text: I18n.tr("Y Axis")
|
||||||
|
description: I18n.tr("Action performed when scrolling vertically on the bar")
|
||||||
model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
|
model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
|
||||||
currentIndex: {
|
currentIndex: {
|
||||||
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
|
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
|
||||||
@@ -1694,6 +1725,7 @@ Item {
|
|||||||
|
|
||||||
SettingsButtonGroupRow {
|
SettingsButtonGroupRow {
|
||||||
text: I18n.tr("X Axis")
|
text: I18n.tr("X Axis")
|
||||||
|
description: I18n.tr("Action performed when scrolling horizontally on the bar")
|
||||||
visible: CompositorService.isNiri
|
visible: CompositorService.isNiri
|
||||||
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
|
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
|
||||||
currentIndex: {
|
currentIndex: {
|
||||||
|
|||||||
@@ -1121,6 +1121,15 @@ Item {
|
|||||||
onToggled: checked => SessionData.setSearchAppActions(checked)
|
onToggled: checked => SessionData.setSearchAppActions(checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "rememberLastMode"
|
||||||
|
tags: ["launcher", "remember", "last", "mode", "tab"]
|
||||||
|
text: I18n.tr("Remember Last Mode")
|
||||||
|
description: I18n.tr("Restore the last selected mode (tab) when the launcher is opened")
|
||||||
|
checked: SettingsData.rememberLastMode
|
||||||
|
onToggled: checked => SettingsData.set("rememberLastMode", checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
settingKey: "rememberLastQuery"
|
settingKey: "rememberLastQuery"
|
||||||
tags: ["launcher", "remember", "last", "search", "query"]
|
tags: ["launcher", "remember", "last", "search", "query"]
|
||||||
|
|||||||
@@ -273,6 +273,17 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("notificationCompactMode", checked)
|
onToggled: checked => SettingsData.set("notificationCompactMode", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "notificationDedupeEnabled"
|
||||||
|
tags: ["notification", "duplicate", "dedupe", "stack", "coalesce", "repeat"]
|
||||||
|
text: I18n.tr("Suppress Duplicate Notifications")
|
||||||
|
description: SettingsData.notificationDedupeEnabled
|
||||||
|
? I18n.tr("Identical alerts show as one popup instead of stacking")
|
||||||
|
: I18n.tr("Identical alerts stack as separate notification cards")
|
||||||
|
checked: SettingsData.notificationDedupeEnabled
|
||||||
|
onToggled: checked => SettingsData.set("notificationDedupeEnabled", checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
settingKey: "notificationPopupShadowEnabled"
|
settingKey: "notificationPopupShadowEnabled"
|
||||||
tags: ["notification", "popup", "shadow", "radius", "rounded"]
|
tags: ["notification", "popup", "shadow", "radius", "rounded"]
|
||||||
|
|||||||
@@ -455,6 +455,11 @@ Item {
|
|||||||
label: I18n.tr("Show Restart DMS"),
|
label: I18n.tr("Show Restart DMS"),
|
||||||
desc: I18n.tr("Restart the DankMaterialShell")
|
desc: I18n.tr("Restart the DankMaterialShell")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "switchuser",
|
||||||
|
label: I18n.tr("Show Switch User"),
|
||||||
|
desc: I18n.tr("Opens a picker of other active sessions on this seat")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "hibernate",
|
key: "hibernate",
|
||||||
label: I18n.tr("Show Hibernate"),
|
label: I18n.tr("Show Hibernate"),
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Settings.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string statusText: ""
|
||||||
|
property bool statusIsError: false
|
||||||
|
property bool operationPending: false
|
||||||
|
property string pendingUsername: ""
|
||||||
|
property string pendingPassword: ""
|
||||||
|
property string pendingConfirm: ""
|
||||||
|
property bool pendingAdmin: false
|
||||||
|
|
||||||
|
function _resetForm() {
|
||||||
|
pendingUsername = "";
|
||||||
|
pendingPassword = "";
|
||||||
|
pendingConfirm = "";
|
||||||
|
pendingAdmin = false;
|
||||||
|
usernameField.text = "";
|
||||||
|
passwordField.text = "";
|
||||||
|
confirmField.text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function _passwordsMatch() {
|
||||||
|
return pendingPassword.length > 0 && pendingPassword === pendingConfirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _createCanProceed() {
|
||||||
|
return !operationPending && UsersService.isValidUsername(pendingUsername) && !UsersService.userExists(pendingUsername) && _passwordsMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: UsersService
|
||||||
|
function onOperationCompleted(op, username, success, message) {
|
||||||
|
root.operationPending = false;
|
||||||
|
root.statusIsError = !success;
|
||||||
|
if (success) {
|
||||||
|
root.statusText = message + (username ? (" — " + username) : "");
|
||||||
|
if (op === "create")
|
||||||
|
root._resetForm();
|
||||||
|
} else {
|
||||||
|
root.statusText = (username ? (username + ": ") : "") + message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmModal {
|
||||||
|
id: deleteUserConfirm
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmModal {
|
||||||
|
id: adminToggleConfirm
|
||||||
|
}
|
||||||
|
|
||||||
|
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.spacingXL
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
visible: !PolkitService.polkitAvailable
|
||||||
|
text: I18n.tr("Polkit integration is disabled. User management requires Polkit to elevate privileges.")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.error
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "group"
|
||||||
|
title: I18n.tr("Existing Users")
|
||||||
|
settingKey: "usersList"
|
||||||
|
visible: PolkitService.polkitAvailable
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Administrator group:")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: UsersService.adminGroup
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Theme.spacingM
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: UsersService.refreshing ? I18n.tr("Refreshing…") : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: UsersService.users
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: userRow
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(48, rowContent.implicitHeight + Theme.spacingS * 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
|
||||||
|
readonly property bool isLastAdmin: modelData.isAdmin && UsersService.adminMembers.length <= 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: rowContent
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "account_circle"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.iconSize - actionButtons.width - Theme.spacingM * 3
|
||||||
|
spacing: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: userRow.modelData.username
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: userRow.modelData.isAdmin
|
||||||
|
width: adminChipText.implicitWidth + Theme.spacingS * 2
|
||||||
|
height: adminChipText.implicitHeight + Theme.spacingXS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.primary, 0.15)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: adminChipText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: I18n.tr("admin")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: userRow.modelData.gecos && userRow.modelData.gecos.length > 0 ? userRow.modelData.gecos + " · UID " + userRow.modelData.uid : "UID " + userRow.modelData.uid
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: actionButtons
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: adminToggleBtn
|
||||||
|
readonly property bool actionBlocked: root.operationPending || (userRow.isLastAdmin && userRow.modelData.isAdmin)
|
||||||
|
buttonSize: 36
|
||||||
|
iconSize: 20
|
||||||
|
iconName: userRow.modelData.isAdmin ? "shield_person" : "shield"
|
||||||
|
iconColor: userRow.modelData.isAdmin ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
opacity: actionBlocked ? 0.4 : 1.0
|
||||||
|
tooltipText: (userRow.isLastAdmin && userRow.modelData.isAdmin) ? I18n.tr("Cannot remove the only administrator") : (userRow.modelData.isAdmin ? I18n.tr("Remove admin") : I18n.tr("Make admin"))
|
||||||
|
tooltipSide: "left"
|
||||||
|
onClicked: {
|
||||||
|
if (actionBlocked)
|
||||||
|
return;
|
||||||
|
const makeAdmin = !userRow.modelData.isAdmin;
|
||||||
|
adminToggleConfirm.showWithOptions({
|
||||||
|
title: makeAdmin ? I18n.tr("Grant admin?") : I18n.tr("Remove admin?"),
|
||||||
|
message: makeAdmin ? I18n.tr("Add \"%1\" to the %2 group?").arg(userRow.modelData.username).arg(UsersService.adminGroup) : I18n.tr("Remove \"%1\" from the %2 group?").arg(userRow.modelData.username).arg(UsersService.adminGroup),
|
||||||
|
confirmText: makeAdmin ? I18n.tr("Grant") : I18n.tr("Remove"),
|
||||||
|
confirmColor: Theme.primary,
|
||||||
|
onConfirm: () => {
|
||||||
|
root.operationPending = true;
|
||||||
|
root.statusText = "";
|
||||||
|
UsersService.setAdmin(userRow.modelData.username, makeAdmin, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: deleteBtn
|
||||||
|
readonly property bool actionBlocked: root.operationPending || !UsersService.canDelete(userRow.modelData.username)
|
||||||
|
buttonSize: 36
|
||||||
|
iconSize: 20
|
||||||
|
iconName: "delete"
|
||||||
|
iconColor: Theme.error
|
||||||
|
opacity: actionBlocked ? 0.4 : 1.0
|
||||||
|
tooltipText: userRow.isLastAdmin ? I18n.tr("Cannot delete the only administrator") : I18n.tr("Delete user")
|
||||||
|
tooltipSide: "left"
|
||||||
|
onClicked: {
|
||||||
|
if (actionBlocked)
|
||||||
|
return;
|
||||||
|
deleteUserConfirm.showWithOptions({
|
||||||
|
title: I18n.tr("Delete user?"),
|
||||||
|
message: I18n.tr("Delete \"%1\" and remove the home directory? This cannot be undone.").arg(userRow.modelData.username),
|
||||||
|
confirmText: I18n.tr("Delete"),
|
||||||
|
confirmColor: Theme.primary,
|
||||||
|
onConfirm: () => {
|
||||||
|
root.operationPending = true;
|
||||||
|
root.statusText = "";
|
||||||
|
UsersService.deleteUser(userRow.modelData.username, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
visible: UsersService.users.length === 0 && !UsersService.refreshing
|
||||||
|
text: I18n.tr("No human user accounts found.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "person_add"
|
||||||
|
title: I18n.tr("Create User")
|
||||||
|
settingKey: "createUser"
|
||||||
|
visible: PolkitService.polkitAvailable
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Username")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: usernameField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: I18n.tr("e.g. alice")
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
normalBorderColor: usernameInvalid ? Theme.error : Theme.outlineMedium
|
||||||
|
focusedBorderColor: usernameInvalid ? Theme.error : Theme.primary
|
||||||
|
|
||||||
|
readonly property bool usernameInvalid: text.length > 0 && (!UsersService.isValidUsername(text) || UsersService.userExists(text))
|
||||||
|
|
||||||
|
onTextEdited: {
|
||||||
|
root.pendingUsername = text.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
visible: usernameField.text.length > 0 && !UsersService.isValidUsername(usernameField.text)
|
||||||
|
text: I18n.tr("Username must start with a lowercase letter or underscore and contain only lowercase letters, digits, hyphens, or underscores.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
visible: usernameField.text.length > 0 && UsersService.isValidUsername(usernameField.text) && UsersService.userExists(usernameField.text)
|
||||||
|
text: I18n.tr("A user with that name already exists.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Password")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: passwordField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: I18n.tr("Set initial password")
|
||||||
|
echoMode: TextInput.Password
|
||||||
|
showPasswordToggle: true
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
normalBorderColor: Theme.outlineMedium
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
onTextEdited: root.pendingPassword = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Confirm password")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: confirmField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: I18n.tr("Re-enter password")
|
||||||
|
echoMode: TextInput.Password
|
||||||
|
showPasswordToggle: true
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
normalBorderColor: confirmMismatch ? Theme.error : Theme.outlineMedium
|
||||||
|
focusedBorderColor: confirmMismatch ? Theme.error : Theme.primary
|
||||||
|
|
||||||
|
readonly property bool confirmMismatch: text.length > 0 && text !== passwordField.text
|
||||||
|
|
||||||
|
onTextEdited: root.pendingConfirm = text
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
visible: confirmField.text.length > 0 && confirmField.text !== passwordField.text
|
||||||
|
text: I18n.tr("Passwords do not match.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "createUserAdmin"
|
||||||
|
tags: ["user", "admin", "sudo", "wheel"]
|
||||||
|
text: I18n.tr("Grant administrator privileges")
|
||||||
|
description: I18n.tr("Add the new user to the %1 group so they can use sudo.").arg(UsersService.adminGroup)
|
||||||
|
checked: root.pendingAdmin
|
||||||
|
onToggled: checked => root.pendingAdmin = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: root.operationPending ? I18n.tr("Working…") : I18n.tr("Create User")
|
||||||
|
iconName: "person_add"
|
||||||
|
backgroundColor: Theme.primary
|
||||||
|
textColor: Theme.primaryText
|
||||||
|
enabled: root._createCanProceed()
|
||||||
|
onClicked: {
|
||||||
|
if (!root._createCanProceed())
|
||||||
|
return;
|
||||||
|
root.operationPending = true;
|
||||||
|
root.statusText = "";
|
||||||
|
UsersService.createUser(root.pendingUsername, root.pendingPassword, root.pendingAdmin, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.statusText
|
||||||
|
color: root.statusIsError ? Theme.error : Theme.primary
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width - parent.children[0].width - Theme.spacingM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -338,45 +338,61 @@ Scope {
|
|||||||
border.width: 1
|
border.width: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
LauncherContent {
|
FocusScope {
|
||||||
id: launcherContent
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 0
|
focus: true
|
||||||
|
|
||||||
property var fakeParentModal: QtObject {
|
Keys.onPressed: event => launcherContent.activeContextMenu?.handleKey(event)
|
||||||
property bool spotlightOpen: spotlightContainer.visible
|
|
||||||
property bool isClosing: niriOverviewScope.isClosing
|
Keys.onEscapePressed: event => {
|
||||||
function hide() {
|
launcherContent.activeContextMenu?.handleKey(event);
|
||||||
if (niriOverviewScope.searchActive) {
|
if (!event.accepted)
|
||||||
niriOverviewScope.hideSpotlight();
|
launcherContent.parentModal?.hide();
|
||||||
return;
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LauncherContent {
|
||||||
|
id: launcherContent
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 0
|
||||||
|
|
||||||
|
property var fakeParentModal: QtObject {
|
||||||
|
property bool spotlightOpen: spotlightContainer.visible
|
||||||
|
property bool isClosing: niriOverviewScope.isClosing
|
||||||
|
property real alignedX: spotlightContainer.x
|
||||||
|
property real alignedY: spotlightContainer.y
|
||||||
|
function hide() {
|
||||||
|
if (niriOverviewScope.searchActive) {
|
||||||
|
niriOverviewScope.hideSpotlight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NiriService.toggleOverview();
|
||||||
}
|
}
|
||||||
NiriService.toggleOverview();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: launcherContent.searchField
|
target: launcherContent.searchField
|
||||||
function onTextChanged() {
|
function onTextChanged() {
|
||||||
if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
|
if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
|
||||||
return;
|
return;
|
||||||
niriOverviewScope.hideSpotlight();
|
niriOverviewScope.hideSpotlight();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
parentModal = fakeParentModal;
|
parentModal = fakeParentModal;
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: launcherContent.controller
|
|
||||||
function onItemExecuted() {
|
|
||||||
niriOverviewScope.releaseKeyboard = true;
|
|
||||||
}
|
}
|
||||||
function onModeChanged(mode) {
|
|
||||||
if (launcherContent.controller.autoSwitchedToFiles)
|
Connections {
|
||||||
return;
|
target: launcherContent.controller
|
||||||
SessionData.setNiriOverviewLastMode(mode);
|
function onItemExecuted() {
|
||||||
|
niriOverviewScope.releaseKeyboard = true;
|
||||||
|
}
|
||||||
|
function onModeChanged(mode) {
|
||||||
|
if (launcherContent.controller.autoSwitchedToFiles)
|
||||||
|
return;
|
||||||
|
SessionData.setNiriOverviewLastMode(mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -397,6 +397,14 @@ EOFCONFIG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.source?.audio ?? null
|
||||||
|
|
||||||
|
function onMutedChanged() {
|
||||||
|
root.micMuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function checkGsettings() {
|
function checkGsettings() {
|
||||||
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
|
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
|
||||||
gsettingsAvailable = (exitCode === 0);
|
gsettingsAvailable = (exitCode === 0);
|
||||||
@@ -844,6 +852,36 @@ EOFCONFIG
|
|||||||
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted";
|
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function incrementMicVolume(step) {
|
||||||
|
if (!root.source?.audio)
|
||||||
|
return "No audio source available";
|
||||||
|
|
||||||
|
if (root.source.audio.muted)
|
||||||
|
root.source.audio.muted = false;
|
||||||
|
|
||||||
|
const currentVolume = Math.round(root.source.audio.volume * 100);
|
||||||
|
const stepValue = parseInt(step || "5");
|
||||||
|
const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue));
|
||||||
|
|
||||||
|
root.source.audio.volume = newVolume / 100;
|
||||||
|
return `Microphone volume increased to ${newVolume}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrementMicVolume(step) {
|
||||||
|
if (!root.source?.audio)
|
||||||
|
return "No audio source available";
|
||||||
|
|
||||||
|
if (root.source.audio.muted)
|
||||||
|
root.source.audio.muted = false;
|
||||||
|
|
||||||
|
const currentVolume = Math.round(root.source.audio.volume * 100);
|
||||||
|
const stepValue = parseInt(step || "5");
|
||||||
|
const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue));
|
||||||
|
|
||||||
|
root.source.audio.volume = newVolume / 100;
|
||||||
|
return `Microphone volume decreased to ${newVolume}%`;
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "audio"
|
target: "audio"
|
||||||
|
|
||||||
@@ -892,9 +930,7 @@ EOFCONFIG
|
|||||||
}
|
}
|
||||||
|
|
||||||
function micmute(): string {
|
function micmute(): string {
|
||||||
const result = root.toggleMicMute();
|
return root.toggleMicMute();
|
||||||
root.micMuteChanged();
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function status(): string {
|
function status(): string {
|
||||||
@@ -957,7 +993,6 @@ EOFCONFIG
|
|||||||
return `Switched to: ${result}`;
|
return `Switched to: ${result}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: SettingsData
|
target: SettingsData
|
||||||
function onUseSystemSoundThemeChanged() {
|
function onUseSystemSoundThemeChanged() {
|
||||||
|
|||||||
@@ -28,6 +28,20 @@ Singleton {
|
|||||||
});
|
});
|
||||||
return isConnected;
|
return isConnected;
|
||||||
}
|
}
|
||||||
|
readonly property bool connecting: {
|
||||||
|
if (!adapter || !adapter.devices) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let busy = false;
|
||||||
|
adapter.devices.values.forEach(dev => {
|
||||||
|
if (!dev)
|
||||||
|
return;
|
||||||
|
if (dev.pairing || dev.state === BluetoothDeviceState.Connecting)
|
||||||
|
busy = true;
|
||||||
|
});
|
||||||
|
return busy;
|
||||||
|
}
|
||||||
readonly property var pairedDevices: {
|
readonly property var pairedDevices: {
|
||||||
if (!adapter || !adapter.devices) {
|
if (!adapter || !adapter.devices) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -240,6 +240,17 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pasteClipboard(closeCallback) {
|
||||||
|
if (!wtypeAvailable) {
|
||||||
|
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (closeCallback) {
|
||||||
|
closeCallback();
|
||||||
|
}
|
||||||
|
pasteTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
function pasteEntry(entry, closeCallback) {
|
function pasteEntry(entry, closeCallback) {
|
||||||
if (!wtypeAvailable) {
|
if (!wtypeAvailable) {
|
||||||
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
|
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ Singleton {
|
|||||||
property var savedConnections: []
|
property var savedConnections: []
|
||||||
property var ssidToConnectionName: ({})
|
property var ssidToConnectionName: ({})
|
||||||
property var wifiSignalIcon: {
|
property var wifiSignalIcon: {
|
||||||
|
if (isConnecting) {
|
||||||
|
return "wifi";
|
||||||
|
}
|
||||||
if (!wifiConnected) {
|
if (!wifiConnected) {
|
||||||
return "wifi_off";
|
return "wifi_off";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property string wifiSignalIcon: {
|
readonly property string wifiSignalIcon: {
|
||||||
|
if (isConnecting) {
|
||||||
|
return "wifi";
|
||||||
|
}
|
||||||
if (!wifiConnected || networkStatus !== "wifi") {
|
if (!wifiConnected || networkStatus !== "wifi") {
|
||||||
return "wifi_off";
|
return "wifi_off";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ Singleton {
|
|||||||
|
|
||||||
property string userPreference: activeService?.userPreference ?? "auto"
|
property string userPreference: activeService?.userPreference ?? "auto"
|
||||||
property bool isConnecting: activeService?.isConnecting ?? false
|
property bool isConnecting: activeService?.isConnecting ?? false
|
||||||
|
readonly property bool isWifiConnecting: isConnecting && !ethernetConnected && !wifiToggling
|
||||||
property string connectingSSID: activeService?.connectingSSID ?? ""
|
property string connectingSSID: activeService?.connectingSSID ?? ""
|
||||||
property string connectionError: activeService?.connectionError ?? ""
|
property string connectionError: activeService?.connectionError ?? ""
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ Singleton {
|
|||||||
property int maxIngressPerSecond: 20
|
property int maxIngressPerSecond: 20
|
||||||
property double _lastIngressSec: 0
|
property double _lastIngressSec: 0
|
||||||
property int _ingressCountThisSec: 0
|
property int _ingressCountThisSec: 0
|
||||||
|
readonly property int notificationDedupBurstMs: 5000
|
||||||
|
property var _recentDedupKeys: []
|
||||||
|
|
||||||
property var _dismissQueue: []
|
property var _dismissQueue: []
|
||||||
property int _dismissBatchSize: 8
|
property int _dismissBatchSize: 8
|
||||||
@@ -291,18 +293,58 @@ Singleton {
|
|||||||
return Date.now() / 1000.0;
|
return Date.now() / 1000.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _normalizeDedupText(text) {
|
||||||
|
if (!text)
|
||||||
|
return "";
|
||||||
|
let normalized = text.toString();
|
||||||
|
normalized = normalized.replace(/<img\b[^>]*>/gi, "");
|
||||||
|
normalized = normalized.replace(/<[^>]+>/g, "");
|
||||||
|
normalized = normalized.replace(/\s+/g, " ").trim();
|
||||||
|
return normalized.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _dedupAppId(source) {
|
||||||
|
if (!source)
|
||||||
|
return "";
|
||||||
|
const desktopEntry = (source.desktopEntry || "").toString().trim().toLowerCase();
|
||||||
|
if (desktopEntry)
|
||||||
|
return desktopEntry;
|
||||||
|
return (source.appName || "").toString().trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
function _notificationDedupKey(source) {
|
function _notificationDedupKey(source) {
|
||||||
if (!source)
|
if (!source)
|
||||||
return "";
|
return "";
|
||||||
const app = (source.appName || source.desktopEntry || "").toString();
|
const app = _dedupAppId(source);
|
||||||
const summary = (source.summary || "").toString();
|
const summary = _normalizeDedupText(source.summary);
|
||||||
const body = (source.body || "").toString();
|
const body = _normalizeDedupText(source.body);
|
||||||
const urgency = typeof source.urgency === "number" ? source.urgency : NotificationUrgency.Normal;
|
const urgency = typeof source.urgency === "number" ? source.urgency : NotificationUrgency.Normal;
|
||||||
const icon = (source.appIcon || "").toString();
|
|
||||||
if (!app && !summary && !body)
|
if (!app && !summary && !body)
|
||||||
return "";
|
return "";
|
||||||
const sep = "";
|
const sep = "";
|
||||||
return app + sep + summary + sep + body + sep + urgency + sep + icon;
|
return app + sep + summary + sep + body + sep + urgency;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pruneRecentDedupKeys() {
|
||||||
|
const cutoff = Date.now() - notificationDedupBurstMs;
|
||||||
|
_recentDedupKeys = _recentDedupKeys.filter(entry => entry && entry.atMs >= cutoff);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hasRecentDuplicate(key) {
|
||||||
|
if (!key)
|
||||||
|
return false;
|
||||||
|
_pruneRecentDedupKeys();
|
||||||
|
return _recentDedupKeys.some(entry => entry && entry.key === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _recordDedupKey(key) {
|
||||||
|
if (!key)
|
||||||
|
return;
|
||||||
|
_pruneRecentDedupKeys();
|
||||||
|
_recentDedupKeys.push({
|
||||||
|
"key": key,
|
||||||
|
"atMs": Date.now()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _findActiveDuplicate(notif) {
|
function _findActiveDuplicate(notif) {
|
||||||
@@ -310,17 +352,14 @@ Singleton {
|
|||||||
if (!key)
|
if (!key)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
for (const w of visibleNotifications) {
|
for (const w of allWrappers) {
|
||||||
if (!w || !w.notification || !w.popup)
|
if (!w || !w.notification || !w.popup)
|
||||||
continue;
|
continue;
|
||||||
if (_notificationDedupKey(w.notification) === key)
|
if (_notificationDedupKey(w.notification) !== key)
|
||||||
return w;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const w of notificationQueue) {
|
|
||||||
if (!w || !w.notification)
|
|
||||||
continue;
|
continue;
|
||||||
if (_notificationDedupKey(w.notification) === key)
|
if (visibleNotifications.indexOf(w) !== -1 || notificationQueue.indexOf(w) !== -1)
|
||||||
|
return w;
|
||||||
|
if (w.timer && w.timer.running)
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,14 +676,17 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicate = _findActiveDuplicate(notif);
|
if (SettingsData.notificationDedupeEnabled) {
|
||||||
if (duplicate) {
|
const dedupKey = _notificationDedupKey(notif);
|
||||||
if (duplicate.timer && duplicate.timer.running)
|
const duplicate = _findActiveDuplicate(notif);
|
||||||
duplicate.timer.restart();
|
if (duplicate || _hasRecentDuplicate(dedupKey)) {
|
||||||
try {
|
if (duplicate && duplicate.timer && duplicate.timer.running)
|
||||||
notif.dismiss();
|
duplicate.timer.restart();
|
||||||
} catch (e) {}
|
try {
|
||||||
return;
|
notif.dismiss();
|
||||||
|
} catch (e) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_ingressAllowed(policy.urgency)) {
|
if (!_ingressAllowed(policy.urgency)) {
|
||||||
@@ -686,6 +728,9 @@ Singleton {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
|
if (SettingsData.notificationDedupeEnabled)
|
||||||
|
_recordDedupKey(_notificationDedupKey(notif));
|
||||||
|
|
||||||
root.allWrappers.push(wrapper);
|
root.allWrappers.push(wrapper);
|
||||||
if (shouldKeepInCenter) {
|
if (shouldKeepInCenter) {
|
||||||
root.notifications.push(wrapper);
|
root.notifications.push(wrapper);
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var log: Log.scoped("SessionsService")
|
||||||
|
|
||||||
|
property var sessions: []
|
||||||
|
property string currentSessionId: ""
|
||||||
|
property string currentSeat: ""
|
||||||
|
property bool refreshing: false
|
||||||
|
|
||||||
|
signal switchFailed(string sessionId, string username, string message)
|
||||||
|
signal switchRequested
|
||||||
|
|
||||||
|
function isCurrent(sessionId) {
|
||||||
|
return sessionId === currentSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findByUsername(username) {
|
||||||
|
for (let i = 0; i < sessions.length; i++) {
|
||||||
|
const s = sessions[i];
|
||||||
|
if (s.username === username && !s.current)
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findById(sessionId) {
|
||||||
|
for (let i = 0; i < sessions.length; i++) {
|
||||||
|
if (sessions[i].sessionId === sessionId)
|
||||||
|
return sessions[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function otherSessions() {
|
||||||
|
return sessions.filter(s => !s.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (refreshing)
|
||||||
|
return;
|
||||||
|
refreshing = true;
|
||||||
|
Proc.runCommand("sessionsService-current", ["sh", "-c", "echo \"${XDG_SESSION_ID}:$(loginctl show-session \"${XDG_SESSION_ID}\" -p Seat --value 2>/dev/null)\""], (output, exitCode) => {
|
||||||
|
const trimmed = (output || "").trim();
|
||||||
|
const parts = trimmed.split(":");
|
||||||
|
root.currentSessionId = parts[0] || "";
|
||||||
|
root.currentSeat = parts[1] || "";
|
||||||
|
_loadSessions();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _loadSessions() {
|
||||||
|
const script = "loginctl list-sessions --no-legend 2>/dev/null | awk '{print $1}' | while read id; do loginctl show-session \"$id\" -p Id -p User -p Name -p Seat -p TTY -p Type -p Class -p Active -p State -p Remote 2>/dev/null | tr '\\n' '|'; echo; done";
|
||||||
|
Proc.runCommand("sessionsService-list", ["sh", "-c", script], (output, exitCode) => {
|
||||||
|
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
||||||
|
const list = [];
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const fields = {};
|
||||||
|
const pairs = lines[i].split("|");
|
||||||
|
for (let j = 0; j < pairs.length; j++) {
|
||||||
|
const eq = pairs[j].indexOf("=");
|
||||||
|
if (eq <= 0)
|
||||||
|
continue;
|
||||||
|
fields[pairs[j].substring(0, eq)] = pairs[j].substring(eq + 1);
|
||||||
|
}
|
||||||
|
if (!fields.Id)
|
||||||
|
continue;
|
||||||
|
if (fields.Class !== "user")
|
||||||
|
continue;
|
||||||
|
if (fields.State === "closing")
|
||||||
|
continue;
|
||||||
|
const sessionId = fields.Id;
|
||||||
|
list.push({
|
||||||
|
sessionId: sessionId,
|
||||||
|
uid: parseInt(fields.User || "0", 10),
|
||||||
|
username: fields.Name || "",
|
||||||
|
seat: fields.Seat || "",
|
||||||
|
tty: fields.TTY || "",
|
||||||
|
type: fields.Type || "",
|
||||||
|
sessionClass: fields.Class || "",
|
||||||
|
active: fields.Active === "yes",
|
||||||
|
state: fields.State || "",
|
||||||
|
remote: fields.Remote === "yes",
|
||||||
|
current: sessionId === root.currentSessionId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
list.sort((a, b) => {
|
||||||
|
if (a.current !== b.current)
|
||||||
|
return a.current ? -1 : 1;
|
||||||
|
if (a.username !== b.username)
|
||||||
|
return a.username.localeCompare(b.username);
|
||||||
|
return parseInt(a.sessionId, 10) - parseInt(b.sessionId, 10);
|
||||||
|
});
|
||||||
|
root.sessions = list;
|
||||||
|
root.refreshing = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(sessionId, callback) {
|
||||||
|
if (!sessionId) {
|
||||||
|
_fail("", "", I18n.tr("No session selected"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sessionId === root.currentSessionId) {
|
||||||
|
_fail(sessionId, "", I18n.tr("Already on that session"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = findById(sessionId);
|
||||||
|
const username = session ? session.username : "";
|
||||||
|
_spawnActivate(sessionId, username, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToUser(target, callback) {
|
||||||
|
if (!target) {
|
||||||
|
_fail("", "", I18n.tr("No user specified"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let session = findById(target);
|
||||||
|
if (!session)
|
||||||
|
session = findByUsername(target);
|
||||||
|
if (!session) {
|
||||||
|
_fail("", target, I18n.tr("No active session found for %1").arg(target), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (session.current) {
|
||||||
|
_fail(session.sessionId, session.username, I18n.tr("Already on that session"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_spawnActivate(session.sessionId, session.username, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _fail(sessionId, username, message, callback) {
|
||||||
|
log.warn("switch failed:", sessionId, username, message);
|
||||||
|
root.switchFailed(sessionId, username, message);
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
try {
|
||||||
|
callback(false, message);
|
||||||
|
} catch (e) {
|
||||||
|
log.warn("SessionsService callback error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: activateComp
|
||||||
|
Process {
|
||||||
|
id: activateProc
|
||||||
|
property string targetSession: ""
|
||||||
|
property string targetUsername: ""
|
||||||
|
property var cb: null
|
||||||
|
property string capturedErr: ""
|
||||||
|
running: false
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: activateProc.capturedErr = text || ""
|
||||||
|
}
|
||||||
|
onExited: exitCode => {
|
||||||
|
const svc = root;
|
||||||
|
const sessionId = activateProc.targetSession;
|
||||||
|
const username = activateProc.targetUsername;
|
||||||
|
const cb = activateProc.cb;
|
||||||
|
const err = (activateProc.capturedErr || "").trim();
|
||||||
|
Qt.callLater(() => activateProc.destroy());
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
svc._fail(sessionId, username, err || I18n.tr("loginctl activate failed (exit %1)").arg(exitCode), cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof cb === "function") {
|
||||||
|
try {
|
||||||
|
cb(true, "");
|
||||||
|
} catch (e) {
|
||||||
|
svc.log.warn("activate cb error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _spawnActivate(sessionId, username, callback) {
|
||||||
|
const proc = activateComp.createObject(root, {
|
||||||
|
command: ["loginctl", "activate", sessionId],
|
||||||
|
targetSession: sessionId,
|
||||||
|
targetUsername: username,
|
||||||
|
cb: callback
|
||||||
|
});
|
||||||
|
proc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "sessions"
|
||||||
|
|
||||||
|
function list(): string {
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < root.sessions.length; i++) {
|
||||||
|
const s = root.sessions[i];
|
||||||
|
lines.push([s.sessionId, s.username, s.seat || "-", s.tty || "-", s.type || "-", s.current ? "*current*" : ""].join("\t"));
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh(): string {
|
||||||
|
root.refresh();
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(): string {
|
||||||
|
root.refresh();
|
||||||
|
root.switchRequested();
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(sessionId: string): string {
|
||||||
|
if (!sessionId)
|
||||||
|
return "ERROR: missing session id";
|
||||||
|
root.activate(sessionId, null);
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTo(target: string): string {
|
||||||
|
if (!target)
|
||||||
|
return "ERROR: missing target (username or session id)";
|
||||||
|
root.switchToUser(target, null);
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: refresh()
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var log: Log.scoped("UsersService")
|
||||||
|
|
||||||
|
property var users: []
|
||||||
|
property string adminGroup: "wheel"
|
||||||
|
property var adminMembers: []
|
||||||
|
property bool refreshing: false
|
||||||
|
|
||||||
|
signal operationCompleted(string op, string username, bool success, string message)
|
||||||
|
|
||||||
|
readonly property var _usernameRegex: /^[a-z_][a-z0-9_-]{0,30}\$?$/
|
||||||
|
|
||||||
|
function isValidUsername(name) {
|
||||||
|
if (typeof name !== "string")
|
||||||
|
return false;
|
||||||
|
return _usernameRegex.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function userExists(name) {
|
||||||
|
for (let i = 0; i < users.length; i++) {
|
||||||
|
if (users[i].username === name)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _findUser(name) {
|
||||||
|
for (let i = 0; i < users.length; i++) {
|
||||||
|
if (users[i].username === name)
|
||||||
|
return users[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canDelete(name) {
|
||||||
|
const u = _findUser(name);
|
||||||
|
if (!u)
|
||||||
|
return false;
|
||||||
|
if (u.isAdmin && adminMembers.length <= 1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (refreshing)
|
||||||
|
return;
|
||||||
|
refreshing = true;
|
||||||
|
_detectAdminGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _detectAdminGroup() {
|
||||||
|
Proc.runCommand("usersService-detectGroup", ["sh", "-c", "getent group wheel >/dev/null && echo wheel || (getent group sudo >/dev/null && echo sudo || echo wheel)"], (output, exitCode) => {
|
||||||
|
const detected = (output || "").trim() || "wheel";
|
||||||
|
root.adminGroup = detected;
|
||||||
|
_loadAdminMembers();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _loadAdminMembers() {
|
||||||
|
Proc.runCommand("usersService-adminMembers", ["sh", "-c", "getent group " + root.adminGroup + " | awk -F: '{print $4}'"], (output, exitCode) => {
|
||||||
|
const members = (output || "").trim().split(",").map(s => s.trim()).filter(s => s.length > 0);
|
||||||
|
root.adminMembers = members;
|
||||||
|
_loadUsers();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _loadUsers() {
|
||||||
|
Proc.runCommand("usersService-loadUsers", ["sh", "-c", "getent passwd | awk -F: '$3>=1000 && $3<60000 && $1!=\"nobody\" {print $1\":\"$3\":\"$5\":\"$6\":\"$7}'"], (output, exitCode) => {
|
||||||
|
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
||||||
|
const list = [];
|
||||||
|
const adminSet = {};
|
||||||
|
for (let i = 0; i < root.adminMembers.length; i++)
|
||||||
|
adminSet[root.adminMembers[i]] = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const parts = lines[i].split(":");
|
||||||
|
if (parts.length < 5)
|
||||||
|
continue;
|
||||||
|
const username = parts[0];
|
||||||
|
list.push({
|
||||||
|
username,
|
||||||
|
uid: parseInt(parts[1], 10),
|
||||||
|
gecos: (parts[2] || "").split(",")[0],
|
||||||
|
home: parts[3] || "",
|
||||||
|
shell: parts[4] || "",
|
||||||
|
isAdmin: adminSet[username] === true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
list.sort((a, b) => a.username.localeCompare(b.username));
|
||||||
|
root.users = list;
|
||||||
|
root.refreshing = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUser(username, password, addToAdmin, callback) {
|
||||||
|
if (!isValidUsername(username)) {
|
||||||
|
_emit("create", username, false, I18n.tr("Invalid username"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!password || password.length < 1) {
|
||||||
|
_emit("create", username, false, I18n.tr("Password cannot be empty"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (userExists(username)) {
|
||||||
|
_emit("create", username, false, I18n.tr("User already exists"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_runUseradd(username, password, addToAdmin === true, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPassword(username, newPassword, callback) {
|
||||||
|
if (!isValidUsername(username) || !userExists(username)) {
|
||||||
|
_emit("passwd", username, false, I18n.tr("User not found"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newPassword || newPassword.length < 1) {
|
||||||
|
_emit("passwd", username, false, I18n.tr("Password cannot be empty"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_runChpasswd(username, newPassword, "passwd", callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUser(username, callback) {
|
||||||
|
if (!userExists(username)) {
|
||||||
|
_emit("delete", username, false, I18n.tr("User not found"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!canDelete(username)) {
|
||||||
|
_emit("delete", username, false, I18n.tr("Cannot delete the only administrator"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_runUserdel(username, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAdmin(username, makeAdmin, callback) {
|
||||||
|
if (!userExists(username)) {
|
||||||
|
_emit("admin", username, false, I18n.tr("User not found"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!makeAdmin) {
|
||||||
|
const u = _findUser(username);
|
||||||
|
if (u && u.isAdmin && root.adminMembers.length <= 1) {
|
||||||
|
_emit("admin", username, false, I18n.tr("Cannot remove the only administrator"), callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_runAdminToggle(username, makeAdmin === true, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _emit(op, username, success, message, callback) {
|
||||||
|
root.operationCompleted(op, username, success, message);
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
try {
|
||||||
|
callback(success, message);
|
||||||
|
} catch (e) {
|
||||||
|
log.warn("UsersService callback error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: useraddComp
|
||||||
|
Process {
|
||||||
|
id: useraddProc
|
||||||
|
property string targetUser: ""
|
||||||
|
property string targetPassword: ""
|
||||||
|
property bool addAdmin: false
|
||||||
|
property var cb: null
|
||||||
|
property string capturedErr: ""
|
||||||
|
running: false
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: useraddProc.capturedErr = text || ""
|
||||||
|
}
|
||||||
|
onExited: exitCode => {
|
||||||
|
const svc = root;
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
svc._emit("create", useraddProc.targetUser, false, (useraddProc.capturedErr || "").trim() || I18n.tr("useradd failed (exit %1)").arg(exitCode), useraddProc.cb);
|
||||||
|
Qt.callLater(() => useraddProc.destroy());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetUser = useraddProc.targetUser;
|
||||||
|
const targetPassword = useraddProc.targetPassword;
|
||||||
|
const addAdmin = useraddProc.addAdmin;
|
||||||
|
const outerCb = useraddProc.cb;
|
||||||
|
Qt.callLater(() => useraddProc.destroy());
|
||||||
|
|
||||||
|
svc._runChpasswd(targetUser, targetPassword, "create", (pwOk, pwMsg) => {
|
||||||
|
if (!pwOk) {
|
||||||
|
svc._emit("create", targetUser, false, pwMsg, outerCb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (addAdmin) {
|
||||||
|
svc._runAdminToggle(targetUser, true, (adminOk, adminMsg) => {
|
||||||
|
if (adminOk) {
|
||||||
|
svc._emit("create", targetUser, true, I18n.tr("User created with administrator privileges"), outerCb);
|
||||||
|
} else {
|
||||||
|
svc._emit("create", targetUser, false, adminMsg, outerCb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
svc._emit("create", targetUser, true, I18n.tr("User created"), outerCb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: chpasswdComp
|
||||||
|
Process {
|
||||||
|
id: chpasswdProc
|
||||||
|
property string targetUser: ""
|
||||||
|
property string targetPassword: ""
|
||||||
|
property string op: "passwd"
|
||||||
|
property var cb: null
|
||||||
|
property string capturedErr: ""
|
||||||
|
command: ["pkexec", "sh", "-c", "head -n1 | chpasswd"]
|
||||||
|
stdinEnabled: true
|
||||||
|
running: false
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: chpasswdProc.capturedErr = text || ""
|
||||||
|
}
|
||||||
|
onStarted: {
|
||||||
|
chpasswdProc.write(chpasswdProc.targetUser + ":" + chpasswdProc.targetPassword + "\n");
|
||||||
|
}
|
||||||
|
onExited: exitCode => {
|
||||||
|
const op = chpasswdProc.op;
|
||||||
|
const targetUser = chpasswdProc.targetUser;
|
||||||
|
const cb = chpasswdProc.cb;
|
||||||
|
const err = (chpasswdProc.capturedErr || "").trim();
|
||||||
|
Qt.callLater(() => chpasswdProc.destroy());
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
const msg = err || I18n.tr("Password change failed (exit %1)").arg(exitCode);
|
||||||
|
if (op === "create") {
|
||||||
|
if (typeof cb === "function")
|
||||||
|
cb(false, msg);
|
||||||
|
} else {
|
||||||
|
root._emit("passwd", targetUser, false, msg, cb);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root.refresh();
|
||||||
|
if (op === "create") {
|
||||||
|
if (typeof cb === "function")
|
||||||
|
cb(true, I18n.tr("Password set"));
|
||||||
|
} else {
|
||||||
|
root._emit("passwd", targetUser, true, I18n.tr("Password updated"), cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: userdelComp
|
||||||
|
Process {
|
||||||
|
id: userdelProc
|
||||||
|
property string targetUser: ""
|
||||||
|
property var cb: null
|
||||||
|
property string capturedErr: ""
|
||||||
|
running: false
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: userdelProc.capturedErr = text || ""
|
||||||
|
}
|
||||||
|
onExited: exitCode => {
|
||||||
|
const targetUser = userdelProc.targetUser;
|
||||||
|
const cb = userdelProc.cb;
|
||||||
|
const err = (userdelProc.capturedErr || "").trim();
|
||||||
|
Qt.callLater(() => userdelProc.destroy());
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root._emit("delete", targetUser, false, err || I18n.tr("userdel failed (exit %1)").arg(exitCode), cb);
|
||||||
|
} else {
|
||||||
|
root.refresh();
|
||||||
|
root._emit("delete", targetUser, true, I18n.tr("User deleted"), cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: adminToggleComp
|
||||||
|
Process {
|
||||||
|
id: adminToggleProc
|
||||||
|
property string targetUser: ""
|
||||||
|
property bool makeAdmin: false
|
||||||
|
property var cb: null
|
||||||
|
property string capturedErr: ""
|
||||||
|
running: false
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: adminToggleProc.capturedErr = text || ""
|
||||||
|
}
|
||||||
|
onExited: exitCode => {
|
||||||
|
const targetUser = adminToggleProc.targetUser;
|
||||||
|
const makeAdmin = adminToggleProc.makeAdmin;
|
||||||
|
const cb = adminToggleProc.cb;
|
||||||
|
const err = (adminToggleProc.capturedErr || "").trim();
|
||||||
|
Qt.callLater(() => adminToggleProc.destroy());
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root._emit("admin", targetUser, false, err || I18n.tr("usermod failed (exit %1)").arg(exitCode), cb);
|
||||||
|
} else {
|
||||||
|
root.refresh();
|
||||||
|
root._emit("admin", targetUser, true, makeAdmin ? I18n.tr("Granted administrator privileges") : I18n.tr("Removed administrator privileges"), cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _runUseradd(username, password, addToAdmin, callback) {
|
||||||
|
const proc = useraddComp.createObject(root, {
|
||||||
|
command: ["pkexec", "useradd", "-m", "-s", "/bin/bash", username],
|
||||||
|
targetUser: username,
|
||||||
|
targetPassword: password,
|
||||||
|
addAdmin: addToAdmin,
|
||||||
|
cb: callback
|
||||||
|
});
|
||||||
|
proc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _runChpasswd(username, password, op, callback) {
|
||||||
|
const proc = chpasswdComp.createObject(root, {
|
||||||
|
targetUser: username,
|
||||||
|
targetPassword: password,
|
||||||
|
op: op,
|
||||||
|
cb: callback
|
||||||
|
});
|
||||||
|
proc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _runUserdel(username, callback) {
|
||||||
|
const proc = userdelComp.createObject(root, {
|
||||||
|
command: ["pkexec", "userdel", "-r", username],
|
||||||
|
targetUser: username,
|
||||||
|
cb: callback
|
||||||
|
});
|
||||||
|
proc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _runAdminToggle(username, makeAdmin, callback) {
|
||||||
|
const cmd = makeAdmin ? ["pkexec", "usermod", "-aG", root.adminGroup, username] : ["pkexec", "gpasswd", "-d", username, root.adminGroup];
|
||||||
|
const proc = adminToggleComp.createObject(root, {
|
||||||
|
command: cmd,
|
||||||
|
targetUser: username,
|
||||||
|
makeAdmin: makeAdmin,
|
||||||
|
cb: callback
|
||||||
|
});
|
||||||
|
proc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: refresh()
|
||||||
|
}
|
||||||
@@ -345,4 +345,13 @@ Singleton {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionService
|
||||||
|
|
||||||
|
function onSessionResumed() {
|
||||||
|
log.info("Session resumed, re-requesting output state, current outputs:", outputs.length);
|
||||||
|
requestState();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property Item target
|
||||||
|
property real minOpacity: 0.3
|
||||||
|
property int pulseDuration: 600
|
||||||
|
|
||||||
|
loops: Animation.Infinite
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: root.target
|
||||||
|
property: "opacity"
|
||||||
|
to: root.minOpacity
|
||||||
|
duration: root.pulseDuration
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: root.target
|
||||||
|
property: "opacity"
|
||||||
|
to: 1.0
|
||||||
|
duration: root.pulseDuration
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
|
||||||
|
onStopped: if (root.target) root.target.opacity = 1.0
|
||||||
|
}
|
||||||
@@ -5807,6 +5807,28 @@
|
|||||||
],
|
],
|
||||||
"description": "Use smaller notification cards"
|
"description": "Use smaller notification cards"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "notificationDedupeEnabled",
|
||||||
|
"label": "Suppress Duplicate Notifications",
|
||||||
|
"tabIndex": 17,
|
||||||
|
"category": "Notifications",
|
||||||
|
"keywords": [
|
||||||
|
"alert",
|
||||||
|
"alerts",
|
||||||
|
"coalesce",
|
||||||
|
"dedupe",
|
||||||
|
"duplicate",
|
||||||
|
"duplicates",
|
||||||
|
"messages",
|
||||||
|
"notif",
|
||||||
|
"notification",
|
||||||
|
"notifications",
|
||||||
|
"repeat",
|
||||||
|
"stack",
|
||||||
|
"toast"
|
||||||
|
],
|
||||||
|
"description": "Control whether identical alerts stack or show as a single popup"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "notificationHistorySaveCritical",
|
"section": "notificationHistorySaveCritical",
|
||||||
"label": "Critical Priority",
|
"label": "Critical Priority",
|
||||||
|
|||||||
Reference in New Issue
Block a user