mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 11:38:30 -04:00
Compare commits
35 Commits
fa629fbf99
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 11287459c3 | |||
| ef191babb7 | |||
| fe64a342f9 | |||
| e54be7d12d | |||
| 81c886784b | |||
| 6682bb120c | |||
| 5c02ec4789 | |||
| de1e1757c3 | |||
| 43d331d6cf | |||
| 01832856d4 | |||
| 0033e3f0e0 | |||
| 2d3706321a | |||
| 5bb884db57 | |||
| b42763ccbf | |||
| f2a6d62d65 | |||
| f66693df6b | |||
| a710d6d7cc | |||
| 64461c534f | |||
| 73da4879f6 | |||
| 8594a414ec | |||
| df396bfa43 | |||
| 33677150b1 | |||
| c67b185076 | |||
| bab2078dfd | |||
| e9bc0169f6 | |||
| 59c83d19e4 | |||
| 9b7d3c64fe | |||
| c367153bac | |||
| 1f94c3cbd4 | |||
| 564583951c | |||
| eed3617a0d | |||
| 24cb3d19a0 | |||
| 3182c70857 | |||
| 8bb7396362 | |||
| 62648ed75a |
+1
-1
@@ -69,7 +69,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AvengeMedia/dankgo v0.0.0-20260724133713-a4ef23371e05
|
||||
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/AvengeMedia/dankgo v0.0.0-20260724133713-a4ef23371e05 h1:Ij/yzOT8y2HL7V5Rkec1GxJV0rUvhKqfAzyGxVRTk1o=
|
||||
github.com/AvengeMedia/dankgo v0.0.0-20260724133713-a4ef23371e05/go.mod h1:xt8RldAfti0QCWidwYIzsSSoJWsE61WgEhTu2H9UpD4=
|
||||
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b h1:UwX1H4BkzazL7ips9ljnHzBXGttYARcIi5Njj9aqIt4=
|
||||
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b/go.mod h1:xt8RldAfti0QCWidwYIzsSSoJWsE61WgEhTu2H9UpD4=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -224,6 +225,44 @@ func (h *HyprlandProvider) validateAction(action string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var luaExprActionPattern = regexp.MustCompile(`^(function\s*\(|hl\.)`)
|
||||
|
||||
// isRawLuaActionText reports that action is a Lua expression to re-emit
|
||||
// verbatim rather than freeform dispatcher text to wrap for hyprctl. The
|
||||
// balance check keeps malformed input from corrupting the generated file.
|
||||
func isRawLuaActionText(action string) bool {
|
||||
if !luaExprActionPattern.MatchString(action) {
|
||||
return false
|
||||
}
|
||||
depth := 0
|
||||
var quote byte
|
||||
escaped := false
|
||||
for i := 0; i < len(action); i++ {
|
||||
c := action[i]
|
||||
switch {
|
||||
case escaped:
|
||||
escaped = false
|
||||
case quote != 0:
|
||||
switch c {
|
||||
case '\\':
|
||||
escaped = true
|
||||
case quote:
|
||||
quote = 0
|
||||
}
|
||||
case c == '"' || c == '\'':
|
||||
quote = c
|
||||
case c == '(':
|
||||
depth++
|
||||
case c == ')':
|
||||
depth--
|
||||
if depth < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return depth == 0 && quote == 0 && !escaped
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error {
|
||||
if err := h.ensureWritableConfig(); err != nil {
|
||||
return err
|
||||
@@ -254,11 +293,12 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
|
||||
canonicalKey := canonicalHyprlandOverrideKey(key)
|
||||
normalizedKey := hyprlandOverrideMapKey(canonicalKey)
|
||||
existingBinds[normalizedKey] = &hyprlandOverrideBind{
|
||||
Key: canonicalKey,
|
||||
Action: action,
|
||||
Description: description,
|
||||
Flags: flags,
|
||||
Options: options,
|
||||
Key: canonicalKey,
|
||||
Action: action,
|
||||
Description: description,
|
||||
Flags: flags,
|
||||
Options: options,
|
||||
RawLuaAction: isRawLuaActionText(action),
|
||||
}
|
||||
|
||||
return h.writeOverrideBinds(existingBinds)
|
||||
|
||||
@@ -463,6 +463,56 @@ func TestHyprlandSetBindLeavesConfOnlyInstallReadOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRawLuaActionText(t *testing.T) {
|
||||
cases := []struct {
|
||||
action string
|
||||
want bool
|
||||
}{
|
||||
{`function() hl.plugin.scrolloverview.overview("toggle") end`, true},
|
||||
{`hl.dsp.exec_cmd("foo")`, true},
|
||||
{`hl.dsp.no_op()`, true},
|
||||
{"workspace 3", false},
|
||||
{"exec zeditor", false},
|
||||
{`function() hl.foo( end`, false},
|
||||
{`function() hl.foo("a) end`, false},
|
||||
{`hl.foo()) hl.bar((`, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := isRawLuaActionText(tc.action); got != tc.want {
|
||||
t.Errorf("isRawLuaActionText(%q) = %v, want %v", tc.action, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandSetBindPreservesRawLuaAction(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dmsDir := filepath.Join(tmpDir, "dms")
|
||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte("-- DMS user keybind overrides\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
provider := NewHyprlandProvider(tmpDir)
|
||||
rawAction := `function() hl.plugin.scrolloverview.overview("toggle") end`
|
||||
if err := provider.SetBind("SUPER + G", rawAction, "Toggle overview", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(data)
|
||||
if !strings.Contains(got, `hl.bind("SUPER + G", `+rawAction) {
|
||||
t.Fatalf("expected raw Lua action to be written verbatim, got:\n%s", got)
|
||||
}
|
||||
if strings.Contains(got, "hyprctl dispatch function") {
|
||||
t.Fatalf("expected raw Lua action to not be wrapped in hyprctl dispatch, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandSetBindUpdatesSpacedLuaOverrideWithoutDuplicates(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dmsDir := filepath.Join(tmpDir, "dms")
|
||||
|
||||
@@ -471,12 +471,9 @@ output_path = '%s'
|
||||
case TemplateKindTerminal:
|
||||
appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||
case TemplateKindVSCode:
|
||||
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
|
||||
for _, editor := range vscodeEditors {
|
||||
appendVSCodeConfig(cfgFile, editor.name, editor.extensionsDir(homeDir), opts.ShellDir)
|
||||
}
|
||||
case TemplateKindEmacs:
|
||||
if utils.EmacsConfigDir() != "" {
|
||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
|
||||
@@ -633,6 +630,23 @@ func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []stri
|
||||
return false
|
||||
}
|
||||
|
||||
type vscodeEditor struct {
|
||||
name string
|
||||
dataDir string
|
||||
}
|
||||
|
||||
var vscodeEditors = []vscodeEditor{
|
||||
{"vscode", ".vscode"},
|
||||
{"codium", ".vscode-oss"},
|
||||
{"cursor", ".cursor"},
|
||||
{"windsurf", ".windsurf"},
|
||||
{"vscode-insiders", ".vscode-insiders"},
|
||||
}
|
||||
|
||||
func (e vscodeEditor) extensionsDir(homeDir string) string {
|
||||
return filepath.Join(homeDir, e.dataDir, "extensions")
|
||||
}
|
||||
|
||||
func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) {
|
||||
pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*")
|
||||
matches, err := filepath.Glob(pattern)
|
||||
@@ -968,11 +982,32 @@ func applyKDEColorScheme(mode ColorMode) {
|
||||
}
|
||||
}
|
||||
|
||||
func gtkThemeInstalled(theme string) bool {
|
||||
home, _ := os.UserHomeDir()
|
||||
candidates := []string{
|
||||
filepath.Join(home, ".local/share/themes", theme),
|
||||
filepath.Join(home, ".themes", theme),
|
||||
filepath.Join("/usr/share/themes", theme),
|
||||
filepath.Join("/usr/local/share/themes", theme),
|
||||
}
|
||||
for _, dir := range candidates {
|
||||
if info, err := os.Stat(dir); err == nil && info.IsDir() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func refreshGTKTheme(mode ColorMode) {
|
||||
theme := mode.GTKTheme()
|
||||
if !gtkThemeInstalled(theme) {
|
||||
log.Infof("Skipping gtk-theme refresh: %s is not installed", theme)
|
||||
return
|
||||
}
|
||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
|
||||
log.Warnf("Failed to reset gtk-theme: %v", err)
|
||||
}
|
||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()); err != nil {
|
||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", theme); err != nil {
|
||||
log.Warnf("Failed to set gtk-theme: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1147,16 +1182,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
|
||||
}
|
||||
|
||||
func checkVSCodeExtension(homeDir string) bool {
|
||||
extDirs := []string{
|
||||
filepath.Join(homeDir, ".vscode/extensions"),
|
||||
filepath.Join(homeDir, ".vscode-oss/extensions"),
|
||||
filepath.Join(homeDir, ".config/Code - OSS/extensions"),
|
||||
filepath.Join(homeDir, ".cursor/extensions"),
|
||||
filepath.Join(homeDir, ".windsurf/extensions"),
|
||||
}
|
||||
|
||||
for _, extDir := range extDirs {
|
||||
pattern := filepath.Join(extDir, "danklinux.dms-theme-*")
|
||||
for _, editor := range vscodeEditors {
|
||||
pattern := filepath.Join(editor.extensionsDir(homeDir), "danklinux.dms-theme-*")
|
||||
if matches, err := filepath.Glob(pattern); err == nil && len(matches) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -22,6 +22,16 @@ func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
|
||||
if !strings.Contains(content, "Keys.onPressed") || !strings.Contains(content, "event.text") {
|
||||
t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control")
|
||||
}
|
||||
|
||||
// Wayland IMEs commit unconsumed printable keys as text-input text rather
|
||||
// than forwarding raw keys, so the lock screen needs an IME commit sink
|
||||
// alongside raw key handling.
|
||||
if !strings.Contains(content, "id: imeCommitSink") {
|
||||
t.Fatalf("passwordField must keep the imeCommitSink TextInput so IME-routed keyboards can type (#2950)")
|
||||
}
|
||||
if !strings.Contains(content, "Qt.ImhSensitiveData") {
|
||||
t.Fatalf("imeCommitSink must advertise hidden-text hints so IMEs treat it as a password field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockScreenPamSupportsManagedAndSystemPolicies(t *testing.T) {
|
||||
|
||||
@@ -229,6 +229,9 @@ func (m *Manager) snapshotState() CUPSState {
|
||||
func (m *Manager) Subscribe(id string) chan CUPSState {
|
||||
ch := make(chan CUPSState, 64)
|
||||
|
||||
m.subLifecycleMu.Lock()
|
||||
defer m.subLifecycleMu.Unlock()
|
||||
|
||||
wasEmpty := true
|
||||
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
|
||||
wasEmpty = false
|
||||
@@ -237,19 +240,25 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
|
||||
|
||||
m.subscribers.Store(id, ch)
|
||||
|
||||
if wasEmpty && m.subscription != nil {
|
||||
if err := m.subscription.Start(); err != nil {
|
||||
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
|
||||
} else {
|
||||
m.eventWG.Add(1)
|
||||
go m.eventHandler()
|
||||
}
|
||||
if !wasEmpty || m.subscription == nil {
|
||||
return ch
|
||||
}
|
||||
|
||||
if err := m.subscription.Start(); err != nil {
|
||||
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
|
||||
return ch
|
||||
}
|
||||
|
||||
m.eventWG.Add(1)
|
||||
go m.eventHandler()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
m.subLifecycleMu.Lock()
|
||||
defer m.subLifecycleMu.Unlock()
|
||||
|
||||
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||
close(val)
|
||||
}
|
||||
@@ -260,18 +269,22 @@ func (m *Manager) Unsubscribe(id string) {
|
||||
return false
|
||||
})
|
||||
|
||||
if isEmpty && m.subscription != nil {
|
||||
m.subscription.Stop()
|
||||
m.eventWG.Wait()
|
||||
if !isEmpty || m.subscription == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.subscription.Stop()
|
||||
m.eventWG.Wait()
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
close(m.stopChan)
|
||||
|
||||
m.subLifecycleMu.Lock()
|
||||
if m.subscription != nil {
|
||||
m.subscription.Stop()
|
||||
}
|
||||
m.subLifecycleMu.Unlock()
|
||||
|
||||
m.eventWG.Wait()
|
||||
m.notifierWg.Wait()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
|
||||
@@ -75,6 +78,67 @@ func TestManager_Subscribe(t *testing.T) {
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
// mirrors the real managers: eventChan guarded by mu, conn/running deliberately
|
||||
// unsynchronized so overlapping Start/Stop trips the race detector
|
||||
type stubSubscription struct {
|
||||
mu sync.Mutex
|
||||
events chan SubscriptionEvent
|
||||
conn *int
|
||||
running bool
|
||||
}
|
||||
|
||||
func (s *stubSubscription) Start() error {
|
||||
if s.running {
|
||||
return errors.New("already running")
|
||||
}
|
||||
s.running = true
|
||||
|
||||
s.mu.Lock()
|
||||
s.events = make(chan SubscriptionEvent)
|
||||
s.mu.Unlock()
|
||||
|
||||
v := 0
|
||||
s.conn = &v
|
||||
*s.conn++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubSubscription) Stop() {
|
||||
if !s.running {
|
||||
return
|
||||
}
|
||||
s.running = false
|
||||
s.conn = nil
|
||||
|
||||
s.mu.Lock()
|
||||
close(s.events)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *stubSubscription) Events() <-chan SubscriptionEvent {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.events
|
||||
}
|
||||
|
||||
func TestManager_SubscribeUnsubscribeRace(t *testing.T) {
|
||||
m := NewTestManager(mocks_cups.NewMockCUPSClientInterface(t), nil)
|
||||
m.subscription = &stubSubscription{}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range 8 {
|
||||
wg.Go(func() {
|
||||
id := fmt.Sprintf("client-%d", i)
|
||||
for range 50 {
|
||||
m.Subscribe(id)
|
||||
m.Unsubscribe(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestManager_Close(t *testing.T) {
|
||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ type Manager struct {
|
||||
client CUPSClientInterface
|
||||
pkHelper PkHelper
|
||||
subscription SubscriptionManagerInterface
|
||||
subLifecycleMu sync.Mutex
|
||||
stateMutex sync.RWMutex
|
||||
subscribers syncmap.Map[string, chan CUPSState]
|
||||
stopChan chan struct{}
|
||||
|
||||
@@ -302,17 +302,17 @@ func (a *SecretAgent) GetSecrets(
|
||||
}
|
||||
a.backend.cachedVPNCredsMu.Unlock()
|
||||
|
||||
a.backend.cachedGPSamlMu.Lock()
|
||||
cachedGPSaml := a.backend.cachedGPSamlCookie
|
||||
if cachedGPSaml != nil && cachedGPSaml.ConnectionUUID == connUuid {
|
||||
a.backend.cachedGPSamlCookie = nil
|
||||
a.backend.cachedGPSamlMu.Unlock()
|
||||
a.backend.cachedOpenConnectMu.Lock()
|
||||
cachedOpenConnect := a.backend.cachedOpenConnectAuth
|
||||
if cachedOpenConnect != nil && cachedOpenConnect.ConnectionUUID == connUuid {
|
||||
a.backend.cachedOpenConnectAuth = nil
|
||||
a.backend.cachedOpenConnectMu.Unlock()
|
||||
|
||||
log.Infof("[SecretAgent] Using cached GlobalProtect SAML cookie for %s", connUuid)
|
||||
log.Infof("[SecretAgent] Using cached OpenConnect authentication for %s", connUuid)
|
||||
|
||||
return buildGPSamlSecretsResponse(settingName, cachedGPSaml.Cookie, cachedGPSaml.Host, cachedGPSaml.Fingerprint), nil
|
||||
return buildOpenConnectSecretsResponse(settingName, cachedOpenConnect.Cookie, cachedOpenConnect.Host, cachedOpenConnect.Fingerprint), nil
|
||||
}
|
||||
a.backend.cachedGPSamlMu.Unlock()
|
||||
a.backend.cachedOpenConnectMu.Unlock()
|
||||
|
||||
if len(fields) == 1 && fields[0] == "gp-saml" {
|
||||
gateway := ""
|
||||
@@ -347,17 +347,17 @@ func (a *SecretAgent) GetSecrets(
|
||||
|
||||
log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager")
|
||||
|
||||
a.backend.cachedGPSamlMu.Lock()
|
||||
a.backend.cachedGPSamlCookie = &cachedGPSamlCookie{
|
||||
a.backend.cachedOpenConnectMu.Lock()
|
||||
a.backend.cachedOpenConnectAuth = &cachedOpenConnectAuth{
|
||||
ConnectionUUID: connUuid,
|
||||
Cookie: authResult.Cookie,
|
||||
Host: authResult.Host,
|
||||
User: authResult.User,
|
||||
Fingerprint: authResult.Fingerprint,
|
||||
}
|
||||
a.backend.cachedGPSamlMu.Unlock()
|
||||
a.backend.cachedOpenConnectMu.Unlock()
|
||||
|
||||
return buildGPSamlSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
|
||||
return buildOpenConnectSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,7 +987,7 @@ func buildWiFiSecretsResponse(settingName string, secrets map[string]string) nmS
|
||||
return out
|
||||
}
|
||||
|
||||
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
|
||||
func buildOpenConnectSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
|
||||
out := nmSettingMap{}
|
||||
vpnSec := nmVariantMap{}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ func TestNeedsExternalBrowserAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGPSamlSecretsResponse(t *testing.T) {
|
||||
func TestBuildOpenConnectSecretsResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
settingName string
|
||||
@@ -155,7 +155,7 @@ func TestBuildGPSamlSecretsResponse(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := buildGPSamlSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint)
|
||||
result := buildOpenConnectSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint)
|
||||
|
||||
assert.NotNil(t, result)
|
||||
assert.Contains(t, result, tt.settingName)
|
||||
|
||||
@@ -80,16 +80,16 @@ type NetworkManagerBackend struct {
|
||||
|
||||
hotspotPendingDevice string
|
||||
|
||||
pendingVPNSave *pendingVPNCredentials
|
||||
pendingVPNSaveMu sync.Mutex
|
||||
cachedVPNCreds *cachedVPNCredentials
|
||||
cachedVPNCredsMu sync.Mutex
|
||||
cachedPKCS11PIN *cachedPKCS11PIN
|
||||
cachedPKCS11Mu sync.Mutex
|
||||
cachedGPSamlCookie *cachedGPSamlCookie
|
||||
cachedGPSamlMu sync.Mutex
|
||||
cachedWiFiSecret *cachedWiFiSecret
|
||||
cachedWiFiSecretMu sync.Mutex
|
||||
pendingVPNSave *pendingVPNCredentials
|
||||
pendingVPNSaveMu sync.Mutex
|
||||
cachedVPNCreds *cachedVPNCredentials
|
||||
cachedVPNCredsMu sync.Mutex
|
||||
cachedPKCS11PIN *cachedPKCS11PIN
|
||||
cachedPKCS11Mu sync.Mutex
|
||||
cachedOpenConnectAuth *cachedOpenConnectAuth
|
||||
cachedOpenConnectMu sync.Mutex
|
||||
cachedWiFiSecret *cachedWiFiSecret
|
||||
cachedWiFiSecretMu sync.Mutex
|
||||
|
||||
onStateChange func()
|
||||
}
|
||||
@@ -100,8 +100,9 @@ type pendingVPNCredentials struct {
|
||||
Password string
|
||||
// Secrets holds all VPN secret fields keyed by name (e.g. "cert-pass");
|
||||
// falls back to Password under the "password" key when empty.
|
||||
Secrets map[string]string
|
||||
SavePassword bool
|
||||
Secrets map[string]string
|
||||
PersistentSecrets map[string]string
|
||||
SavePassword bool
|
||||
}
|
||||
|
||||
type cachedVPNCredentials struct {
|
||||
@@ -124,7 +125,7 @@ type cachedWiFiSecret struct {
|
||||
Secrets map[string]string
|
||||
}
|
||||
|
||||
type cachedGPSamlCookie struct {
|
||||
type cachedOpenConnectAuth struct {
|
||||
ConnectionUUID string
|
||||
Cookie string
|
||||
Host string
|
||||
|
||||
@@ -3,6 +3,7 @@ package network
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -10,16 +11,29 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
type gpSamlAuthResult struct {
|
||||
type openConnectAuthResult struct {
|
||||
Cookie string
|
||||
Host string
|
||||
User string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
type openConnectAuthError struct {
|
||||
cause error
|
||||
serverCert string
|
||||
}
|
||||
|
||||
func (e *openConnectAuthError) Error() string {
|
||||
return fmt.Sprintf("openconnect --authenticate failed: %v", e.cause)
|
||||
}
|
||||
|
||||
func (e *openConnectAuthError) Unwrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// runGlobalProtectSAMLAuth handles GlobalProtect SAML/SSO authentication using gp-saml-gui.
|
||||
// Only supports protocol=gp. Other protocols need their own implementations.
|
||||
func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*gpSamlAuthResult, error) {
|
||||
func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*openConnectAuthResult, error) {
|
||||
if gateway == "" {
|
||||
return nil, fmt.Errorf("GP SAML auth: gateway is empty")
|
||||
}
|
||||
@@ -63,7 +77,7 @@ func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, ga
|
||||
}
|
||||
}()
|
||||
|
||||
result := &gpSamlAuthResult{Host: gateway}
|
||||
result := &openConnectAuthResult{Host: gateway}
|
||||
var allOutput []string
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
@@ -117,13 +131,8 @@ func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, ga
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*gpSamlAuthResult, error) {
|
||||
ocPath, err := exec.LookPath("openconnect")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openconnect not found: %w", err)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*openConnectAuthResult, error) {
|
||||
return runOpenConnectAuthenticate(ctx, []string{
|
||||
"--protocol=gp",
|
||||
"--usergroup=gateway:prelogin-cookie",
|
||||
"--user=" + user,
|
||||
@@ -131,18 +140,83 @@ func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user
|
||||
"--allow-insecure-crypto",
|
||||
"--authenticate",
|
||||
gateway,
|
||||
}, preloginCookie)
|
||||
}
|
||||
|
||||
func runOpenConnectPasswordAuth(
|
||||
ctx context.Context,
|
||||
data map[string]string,
|
||||
username, password, serverCert string,
|
||||
) (*openConnectAuthResult, error) {
|
||||
if data["protocol"] != "fortinet" {
|
||||
return nil, fmt.Errorf("only Fortinet password authentication is supported")
|
||||
}
|
||||
gateway := data["gateway"]
|
||||
if gateway == "" {
|
||||
return nil, fmt.Errorf("OpenConnect gateway is empty")
|
||||
}
|
||||
if username == "" || password == "" {
|
||||
return nil, fmt.Errorf("OpenConnect username and password are required")
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--protocol=fortinet",
|
||||
"--user=" + username,
|
||||
"--passwd-on-stdin",
|
||||
"--non-inter",
|
||||
}
|
||||
if usergroup := data["usergroup"]; usergroup != "" {
|
||||
args = append(args, "--usergroup="+usergroup)
|
||||
}
|
||||
if serverCert != "" {
|
||||
args = append(args, "--servercert="+serverCert)
|
||||
}
|
||||
args = append(args, "--authenticate", gateway)
|
||||
|
||||
result, err := runOpenConnectAuthenticate(ctx, args, password)
|
||||
if err == nil {
|
||||
result.Host = gateway
|
||||
if result.Fingerprint == "" {
|
||||
result.Fingerprint = serverCert
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func runOpenConnectAuthenticate(ctx context.Context, args []string, secret string) (*openConnectAuthResult, error) {
|
||||
ocPath, err := exec.LookPath("openconnect")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openconnect not found: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, ocPath, args...)
|
||||
cmd.Stdin = strings.NewReader(preloginCookie)
|
||||
cmd.Stdin = strings.NewReader(secret + "\n")
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
result := parseOpenConnectAuthenticateOutput(string(output))
|
||||
serverCert := suggestedOpenConnectServerCert(string(output))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openconnect --authenticate failed: %w\noutput: %s", err, string(output))
|
||||
if ctx.Err() != nil {
|
||||
return nil, fmt.Errorf("openconnect authentication timed out or was cancelled: %w", ctx.Err())
|
||||
}
|
||||
return nil, &openConnectAuthError{cause: err, serverCert: serverCert}
|
||||
}
|
||||
if result.Cookie == "" {
|
||||
return nil, &openConnectAuthError{
|
||||
cause: errors.New("no COOKIE in command output"),
|
||||
serverCert: serverCert,
|
||||
}
|
||||
}
|
||||
|
||||
result := &gpSamlAuthResult{}
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
log.Infof("[OpenConnect] Authentication successful: cookie_len=%d, host=%s, has_fingerprint=%v",
|
||||
len(result.Cookie), result.Host, result.Fingerprint != "")
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseOpenConnectAuthenticateOutput(output string) *openConnectAuthResult {
|
||||
result := &openConnectAuthResult{}
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "COOKIE="):
|
||||
@@ -158,15 +232,7 @@ func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.Cookie == "" {
|
||||
return nil, fmt.Errorf("no COOKIE in openconnect --authenticate output: %s", string(output))
|
||||
}
|
||||
|
||||
log.Infof("[GP-SAML] openconnect --authenticate: cookie_len=%d, host=%s, fingerprint=%s",
|
||||
len(result.Cookie), result.Host, result.Fingerprint)
|
||||
|
||||
return result, nil
|
||||
return result
|
||||
}
|
||||
|
||||
func unshellQuote(s string) string {
|
||||
@@ -179,7 +245,7 @@ func unshellQuote(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func parseGPSamlFromCommandLine(line string, result *gpSamlAuthResult) {
|
||||
func parseGPSamlFromCommandLine(line string, result *openConnectAuthResult) {
|
||||
if !strings.Contains(line, "openconnect") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -71,7 +75,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
initialResult *gpSamlAuthResult
|
||||
initialResult *openConnectAuthResult
|
||||
expectedCookie string
|
||||
expectedUser string
|
||||
expectedFP string
|
||||
@@ -79,7 +83,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "full openconnect command",
|
||||
line: "openconnect --protocol=gp --cookie=AUTH123 --servercert=pin-sha256:ABC --user=john",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "AUTH123",
|
||||
expectedUser: "john",
|
||||
expectedFP: "pin-sha256:ABC",
|
||||
@@ -87,7 +91,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "with equals signs in cookie",
|
||||
line: "openconnect --cookie=authcookie=xyz123&portal=GATE --user=jane",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "authcookie=xyz123&portal=GATE",
|
||||
expectedUser: "jane",
|
||||
expectedFP: "",
|
||||
@@ -95,7 +99,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "non-openconnect line",
|
||||
line: "some other output",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "",
|
||||
expectedUser: "",
|
||||
expectedFP: "",
|
||||
@@ -103,7 +107,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "preserves existing values",
|
||||
line: "openconnect --user=newuser",
|
||||
initialResult: &gpSamlAuthResult{Cookie: "existing", Fingerprint: "existing-fp"},
|
||||
initialResult: &openConnectAuthResult{Cookie: "existing", Fingerprint: "existing-fp"},
|
||||
expectedCookie: "existing",
|
||||
expectedUser: "newuser",
|
||||
expectedFP: "existing-fp",
|
||||
@@ -111,7 +115,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "only updates empty fields",
|
||||
line: "openconnect --cookie=NEW --user=NEW",
|
||||
initialResult: &gpSamlAuthResult{Cookie: "OLD"},
|
||||
initialResult: &openConnectAuthResult{Cookie: "OLD"},
|
||||
expectedCookie: "OLD",
|
||||
expectedUser: "NEW",
|
||||
expectedFP: "",
|
||||
@@ -119,7 +123,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "real gp-saml-gui output",
|
||||
line: "openconnect --protocol=gp --user=john.doe@example.com --os=linux-64 --usergroup=gateway:prelogin-cookie --passwd-on-stdin",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "",
|
||||
expectedUser: "john.doe@example.com",
|
||||
expectedFP: "",
|
||||
@@ -127,7 +131,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "with server cert flag",
|
||||
line: "openconnect --servercert=pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE= vpn.example.com",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "",
|
||||
expectedUser: "",
|
||||
expectedFP: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
|
||||
@@ -158,7 +162,7 @@ func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
|
||||
"",
|
||||
}
|
||||
|
||||
result := &gpSamlAuthResult{}
|
||||
result := &openConnectAuthResult{}
|
||||
for _, line := range lines {
|
||||
parseGPSamlFromCommandLine(line, result)
|
||||
}
|
||||
@@ -167,3 +171,19 @@ func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
|
||||
assert.Empty(t, result.Cookie, "cookie should not be parsed from command line")
|
||||
assert.Empty(t, result.Fingerprint)
|
||||
}
|
||||
|
||||
func TestRunOpenConnectAuthenticateSanitizesFailure(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
openConnectPath := filepath.Join(binDir, "openconnect")
|
||||
script := "#!/bin/sh\nprintf '%s\\n' 'Cookie: should-not-leak' 'Add --servercert pin-sha256:TEST-FINGERPRINT' >&2\nexit 1\n"
|
||||
assert.NoError(t, os.WriteFile(openConnectPath, []byte(script), 0o755))
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
_, err := runOpenConnectAuthenticate(context.Background(), []string{"--authenticate", "vpn.example.test"}, "password")
|
||||
assert.Error(t, err)
|
||||
assert.NotContains(t, err.Error(), "should-not-leak")
|
||||
|
||||
var authErr *openConnectAuthError
|
||||
assert.True(t, errors.As(err, &authErr))
|
||||
assert.Equal(t, "pin-sha256:TEST-FINGERPRINT", authErr.serverCert)
|
||||
}
|
||||
|
||||
@@ -326,6 +326,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
}
|
||||
|
||||
authAction := detectVPNAuthAction(vpnServiceType, vpnData)
|
||||
var openConnectAuth *openConnectAuthResult
|
||||
|
||||
switch authAction {
|
||||
case "openvpn_username":
|
||||
@@ -335,6 +336,19 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
|
||||
return err
|
||||
}
|
||||
case "openconnect_password":
|
||||
if err := b.ensureOpenConnectAgentFlags(targetConn, vpnData); err != nil {
|
||||
return fmt.Errorf("failed to prepare OpenConnect connection: %w", err)
|
||||
}
|
||||
|
||||
authCtx, authCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
openConnectAuth, err = b.handleOpenConnectPasswordAuth(
|
||||
authCtx, targetConn, connName, targetUUID, vpnServiceType, vpnData,
|
||||
)
|
||||
authCancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenConnect authentication failed: %w", err)
|
||||
}
|
||||
case "gp_saml":
|
||||
gateway := vpnData["gateway"]
|
||||
protocol := vpnData["protocol"]
|
||||
@@ -345,7 +359,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
log.Infof("[ConnectVPN] GlobalProtect SAML/SSO authentication required for %s (gateway=%s)", connName, gateway)
|
||||
|
||||
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
authResult, err := b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
|
||||
openConnectAuth, err = b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
|
||||
samlCancel()
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
@@ -363,16 +377,6 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
}
|
||||
}
|
||||
|
||||
b.cachedGPSamlMu.Lock()
|
||||
b.cachedGPSamlCookie = &cachedGPSamlCookie{
|
||||
ConnectionUUID: targetUUID,
|
||||
Cookie: authResult.Cookie,
|
||||
Host: authResult.Host,
|
||||
User: authResult.User,
|
||||
Fingerprint: authResult.Fingerprint,
|
||||
}
|
||||
b.cachedGPSamlMu.Unlock()
|
||||
|
||||
if err := targetConn.ClearSecrets(); err != nil {
|
||||
log.Warnf("[ConnectVPN] ClearSecrets failed (non-fatal): %v", err)
|
||||
} else {
|
||||
@@ -382,6 +386,19 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
log.Infof("[ConnectVPN] GlobalProtect SAML cookie cached for %s, proceeding with activation", connName)
|
||||
}
|
||||
|
||||
if openConnectAuth != nil {
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = &cachedOpenConnectAuth{
|
||||
ConnectionUUID: targetUUID,
|
||||
Cookie: openConnectAuth.Cookie,
|
||||
Host: openConnectAuth.Host,
|
||||
User: openConnectAuth.User,
|
||||
Fingerprint: openConnectAuth.Fingerprint,
|
||||
}
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
log.Infof("[ConnectVPN] OpenConnect authentication cached for %s, proceeding with activation", connName)
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = true
|
||||
b.state.ConnectingVPNUUID = targetUUID
|
||||
@@ -394,6 +411,13 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
_, err = nm.ActivateConnection(targetConn, nil, nil)
|
||||
if err != nil {
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = nil
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
b.pendingVPNSave = nil
|
||||
b.pendingVPNSaveMu.Unlock()
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = false
|
||||
b.state.ConnectingVPNUUID = ""
|
||||
@@ -425,6 +449,9 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
|
||||
log.Infof("[VPN] External browser auth detected for protocol '%s' but only GlobalProtect (gp) is currently supported", protocol)
|
||||
}
|
||||
}
|
||||
if protocol == "fortinet" && data["authtype"] == "password" {
|
||||
return "openconnect_password"
|
||||
}
|
||||
case strings.Contains(serviceType, "openvpn"):
|
||||
connType := data["connection-type"]
|
||||
username := data["username"]
|
||||
@@ -435,6 +462,200 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func setOpenConnectAgentFlags(data map[string]string) bool {
|
||||
changed := false
|
||||
for _, field := range []string{"cookie", "gateway", "gwcert"} {
|
||||
key := field + "-flags"
|
||||
if data[key] != "2" {
|
||||
data[key] = "2"
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ensureOpenConnectAgentFlags(conn gonetworkmanager.Connection, data map[string]string) error {
|
||||
if !setOpenConnectAgentFlags(data) {
|
||||
return nil
|
||||
}
|
||||
if b.dbusConn == nil {
|
||||
return fmt.Errorf("NetworkManager D-Bus connection is unavailable")
|
||||
}
|
||||
|
||||
connObj := b.dbusConn.Object("org.freedesktop.NetworkManager", conn.GetPath())
|
||||
var existingSettings map[string]map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
|
||||
return fmt.Errorf("failed to get connection settings: %w", err)
|
||||
}
|
||||
|
||||
vpn, ok := existingSettings["vpn"]
|
||||
if !ok {
|
||||
return fmt.Errorf("VPN settings are missing")
|
||||
}
|
||||
vpn["data"] = dbus.MakeVariant(data)
|
||||
|
||||
var stored map[string]map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSecrets", 0, "vpn").Store(&stored); err != nil {
|
||||
return fmt.Errorf("failed to preserve VPN secrets: %w", err)
|
||||
}
|
||||
if storedVPN, ok := stored["vpn"]; ok {
|
||||
if secrets, ok := storedVPN["secrets"]; ok {
|
||||
vpn["secrets"] = secrets
|
||||
}
|
||||
}
|
||||
|
||||
settings := map[string]map[string]dbus.Variant{"vpn": vpn}
|
||||
if connection, ok := existingSettings["connection"]; ok {
|
||||
settings["connection"] = connection
|
||||
}
|
||||
|
||||
var result map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
|
||||
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil {
|
||||
return fmt.Errorf("failed to set NetworkManager secret-agent flags: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) handleOpenConnectPasswordAuth(
|
||||
ctx context.Context,
|
||||
targetConn gonetworkmanager.Connection,
|
||||
connName, targetUUID, vpnServiceType string,
|
||||
data map[string]string,
|
||||
) (*openConnectAuthResult, error) {
|
||||
username := data["username"]
|
||||
secrets := map[string]string{}
|
||||
if stored, err := targetConn.GetSecrets("vpn"); err == nil {
|
||||
if vpn, ok := stored["vpn"]; ok {
|
||||
if saved, ok := vpn["secrets"].(map[string]string); ok {
|
||||
secrets = saved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
password := secrets["password"]
|
||||
serverCert := secrets["certificate:"+data["gateway"]]
|
||||
if serverCert == "" {
|
||||
serverCert = secrets["gwcert"]
|
||||
}
|
||||
|
||||
var reply PromptReply
|
||||
if username == "" || password == "" {
|
||||
if b.promptBroker == nil {
|
||||
return nil, fmt.Errorf("password authentication requires an interactive prompt")
|
||||
}
|
||||
|
||||
fields := []string{}
|
||||
fieldsInfo := []FieldInfo{}
|
||||
if username == "" {
|
||||
fields = append(fields, "username")
|
||||
fieldsInfo = append(fieldsInfo, FieldInfo{Name: "username", Label: "Username", IsSecret: false})
|
||||
}
|
||||
if password == "" {
|
||||
fields = append(fields, "password")
|
||||
fieldsInfo = append(fieldsInfo, FieldInfo{Name: "password", Label: "Password", IsSecret: true})
|
||||
}
|
||||
|
||||
token, err := b.promptBroker.Ask(ctx, PromptRequest{
|
||||
Name: connName,
|
||||
ConnType: "vpn",
|
||||
VpnService: vpnServiceType,
|
||||
SettingName: "vpn",
|
||||
Fields: fields,
|
||||
FieldsInfo: fieldsInfo,
|
||||
Reason: "required",
|
||||
ConnectionId: connName,
|
||||
ConnectionUuid: targetUUID,
|
||||
ConnectionPath: string(targetConn.GetPath()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to request credentials: %w", err)
|
||||
}
|
||||
|
||||
reply, err = b.promptBroker.Wait(ctx, token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials prompt failed: %w", err)
|
||||
}
|
||||
if username == "" {
|
||||
username = reply.Secrets["username"]
|
||||
}
|
||||
if password == "" {
|
||||
password = reply.Secrets["password"]
|
||||
}
|
||||
}
|
||||
|
||||
auth, err := runOpenConnectPasswordAuth(ctx, data, username, password, serverCert)
|
||||
persistentSecrets := map[string]string{}
|
||||
var authErr *openConnectAuthError
|
||||
if err != nil && errors.As(err, &authErr) && authErr.serverCert != "" && authErr.serverCert != serverCert {
|
||||
if b.promptBroker == nil {
|
||||
return nil, fmt.Errorf("VPN server certificate is untrusted: %s", authErr.serverCert)
|
||||
}
|
||||
|
||||
reason := "server-certificate"
|
||||
if serverCert != "" {
|
||||
reason = "server-certificate-changed"
|
||||
}
|
||||
|
||||
token, promptErr := b.promptBroker.Ask(ctx, PromptRequest{
|
||||
Name: connName,
|
||||
ConnType: "vpn",
|
||||
VpnService: vpnServiceType,
|
||||
SettingName: "vpn",
|
||||
Hints: []string{authErr.serverCert},
|
||||
Reason: reason,
|
||||
ConnectionId: connName,
|
||||
ConnectionUuid: targetUUID,
|
||||
ConnectionPath: string(targetConn.GetPath()),
|
||||
})
|
||||
if promptErr != nil {
|
||||
return nil, fmt.Errorf("failed to request certificate confirmation: %w", promptErr)
|
||||
}
|
||||
if _, promptErr = b.promptBroker.Wait(ctx, token); promptErr != nil {
|
||||
return nil, fmt.Errorf("certificate confirmation failed: %w", promptErr)
|
||||
}
|
||||
|
||||
auth, err = runOpenConnectPasswordAuth(ctx, data, username, password, authErr.serverCert)
|
||||
if err == nil {
|
||||
persistentSecrets["certificate:"+data["gateway"]] = authErr.serverCert
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(reply.Secrets) > 0 || len(persistentSecrets) > 0 {
|
||||
creds := &pendingVPNCredentials{
|
||||
ConnectionPath: string(targetConn.GetPath()),
|
||||
PersistentSecrets: persistentSecrets,
|
||||
}
|
||||
if _, ok := reply.Secrets["username"]; ok {
|
||||
creds.Username = username
|
||||
}
|
||||
if reply.Save {
|
||||
creds.Username = username
|
||||
creds.Password = password
|
||||
creds.Secrets = map[string]string{"password": password}
|
||||
creds.SavePassword = true
|
||||
}
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
b.pendingVPNSave = creds
|
||||
b.pendingVPNSaveMu.Unlock()
|
||||
}
|
||||
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func suggestedOpenConnectServerCert(output string) string {
|
||||
for _, field := range strings.Fields(output) {
|
||||
field = strings.Trim(field, "'\".,")
|
||||
if strings.HasPrefix(field, "pin-sha256:") {
|
||||
return field
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkmanager.Connection, connName, targetUUID, vpnServiceType string) error {
|
||||
log.Infof("[ConnectVPN] OpenVPN requires username in vpn.data - prompting before activation")
|
||||
|
||||
@@ -758,13 +979,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
||||
b.state.VPNErrorUuid = ""
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
// Clear cached PKCS11 PIN and SAML cookie on success
|
||||
// Clear cached one-shot authentication values on success.
|
||||
b.cachedPKCS11Mu.Lock()
|
||||
b.cachedPKCS11PIN = nil
|
||||
b.cachedPKCS11Mu.Unlock()
|
||||
b.cachedGPSamlMu.Lock()
|
||||
b.cachedGPSamlCookie = nil
|
||||
b.cachedGPSamlMu.Unlock()
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = nil
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
pending := b.pendingVPNSave
|
||||
@@ -787,13 +1008,16 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
||||
b.state.VPNErrorUuid = connectingVPNUUID
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
// Clear cached PKCS11 PIN and SAML cookie on failure
|
||||
// Clear cached one-shot authentication values on failure.
|
||||
b.cachedPKCS11Mu.Lock()
|
||||
b.cachedPKCS11PIN = nil
|
||||
b.cachedPKCS11Mu.Unlock()
|
||||
b.cachedGPSamlMu.Lock()
|
||||
b.cachedGPSamlCookie = nil
|
||||
b.cachedGPSamlMu.Unlock()
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = nil
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
b.pendingVPNSave = nil
|
||||
b.pendingVPNSaveMu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -811,13 +1035,16 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
||||
b.state.VPNErrorUuid = connectingVPNUUID
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
// Clear cached PKCS11 PIN and SAML cookie
|
||||
// Clear cached one-shot authentication values.
|
||||
b.cachedPKCS11Mu.Lock()
|
||||
b.cachedPKCS11PIN = nil
|
||||
b.cachedPKCS11Mu.Unlock()
|
||||
b.cachedGPSamlMu.Lock()
|
||||
b.cachedGPSamlCookie = nil
|
||||
b.cachedGPSamlMu.Unlock()
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = nil
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
b.pendingVPNSave = nil
|
||||
b.pendingVPNSaveMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,17 +1090,40 @@ func (b *NetworkManagerBackend) saveVPNCredentials(creds *pendingVPNCredentials)
|
||||
log.Infof("[saveVPNCredentials] Saving username")
|
||||
}
|
||||
|
||||
// Save secrets if requested
|
||||
if creds.SavePassword {
|
||||
secs := creds.Secrets
|
||||
if len(secs) == 0 {
|
||||
secs = map[string]string{"password": creds.Password}
|
||||
secs := map[string]string{}
|
||||
if len(creds.PersistentSecrets) > 0 {
|
||||
var stored map[string]map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSecrets", 0, "vpn").Store(&stored); err != nil {
|
||||
log.Warnf("[saveVPNCredentials] GetSecrets failed: %v", err)
|
||||
return
|
||||
}
|
||||
for field := range secs {
|
||||
if storedVPN, ok := stored["vpn"]; ok {
|
||||
if storedSecrets, ok := storedVPN["secrets"]; ok {
|
||||
saved, _ := storedSecrets.Value().(map[string]string)
|
||||
for field, value := range saved {
|
||||
secs[field] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
for field, value := range creds.PersistentSecrets {
|
||||
secs[field] = value
|
||||
data[field+"-flags"] = "0"
|
||||
}
|
||||
}
|
||||
|
||||
if creds.SavePassword {
|
||||
toSave := creds.Secrets
|
||||
if len(toSave) == 0 {
|
||||
toSave = map[string]string{"password": creds.Password}
|
||||
}
|
||||
for field, value := range toSave {
|
||||
secs[field] = value
|
||||
data[field+"-flags"] = "0"
|
||||
}
|
||||
}
|
||||
if len(secs) > 0 {
|
||||
vpn["secrets"] = dbus.MakeVariant(secs)
|
||||
log.Infof("[saveVPNCredentials] Saving %d secret field(s) with flags=0", len(secs))
|
||||
log.Infof("[saveVPNCredentials] Saving %d secret field(s)", len(secs))
|
||||
}
|
||||
|
||||
vpn["data"] = dbus.MakeVariant(data)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -136,3 +140,137 @@ func TestNetworkManagerBackend_UpdateVPNConnectionState_EmptyUUID(t *testing.T)
|
||||
backend.updateVPNConnectionState()
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectVPNAuthAction_FortinetPasswordOnly(t *testing.T) {
|
||||
service := "org.freedesktop.NetworkManager.openconnect"
|
||||
|
||||
assert.Equal(t, "openconnect_password", detectVPNAuthAction(service, map[string]string{
|
||||
"protocol": "fortinet",
|
||||
"authtype": "password",
|
||||
}))
|
||||
assert.Empty(t, detectVPNAuthAction(service, map[string]string{
|
||||
"protocol": "anyconnect",
|
||||
"authtype": "password",
|
||||
}))
|
||||
assert.Empty(t, detectVPNAuthAction(service, map[string]string{
|
||||
"protocol": "fortinet",
|
||||
"authtype": "saml",
|
||||
}))
|
||||
}
|
||||
|
||||
func TestEnsureOpenConnectAgentFlags(t *testing.T) {
|
||||
data := map[string]string{"protocol": "fortinet"}
|
||||
assert.True(t, setOpenConnectAgentFlags(data))
|
||||
assert.Equal(t, "2", data["cookie-flags"])
|
||||
assert.Equal(t, "2", data["gateway-flags"])
|
||||
assert.Equal(t, "2", data["gwcert-flags"])
|
||||
assert.False(t, setOpenConnectAgentFlags(data))
|
||||
}
|
||||
|
||||
func TestOpenConnectCertificateConfirmation(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
openConnectPath := filepath.Join(binDir, "openconnect")
|
||||
script := `#!/bin/sh
|
||||
case "$*" in
|
||||
*--servercert=pin-sha256:TEST-FINGERPRINT*)
|
||||
printf '%s\n' "COOKIE='SVPNCOOKIE=test'" "HOST='vpn.example.test'" "FINGERPRINT='pin-sha256:TEST-FINGERPRINT'"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' 'Add --servercert pin-sha256:TEST-FINGERPRINT' >&2
|
||||
exit 1
|
||||
`
|
||||
assert.NoError(t, os.WriteFile(openConnectPath, []byte(script), 0o755))
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
conn := mock_gonetworkmanager.NewMockConnection(t)
|
||||
connPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings/999")
|
||||
conn.EXPECT().GetSecrets("vpn").Return(gonetworkmanager.ConnectionSettings{
|
||||
"vpn": {"secrets": map[string]string{"password": "test-password"}},
|
||||
}, nil)
|
||||
conn.EXPECT().GetPath().Return(connPath).Twice()
|
||||
|
||||
broker := &fakePromptBroker{
|
||||
asked: make(chan PromptRequest, 1),
|
||||
reply: PromptReply{},
|
||||
}
|
||||
backend := &NetworkManagerBackend{promptBroker: broker}
|
||||
data := map[string]string{
|
||||
"gateway": "vpn.example.test:443",
|
||||
"protocol": "fortinet",
|
||||
"authtype": "password",
|
||||
"username": "test-user",
|
||||
}
|
||||
|
||||
result, err := backend.handleOpenConnectPasswordAuth(
|
||||
context.Background(), conn, "Test VPN", "test-uuid",
|
||||
"org.freedesktop.NetworkManager.openconnect", data,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SVPNCOOKIE=test", result.Cookie)
|
||||
assert.Equal(t, "vpn.example.test:443", result.Host)
|
||||
|
||||
prompt := <-broker.asked
|
||||
assert.Equal(t, "server-certificate", prompt.Reason)
|
||||
assert.Equal(t, []string{"pin-sha256:TEST-FINGERPRINT"}, prompt.Hints)
|
||||
|
||||
assert.Equal(t, map[string]string{
|
||||
"certificate:vpn.example.test:443": "pin-sha256:TEST-FINGERPRINT",
|
||||
}, backend.pendingVPNSave.PersistentSecrets)
|
||||
}
|
||||
|
||||
func TestOpenConnectCertificateRotationReprompts(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
openConnectPath := filepath.Join(binDir, "openconnect")
|
||||
script := `#!/bin/sh
|
||||
case "$*" in
|
||||
*--servercert=pin-sha256:NEW-FINGERPRINT*)
|
||||
printf '%s\n' "COOKIE='SVPNCOOKIE=test'" "HOST='vpn.example.test'" "FINGERPRINT='pin-sha256:NEW-FINGERPRINT'"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' 'Add --servercert pin-sha256:NEW-FINGERPRINT' >&2
|
||||
exit 1
|
||||
`
|
||||
assert.NoError(t, os.WriteFile(openConnectPath, []byte(script), 0o755))
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
conn := mock_gonetworkmanager.NewMockConnection(t)
|
||||
connPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings/999")
|
||||
conn.EXPECT().GetSecrets("vpn").Return(gonetworkmanager.ConnectionSettings{
|
||||
"vpn": {"secrets": map[string]string{
|
||||
"password": "test-password",
|
||||
"certificate:vpn.example.test:443": "pin-sha256:OLD-FINGERPRINT",
|
||||
}},
|
||||
}, nil)
|
||||
conn.EXPECT().GetPath().Return(connPath).Twice()
|
||||
|
||||
broker := &fakePromptBroker{
|
||||
asked: make(chan PromptRequest, 1),
|
||||
reply: PromptReply{},
|
||||
}
|
||||
backend := &NetworkManagerBackend{promptBroker: broker}
|
||||
data := map[string]string{
|
||||
"gateway": "vpn.example.test:443",
|
||||
"protocol": "fortinet",
|
||||
"authtype": "password",
|
||||
"username": "test-user",
|
||||
}
|
||||
|
||||
result, err := backend.handleOpenConnectPasswordAuth(
|
||||
context.Background(), conn, "Test VPN", "test-uuid",
|
||||
"org.freedesktop.NetworkManager.openconnect", data,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SVPNCOOKIE=test", result.Cookie)
|
||||
|
||||
prompt := <-broker.asked
|
||||
assert.Equal(t, "server-certificate-changed", prompt.Reason)
|
||||
assert.Equal(t, []string{"pin-sha256:NEW-FINGERPRINT"}, prompt.Hints)
|
||||
|
||||
assert.Equal(t, map[string]string{
|
||||
"certificate:vpn.example.test:443": "pin-sha256:NEW-FINGERPRINT",
|
||||
}, backend.pendingVPNSave.PersistentSecrets)
|
||||
assert.False(t, backend.pendingVPNSave.SavePassword)
|
||||
assert.Empty(t, backend.pendingVPNSave.Secrets)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
handleGetNetworkQRCode(conn, req, manager)
|
||||
case "network.qrcode-content":
|
||||
handleGetNetworkQRCodeContent(conn, req, manager)
|
||||
case "network.generate-qrcode":
|
||||
handleGenerateQRCode(conn, req)
|
||||
case "network.delete-qrcode":
|
||||
handleDeleteQRCode(conn, req, manager)
|
||||
case "network.ethernet.info":
|
||||
@@ -365,6 +367,22 @@ func handleGetNetworkQRCodeContent(conn *models.Conn, req models.Request, manage
|
||||
models.Respond(conn, req.ID, content)
|
||||
}
|
||||
|
||||
func handleGenerateQRCode(conn *models.Conn, req models.Request) {
|
||||
text, err := params.String(req.Params, "text")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paths, err := generateTextQRCode(text)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, paths)
|
||||
}
|
||||
|
||||
func handleDeleteQRCode(conn *models.Conn, req models.Request, _ *Manager) {
|
||||
path, err := params.String(req.Params, "path")
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/yeqown/go-qrcode/v2"
|
||||
"github.com/yeqown/go-qrcode/writer/standard"
|
||||
)
|
||||
|
||||
const textQRCodeTmpPrefix = "/tmp/dank-text-qrcode-"
|
||||
|
||||
func generateTextQRCode(text string) ([2]string, error) {
|
||||
qrc, err := qrcode.New(text)
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code for text: %w", err)
|
||||
}
|
||||
|
||||
pathThemed, pathNormal := textQRCodePaths(text)
|
||||
|
||||
if err := saveQRCodePNG(qrc, pathThemed, standard.WithBgTransparent(), standard.WithFgColorRGBHex("#ffffff")); err != nil {
|
||||
return [2]string{}, err
|
||||
}
|
||||
if err := saveQRCodePNG(qrc, pathNormal); err != nil {
|
||||
return [2]string{}, err
|
||||
}
|
||||
|
||||
return [2]string{pathThemed, pathNormal}, nil
|
||||
}
|
||||
|
||||
// Write to a temp file and rename into place so the shell's Image never
|
||||
// observes a partially written PNG.
|
||||
func saveQRCodePNG(qrc *qrcode.QRCode, path string, opts ...standard.ImageOption) error {
|
||||
tmpPath := path + ".tmp"
|
||||
opts = append(opts, standard.WithBuiltinImageEncoder(standard.PNG_FORMAT))
|
||||
|
||||
w, err := standard.New(tmpPath, opts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create QR code writer: %w", err)
|
||||
}
|
||||
if err := qrc.Save(w); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to save QR code: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to move QR code into place: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Paths are unique per generation, not per text: the library's mask selection
|
||||
// is non-deterministic, so regenerating the same text produces different
|
||||
// bytes, and reusing a path lets the shell's URL-keyed pixmap cache serve a
|
||||
// stale pattern over the new file.
|
||||
func textQRCodePaths(text string) (themed, normal string) {
|
||||
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(text)))[:8]
|
||||
nonce := time.Now().UnixNano()
|
||||
themed = fmt.Sprintf("%s%s-%d-themed.png", textQRCodeTmpPrefix, hash, nonce)
|
||||
normal = fmt.Sprintf("%s%s-%d-normal.png", textQRCodeTmpPrefix, hash, nonce)
|
||||
return
|
||||
}
|
||||
@@ -24,7 +24,7 @@ func qrCodePaths(ssid string) (themed, normal string) {
|
||||
|
||||
func isValidQRCodePath(path string) bool {
|
||||
clean := filepath.Clean(path)
|
||||
return strings.HasPrefix(clean, qrCodeTmpPrefix) && strings.HasSuffix(clean, ".png")
|
||||
return (strings.HasPrefix(clean, qrCodeTmpPrefix) || strings.HasPrefix(clean, textQRCodeTmpPrefix)) && strings.HasSuffix(clean, ".png")
|
||||
}
|
||||
|
||||
var safePathChar = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
|
||||
|
||||
@@ -143,7 +143,8 @@ func wrapInTerminal(term, title, shellCmd string, extraArgs []string) []string {
|
||||
case "konsole":
|
||||
argv = []string{term, "-p", "tabtitle=" + title}
|
||||
case "gnome-terminal":
|
||||
argv = []string{term, "--title=" + title}
|
||||
// --wait: the factory process otherwise returns immediately
|
||||
argv = []string{term, "--wait", "--title=" + title}
|
||||
execFlag = "--"
|
||||
default:
|
||||
argv = []string{term}
|
||||
|
||||
@@ -412,25 +412,30 @@ func (m *Manager) runCustomUpgrade(ctx context.Context, opts UpgradeOptions) {
|
||||
onLine := func(line string) { m.appendLog(line) }
|
||||
argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs)
|
||||
if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil {
|
||||
code := ErrCodeBackendFailed
|
||||
switch {
|
||||
case errors.Is(ctx.Err(), context.DeadlineExceeded):
|
||||
code = ErrCodeTimeout
|
||||
m.failCustomUpgrade(ErrCodeTimeout, err)
|
||||
return
|
||||
case errors.Is(ctx.Err(), context.Canceled):
|
||||
code = ErrCodeCancelled
|
||||
m.failCustomUpgrade(ErrCodeCancelled, err)
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.state.Phase = PhaseError
|
||||
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
|
||||
m.mu.Unlock()
|
||||
m.markDirty()
|
||||
return
|
||||
// exit status reflects the trailing `read`, not the update command
|
||||
m.appendLog(fmt.Sprintf("Terminal exited early: %v", err))
|
||||
}
|
||||
|
||||
m.finishSuccessfulUpgrade(false)
|
||||
m.runRefresh(context.Background(), false)
|
||||
}
|
||||
|
||||
func (m *Manager) failCustomUpgrade(code ErrorCode, err error) {
|
||||
m.mu.Lock()
|
||||
m.state.Phase = PhaseError
|
||||
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
|
||||
m.mu.Unlock()
|
||||
m.markDirty()
|
||||
}
|
||||
|
||||
func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
|
||||
m.appendLog("Upgrade complete.")
|
||||
|
||||
|
||||
@@ -233,3 +233,27 @@ func TestUpgradeBackendsFiltersFlatpakOnly(t *testing.T) {
|
||||
t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapInTerminal(t *testing.T) {
|
||||
tests := []struct {
|
||||
term string
|
||||
wantPrefix []string
|
||||
}{
|
||||
{"kitty", []string{"kitty", "--class", "com.danklinux.dms", "-T", "Title"}},
|
||||
{"gnome-terminal", []string{"gnome-terminal", "--wait", "--title=Title"}},
|
||||
{"foot", []string{"foot", "--app-id=com.danklinux.dms", "--title=Title"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := wrapInTerminal(tt.term, "Title", "echo hi", nil)
|
||||
if len(got) < len(tt.wantPrefix) || !reflect.DeepEqual(got[:len(tt.wantPrefix)], tt.wantPrefix) {
|
||||
t.Errorf("wrapInTerminal(%q) = %#v, want prefix %#v", tt.term, got, tt.wantPrefix)
|
||||
}
|
||||
tail := got[len(got)-3:]
|
||||
if tail[0] != "sh" || tail[1] != "-c" {
|
||||
t.Errorf("wrapInTerminal(%q) tail = %#v, want [sh -c <cmd>]", tt.term, tail)
|
||||
}
|
||||
if !strings.Contains(tail[2], "echo hi") {
|
||||
t.Errorf("wrapInTerminal(%q) command %q does not contain shell command", tt.term, tail[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
Submodule dank-qml-common updated: 9cc787912d...3b06bd9372
Generated
+3
-3
@@ -3,11 +3,11 @@
|
||||
"dank-qml-common": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1784916690,
|
||||
"narHash": "sha256-KZQjJRLRecUHSY8leyW+bWozsrpqKA9wzkQaL/TkYT0=",
|
||||
"lastModified": 1785445867,
|
||||
"narHash": "sha256-l9IRsLIVZ7bV+KMuTdCga89tGt4lpdMXhQR7f/2qoe4=",
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"rev": "9cc787912db7886051ce2b23bfd82a832d13c009",
|
||||
"rev": "3b06bd9372e18bc8086cc3958d4f677d57fbfdd8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-ZvaOPC92ZFRPqSyLJa2TA9OUKQ3QnWCIMxrnYLGnC58=";
|
||||
vendorHash = "sha256-pjaRyB6E2TZvVd5a4xcdGSRVr9Dg9wEG/5e+HdtZJCg=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
|
||||
@@ -11,16 +11,27 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("CacheData")
|
||||
|
||||
readonly property int cacheConfigVersion: 1
|
||||
readonly property int cacheConfigVersion: 2
|
||||
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
property bool _loading: false
|
||||
property bool _hasLoaded: false
|
||||
property int _loadedCacheVersion: 0
|
||||
|
||||
readonly property var _pinKeys: ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"]
|
||||
readonly property var _dataKeys: ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings"].concat(_pinKeys)
|
||||
|
||||
property string wallpaperLastPath: ""
|
||||
property string profileLastPath: ""
|
||||
|
||||
property var brightnessDevicePins: ({})
|
||||
property var wifiNetworkPins: ({})
|
||||
property var bluetoothDevicePins: ({})
|
||||
property var audioInputDevicePins: ({})
|
||||
property var audioOutputDevicePins: ({})
|
||||
|
||||
property var fileBrowserSettings: ({
|
||||
"wallpaper": {
|
||||
"lastPath": "",
|
||||
@@ -78,8 +89,46 @@ Singleton {
|
||||
|
||||
function loadCache() {
|
||||
_loading = true;
|
||||
parseCache(cacheFile.text());
|
||||
_loading = false;
|
||||
try {
|
||||
parseCache(cacheFile.text());
|
||||
} finally {
|
||||
_loading = false;
|
||||
_hasLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function set(key, value) {
|
||||
if (_dataKeys.indexOf(key) < 0) {
|
||||
log.warn("Unknown cache key:", key);
|
||||
return;
|
||||
}
|
||||
root[key] = value;
|
||||
saveCache();
|
||||
}
|
||||
|
||||
function migratePins(pins) {
|
||||
if (!pins)
|
||||
return;
|
||||
if (!_hasLoaded)
|
||||
loadCache();
|
||||
if (_loadedCacheVersion >= cacheConfigVersion)
|
||||
return;
|
||||
|
||||
let migrated = false;
|
||||
for (const key of _pinKeys) {
|
||||
const legacy = pins[key];
|
||||
if (!legacy || Object.keys(legacy).length === 0)
|
||||
continue;
|
||||
if (Object.keys(root[key] || {}).length > 0)
|
||||
continue;
|
||||
root[key] = legacy;
|
||||
migrated = true;
|
||||
}
|
||||
|
||||
if (!migrated)
|
||||
return;
|
||||
log.info("Migrated device pins from settings.json");
|
||||
saveCache();
|
||||
}
|
||||
|
||||
function parseCache(content) {
|
||||
@@ -87,6 +136,7 @@ Singleton {
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
const cache = JSON.parse(content);
|
||||
_loadedCacheVersion = cache.configVersion || 0;
|
||||
|
||||
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : "";
|
||||
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : "";
|
||||
@@ -122,6 +172,10 @@ Singleton {
|
||||
};
|
||||
}
|
||||
|
||||
for (const key of _pinKeys) {
|
||||
root[key] = cache[key] !== undefined ? cache[key] : {};
|
||||
}
|
||||
|
||||
if (cache.configVersion === undefined) {
|
||||
migrateFromUndefinedToV1(cache);
|
||||
cleanupUnusedKeys();
|
||||
@@ -138,12 +192,16 @@ Singleton {
|
||||
function saveCache() {
|
||||
if (_loading)
|
||||
return;
|
||||
cacheFile.setText(JSON.stringify({
|
||||
const data = {
|
||||
"wallpaperLastPath": wallpaperLastPath,
|
||||
"profileLastPath": profileLastPath,
|
||||
"fileBrowserSettings": fileBrowserSettings,
|
||||
"configVersion": cacheConfigVersion
|
||||
}, null, 2));
|
||||
};
|
||||
for (const key of _pinKeys) {
|
||||
data[key] = root[key];
|
||||
}
|
||||
cacheFile.setText(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
function migrateFromUndefinedToV1(cache) {
|
||||
@@ -151,7 +209,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function cleanupUnusedKeys() {
|
||||
const validKeys = ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings", "configVersion"];
|
||||
const validKeys = _dataKeys.concat(["configVersion"]);
|
||||
|
||||
try {
|
||||
const content = cacheFile.text();
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function clearImageCache() {
|
||||
Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]);
|
||||
Paths.mkdir(Paths.imagecache);
|
||||
}
|
||||
|
||||
function clearOldCache(ageInMinutes) {
|
||||
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]);
|
||||
}
|
||||
|
||||
function clearCacheForSize(size) {
|
||||
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]);
|
||||
}
|
||||
|
||||
function getCacheSize(callback) {
|
||||
Proc.runCommand("cache_size", ["du", "-sm", Paths.stringify(Paths.imagecache)], function (output, exitCode) {
|
||||
const sizeMB = parseInt(output.split("\t")[0]) || 0;
|
||||
callback(sizeMB);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -114,26 +114,6 @@ function withRevision(descriptor, revision) {
|
||||
return next;
|
||||
}
|
||||
|
||||
function withAnimationOffset(descriptor, x, y) {
|
||||
var next = normalize(descriptor);
|
||||
next.animationOffset = {
|
||||
"x": x === undefined ? next.animationOffset.x : _number(x, next.animationOffset.x),
|
||||
"y": y === undefined ? next.animationOffset.y : _number(y, next.animationOffset.y)
|
||||
};
|
||||
return next;
|
||||
}
|
||||
|
||||
function withBodyRect(descriptor, x, y, width, height) {
|
||||
var next = normalize(descriptor);
|
||||
next.bodyRect = {
|
||||
"x": x === undefined ? next.bodyRect.x : _number(x, next.bodyRect.x),
|
||||
"y": y === undefined ? next.bodyRect.y : _number(y, next.bodyRect.y),
|
||||
"width": width === undefined ? next.bodyRect.width : Math.max(0, _number(width, next.bodyRect.width)),
|
||||
"height": height === undefined ? next.bodyRect.height : Math.max(0, _number(height, next.bodyRect.height))
|
||||
};
|
||||
return next;
|
||||
}
|
||||
|
||||
function same(a, b, threshold) {
|
||||
if (!a || !b)
|
||||
return false;
|
||||
|
||||
@@ -162,21 +162,6 @@ function fillBounds(rect, side, seamOverlap, dpr) {
|
||||
};
|
||||
}
|
||||
|
||||
function clipEnvelope(rect, side, radii, seamOverlap, dpr) {
|
||||
var fill = fillBounds(rect, side, seamOverlap, dpr);
|
||||
var chrome = chromeBounds(fill, side, radii.start, radii.end, radii.farExtent, dpr);
|
||||
return {
|
||||
"x": chrome.x,
|
||||
"y": chrome.y,
|
||||
"width": chrome.width,
|
||||
"height": chrome.height,
|
||||
"bodyX": snap(fill.x - chrome.x, dpr),
|
||||
"bodyY": snap(fill.y - chrome.y, dpr),
|
||||
"bodyWidth": fill.width,
|
||||
"bodyHeight": fill.height
|
||||
};
|
||||
}
|
||||
|
||||
function blurRegions(descriptor, rect, radii, dpr) {
|
||||
var side = descriptor.barSide;
|
||||
var regions = [bodyRect(rect, dpr)];
|
||||
@@ -219,14 +204,3 @@ function unionBounds(rects, padding, dpr) {
|
||||
"height": Math.max(0, snap(maxY - minY + pad * 2, dpr))
|
||||
};
|
||||
}
|
||||
|
||||
function shadowSourceBounds(descriptor, rect, radii, padding, dpr) {
|
||||
return unionBounds(blurRegions(descriptor, rect, radii, dpr), padding, dpr);
|
||||
}
|
||||
|
||||
function stableEqual(a, b, dpr) {
|
||||
if (!a || !b)
|
||||
return false;
|
||||
var threshold = 0.5 / (dpr || 1);
|
||||
return Math.abs(a.x - b.x) < threshold && Math.abs(a.y - b.y) < threshold && Math.abs(a.width - b.width) < threshold && Math.abs(a.height - b.height) < threshold;
|
||||
}
|
||||
|
||||
@@ -44,16 +44,6 @@ function connectorX(barSide, baseX, bodyWidth, placement, spacing, radius) {
|
||||
return barSide === "left" ? s : s - w;
|
||||
}
|
||||
|
||||
function connectorY(barSide, baseY, bodyHeight, placement, spacing, radius) {
|
||||
var s = seamY(barSide, baseY, bodyHeight, placement);
|
||||
var h = connectorHeight(barSide, spacing, radius);
|
||||
if (barSide === "top")
|
||||
return s;
|
||||
if (barSide === "bottom")
|
||||
return s - h;
|
||||
return placement === "left" ? s - h : s;
|
||||
}
|
||||
|
||||
// Which corner of the connector's bounding rect hosts the concave arc that
|
||||
// carves into the body. Used for arc-sweep orientation.
|
||||
function arcCorner(barSide, placement) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
.pragma library
|
||||
|
||||
function formatRate(bytesPerSec, gbDecimals) {
|
||||
if (bytesPerSec < 1024)
|
||||
return bytesPerSec.toFixed(0) + " B/s";
|
||||
if (bytesPerSec < 1024 * 1024)
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
||||
if (bytesPerSec < 1024 * 1024 * 1024)
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(gbDecimals ?? 2) + " GB/s";
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024)
|
||||
return bytes.toFixed(0) + "B";
|
||||
if (bytes < 1024 * 1024)
|
||||
return (bytes / 1024).toFixed(0) + "K";
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + "M";
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + "G";
|
||||
}
|
||||
|
||||
function formatIsoTime(isoString) {
|
||||
if (!isoString)
|
||||
return "";
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
if (isNaN(date.getTime()))
|
||||
return "";
|
||||
return date.toLocaleTimeString(Qt.locale(), "HH:mm");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function formatRemaining(ms, zeroText, minText, hText, hmText) {
|
||||
if (ms <= 0)
|
||||
return zeroText;
|
||||
const totalMinutes = Math.ceil(ms / 60000);
|
||||
if (totalMinutes < 60)
|
||||
return minText.arg(totalMinutes);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const mins = totalMinutes - hours * 60;
|
||||
if (mins === 0)
|
||||
return hText.arg(hours);
|
||||
return hmText.arg(hours).arg(mins);
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
return n < 10 ? "0" + n : "" + n;
|
||||
}
|
||||
|
||||
function formatUntil(ts, use24h) {
|
||||
if (!ts)
|
||||
return "";
|
||||
const d = new Date(ts);
|
||||
const hours = d.getHours();
|
||||
const minutes = d.getMinutes();
|
||||
if (use24h)
|
||||
return pad2(hours) + ":" + pad2(minutes);
|
||||
const suffix = hours >= 12 ? "PM" : "AM";
|
||||
const h12 = ((hours + 11) % 12) + 1;
|
||||
return h12 + ":" + pad2(minutes) + " " + suffix;
|
||||
}
|
||||
|
||||
function addToHistory(arr, val, max) {
|
||||
const newArr = arr.slice();
|
||||
newArr.push(val);
|
||||
if (newArr.length > max)
|
||||
newArr.shift();
|
||||
return newArr;
|
||||
}
|
||||
@@ -1370,10 +1370,3 @@ function buildDmsAction(baseKey, args) {
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function getScreenshotOptions() {
|
||||
return [
|
||||
{ id: "write-to-disk", label: "Save to disk", type: "bool" },
|
||||
{ id: "show-pointer", label: "Show pointer", type: "bool" }
|
||||
];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ Singleton {
|
||||
|
||||
readonly property url imagecache: `${cache}/imagecache`
|
||||
|
||||
property var iconResolver: null
|
||||
property var desktopIconResolver: null
|
||||
property var trashHandler: null
|
||||
|
||||
Component.onCompleted: mkdir(imagecache)
|
||||
|
||||
function stringify(path: url): string {
|
||||
@@ -88,7 +92,7 @@ Singleton {
|
||||
function themedIconPath(name: string): string {
|
||||
if (!name)
|
||||
return "";
|
||||
const themed = (typeof IconThemeService !== "undefined") ? IconThemeService.resolve(name) : "";
|
||||
const themed = iconResolver ? iconResolver(name) : "";
|
||||
if (themed)
|
||||
return themed;
|
||||
return Quickshell.iconPath(name, true);
|
||||
@@ -105,11 +109,15 @@ Singleton {
|
||||
return moddedId;
|
||||
return themedIconPath(moddedId);
|
||||
}
|
||||
return themedIconPath(iconName) || DesktopService.resolveIconPath(iconName);
|
||||
if (!desktopIconResolver)
|
||||
return themedIconPath(iconName);
|
||||
return themedIconPath(iconName) || desktopIconResolver(iconName);
|
||||
}
|
||||
|
||||
function trashPath(path: string, callback): void {
|
||||
TrashService.trashPath(path, callback);
|
||||
if (!trashHandler)
|
||||
return;
|
||||
trashHandler(path, callback);
|
||||
}
|
||||
|
||||
function copyPathToClipboard(path: string): void {
|
||||
@@ -125,7 +133,7 @@ Singleton {
|
||||
return toFileUrl(expandTilde(target));
|
||||
if (target.startsWith("file://"))
|
||||
return target;
|
||||
const themed = (typeof IconThemeService !== "undefined") ? IconThemeService.resolve(target) : "";
|
||||
const themed = iconResolver ? iconResolver(target) : "";
|
||||
if (themed)
|
||||
return themed;
|
||||
return "image://icon/" + target;
|
||||
@@ -148,7 +156,9 @@ Singleton {
|
||||
if (icon && icon !== "")
|
||||
return icon;
|
||||
|
||||
return DesktopService.resolveIconPath(appId);
|
||||
if (!desktopIconResolver)
|
||||
return "";
|
||||
return desktopIconResolver(appId);
|
||||
}
|
||||
|
||||
function getAppName(appId: string, desktopEntry: var): string {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
.pragma library
|
||||
|
||||
function findParentFlickable(item) {
|
||||
while (item) {
|
||||
if (item.hasOwnProperty("contentY") && item.hasOwnProperty("contentItem"))
|
||||
return item;
|
||||
item = item.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findSettings(item) {
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined)
|
||||
return item;
|
||||
item = item.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizePinList(value) {
|
||||
if (Array.isArray(value))
|
||||
return value.filter(v => v);
|
||||
if (typeof value === "string" && value.length > 0)
|
||||
return [value];
|
||||
return [];
|
||||
}
|
||||
@@ -16,6 +16,10 @@ Singleton {
|
||||
|
||||
readonly property int sessionConfigVersion: 3
|
||||
|
||||
signal loaded
|
||||
signal brightnessDisplayHintChanged(string deviceName)
|
||||
signal loadErrorOccurred(string file, string message)
|
||||
|
||||
property bool _parseError: false
|
||||
property bool _hasLoaded: false
|
||||
property bool _isReadOnly: false
|
||||
@@ -276,15 +280,14 @@ Singleton {
|
||||
if (typeof Theme !== "undefined")
|
||||
Theme.generateSystemThemesFromCurrentTheme();
|
||||
|
||||
if (typeof WallpaperCyclingService !== "undefined")
|
||||
WallpaperCyclingService.updateCyclingState();
|
||||
loaded();
|
||||
|
||||
_checkSessionWritable();
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
const msg = e.message;
|
||||
log.error("Failed to parse session.json - file will not be overwritten.");
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("session.json"), msg));
|
||||
Qt.callLater(() => loadErrorOccurred("session.json", msg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,13 +361,12 @@ Singleton {
|
||||
if (typeof Theme !== "undefined")
|
||||
Theme.generateSystemThemesFromCurrentTheme();
|
||||
|
||||
if (typeof WallpaperCyclingService !== "undefined")
|
||||
WallpaperCyclingService.updateCyclingState();
|
||||
loaded();
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
const msg = e.message;
|
||||
log.error("Failed to parse session.json - file will not be overwritten.");
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("session.json"), msg));
|
||||
Qt.callLater(() => loadErrorOccurred("session.json", msg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,11 +467,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setWallpaperPath(path) {
|
||||
wallpaperPath = path;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setWallpaper(imagePath) {
|
||||
wallpaperPath = imagePath;
|
||||
if (perModeWallpaper) {
|
||||
@@ -848,11 +845,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setNightModeLocationProvider(provider) {
|
||||
nightModeLocationProvider = provider;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setThemeModeAutoEnabled(enabled) {
|
||||
themeModeAutoEnabled = enabled;
|
||||
saveSettings();
|
||||
@@ -941,10 +933,6 @@ Singleton {
|
||||
setBarPinnedApps(currentPinned);
|
||||
}
|
||||
|
||||
function isBarPinnedApp(appId) {
|
||||
return appId && barPinnedApps.indexOf(appId) !== -1;
|
||||
}
|
||||
|
||||
function hideTrayId(trayId) {
|
||||
if (!trayId)
|
||||
return;
|
||||
@@ -1007,11 +995,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setLaunchPrefix(prefix) {
|
||||
launchPrefix = prefix;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setLastBrightnessDevice(device) {
|
||||
lastBrightnessDevice = device;
|
||||
saveSettings();
|
||||
@@ -1026,10 +1009,7 @@ Singleton {
|
||||
}
|
||||
brightnessExponentialDevices = newSettings;
|
||||
saveSettings();
|
||||
|
||||
if (typeof DisplayService !== "undefined") {
|
||||
DisplayService.updateDeviceBrightnessDisplay(deviceName);
|
||||
}
|
||||
brightnessDisplayHintChanged(deviceName);
|
||||
}
|
||||
|
||||
function getBrightnessExponential(deviceName) {
|
||||
@@ -1043,10 +1023,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function getBrightnessUserSetValue(deviceName) {
|
||||
return brightnessUserSetValues[deviceName];
|
||||
}
|
||||
|
||||
function clearBrightnessUserSetValue(deviceName) {
|
||||
var newValues = Object.assign({}, brightnessUserSetValues);
|
||||
delete newValues[deviceName];
|
||||
@@ -1070,21 +1046,6 @@ Singleton {
|
||||
return value !== undefined ? value : 1.2;
|
||||
}
|
||||
|
||||
function setSelectedGpuIndex(index) {
|
||||
selectedGpuIndex = index;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setNvidiaGpuTempEnabled(enabled) {
|
||||
nvidiaGpuTempEnabled = enabled;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setNonNvidiaGpuTempEnabled(enabled) {
|
||||
nonNvidiaGpuTempEnabled = enabled;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setEnabledGpuPciIds(pciIds) {
|
||||
enabledGpuPciIds = pciIds;
|
||||
saveSettings();
|
||||
@@ -1200,15 +1161,6 @@ Singleton {
|
||||
return deviceMaxVolumes[nodeName] ?? 100;
|
||||
}
|
||||
|
||||
function removeDeviceMaxVolume(nodeName) {
|
||||
if (!nodeName)
|
||||
return;
|
||||
const updated = Object.assign({}, deviceMaxVolumes);
|
||||
delete updated[nodeName];
|
||||
deviceMaxVolumes = updated;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function updateLocale() {
|
||||
if (!locale) {
|
||||
I18n._pickTranslation();
|
||||
@@ -1272,12 +1224,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function clearLauncherHistory() {
|
||||
launcherLastQuery = "";
|
||||
launcherSearchHistory = [];
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setAppDrawerLastMode(mode) {
|
||||
appDrawerLastMode = mode;
|
||||
saveSettings();
|
||||
@@ -1417,141 +1363,4 @@ Singleton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "wallpaper"
|
||||
|
||||
function get(): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead.";
|
||||
}
|
||||
return root.wallpaperPath || "";
|
||||
}
|
||||
|
||||
function set(path: string): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead.";
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return "ERROR: No path provided";
|
||||
}
|
||||
|
||||
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path;
|
||||
|
||||
try {
|
||||
root.setWallpaper(absolutePath);
|
||||
return "SUCCESS: Wallpaper set to " + absolutePath;
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to set wallpaper: " + e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function clear(): string {
|
||||
root.setWallpaper("");
|
||||
root.setPerMonitorWallpaper(false);
|
||||
root.monitorWallpapers = {};
|
||||
root.saveSettings();
|
||||
return "SUCCESS: All wallpapers cleared";
|
||||
}
|
||||
|
||||
function next(): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead.";
|
||||
}
|
||||
|
||||
if (!root.wallpaperPath) {
|
||||
return "ERROR: No wallpaper set";
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cycleNextManually();
|
||||
return "SUCCESS: Cycling to next wallpaper";
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper: " + e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function prev(): string {
|
||||
if (root.perMonitorWallpaper) {
|
||||
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead.";
|
||||
}
|
||||
|
||||
if (!root.wallpaperPath) {
|
||||
return "ERROR: No wallpaper set";
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cyclePrevManually();
|
||||
return "SUCCESS: Cycling to previous wallpaper";
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper: " + e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function getFor(screenName: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided";
|
||||
}
|
||||
return root.getMonitorWallpaper(screenName) || "";
|
||||
}
|
||||
|
||||
function setFor(screenName: string, path: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided";
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return "ERROR: No path provided";
|
||||
}
|
||||
|
||||
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path;
|
||||
|
||||
try {
|
||||
if (!root.perMonitorWallpaper) {
|
||||
root.setPerMonitorWallpaper(true);
|
||||
}
|
||||
root.setMonitorWallpaper(screenName, absolutePath);
|
||||
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath;
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function nextFor(screenName: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided";
|
||||
}
|
||||
|
||||
var currentWallpaper = root.getMonitorWallpaper(screenName);
|
||||
if (!currentWallpaper) {
|
||||
return "ERROR: No wallpaper set for " + screenName;
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cycleNextForMonitor(screenName);
|
||||
return "SUCCESS: Cycling to next wallpaper for " + screenName;
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function prevFor(screenName: string): string {
|
||||
if (!screenName) {
|
||||
return "ERROR: No screen name provided";
|
||||
}
|
||||
|
||||
var currentWallpaper = root.getMonitorWallpaper(screenName);
|
||||
if (!currentWallpaper) {
|
||||
return "ERROR: No wallpaper set for " + screenName;
|
||||
}
|
||||
|
||||
try {
|
||||
WallpaperCyclingService.cyclePrevForMonitor(screenName);
|
||||
return "SUCCESS: Cycling to previous wallpaper for " + screenName;
|
||||
} catch (e) {
|
||||
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("SettingsData")
|
||||
|
||||
readonly property int settingsConfigVersion: 12
|
||||
readonly property int settingsConfigVersion: 13
|
||||
|
||||
enum Position {
|
||||
Top,
|
||||
@@ -712,16 +712,23 @@ Singleton {
|
||||
property bool notepadUseCompositorGap: false
|
||||
property int notepadEdgeGap: 0
|
||||
|
||||
property string activeCompositor: ""
|
||||
|
||||
// Compositor layout gap when enabled and available, else the manual value.
|
||||
readonly property int notepadEffectiveEdgeGap: {
|
||||
if (notepadUseCompositorGap) {
|
||||
var g = -1;
|
||||
if (CompositorService.isNiri)
|
||||
switch (activeCompositor) {
|
||||
case "niri":
|
||||
g = niriLayoutGapsOverride;
|
||||
else if (CompositorService.isHyprland)
|
||||
break;
|
||||
case "hyprland":
|
||||
g = hyprlandLayoutGapsOverride;
|
||||
else if (CompositorService.isMango)
|
||||
break;
|
||||
case "mango":
|
||||
g = mangoLayoutGapsOverride;
|
||||
break;
|
||||
}
|
||||
if (g >= 0)
|
||||
return g;
|
||||
}
|
||||
@@ -790,11 +797,6 @@ Singleton {
|
||||
property bool fadeToDpmsEnabled: true
|
||||
property int fadeToDpmsGracePeriod: 5
|
||||
property string launchPrefix: ""
|
||||
property var brightnessDevicePins: ({})
|
||||
property var wifiNetworkPins: ({})
|
||||
property var bluetoothDevicePins: ({})
|
||||
property var audioInputDevicePins: ({})
|
||||
property var audioOutputDevicePins: ({})
|
||||
|
||||
property bool gtkThemingEnabled: false
|
||||
property bool qtThemingEnabled: false
|
||||
@@ -1131,44 +1133,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function getSystemMonitorVariants() {
|
||||
return systemMonitorVariants || [];
|
||||
}
|
||||
|
||||
function createSystemMonitorVariant(name, config) {
|
||||
const id = "sysmon_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
|
||||
const variant = {
|
||||
id: id,
|
||||
name: name,
|
||||
config: config || getDefaultSystemMonitorConfig()
|
||||
};
|
||||
const variants = JSON.parse(JSON.stringify(systemMonitorVariants || []));
|
||||
variants.push(variant);
|
||||
systemMonitorVariants = variants;
|
||||
saveSettings();
|
||||
return variant;
|
||||
}
|
||||
|
||||
function updateSystemMonitorVariant(variantId, updates) {
|
||||
const variants = JSON.parse(JSON.stringify(systemMonitorVariants || []));
|
||||
const idx = variants.findIndex(v => v.id === variantId);
|
||||
if (idx === -1)
|
||||
return;
|
||||
Object.assign(variants[idx], updates);
|
||||
systemMonitorVariants = variants;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function removeSystemMonitorVariant(variantId) {
|
||||
const variants = (systemMonitorVariants || []).filter(v => v.id !== variantId);
|
||||
systemMonitorVariants = variants;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function getSystemMonitorVariant(variantId) {
|
||||
return (systemMonitorVariants || []).find(v => v.id === variantId) || null;
|
||||
}
|
||||
|
||||
function getDefaultSystemMonitorConfig() {
|
||||
return {
|
||||
showHeader: true,
|
||||
@@ -1309,70 +1273,6 @@ Singleton {
|
||||
return (desktopWidgetInstances || []).find(inst => inst.id === instanceId) || null;
|
||||
}
|
||||
|
||||
function getDesktopWidgetInstancesOfType(widgetType) {
|
||||
return (desktopWidgetInstances || []).filter(inst => inst.widgetType === widgetType);
|
||||
}
|
||||
|
||||
function getEnabledDesktopWidgetInstances() {
|
||||
return (desktopWidgetInstances || []).filter(inst => inst.enabled);
|
||||
}
|
||||
|
||||
function moveDesktopWidgetInstance(instanceId, direction) {
|
||||
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
|
||||
const idx = instances.findIndex(inst => inst.id === instanceId);
|
||||
if (idx === -1)
|
||||
return false;
|
||||
const targetIdx = direction === "up" ? idx - 1 : idx + 1;
|
||||
if (targetIdx < 0 || targetIdx >= instances.length)
|
||||
return false;
|
||||
const temp = instances[idx];
|
||||
instances[idx] = instances[targetIdx];
|
||||
instances[targetIdx] = temp;
|
||||
desktopWidgetInstances = instances;
|
||||
saveSettings();
|
||||
return true;
|
||||
}
|
||||
|
||||
function reorderDesktopWidgetInstance(instanceId, newIndex) {
|
||||
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
|
||||
const idx = instances.findIndex(inst => inst.id === instanceId);
|
||||
if (idx === -1 || newIndex < 0 || newIndex >= instances.length)
|
||||
return false;
|
||||
const [item] = instances.splice(idx, 1);
|
||||
instances.splice(newIndex, 0, item);
|
||||
desktopWidgetInstances = instances;
|
||||
saveSettings();
|
||||
return true;
|
||||
}
|
||||
|
||||
function reorderDesktopWidgetInstanceInGroup(instanceId, groupId, newIndexInGroup) {
|
||||
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
|
||||
const groups = desktopWidgetGroups || [];
|
||||
const groupMatches = inst => {
|
||||
if (groupId === null)
|
||||
return !inst.group || !groups.some(g => g.id === inst.group);
|
||||
return inst.group === groupId;
|
||||
};
|
||||
const groupInstances = instances.filter(groupMatches);
|
||||
const currentGroupIdx = groupInstances.findIndex(inst => inst.id === instanceId);
|
||||
if (currentGroupIdx === -1 || currentGroupIdx === newIndexInGroup)
|
||||
return false;
|
||||
if (newIndexInGroup < 0 || newIndexInGroup >= groupInstances.length)
|
||||
return false;
|
||||
const globalIdx = instances.findIndex(inst => inst.id === instanceId);
|
||||
if (globalIdx === -1)
|
||||
return false;
|
||||
const [item] = instances.splice(globalIdx, 1);
|
||||
const targetInstance = groupInstances[newIndexInGroup];
|
||||
let targetGlobalIdx = instances.findIndex(inst => inst.id === targetInstance.id);
|
||||
if (newIndexInGroup > currentGroupIdx)
|
||||
targetGlobalIdx++;
|
||||
instances.splice(targetGlobalIdx, 0, item);
|
||||
desktopWidgetInstances = instances;
|
||||
saveSettings();
|
||||
return true;
|
||||
}
|
||||
|
||||
function moveDesktopWidgetInstanceToGroup(instanceId, groupId, newIndexInGroup) {
|
||||
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
|
||||
const groups = desktopWidgetGroups || [];
|
||||
@@ -1438,22 +1338,14 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function getDesktopWidgetGroup(groupId) {
|
||||
return (desktopWidgetGroups || []).find(g => g.id === groupId) || null;
|
||||
}
|
||||
|
||||
function getDesktopWidgetInstancesByGroup(groupId) {
|
||||
return (desktopWidgetInstances || []).filter(inst => inst.group === groupId);
|
||||
}
|
||||
|
||||
function getUngroupedDesktopWidgetInstances() {
|
||||
return (desktopWidgetInstances || []).filter(inst => !inst.group);
|
||||
}
|
||||
|
||||
signal forceDankBarLayoutRefresh
|
||||
signal forceDockLayoutRefresh
|
||||
signal widgetDataChanged
|
||||
signal workspaceIconsUpdated
|
||||
signal compositorLayoutRefreshNeeded(bool frame)
|
||||
signal compositorInputRefreshNeeded
|
||||
signal compositorCursorRefreshNeeded
|
||||
signal notificationPopupsInvalidated
|
||||
|
||||
function refreshAuthAvailability() {
|
||||
Processes.detectAuthCapabilities();
|
||||
@@ -1488,31 +1380,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function updateCompositorLayout() {
|
||||
if (typeof CompositorService === "undefined")
|
||||
return;
|
||||
if (CompositorService.isNiri && typeof NiriService !== "undefined")
|
||||
NiriService.generateNiriLayoutConfig();
|
||||
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined")
|
||||
HyprlandService.generateLayoutConfig();
|
||||
if (CompositorService.isMango && typeof MangoService !== "undefined")
|
||||
MangoService.generateLayoutConfig();
|
||||
compositorLayoutRefreshNeeded(false);
|
||||
}
|
||||
|
||||
function updateCompositorInput() {
|
||||
if (typeof CompositorService === "undefined")
|
||||
return;
|
||||
if (CompositorService.isNiri && typeof NiriService !== "undefined")
|
||||
NiriService.generateNiriInputConfig();
|
||||
compositorInputRefreshNeeded();
|
||||
}
|
||||
|
||||
function updateFrameCompositorLayout() {
|
||||
// Generate before begin() so compositor readiness is already pending at transitionRequested
|
||||
if (typeof CompositorService !== "undefined") {
|
||||
if (CompositorService.isNiri && typeof NiriService !== "undefined")
|
||||
NiriService.generateNiriLayoutConfig(true);
|
||||
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined")
|
||||
HyprlandService.generateLayoutConfig(true);
|
||||
}
|
||||
compositorLayoutRefreshNeeded(true);
|
||||
FrameTransitionState.begin();
|
||||
}
|
||||
|
||||
@@ -1757,6 +1633,7 @@ Singleton {
|
||||
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
|
||||
|
||||
const oldVersion = obj?.configVersion ?? 0;
|
||||
const legacyPins = oldVersion < 13 ? Store.extractPins(obj) : null;
|
||||
if (oldVersion < settingsConfigVersion) {
|
||||
const migrated = Store.migrateToVersion(obj, settingsConfigVersion);
|
||||
if (migrated) {
|
||||
@@ -1764,6 +1641,8 @@ Singleton {
|
||||
obj = migrated;
|
||||
}
|
||||
}
|
||||
if (legacyPins)
|
||||
Qt.callLater(() => CacheData.migratePins(legacyPins));
|
||||
|
||||
if (obj?.lockScreenActiveMonitor !== undefined) {
|
||||
var oldVal = obj.lockScreenActiveMonitor;
|
||||
@@ -2162,14 +2041,6 @@ Singleton {
|
||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
||||
}
|
||||
|
||||
function getEffectiveClockDateFormat() {
|
||||
return clockDateFormat && clockDateFormat.length > 0 ? clockDateFormat : "ddd d";
|
||||
}
|
||||
|
||||
function getEffectiveLockDateFormat() {
|
||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
|
||||
}
|
||||
|
||||
function initializeListModels() {
|
||||
const defaultBar = barConfigs[0] || getBarConfig("default");
|
||||
if (defaultBar) {
|
||||
@@ -2182,39 +2053,6 @@ Singleton {
|
||||
widgetDataChanged();
|
||||
}
|
||||
|
||||
function hasNamedWorkspaces() {
|
||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
var ws = NiriService.allWorkspaces[i];
|
||||
if (ws.name && ws.name.trim() !== "")
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getNamedWorkspaces() {
|
||||
var namedWorkspaces = [];
|
||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
|
||||
return namedWorkspaces;
|
||||
|
||||
for (const ws of NiriService.allWorkspaces) {
|
||||
if (ws.name && ws.name.trim() !== "") {
|
||||
namedWorkspaces.push(ws.name);
|
||||
}
|
||||
}
|
||||
return namedWorkspaces;
|
||||
}
|
||||
|
||||
function getPopupYPosition(barHeight) {
|
||||
const defaultBar = barConfigs[0] || getBarConfig("default");
|
||||
const gothOffset = defaultBar?.gothCornersEnabled ? Theme.cornerRadius : 0;
|
||||
const spacing = defaultBar?.spacing ?? 4;
|
||||
const bottomGap = defaultBar?.bottomGap ?? 0;
|
||||
return barHeight + spacing + bottomGap - gothOffset + Theme.popupDistance;
|
||||
}
|
||||
|
||||
function getPopupTriggerPosition(pos, screen, barThickness, widgetWidth, barSpacing, barPosition, barConfig) {
|
||||
const relativeX = pos.x;
|
||||
const relativeY = pos.y;
|
||||
@@ -2508,51 +2346,10 @@ Singleton {
|
||||
updateBarConfigs();
|
||||
|
||||
if (positionChanged) {
|
||||
NotificationService.dismissAllPopups();
|
||||
notificationPopupsInvalidated();
|
||||
}
|
||||
}
|
||||
|
||||
function checkBarCollisions(barId) {
|
||||
const bar = getBarConfig(barId);
|
||||
if (!bar || !bar.enabled)
|
||||
return [];
|
||||
|
||||
const conflicts = [];
|
||||
const enabledBars = getEnabledBarConfigs();
|
||||
|
||||
for (var i = 0; i < enabledBars.length; i++) {
|
||||
const other = enabledBars[i];
|
||||
if (other.id === barId)
|
||||
continue;
|
||||
const samePosition = bar.position === other.position;
|
||||
if (!samePosition)
|
||||
continue;
|
||||
const barScreens = bar.screenPreferences || ["all"];
|
||||
const otherScreens = other.screenPreferences || ["all"];
|
||||
|
||||
const hasAll = barScreens.includes("all") || otherScreens.includes("all");
|
||||
if (hasAll) {
|
||||
conflicts.push({
|
||||
"barId": other.id,
|
||||
"barName": other.name,
|
||||
"reason": "Same position on all screens"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const overlapping = barScreens.some(screen => otherScreens.includes(screen));
|
||||
if (overlapping) {
|
||||
conflicts.push({
|
||||
"barId": other.id,
|
||||
"barName": other.name,
|
||||
"reason": "Same position on overlapping screens"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
function deleteBarConfig(barId) {
|
||||
if (barId === "default")
|
||||
return;
|
||||
@@ -2701,38 +2498,6 @@ Singleton {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function getFrameFilteredScreens() {
|
||||
var prefs = frameScreenPreferences || ["all"];
|
||||
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
|
||||
return Quickshell.screens;
|
||||
}
|
||||
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
|
||||
}
|
||||
|
||||
function getActiveBarEdgeForScreen(screen) {
|
||||
if (!screen)
|
||||
return "";
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled)
|
||||
continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
|
||||
continue;
|
||||
switch (bc.position ?? 0) {
|
||||
case SettingsData.Position.Top:
|
||||
return "top";
|
||||
case SettingsData.Position.Bottom:
|
||||
return "bottom";
|
||||
case SettingsData.Position.Left:
|
||||
return "left";
|
||||
case SettingsData.Position.Right:
|
||||
return "right";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getActiveBarEdgesForScreen(screen) {
|
||||
if (!screen)
|
||||
return [];
|
||||
@@ -2791,51 +2556,19 @@ Singleton {
|
||||
return edges;
|
||||
}
|
||||
|
||||
function frameEdgeInsetForSide(screen, side) {
|
||||
if (!frameEnabled || !screen)
|
||||
return 0;
|
||||
const edges = getActiveBarEdgesForScreen(screen);
|
||||
return edges.includes(side) ? frameBarSize : frameThickness;
|
||||
}
|
||||
readonly property real frameBarContentGap: frameBarInsetPadding < 0 ? frameThickness : frameBarInsetPadding
|
||||
readonly property real frameBarContentGapExtra: Math.max(0, frameBarContentGap - frameThickness)
|
||||
|
||||
function getActiveBarThicknessForScreen(screen) {
|
||||
if (frameEnabled)
|
||||
return frameBarSize;
|
||||
function frameEdgeReservation(screen, edge) {
|
||||
if (!screen)
|
||||
return frameThickness;
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled)
|
||||
continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
|
||||
continue;
|
||||
const innerPadding = bc.innerPadding ?? 4;
|
||||
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
|
||||
const spacing = bc.spacing ?? 4;
|
||||
const bottomGap = bc.bottomGap ?? 0;
|
||||
return barT + spacing + bottomGap;
|
||||
}
|
||||
return frameThickness;
|
||||
return 0;
|
||||
return getActiveBarEdgesForScreen(screen).includes(edge) ? frameBarSize : frameThickness;
|
||||
}
|
||||
|
||||
function sendTestNotifications() {
|
||||
NotificationService.dismissAllPopups();
|
||||
sendTestNotification(0);
|
||||
testNotifTimer1.start();
|
||||
testNotifTimer2.start();
|
||||
}
|
||||
|
||||
function sendTestNotification(index) {
|
||||
const notifications = [["Notification Position Test", "DMS test notification 1 of 3 ~ Hi there!", "preferences-system"], ["Second Test", "DMS Notification 2 of 3 ~ Check it out!", "applications-graphics"], ["Third Test", "DMS notification 3 of 3 ~ Enjoy!", "face-smile"]];
|
||||
|
||||
if (index < 0 || index >= notifications.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notif = notifications[index];
|
||||
testNotificationProcess.command = ["notify-send", "-h", "int:transient:1", "-a", "DMS", "-i", notif[2], notif[0], notif[1]];
|
||||
testNotificationProcess.running = true;
|
||||
function frameEdgeInsetForSide(screen, side) {
|
||||
if (!frameEnabled)
|
||||
return 0;
|
||||
return frameEdgeReservation(screen, side);
|
||||
}
|
||||
|
||||
function setMatugenScheme(scheme) {
|
||||
@@ -2854,15 +2587,6 @@ Singleton {
|
||||
set("matugenContrast", value);
|
||||
}
|
||||
|
||||
function setRunUserMatugenTemplates(enabled) {
|
||||
if (runUserMatugenTemplates === enabled)
|
||||
return;
|
||||
set("runUserMatugenTemplates", enabled);
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme();
|
||||
}
|
||||
}
|
||||
|
||||
function setMatugenTargetMonitor(monitorName) {
|
||||
if (matugenTargetMonitor === monitorName)
|
||||
return;
|
||||
@@ -2881,11 +2605,6 @@ Singleton {
|
||||
SessionData.setWeatherLocation(displayName, coordinates);
|
||||
}
|
||||
|
||||
function setIconTheme(themeName) {
|
||||
const light = iconThemePerMode && typeof SessionData !== "undefined" && SessionData.isLightMode;
|
||||
setIconThemeForMode(themeName, light);
|
||||
}
|
||||
|
||||
function setIconThemeForMode(themeName, light) {
|
||||
if (light)
|
||||
iconThemeLight = themeName;
|
||||
@@ -2931,20 +2650,7 @@ Singleton {
|
||||
// https://github.com/Supreeeme/xwayland-satellite/issues/104
|
||||
// no idea if this matters on other compositors but we also set XCURSOR stuff in the launcher
|
||||
function updateCompositorCursor() {
|
||||
if (typeof CompositorService === "undefined")
|
||||
return;
|
||||
if (CompositorService.isNiri && typeof NiriService !== "undefined") {
|
||||
NiriService.generateNiriCursorConfig();
|
||||
return;
|
||||
}
|
||||
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined") {
|
||||
HyprlandService.generateCursorConfig();
|
||||
return;
|
||||
}
|
||||
if (CompositorService.isMango && typeof MangoService !== "undefined") {
|
||||
MangoService.generateCursorConfig();
|
||||
return;
|
||||
}
|
||||
compositorCursorRefreshNeeded();
|
||||
}
|
||||
|
||||
function updateXResources() {
|
||||
@@ -3012,20 +2718,6 @@ Singleton {
|
||||
return env;
|
||||
}
|
||||
|
||||
function setGtkThemingEnabled(enabled) {
|
||||
set("gtkThemingEnabled", enabled);
|
||||
if (enabled && typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme();
|
||||
}
|
||||
}
|
||||
|
||||
function setQtThemingEnabled(enabled) {
|
||||
set("qtThemingEnabled", enabled);
|
||||
if (enabled && typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme();
|
||||
}
|
||||
}
|
||||
|
||||
function setShowDock(enabled) {
|
||||
showDock = enabled;
|
||||
const defaultBar = barConfigs[0] || getBarConfig("default");
|
||||
@@ -3071,16 +2763,6 @@ Singleton {
|
||||
Qt.callLater(() => forceDockLayoutRefresh());
|
||||
}
|
||||
|
||||
function setDankBarSpacing(spacing) {
|
||||
const defaultBar = barConfigs[0] || getBarConfig("default");
|
||||
if (defaultBar) {
|
||||
updateBarConfig(defaultBar.id, {
|
||||
"spacing": spacing
|
||||
});
|
||||
}
|
||||
updateCompositorLayout();
|
||||
}
|
||||
|
||||
function setDankBarPosition(position) {
|
||||
const defaultBar = barConfigs[0] || getBarConfig("default");
|
||||
if (!defaultBar)
|
||||
@@ -3136,39 +2818,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function resetDankBarWidgetsToDefault() {
|
||||
var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"];
|
||||
var defaultCenter = ["music", "clock", "weather"];
|
||||
var defaultRight = ["systemTray", "clipboard", "notificationButton", "battery", "controlCenterButton"];
|
||||
const defaultBar = barConfigs[0] || getBarConfig("default");
|
||||
if (defaultBar) {
|
||||
updateBarConfig(defaultBar.id, {
|
||||
"leftWidgets": defaultLeft,
|
||||
"centerWidgets": defaultCenter,
|
||||
"rightWidgets": defaultRight
|
||||
});
|
||||
}
|
||||
updateListModel(leftWidgetsModel, defaultLeft);
|
||||
updateListModel(centerWidgetsModel, defaultCenter);
|
||||
updateListModel(rightWidgetsModel, defaultRight);
|
||||
showLauncherButton = true;
|
||||
showWorkspaceSwitcher = true;
|
||||
showFocusedWindow = true;
|
||||
showWeather = true;
|
||||
showMusic = true;
|
||||
showClipboard = true;
|
||||
showCpuUsage = true;
|
||||
showMemUsage = true;
|
||||
showCpuTemp = true;
|
||||
showGpuTemp = true;
|
||||
showSystemTray = true;
|
||||
showClock = true;
|
||||
showNotificationButton = true;
|
||||
showBattery = true;
|
||||
showControlCenterButton = true;
|
||||
showCapsLockIndicator = true;
|
||||
}
|
||||
|
||||
function setWorkspaceNameIcon(workspaceName, iconData) {
|
||||
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons));
|
||||
iconMap[workspaceName] = iconData;
|
||||
@@ -3450,15 +3099,6 @@ Singleton {
|
||||
Theme.reloadCustomThemeVariant();
|
||||
}
|
||||
|
||||
function toggleDankBarVisible() {
|
||||
const defaultBar = barConfigs[0] || getBarConfig("default");
|
||||
if (defaultBar) {
|
||||
updateBarConfig(defaultBar.id, {
|
||||
"visible": !defaultBar.visible
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleShowDock() {
|
||||
setShowDock(!showDock);
|
||||
}
|
||||
@@ -3480,13 +3120,6 @@ Singleton {
|
||||
savePluginSettings();
|
||||
}
|
||||
|
||||
function removePluginSettings(pluginId) {
|
||||
if (pluginSettings[pluginId]) {
|
||||
delete pluginSettings[pluginId];
|
||||
savePluginSettings();
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginSettingsForPlugin(pluginId) {
|
||||
const settings = pluginSettings[pluginId];
|
||||
return settings ? JSON.parse(JSON.stringify(settings)) : {};
|
||||
@@ -3512,22 +3145,6 @@ Singleton {
|
||||
return settings ? JSON.parse(JSON.stringify(settings)) : {};
|
||||
}
|
||||
|
||||
function setNiriOutputSettings(outputId, settings) {
|
||||
const updated = JSON.parse(JSON.stringify(niriOutputSettings));
|
||||
updated[outputId] = settings;
|
||||
niriOutputSettings = updated;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function removeNiriOutputSettings(outputId) {
|
||||
if (!niriOutputSettings[outputId])
|
||||
return;
|
||||
const updated = JSON.parse(JSON.stringify(niriOutputSettings));
|
||||
delete updated[outputId];
|
||||
niriOutputSettings = updated;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function getHyprlandOutputSetting(outputId, key, defaultValue) {
|
||||
if (!hyprlandOutputSettings[outputId])
|
||||
return defaultValue;
|
||||
@@ -3552,40 +3169,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function getHyprlandOutputSettings(outputId) {
|
||||
const settings = hyprlandOutputSettings[outputId];
|
||||
return settings ? JSON.parse(JSON.stringify(settings)) : {};
|
||||
}
|
||||
|
||||
function setHyprlandOutputSettings(outputId, settings) {
|
||||
const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings));
|
||||
updated[outputId] = settings;
|
||||
hyprlandOutputSettings = updated;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function removeHyprlandOutputSettings(outputId) {
|
||||
if (!hyprlandOutputSettings[outputId])
|
||||
return;
|
||||
const updated = JSON.parse(JSON.stringify(hyprlandOutputSettings));
|
||||
delete updated[outputId];
|
||||
hyprlandOutputSettings = updated;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function getDisplayProfiles(compositor) {
|
||||
return displayProfiles[compositor] || {};
|
||||
}
|
||||
|
||||
function setDisplayProfile(compositor, profileId, data) {
|
||||
const updated = JSON.parse(JSON.stringify(displayProfiles));
|
||||
if (!updated[compositor])
|
||||
updated[compositor] = {};
|
||||
updated[compositor][profileId] = data;
|
||||
displayProfiles = updated;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function removeDisplayProfile(compositor, profileId) {
|
||||
if (!displayProfiles[compositor] || !displayProfiles[compositor][profileId])
|
||||
return;
|
||||
@@ -3639,29 +3222,6 @@ Singleton {
|
||||
id: rightWidgetsModel
|
||||
}
|
||||
|
||||
property Process testNotificationProcess
|
||||
|
||||
testNotificationProcess: Process {
|
||||
command: []
|
||||
running: false
|
||||
}
|
||||
|
||||
property Timer testNotifTimer1
|
||||
|
||||
testNotifTimer1: Timer {
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: sendTestNotification(1)
|
||||
}
|
||||
|
||||
property Timer testNotifTimer2
|
||||
|
||||
testNotifTimer2: Timer {
|
||||
interval: 800
|
||||
repeat: false
|
||||
onTriggered: sendTestNotification(2)
|
||||
}
|
||||
|
||||
property alias settingsFile: settingsFile
|
||||
|
||||
Timer {
|
||||
|
||||
@@ -416,24 +416,6 @@ const StockThemes = {
|
||||
},
|
||||
};
|
||||
|
||||
const ThemeCategories = {
|
||||
GENERIC: {
|
||||
name: "Generic",
|
||||
variants: [
|
||||
"blue",
|
||||
"purple",
|
||||
"green",
|
||||
"orange",
|
||||
"red",
|
||||
"cyan",
|
||||
"pink",
|
||||
"amber",
|
||||
"coral",
|
||||
"monochrome",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const ThemeNames = {
|
||||
BLUE: "blue",
|
||||
PURPLE: "purple",
|
||||
@@ -448,10 +430,6 @@ const ThemeNames = {
|
||||
DYNAMIC: "dynamic",
|
||||
};
|
||||
|
||||
function isStockTheme(themeName) {
|
||||
return Object.keys(StockThemes.DARK).includes(themeName);
|
||||
}
|
||||
|
||||
function getAvailableThemes(isLight = false) {
|
||||
return isLight ? StockThemes.LIGHT : StockThemes.DARK;
|
||||
}
|
||||
@@ -464,7 +442,3 @@ function getThemeByName(themeName, isLight = false) {
|
||||
function getAllThemeNames() {
|
||||
return Object.keys(StockThemes.DARK);
|
||||
}
|
||||
|
||||
function getThemeCategories() {
|
||||
return ThemeCategories;
|
||||
}
|
||||
|
||||
+26
-607
@@ -107,8 +107,11 @@ Singleton {
|
||||
property int _colorsRetryCount: 0
|
||||
property double _lastGenerateMs: 0
|
||||
|
||||
property bool themeModeAutomationActive: false
|
||||
property bool dmsServiceWasDisconnected: true
|
||||
property bool blurLayersActive: false
|
||||
property bool matugenToastSuppressed: false
|
||||
|
||||
signal screenTransitionNeeded
|
||||
signal themeGenerationStarting
|
||||
|
||||
readonly property var dank16: {
|
||||
const raw = matugenColors?.dank16;
|
||||
@@ -144,7 +147,7 @@ Singleton {
|
||||
Quickshell.execDetached(["mkdir", "-p", stateDir]);
|
||||
// shellDir may be an embedded-UI extraction, which is read-only and
|
||||
// unexecutable (dankgo shellapp/shellfs makeReadOnly chmods 0444)
|
||||
Quickshell.execDetached(["bash", shellDir + "/scripts/gtk.sh", configDir, "", "", shellDir]);
|
||||
Quickshell.execDetached(["bash", shellDir + "/scripts/gtk.sh", configDir, "assets", "", shellDir]);
|
||||
Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
|
||||
matugenAvailable = (code === 0) && !envDisableMatugen;
|
||||
|
||||
@@ -196,235 +199,6 @@ Singleton {
|
||||
const currentIsLight = (typeof SessionData !== "undefined") ? SessionData.isLightMode : false;
|
||||
SettingsData.updateCosmicThemeMode(currentIsLight);
|
||||
}
|
||||
|
||||
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
|
||||
startThemeModeAutomation();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
enabled: typeof SessionData !== "undefined"
|
||||
|
||||
function onThemeModeAutoEnabledChanged() {
|
||||
if (SessionData.themeModeAutoEnabled) {
|
||||
root.startThemeModeAutomation();
|
||||
} else {
|
||||
root.stopThemeModeAutomation();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeAutoModeChanged() {
|
||||
if (root.themeModeAutomationActive) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeStartHourChanged() {
|
||||
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeStartMinuteChanged() {
|
||||
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeEndHourChanged() {
|
||||
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeEndMinuteChanged() {
|
||||
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onThemeModeShareGammaSettingsChanged() {
|
||||
if (root.themeModeAutomationActive) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeStartHourChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeStartMinuteChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeEndHourChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeEndMinuteChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||
root.evaluateThemeMode();
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onLatitudeChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
|
||||
if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
});
|
||||
}
|
||||
root.evaluateThemeMode();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onLongitudeChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
|
||||
if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
});
|
||||
}
|
||||
root.evaluateThemeMode();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
|
||||
function onNightModeUseIPLocationChanged() {
|
||||
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
|
||||
if (typeof DMSService !== "undefined") {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": SessionData.nightModeUseIPLocation
|
||||
}, response => {
|
||||
if (!response.error && !SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
root.evaluateThemeMode();
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// React to gamma backend's isDay state changes for location-based mode
|
||||
Connections {
|
||||
target: DisplayService
|
||||
enabled: typeof DisplayService !== "undefined" && typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location" && !themeAutoBackendAvailable()
|
||||
|
||||
function onGammaIsDayChanged() {
|
||||
if (root.isLightMode !== DisplayService.gammaIsDay) {
|
||||
root.setLightMode(DisplayService.gammaIsDay, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
|
||||
function onThemeAutoStateUpdate(data) {
|
||||
if (!SessionData.themeModeAutoEnabled) {
|
||||
return;
|
||||
}
|
||||
applyThemeAutoState(data);
|
||||
}
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected && SessionData.themeModeAutoMode === "time") {
|
||||
root.syncTimeThemeSchedule();
|
||||
}
|
||||
|
||||
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
|
||||
root.syncLocationThemeSchedule();
|
||||
}
|
||||
|
||||
if (themeAutoBackendAvailable() && SessionData.themeModeAutoEnabled) {
|
||||
DMSService.sendRequest("theme.auto.getState", null, response => {
|
||||
if (response && response.result) {
|
||||
applyThemeAutoState(response.result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!SessionData.themeModeAutoEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": true
|
||||
}, response => {
|
||||
if (!response.error) {
|
||||
log.info("Theme automation: IP location enabled after connection");
|
||||
}
|
||||
});
|
||||
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": false
|
||||
}, response => {
|
||||
if (!response.error) {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
}, locationResponse => {
|
||||
if (locationResponse?.error) {
|
||||
log.warn("Theme automation: Failed to set location", locationResponse.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.warn("Theme automation: No location configured");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionService
|
||||
enabled: SessionData.themeModeAutoEnabled
|
||||
|
||||
function onSessionUnlocked() {
|
||||
root.triggerThemeAutomationRefresh();
|
||||
}
|
||||
|
||||
function onSessionResumed() {
|
||||
root.triggerThemeAutomationRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
function triggerThemeAutomationRefresh() {
|
||||
if (!themeAutoBackendAvailable()) {
|
||||
root.evaluateThemeMode();
|
||||
return;
|
||||
}
|
||||
DMSService.sendRequest("theme.auto.trigger", {});
|
||||
}
|
||||
|
||||
function getMatugenColor(path, fallback) {
|
||||
@@ -571,8 +345,8 @@ Singleton {
|
||||
property color surfaceVariantAlpha: withAlpha(surfaceVariant, 0.2)
|
||||
|
||||
readonly property bool foregroundLayers: typeof SettingsData === "undefined" || (SettingsData.blurForegroundLayers ?? true)
|
||||
readonly property bool blurForegroundLayers: BlurService.enabled && foregroundLayers
|
||||
readonly property bool transparentBlurLayers: BlurService.enabled && !foregroundLayers
|
||||
readonly property bool blurForegroundLayers: blurLayersActive && foregroundLayers
|
||||
readonly property bool transparentBlurLayers: blurLayersActive && !foregroundLayers
|
||||
readonly property bool notificationForegroundLayers: typeof SettingsData === "undefined" || (SettingsData.notificationForegroundLayers ?? true)
|
||||
readonly property color readableSurface: withAlpha(surfaceContainer, popupTransparency)
|
||||
readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
|
||||
@@ -658,7 +432,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (foregroundLayers ? withAlpha(surfaceContainerHigh, BlurService.enabled ? Math.min(popupTransparency, 0.24) : popupTransparency) : withAlpha(surfaceContainer, 0))
|
||||
readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (foregroundLayers ? withAlpha(surfaceContainerHigh, blurLayersActive ? Math.min(popupTransparency, 0.24) : popupTransparency) : withAlpha(surfaceContainer, 0))
|
||||
readonly property color ccPillInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.08) : nestedSurface
|
||||
readonly property color ccPillInactiveHoverBg: transparentBlurLayers ? withAlpha(primary, 0.10) : primaryPressed
|
||||
readonly property color ccSliderTrackColor: transparentBlurLayers ? surfaceText : surfaceContainerHigh
|
||||
@@ -978,22 +752,6 @@ Singleton {
|
||||
};
|
||||
}
|
||||
|
||||
function elevationTintOpacity(level) {
|
||||
if (!level)
|
||||
return 0;
|
||||
if (level === elevationLevel1)
|
||||
return 0.05;
|
||||
if (level === elevationLevel2)
|
||||
return 0.08;
|
||||
if (level === elevationLevel3)
|
||||
return 0.11;
|
||||
if (level === elevationLevel4)
|
||||
return 0.12;
|
||||
if (level === elevationLevel5)
|
||||
return 0.14;
|
||||
return 0.08;
|
||||
}
|
||||
|
||||
readonly property var animationDurations: [
|
||||
{
|
||||
"shorter": 0,
|
||||
@@ -1261,9 +1019,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function screenTransition() {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.doScreenTransition();
|
||||
}
|
||||
screenTransitionNeeded();
|
||||
}
|
||||
|
||||
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
|
||||
@@ -1315,7 +1071,6 @@ Singleton {
|
||||
SessionData.setLightMode(light);
|
||||
}
|
||||
|
||||
PortalService.setLightMode(light);
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
SettingsData.updateCosmicThemeMode(light);
|
||||
}
|
||||
@@ -1326,22 +1081,6 @@ Singleton {
|
||||
setLightMode(!isLightMode, savePrefs, true);
|
||||
}
|
||||
|
||||
function forceGenerateSystemThemes() {
|
||||
if (!matugenAvailable) {
|
||||
return;
|
||||
}
|
||||
generateSystemThemesFromCurrentTheme();
|
||||
}
|
||||
|
||||
function getAvailableThemes() {
|
||||
return StockThemes.getAllThemeNames();
|
||||
}
|
||||
|
||||
function getThemeDisplayName(themeName) {
|
||||
const themeData = StockThemes.getThemeByName(themeName, isLightMode);
|
||||
return themeData.name;
|
||||
}
|
||||
|
||||
function getThemeColors(themeName) {
|
||||
if (themeName === "custom" && customThemeData) {
|
||||
return customThemeData;
|
||||
@@ -1457,10 +1196,6 @@ Singleton {
|
||||
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
|
||||
property string currentThemeName: currentTheme
|
||||
|
||||
function panelBackground() {
|
||||
return withAlpha(surfaceContainer, panelTransparency);
|
||||
}
|
||||
|
||||
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
|
||||
|
||||
property bool widgetBackgroundHasAlpha: {
|
||||
@@ -1537,10 +1272,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function isColorDark(c) {
|
||||
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
|
||||
}
|
||||
|
||||
function barIconSize(barThickness, offset, maximizeIcon, iconScale) {
|
||||
const defaultOffset = offset !== undefined ? offset : -6;
|
||||
const size = (maximizeIcon ?? false) ? iconSizeLarge : iconSize;
|
||||
@@ -1623,19 +1354,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function getPowerProfileDescription(profile) {
|
||||
switch (profile) {
|
||||
case 0:
|
||||
return I18n.tr("Extend battery life", "power profile description");
|
||||
case 1:
|
||||
return I18n.tr("Balance power and performance", "power profile description");
|
||||
case 2:
|
||||
return I18n.tr("Prioritize performance", "power profile description");
|
||||
default:
|
||||
return I18n.tr("Custom power profile", "power profile description");
|
||||
}
|
||||
}
|
||||
|
||||
function onLightModeChanged() {
|
||||
if (currentTheme === "custom" && customThemeFileView.path) {
|
||||
customThemeFileView.reload();
|
||||
@@ -1663,9 +1381,7 @@ Singleton {
|
||||
|
||||
log.info("Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", stockColors ? "(stock colors)" : "(dynamic)");
|
||||
|
||||
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
|
||||
NiriService.suppressNextToast();
|
||||
}
|
||||
themeGenerationStarting();
|
||||
|
||||
const desired = {
|
||||
"kind": kind,
|
||||
@@ -1777,8 +1493,7 @@ Singleton {
|
||||
|
||||
if (currentTheme === dynamic) {
|
||||
if (!rawWallpaperPath) {
|
||||
log.warn("Auto theme has no wallpaper - skipping matugen, syncing portal mode only");
|
||||
PortalService.setLightMode(isLight);
|
||||
log.warn("Auto theme has no wallpaper - skipping matugen");
|
||||
return;
|
||||
}
|
||||
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
|
||||
@@ -1955,7 +1670,13 @@ Singleton {
|
||||
function patchGtk3colors() {
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
|
||||
Proc.runCommand("gtk3Patcher", ["bash", shellDir + "/scripts/gtk.sh", configDir, "patch", isLight, shellDir], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
switch (exitCode) {
|
||||
case 0:
|
||||
refreshGtkTheme();
|
||||
break;
|
||||
case 2:
|
||||
break;
|
||||
default:
|
||||
log.warn(`Failed to patch GTK3 colors: ${output}`);
|
||||
}
|
||||
});
|
||||
@@ -1972,7 +1693,7 @@ Singleton {
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
|
||||
Proc.runCommand("gtkApplier", ["bash", shellDir + "/scripts/gtk.sh", configDir, "apply", isLight, shellDir], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) {
|
||||
if (typeof ToastService !== "undefined" && !root.matugenToastSuppressed) {
|
||||
ToastService.showInfo(I18n.tr("GTK colors applied successfully"));
|
||||
}
|
||||
} else {
|
||||
@@ -2010,18 +1731,17 @@ Singleton {
|
||||
return Qt.rgba(c.r, c.g, c.b, a);
|
||||
}
|
||||
|
||||
function popupLayerColor(baseColor) {
|
||||
if (isConnectedEffect)
|
||||
return connectedSurfaceColor;
|
||||
return withAlpha(baseColor, popupTransparency);
|
||||
}
|
||||
|
||||
function blendAlpha(c, a) {
|
||||
if (!c || c.r === undefined)
|
||||
return Qt.rgba(0, 0, 0, 0);
|
||||
return Qt.rgba(c.r, c.g, c.b, c.a * a);
|
||||
}
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2;
|
||||
return isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||
}
|
||||
|
||||
function blend(c1, c2, r) {
|
||||
return Qt.rgba(c1.r * (1 - r) + c2.r * r, c1.g * (1 - r) + c2.g * r, c1.b * (1 - r) + c2.b * r, c1.a * (1 - r) + c2.a * r);
|
||||
}
|
||||
@@ -2167,10 +1887,8 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!pendingThemeRequest) {
|
||||
if (SettingsData.matugenTemplateGtk) {
|
||||
if (SettingsData.matugenTemplateGtk)
|
||||
patchGtk3colors();
|
||||
refreshGtkTheme();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2340,303 +2058,4 @@ Singleton {
|
||||
root.switchTheme(defaultTheme, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Theme mode automation functions
|
||||
function themeAutoBackendAvailable() {
|
||||
return typeof DMSService !== "undefined" && DMSService.isConnected && Array.isArray(DMSService.capabilities) && DMSService.capabilities.includes("theme.auto");
|
||||
}
|
||||
|
||||
function applyThemeAutoState(state) {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
if (state.config && state.config.mode && state.config.mode !== SessionData.themeModeAutoMode) {
|
||||
return;
|
||||
}
|
||||
if (typeof SessionData !== "undefined" && state.nextTransition !== undefined) {
|
||||
SessionData.themeModeNextTransition = state.nextTransition || "";
|
||||
}
|
||||
if (state.isLight !== undefined && root.isLightMode !== state.isLight) {
|
||||
root.setLightMode(state.isLight, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
function syncTimeThemeSchedule() {
|
||||
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DMSService.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "time";
|
||||
|
||||
if (!timeModeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("theme.auto.setMode", {
|
||||
"mode": "time"
|
||||
});
|
||||
|
||||
const shareSettings = SessionData.themeModeShareGammaSettings;
|
||||
const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour;
|
||||
const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
|
||||
const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour;
|
||||
const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
|
||||
|
||||
DMSService.sendRequest("theme.auto.setSchedule", {
|
||||
"startHour": startHour,
|
||||
"startMinute": startMinute,
|
||||
"endHour": endHour,
|
||||
"endMinute": endMinute
|
||||
}, response => {
|
||||
if (response && response.error) {
|
||||
log.error("Theme automation: Failed to sync time schedule:", response.error);
|
||||
}
|
||||
});
|
||||
|
||||
DMSService.sendRequest("theme.auto.setEnabled", {
|
||||
"enabled": true
|
||||
});
|
||||
DMSService.sendRequest("theme.auto.trigger", {});
|
||||
}
|
||||
|
||||
function syncLocationThemeSchedule() {
|
||||
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DMSService.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const locationModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location";
|
||||
|
||||
if (!locationModeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
DMSService.sendRequest("theme.auto.setMode", {
|
||||
"mode": "location"
|
||||
});
|
||||
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
DMSService.sendRequest("theme.auto.setUseIPLocation", {
|
||||
"use": true
|
||||
});
|
||||
} else {
|
||||
DMSService.sendRequest("theme.auto.setUseIPLocation", {
|
||||
"use": false
|
||||
});
|
||||
if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("theme.auto.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DMSService.sendRequest("theme.auto.setEnabled", {
|
||||
"enabled": true
|
||||
});
|
||||
DMSService.sendRequest("theme.auto.trigger", {});
|
||||
}
|
||||
|
||||
function evaluateThemeMode() {
|
||||
if (typeof SessionData === "undefined" || !SessionData.themeModeAutoEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeAutoBackendAvailable()) {
|
||||
DMSService.sendRequest("theme.auto.getState", null, response => {
|
||||
if (response && response.result) {
|
||||
applyThemeAutoState(response.result);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = SessionData.themeModeAutoMode;
|
||||
|
||||
if (mode === "location") {
|
||||
evaluateLocationBasedThemeMode();
|
||||
} else {
|
||||
evaluateTimeBasedThemeMode();
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateLocationBasedThemeMode() {
|
||||
if (typeof DisplayService !== "undefined") {
|
||||
const shouldBeLight = DisplayService.gammaIsDay;
|
||||
if (root.isLightMode !== shouldBeLight) {
|
||||
root.setLightMode(shouldBeLight, true, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
const shouldBeLight = calculateIsDaytime(SessionData.latitude, SessionData.longitude);
|
||||
if (root.isLightMode !== shouldBeLight) {
|
||||
root.setLightMode(shouldBeLight, true, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.themeModeAutomationActive) {
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
log.warn("Theme automation: Waiting for IP location from backend");
|
||||
} else {
|
||||
log.warn("Theme automation: Location mode requires coordinates");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateTimeBasedThemeMode() {
|
||||
const shareSettings = SessionData.themeModeShareGammaSettings;
|
||||
|
||||
const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour;
|
||||
const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
|
||||
const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour;
|
||||
const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
|
||||
|
||||
const now = new Date();
|
||||
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
const startMinutes = startHour * 60 + startMinute;
|
||||
const endMinutes = endHour * 60 + endMinute;
|
||||
|
||||
let shouldBeLight;
|
||||
if (startMinutes < endMinutes) {
|
||||
shouldBeLight = currentMinutes < startMinutes || currentMinutes >= endMinutes;
|
||||
} else {
|
||||
shouldBeLight = currentMinutes >= endMinutes && currentMinutes < startMinutes;
|
||||
}
|
||||
|
||||
if (root.isLightMode !== shouldBeLight) {
|
||||
root.setLightMode(shouldBeLight, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateIsDaytime(lat, lng) {
|
||||
const now = new Date();
|
||||
const start = new Date(now.getFullYear(), 0, 0);
|
||||
const diff = now - start;
|
||||
const dayOfYear = Math.floor(diff / 86400000);
|
||||
const latRad = lat * Math.PI / 180;
|
||||
|
||||
const declination = 23.45 * Math.sin((360 / 365) * (dayOfYear - 81) * Math.PI / 180);
|
||||
const declinationRad = declination * Math.PI / 180;
|
||||
|
||||
const cosHourAngle = -Math.tan(latRad) * Math.tan(declinationRad);
|
||||
|
||||
if (cosHourAngle > 1) {
|
||||
return false; // Polar night
|
||||
}
|
||||
if (cosHourAngle < -1) {
|
||||
return true; // Midnight sun
|
||||
}
|
||||
|
||||
const hourAngle = Math.acos(cosHourAngle);
|
||||
const hourAngleDeg = hourAngle * 180 / Math.PI;
|
||||
|
||||
const sunriseHour = 12 - hourAngleDeg / 15;
|
||||
const sunsetHour = 12 + hourAngleDeg / 15;
|
||||
|
||||
const timeZoneOffset = now.getTimezoneOffset() / 60;
|
||||
const localSunrise = sunriseHour - lng / 15 - timeZoneOffset;
|
||||
const localSunset = sunsetHour - lng / 15 - timeZoneOffset;
|
||||
|
||||
const currentHour = now.getHours() + now.getMinutes() / 60;
|
||||
|
||||
const normalizeSunrise = ((localSunrise % 24) + 24) % 24;
|
||||
const normalizeSunset = ((localSunset % 24) + 24) % 24;
|
||||
|
||||
return currentHour >= normalizeSunrise && currentHour < normalizeSunset;
|
||||
}
|
||||
|
||||
// Helper function to send location to backend
|
||||
function sendLocationToBackend() {
|
||||
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DMSService.isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": true
|
||||
}, response => {
|
||||
if (response?.error) {
|
||||
log.warn("Theme automation: Failed to enable IP location", response.error);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": false
|
||||
}, response => {
|
||||
if (!response.error) {
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
}, locResp => {
|
||||
if (locResp?.error) {
|
||||
log.warn("Theme automation: Failed to set location", locResp.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: locationRetryTimer
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: false
|
||||
property int retryCount: 0
|
||||
|
||||
onTriggered: {
|
||||
if (root.sendLocationToBackend()) {
|
||||
stop();
|
||||
retryCount = 0;
|
||||
root.evaluateThemeMode();
|
||||
} else {
|
||||
retryCount++;
|
||||
if (retryCount >= 10) {
|
||||
stop();
|
||||
retryCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startThemeModeAutomation() {
|
||||
root.themeModeAutomationActive = true;
|
||||
|
||||
root.syncTimeThemeSchedule();
|
||||
root.syncLocationThemeSchedule();
|
||||
|
||||
const sent = root.sendLocationToBackend();
|
||||
|
||||
if (!sent && typeof SessionData !== "undefined" && SessionData.themeModeAutoMode === "location") {
|
||||
locationRetryTimer.start();
|
||||
} else {
|
||||
root.evaluateThemeMode();
|
||||
}
|
||||
}
|
||||
|
||||
function stopThemeModeAutomation() {
|
||||
root.themeModeAutomationActive = false;
|
||||
if (typeof DMSService !== "undefined" && DMSService.isConnected) {
|
||||
DMSService.sendRequest("theme.auto.setEnabled", {
|
||||
"enabled": false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,13 @@
|
||||
function markdownToHtml(text) {
|
||||
if (!text) return "";
|
||||
|
||||
// Store code blocks and inline code to protect them from further processing
|
||||
const codeBlocks = [];
|
||||
const inlineCode = [];
|
||||
let blockIndex = 0;
|
||||
let inlineIndex = 0;
|
||||
|
||||
// First, extract and replace code blocks with placeholders
|
||||
let html = text.replace(/```([\s\S]*?)```/g, (match, code) => {
|
||||
// Trim leading and trailing blank lines only
|
||||
const trimmedCode = code.replace(/^\n+|\n+$/g, '');
|
||||
// Escape HTML entities in code
|
||||
const escapedCode = trimmedCode.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
@@ -22,9 +18,7 @@ function markdownToHtml(text) {
|
||||
return `\x00CODEBLOCK${blockIndex++}\x00`;
|
||||
});
|
||||
|
||||
// Extract and replace inline code
|
||||
html = html.replace(/`([^`]+)`/g, (match, code) => {
|
||||
// Escape HTML entities in code
|
||||
const escapedCode = code.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
@@ -40,12 +34,10 @@ function markdownToHtml(text) {
|
||||
return prefix + `\x00URL${urlIndex++}\x00`;
|
||||
});
|
||||
|
||||
// Escape HTML entities (but not in code blocks or URLs)
|
||||
html = html.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// Headers
|
||||
html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>');
|
||||
html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>');
|
||||
@@ -58,20 +50,16 @@ function markdownToHtml(text) {
|
||||
html = html.replace(/__(.*?)__/g, '<b>$1</b>');
|
||||
html = html.replace(/_(.*?)_/g, '<i>$1</i>');
|
||||
|
||||
// Links
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||
|
||||
// Lists
|
||||
html = html.replace(/^\* (.*?)$/gm, '<li>$1</li>');
|
||||
html = html.replace(/^- (.*?)$/gm, '<li>$1</li>');
|
||||
html = html.replace(/^\d+\. (.*?)$/gm, '<li>$1</li>');
|
||||
|
||||
// Wrap consecutive list items in ul/ol tags
|
||||
html = html.replace(/(<li>[\s\S]*?<\/li>\s*)+/g, function(match) {
|
||||
return '<ul>' + match + '</ul>';
|
||||
});
|
||||
|
||||
// Restore extracted URLs as anchor tags (preserves raw & in href)
|
||||
html = html.replace(/\x00URL(\d+)\x00/g, (_, index) => {
|
||||
const url = urls[parseInt(index)];
|
||||
const display = url.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
@@ -87,30 +75,23 @@ function markdownToHtml(text) {
|
||||
return inlineCode[parseInt(index)];
|
||||
});
|
||||
|
||||
// Line breaks (after code blocks are restored)
|
||||
html = html.replace(/\n\n/g, '</p><p>');
|
||||
html = html.replace(/\n/g, '<br/>');
|
||||
|
||||
// Wrap in paragraph tags if not already wrapped
|
||||
if (!html.startsWith('<')) {
|
||||
html = '<p>' + html + '</p>';
|
||||
}
|
||||
|
||||
// Clean up the final HTML
|
||||
// Remove <br/> tags immediately before block elements
|
||||
html = html.replace(/<br\/>\s*<pre>/g, '<pre>');
|
||||
html = html.replace(/<br\/>\s*<ul>/g, '<ul>');
|
||||
html = html.replace(/<br\/>\s*<h[1-6]>/g, '<h$1>');
|
||||
|
||||
// Remove empty paragraphs
|
||||
html = html.replace(/<p>\s*<\/p>/g, '');
|
||||
html = html.replace(/<p>\s*<br\/>\s*<\/p>/g, '');
|
||||
|
||||
// Remove excessive line breaks
|
||||
html = html.replace(/(<br\/>){3,}/g, '<br/><br/>'); // Max 2 consecutive line breaks
|
||||
html = html.replace(/(<\/p>)\s*(<p>)/g, '$1$2'); // Remove whitespace between paragraphs
|
||||
html = html.replace(/(<br\/>){3,}/g, '<br/><br/>');
|
||||
html = html.replace(/(<\/p>)\s*(<p>)/g, '$1$2');
|
||||
|
||||
// Remove leading/trailing whitespace
|
||||
html = html.trim();
|
||||
|
||||
return html;
|
||||
|
||||
@@ -5,11 +5,13 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
signal toastRequested(int severity, string title, string body, string command, string category)
|
||||
signal toastCategoryDismissed(string category)
|
||||
|
||||
property var settingsRoot: null
|
||||
|
||||
onSettingsRootChanged: {
|
||||
@@ -317,7 +319,7 @@ Singleton {
|
||||
function launchAuthApplyTerminalFallback(fromPrecheck, details) {
|
||||
authApplyTerminalFallbackFromPrecheck = fromPrecheck;
|
||||
if (details && details !== "")
|
||||
ToastService.showInfo(I18n.tr("Authentication changes need sudo. Opening terminal so you can use password or fingerprint."), details, "", "auth-sync");
|
||||
toastRequested(0, I18n.tr("Authentication changes need sudo. Opening terminal so you can use password or fingerprint."), details, "", "auth-sync");
|
||||
authApplyTerminalFallbackStderr = "";
|
||||
authApplyTerminalFallbackProcess.running = true;
|
||||
}
|
||||
@@ -364,21 +366,21 @@ Singleton {
|
||||
}
|
||||
|
||||
function deferGreeterAutoLoginSyncToPill(details) {
|
||||
ToastService.dismissCategory("greeter-autologin-sync");
|
||||
toastCategoryDismissed("greeter-autologin-sync");
|
||||
if (settingsRoot)
|
||||
settingsRoot.set("greeterSyncPending", true);
|
||||
ToastService.showWarning(I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms-greeter sync --autologin", "greeter-autologin-sync");
|
||||
toastRequested(1, I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms-greeter sync --autologin", "greeter-autologin-sync");
|
||||
finishGreeterAutoLoginSync();
|
||||
}
|
||||
|
||||
function greeterAutoLoginSyncSuccessToast(details) {
|
||||
const enabling = settingsRoot && settingsRoot.greeterAutoLogin;
|
||||
// Clear the sticky in-progress toast, then confirm with an auto-dismissing toast.
|
||||
ToastService.dismissCategory("greeter-autologin-sync");
|
||||
toastCategoryDismissed("greeter-autologin-sync");
|
||||
if (enabling) {
|
||||
ToastService.showWarning(I18n.tr("Auto-login enabled"), I18n.tr("You'll skip the greeter password after the next reboot. The lock screen and signing out still require your password.") + (details ? "\n\n" + details : ""));
|
||||
toastRequested(1, I18n.tr("Auto-login enabled"), I18n.tr("You'll skip the greeter password after the next reboot. The lock screen and signing out still require your password.") + (details ? "\n\n" + details : ""), "", "");
|
||||
} else {
|
||||
ToastService.showInfo(I18n.tr("Auto-login disabled"), I18n.tr("You'll enter your password at the greeter after the next reboot.") + (details ? "\n\n" + details : ""));
|
||||
toastRequested(0, I18n.tr("Auto-login disabled"), I18n.tr("You'll enter your password at the greeter after the next reboot.") + (details ? "\n\n" + details : ""), "", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,7 +577,7 @@ Singleton {
|
||||
onExited: exitCode => {
|
||||
const enabling = root.settingsRoot && root.settingsRoot.greeterAutoLogin;
|
||||
if (exitCode === 0) {
|
||||
ToastService.showWarning(enabling ? I18n.tr("Applying auto-login on startup...") : I18n.tr("Disabling auto-login on startup..."), "", "dms-greeter sync --autologin", "greeter-autologin-sync");
|
||||
root.toastRequested(1, enabling ? I18n.tr("Applying auto-login on startup...") : I18n.tr("Disabling auto-login on startup..."), "", "dms-greeter sync --autologin", "greeter-autologin-sync");
|
||||
root.greeterAutoLoginSyncProcess.running = true;
|
||||
return;
|
||||
}
|
||||
@@ -604,7 +606,7 @@ Singleton {
|
||||
let details = out;
|
||||
if (err !== "")
|
||||
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
|
||||
ToastService.showInfo(I18n.tr("Authentication changes applied"), details, "", "auth-sync");
|
||||
root.toastRequested(0, I18n.tr("Authentication changes applied"), details, "", "auth-sync");
|
||||
root.detectAuthCapabilities();
|
||||
root.finishAuthApply();
|
||||
return;
|
||||
@@ -615,7 +617,7 @@ Singleton {
|
||||
details = out;
|
||||
if (err !== "")
|
||||
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
|
||||
ToastService.showWarning(I18n.tr("Background authentication sync failed. Trying terminal mode."), details, "", "auth-sync");
|
||||
root.toastRequested(1, I18n.tr("Background authentication sync failed. Trying terminal mode."), details, "", "auth-sync");
|
||||
root.launchAuthApplyTerminalFallback(false, "");
|
||||
}
|
||||
}
|
||||
@@ -631,7 +633,7 @@ Singleton {
|
||||
onExited: exitCode => {
|
||||
const err = (root.authApplySudoProbeStderr || "").trim();
|
||||
if (exitCode === 0) {
|
||||
ToastService.showInfo(I18n.tr("Applying authentication changes..."), "", "", "auth-sync");
|
||||
root.toastRequested(0, I18n.tr("Applying authentication changes..."), "", "", "auth-sync");
|
||||
root.authApplyProcess.running = true;
|
||||
return;
|
||||
}
|
||||
@@ -651,10 +653,10 @@ Singleton {
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
const message = root.authApplyTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete authentication there; it will close automatically when done.");
|
||||
ToastService.showInfo(message, "", "", "auth-sync");
|
||||
root.toastRequested(0, message, "", "", "auth-sync");
|
||||
} else {
|
||||
let details = (root.authApplyTerminalFallbackStderr || "").trim();
|
||||
ToastService.showError(I18n.tr("Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.") + " (exit " + exitCode + ")", details, "", "auth-sync");
|
||||
root.toastRequested(2, I18n.tr("Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.") + " (exit " + exitCode + ")", details, "", "auth-sync");
|
||||
}
|
||||
root.finishAuthApply();
|
||||
}
|
||||
|
||||
@@ -75,26 +75,3 @@ function migrateToVersion(obj, targetVersion, settingsData) {
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
function cleanup(fileText) {
|
||||
var getValidKeys = SpecModule.getValidKeys;
|
||||
if (!fileText || !fileText.trim()) return null;
|
||||
|
||||
try {
|
||||
var session = JSON.parse(fileText);
|
||||
var validKeys = getValidKeys();
|
||||
var needsSave = false;
|
||||
|
||||
for (var key in session) {
|
||||
if (validKeys.indexOf(key) < 0) {
|
||||
delete session[key];
|
||||
needsSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
return needsSave ? JSON.stringify(session, null, 2) : null;
|
||||
} catch (e) {
|
||||
console.warn("SessionData: Failed to cleanup unused keys:", e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,11 +373,6 @@ var SPEC = {
|
||||
fadeToDpmsEnabled: { def: true },
|
||||
fadeToDpmsGracePeriod: { def: 5 },
|
||||
launchPrefix: { def: "" },
|
||||
brightnessDevicePins: { def: {} },
|
||||
wifiNetworkPins: { def: {} },
|
||||
bluetoothDevicePins: { def: {} },
|
||||
audioInputDevicePins: { def: {} },
|
||||
audioOutputDevicePins: { def: {} },
|
||||
|
||||
gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" },
|
||||
qtThemingEnabled: { def: false, onChange: "regenSystemThemes" },
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
.import "./SettingsSpec.js" as SpecModule
|
||||
|
||||
var PIN_KEYS = ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"];
|
||||
|
||||
function extractPins(obj) {
|
||||
if (!obj) return null;
|
||||
|
||||
var pins = null;
|
||||
for (var i = 0; i < PIN_KEYS.length; i++) {
|
||||
var value = obj[PIN_KEYS[i]];
|
||||
if (!value || Object.keys(value).length === 0) continue;
|
||||
if (!pins) pins = {};
|
||||
pins[PIN_KEYS[i]] = value;
|
||||
}
|
||||
return pins;
|
||||
}
|
||||
|
||||
function parse(root, jsonObj) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
|
||||
@@ -263,28 +278,16 @@ function migrateToVersion(obj, targetVersion) {
|
||||
settings.configVersion = 12;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
if (currentVersion < 13) {
|
||||
console.info("Migrating settings from version", currentVersion, "to version 13");
|
||||
console.info("Moving device and network pins to cache.json");
|
||||
|
||||
function cleanup(fileText) {
|
||||
var getValidKeys = SpecModule.getValidKeys;
|
||||
if (!fileText || !fileText.trim()) return;
|
||||
|
||||
try {
|
||||
var settings = JSON.parse(fileText);
|
||||
var validKeys = getValidKeys();
|
||||
var needsSave = false;
|
||||
|
||||
for (var key in settings) {
|
||||
if (validKeys.indexOf(key) < 0) {
|
||||
delete settings[key];
|
||||
needsSave = true;
|
||||
}
|
||||
for (var p = 0; p < PIN_KEYS.length; p++) {
|
||||
delete settings[PIN_KEYS[p]];
|
||||
}
|
||||
|
||||
return needsSave ? JSON.stringify(settings, null, 2) : null;
|
||||
} catch (e) {
|
||||
console.warn("SettingsData: Failed to cleanup unused keys:", e.message);
|
||||
return null;
|
||||
settings.configVersion = 13;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -122,11 +122,6 @@ const times = [
|
||||
|
||||
// adds a custom time to the times config
|
||||
|
||||
function addTime(angle, riseName, setName) {
|
||||
times.push([angle, riseName, setName]);
|
||||
};
|
||||
|
||||
|
||||
// calculations for sun times
|
||||
|
||||
const J0 = 0.0009;
|
||||
@@ -246,63 +241,3 @@ function getMoonIllumination(date) {
|
||||
angle
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function hoursLater(date, h) {
|
||||
return new Date(date.valueOf() + h * dayMs / 24);
|
||||
}
|
||||
|
||||
// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
|
||||
|
||||
function getMoonTimes(date, lat, lng, inUTC) {
|
||||
const t = new Date(date);
|
||||
if (inUTC) t.setUTCHours(0, 0, 0, 0);
|
||||
else t.setHours(0, 0, 0, 0);
|
||||
|
||||
const hc = 0.133 * rad;
|
||||
let h0 = getMoonPosition(t, lat, lng).altitude - hc,
|
||||
rise, set, ye;
|
||||
|
||||
// go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
|
||||
for (let i = 1; i <= 24; i += 2) {
|
||||
const h1 = getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
|
||||
const h2 = getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
|
||||
const a = (h0 + h2) / 2 - h1;
|
||||
const b = (h2 - h0) / 2;
|
||||
const xe = -b / (2 * a);
|
||||
const d = b * b - 4 * a * h1;
|
||||
let roots = 0, x1 = 0, x2 = 0;
|
||||
ye = (a * xe + b) * xe + h1;
|
||||
|
||||
if (d >= 0) {
|
||||
const dx = Math.sqrt(d) / (Math.abs(a) * 2);
|
||||
x1 = xe - dx;
|
||||
x2 = xe + dx;
|
||||
if (Math.abs(x1) <= 1) roots++;
|
||||
if (Math.abs(x2) <= 1) roots++;
|
||||
if (x1 < -1) x1 = x2;
|
||||
}
|
||||
|
||||
if (roots === 1) {
|
||||
if (h0 < 0) rise = i + x1;
|
||||
else set = i + x1;
|
||||
|
||||
} else if (roots === 2) {
|
||||
rise = i + (ye < 0 ? x2 : x1);
|
||||
set = i + (ye < 0 ? x1 : x2);
|
||||
}
|
||||
|
||||
if (rise && set) break;
|
||||
|
||||
h0 = h2;
|
||||
}
|
||||
|
||||
const result = {};
|
||||
|
||||
if (rise) result.rise = hoursLater(t, rise);
|
||||
if (set) result.set = hoursLater(t, set);
|
||||
|
||||
if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -233,6 +233,11 @@ Item {
|
||||
PolkitService.polkitAvailable;
|
||||
DisplayConfigState.hasOutputBackend;
|
||||
PortalService.systemColorScheme;
|
||||
IconThemeService.revision;
|
||||
DesktopService.isSystemd;
|
||||
TrashService.count;
|
||||
WallpaperCyclingService.cyclingActive;
|
||||
ThemeAutoService.active;
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -440,6 +445,23 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: qrGeneratorModalLoader
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.qrGeneratorModalLoader = qrGeneratorModalLoader;
|
||||
}
|
||||
|
||||
QRGeneratorModal {
|
||||
id: qrGeneratorModalItem
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.qrGeneratorModal = qrGeneratorModalItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: polkitAuthModalLoader
|
||||
active: false
|
||||
|
||||
+75
-46
@@ -25,32 +25,30 @@ Item {
|
||||
required property var windowRuleModalLoader
|
||||
|
||||
function getPreferredBar(refPropertyName) {
|
||||
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
||||
return null;
|
||||
|
||||
const focusedScreenName = BarWidgetService.getFocusedScreenName();
|
||||
|
||||
const loaders = Array.from({
|
||||
length: root.dankBarRepeater.count
|
||||
}, (_, i) => root.dankBarRepeater.itemAt(i));
|
||||
const bars = [];
|
||||
if (root.dankBarRepeater) {
|
||||
for (let i = 0; i < root.dankBarRepeater.count; i++)
|
||||
bars.push(...(root.dankBarRepeater.itemAt(i)?.item?.barVariants?.instances || []));
|
||||
}
|
||||
const frameBars = BarWidgetService.frameHostedBars;
|
||||
for (const screenName in frameBars)
|
||||
bars.push(frameBars[screenName]);
|
||||
|
||||
let currentBar = null;
|
||||
for (const bar of bars) {
|
||||
if (!bar)
|
||||
continue;
|
||||
|
||||
for (const loader of loaders) {
|
||||
const instances = loader?.item?.barVariants?.instances || [];
|
||||
for (const bar of instances) {
|
||||
if (!bar)
|
||||
continue;
|
||||
const onFocusedScreen = focusedScreenName && bar.modelData?.name === focusedScreenName;
|
||||
const hasRef = !refPropertyName || !!bar[refPropertyName];
|
||||
|
||||
const onFocusedScreen = focusedScreenName && bar.modelData?.name === focusedScreenName;
|
||||
const hasRef = !refPropertyName || !!bar[refPropertyName];
|
||||
if (hasRef) {
|
||||
currentBar = bar;
|
||||
|
||||
if (hasRef) {
|
||||
currentBar = bar;
|
||||
|
||||
if (onFocusedScreen)
|
||||
break;
|
||||
}
|
||||
if (onFocusedScreen)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,28 +283,66 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function _resolvePosition(position) {
|
||||
switch ((position || "").toLowerCase()) {
|
||||
case "left":
|
||||
return "left";
|
||||
case "center":
|
||||
return "center";
|
||||
case "right":
|
||||
return "right";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function _dashBar(position) {
|
||||
if (position)
|
||||
return root.getPreferredBar();
|
||||
return root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
||||
}
|
||||
|
||||
function _openDash(tab, position) {
|
||||
const bar = _dashBar(position);
|
||||
if (!bar)
|
||||
return false;
|
||||
|
||||
const tabId = _resolveTabId(tab);
|
||||
const dash = root.dankDashPopoutLoader.item;
|
||||
if (dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name) {
|
||||
if (position && bar.positionDash)
|
||||
bar.positionDash(dash, position);
|
||||
dash.requestTab(tabId);
|
||||
if (dash.updateSurfacePosition)
|
||||
dash.updateSurfacePosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
return bar.triggerDashTab(tabId, position);
|
||||
}
|
||||
|
||||
function _toggleDash(tab, position) {
|
||||
if (root.dankDashPopoutLoader.item?.dashVisible) {
|
||||
root.dankDashPopoutLoader.item.dashVisible = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const bar = _dashBar(position);
|
||||
if (!bar)
|
||||
return false;
|
||||
return bar.triggerDashTab(_resolveTabId(tab), position);
|
||||
}
|
||||
|
||||
function resolveTabIndex(tab: string): int {
|
||||
return SettingsData.dashTabIndexForId(_resolveTabId(tab));
|
||||
}
|
||||
|
||||
function open(tab: string): string {
|
||||
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
||||
if (!bar)
|
||||
return "DASH_OPEN_FAILED";
|
||||
return _openDash(tab, "") ? "DASH_OPEN_SUCCESS" : "DASH_OPEN_FAILED";
|
||||
}
|
||||
|
||||
const tabId = _resolveTabId(tab);
|
||||
const dash = root.dankDashPopoutLoader.item;
|
||||
if (dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name) {
|
||||
dash.requestTab(tabId);
|
||||
if (dash.updateSurfacePosition)
|
||||
dash.updateSurfacePosition();
|
||||
return "DASH_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
if (!bar.triggerDashTab(tabId))
|
||||
return "DASH_OPEN_FAILED";
|
||||
|
||||
return "DASH_OPEN_SUCCESS";
|
||||
function openAt(tab: string, position: string): string {
|
||||
return _openDash(tab, _resolvePosition(position)) ? "DASH_OPEN_SUCCESS" : "DASH_OPEN_FAILED";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
@@ -318,18 +354,11 @@ Item {
|
||||
}
|
||||
|
||||
function toggle(tab: string): string {
|
||||
if (root.dankDashPopoutLoader.item?.dashVisible) {
|
||||
root.dankDashPopoutLoader.item.dashVisible = false;
|
||||
return "DASH_TOGGLE_SUCCESS";
|
||||
}
|
||||
return _toggleDash(tab, "") ? "DASH_TOGGLE_SUCCESS" : "DASH_TOGGLE_FAILED";
|
||||
}
|
||||
|
||||
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
|
||||
if (bar) {
|
||||
if (!bar.triggerDashTab(_resolveTabId(tab)))
|
||||
return "DASH_TOGGLE_FAILED";
|
||||
return "DASH_TOGGLE_SUCCESS";
|
||||
}
|
||||
return "DASH_TOGGLE_FAILED";
|
||||
function toggleAt(tab: string, position: string): string {
|
||||
return _toggleDash(tab, _resolvePosition(position)) ? "DASH_TOGGLE_SUCCESS" : "DASH_TOGGLE_FAILED";
|
||||
}
|
||||
|
||||
target: "dash"
|
||||
|
||||
@@ -314,26 +314,20 @@ Item {
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
|
||||
function _frameEdgeInset(side) {
|
||||
if (!effectiveScreen)
|
||||
return 0;
|
||||
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
|
||||
}
|
||||
|
||||
readonly property real _connectedAlignedX: {
|
||||
switch (resolvedConnectedBarSide) {
|
||||
case "top":
|
||||
case "bottom":
|
||||
{
|
||||
const insetL = _frameEdgeInset("left");
|
||||
const insetR = _frameEdgeInset("right");
|
||||
const insetL = SettingsData.frameEdgeInsetForSide(effectiveScreen, "left");
|
||||
const insetR = SettingsData.frameEdgeInsetForSide(effectiveScreen, "right");
|
||||
const usable = Math.max(0, screenWidth - insetL - insetR);
|
||||
return insetL + Math.max(0, (usable - alignedWidth) / 2);
|
||||
}
|
||||
case "left":
|
||||
return _frameEdgeInset("left");
|
||||
return SettingsData.frameEdgeInsetForSide(effectiveScreen, "left");
|
||||
case "right":
|
||||
return screenWidth - alignedWidth - _frameEdgeInset("right");
|
||||
return screenWidth - alignedWidth - SettingsData.frameEdgeInsetForSide(effectiveScreen, "right");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -341,14 +335,14 @@ Item {
|
||||
readonly property real _connectedAlignedY: {
|
||||
switch (resolvedConnectedBarSide) {
|
||||
case "top":
|
||||
return _frameEdgeInset("top");
|
||||
return SettingsData.frameEdgeInsetForSide(effectiveScreen, "top");
|
||||
case "bottom":
|
||||
return screenHeight - alignedHeight - _frameEdgeInset("bottom");
|
||||
return screenHeight - alignedHeight - SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom");
|
||||
case "left":
|
||||
case "right":
|
||||
{
|
||||
const insetT = _frameEdgeInset("top");
|
||||
const insetB = _frameEdgeInset("bottom");
|
||||
const insetT = SettingsData.frameEdgeInsetForSide(effectiveScreen, "top");
|
||||
const insetB = SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom");
|
||||
const usable = Math.max(0, screenHeight - insetT - insetB);
|
||||
return insetT + Math.max(0, (usable - alignedHeight) / 2);
|
||||
}
|
||||
|
||||
@@ -105,12 +105,6 @@ Item {
|
||||
}
|
||||
readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide)
|
||||
|
||||
function _frameEdgeInset(side) {
|
||||
if (!effectiveScreen)
|
||||
return 0;
|
||||
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
|
||||
}
|
||||
|
||||
readonly property var _connectedModalPos: {
|
||||
const fallback = {
|
||||
"x": (screenWidth - modalWidth) / 2,
|
||||
@@ -120,10 +114,10 @@ Item {
|
||||
case "top":
|
||||
case "bottom":
|
||||
{
|
||||
const insetL = _frameEdgeInset("left");
|
||||
const insetR = _frameEdgeInset("right");
|
||||
const insetT = _frameEdgeInset("top");
|
||||
const insetB = _frameEdgeInset("bottom");
|
||||
const insetL = SettingsData.frameEdgeInsetForSide(effectiveScreen, "left");
|
||||
const insetR = SettingsData.frameEdgeInsetForSide(effectiveScreen, "right");
|
||||
const insetT = SettingsData.frameEdgeInsetForSide(effectiveScreen, "top");
|
||||
const insetB = SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom");
|
||||
const usable = Math.max(0, screenWidth - insetL - insetR);
|
||||
const usableH = Math.max(0, screenHeight - insetT - insetB);
|
||||
return {
|
||||
@@ -134,11 +128,11 @@ Item {
|
||||
case "left":
|
||||
case "right":
|
||||
{
|
||||
const insetT = _frameEdgeInset("top");
|
||||
const insetB = _frameEdgeInset("bottom");
|
||||
const insetT = SettingsData.frameEdgeInsetForSide(effectiveScreen, "top");
|
||||
const insetB = SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom");
|
||||
const usable = Math.max(0, screenHeight - insetT - insetB);
|
||||
return {
|
||||
"x": resolvedConnectedBarSide === "left" ? _frameEdgeInset("left") : screenWidth - modalWidth - _frameEdgeInset("right"),
|
||||
"x": resolvedConnectedBarSide === "left" ? SettingsData.frameEdgeInsetForSide(effectiveScreen, "left") : screenWidth - modalWidth - SettingsData.frameEdgeInsetForSide(effectiveScreen, "right"),
|
||||
"y": insetT + Math.max(0, (usable - modalHeight) / 2)
|
||||
};
|
||||
}
|
||||
@@ -188,16 +182,16 @@ Item {
|
||||
readonly property real _connectedChromeY: {
|
||||
if (!launcherArcExtenderActive)
|
||||
return alignedY;
|
||||
return resolvedConnectedBarSide === "top" ? Theme.snap(_frameEdgeInset("top"), dpr) : alignedY;
|
||||
return resolvedConnectedBarSide === "top" ? Theme.snap(SettingsData.frameEdgeInsetForSide(effectiveScreen, "top"), dpr) : alignedY;
|
||||
}
|
||||
readonly property real _connectedChromeWidth: alignedWidth
|
||||
readonly property real _connectedChromeHeight: {
|
||||
if (!launcherArcExtenderActive)
|
||||
return alignedHeight;
|
||||
if (resolvedConnectedBarSide === "top")
|
||||
return Theme.snap(Math.max(alignedHeight, alignedY + alignedHeight - _frameEdgeInset("top")), dpr);
|
||||
return Theme.snap(Math.max(alignedHeight, alignedY + alignedHeight - SettingsData.frameEdgeInsetForSide(effectiveScreen, "top")), dpr);
|
||||
if (resolvedConnectedBarSide === "bottom")
|
||||
return Theme.snap(Math.max(alignedHeight, screenHeight - _frameEdgeInset("bottom") - alignedY), dpr);
|
||||
return Theme.snap(Math.max(alignedHeight, screenHeight - SettingsData.frameEdgeInsetForSide(effectiveScreen, "bottom") - alignedY), dpr);
|
||||
return alignedHeight;
|
||||
}
|
||||
readonly property real contentSurfaceHeight: launcherArcExtenderActive ? _connectedChromeHeight : alignedHeight
|
||||
|
||||
@@ -89,15 +89,6 @@ function fuzzyScore(text, query) {
|
||||
return bestScore
|
||||
}
|
||||
|
||||
function getTimeBucketWeight(daysSinceUsed) {
|
||||
for (var i = 0; i < TimeBuckets.length; i++) {
|
||||
if (daysSinceUsed <= TimeBuckets[i].maxDays) {
|
||||
return TimeBuckets[i].weight
|
||||
}
|
||||
}
|
||||
return 10
|
||||
}
|
||||
|
||||
function calculateTextScore(name, query) {
|
||||
if (name === query) return Weights.exactMatch
|
||||
if (name.startsWith(query)) return Weights.prefixMatch
|
||||
|
||||
@@ -64,6 +64,10 @@ DankModal {
|
||||
NotificationService.dismissAllPopups();
|
||||
}
|
||||
|
||||
function dismissLastNotification() {
|
||||
NotificationService.dismissLastNotification();
|
||||
}
|
||||
|
||||
modalWidth: Math.min(500, screenWidth - 48)
|
||||
modalHeight: Math.min(700, screenHeight * 0.85)
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
@@ -190,6 +194,11 @@ DankModal {
|
||||
return "NOTIFICATION_MODAL_DISMISS_ALL_POPUPS_SUCCESS";
|
||||
}
|
||||
|
||||
function dismiss(): string {
|
||||
notificationModal.dismissLastNotification();
|
||||
return "NOTIFICATION_DISMISS_SUCCESS";
|
||||
}
|
||||
|
||||
target: "notifications"
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,19 @@ DankModal {
|
||||
open();
|
||||
}
|
||||
|
||||
function getPowerProfileDescription(profile) {
|
||||
switch (profile) {
|
||||
case 0:
|
||||
return I18n.tr("Extend battery life", "power profile description");
|
||||
case 1:
|
||||
return I18n.tr("Balance power and performance", "power profile description");
|
||||
case 2:
|
||||
return I18n.tr("Prioritize performance", "power profile description");
|
||||
default:
|
||||
return I18n.tr("Custom power profile", "power profile description");
|
||||
}
|
||||
}
|
||||
|
||||
function hideDialog() {
|
||||
close();
|
||||
}
|
||||
@@ -244,7 +257,7 @@ DankModal {
|
||||
|
||||
// Selected power profile description
|
||||
StyledText {
|
||||
text: (root.selectedIndex >= 0 && root.selectedIndex < root.profileModel.length) ? Theme.getPowerProfileDescription(root.profileModel[root.selectedIndex]) : ""
|
||||
text: (root.selectedIndex >= 0 && root.selectedIndex < root.profileModel.length) ? root.getPowerProfileDescription(root.profileModel[root.selectedIndex]) : ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
@@ -6,6 +6,7 @@ import qs.Common
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../Common/Format.js" as Format
|
||||
|
||||
FloatingWindow {
|
||||
id: processListModal
|
||||
@@ -67,16 +68,6 @@ FloatingWindow {
|
||||
show();
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024)
|
||||
return bytes.toFixed(0) + " B/s";
|
||||
if (bytes < 1024 * 1024)
|
||||
return (bytes / 1024).toFixed(1) + " KB/s";
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
|
||||
}
|
||||
|
||||
function nextTab() {
|
||||
currentTab = (currentTab + 1) % 4;
|
||||
}
|
||||
@@ -541,7 +532,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↓" + formatBytes(DgopService.networkRxRate) + " ↑" + formatBytes(DgopService.networkTxRate)
|
||||
text: "↓" + Format.formatRate(DgopService.networkRxRate) + " ↑" + Format.formatRate(DgopService.networkTxRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
@@ -559,7 +550,7 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "↓" + formatBytes(DgopService.diskReadRate) + " ↑" + formatBytes(DgopService.diskWriteRate)
|
||||
text: "↓" + Format.formatRate(DgopService.diskReadRate) + " ↑" + Format.formatRate(DgopService.diskWriteRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.family: SettingsData.monoFontFamily
|
||||
color: Theme.surfaceText
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
visible: false
|
||||
layerNamespace: "dms:qr-generator"
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
property bool generating: false
|
||||
property string themedQrCodePath: ""
|
||||
property string normalQrCodePath: ""
|
||||
property string initialText: ""
|
||||
property string _pendingPayload: ""
|
||||
property string _generatingPayload: ""
|
||||
property string _displayedPayload: ""
|
||||
modalWidth: 420
|
||||
modalHeight: 440
|
||||
onBackgroundClicked: hide()
|
||||
onOpened: {
|
||||
Qt.callLater(() => {
|
||||
modalFocusScope.forceActiveFocus();
|
||||
const item = contentLoader.item;
|
||||
if (!item)
|
||||
return;
|
||||
item.saveBrowserLoader = saveBrowserLoader;
|
||||
if (item.textInput) {
|
||||
item.textInput.text = initialText;
|
||||
item.textInput.forceActiveFocus();
|
||||
}
|
||||
if (initialText.length > 0) {
|
||||
_pendingPayload = initialText;
|
||||
generateQR(initialText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function show(text) {
|
||||
generating = false;
|
||||
initialText = text || "";
|
||||
_pendingPayload = "";
|
||||
_generatingPayload = "";
|
||||
_displayedPayload = "";
|
||||
themedQrCodePath = "";
|
||||
normalQrCodePath = "";
|
||||
open();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
deleteQrCodeFiles(themedQrCodePath, normalQrCodePath);
|
||||
themedQrCodePath = "";
|
||||
normalQrCodePath = "";
|
||||
close();
|
||||
}
|
||||
|
||||
function deleteQrCodeFiles(themed, normal) {
|
||||
if (themed.length > 0)
|
||||
DMSService.sendRequest("network.delete-qrcode", {
|
||||
path: themed
|
||||
});
|
||||
if (normal.length > 0)
|
||||
DMSService.sendRequest("network.delete-qrcode", {
|
||||
path: normal
|
||||
});
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: genTimer
|
||||
interval: 200
|
||||
repeat: false
|
||||
onTriggered: root.generateQR(root._pendingPayload)
|
||||
}
|
||||
|
||||
function generateQR(text) {
|
||||
const trimmed = (text || "").trim();
|
||||
if (trimmed.length === 0 || trimmed === _displayedPayload || generating)
|
||||
return;
|
||||
|
||||
_generatingPayload = trimmed;
|
||||
generating = true;
|
||||
|
||||
DMSService.sendRequest("network.generate-qrcode", {
|
||||
text: trimmed
|
||||
}, response => {
|
||||
root.generating = false;
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to generate QR code: %1").arg(JSON.stringify(response.error)));
|
||||
return;
|
||||
}
|
||||
if (!response.result)
|
||||
return;
|
||||
if (root._generatingPayload !== root._pendingPayload.trim()) {
|
||||
root.deleteQrCodeFiles(response.result[0], response.result[1]);
|
||||
genTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
const oldThemed = root.themedQrCodePath;
|
||||
const oldNormal = root.normalQrCodePath;
|
||||
root._displayedPayload = root._generatingPayload;
|
||||
root.themedQrCodePath = response.result[0];
|
||||
root.normalQrCodePath = response.result[1];
|
||||
root.deleteQrCodeFiles(oldThemed, oldNormal);
|
||||
});
|
||||
}
|
||||
|
||||
function onTextChanged(text) {
|
||||
_pendingPayload = text;
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length === 0 || trimmed === _displayedPayload) {
|
||||
genTimer.stop();
|
||||
return;
|
||||
}
|
||||
genTimer.restart();
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: saveBrowserLoader
|
||||
active: false
|
||||
|
||||
FileBrowserSurfaceModal {
|
||||
id: saveBrowser
|
||||
|
||||
browserTitle: I18n.tr("Save QR Code")
|
||||
browserIcon: "qr_code"
|
||||
browserType: "default"
|
||||
fileExtensions: ["*.png"]
|
||||
allowStacking: true
|
||||
saveMode: true
|
||||
defaultFileName: "qrcode.png"
|
||||
onFileSelected: path => {
|
||||
const cleanPath = decodeURI(path.toString().replace(/^file:\/\//, ''));
|
||||
copyQrCodeProcess.exec(["cp", "-f", root.normalQrCodePath, cleanPath]);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: copyQrCodeProcess
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
saveBrowser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
id: contentItem
|
||||
|
||||
property alias textInput: textInput
|
||||
property var saveBrowserLoader: null
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
RowLayout {
|
||||
id: modalTitle
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("QR Generator")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Bold
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.hide()
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: textInput
|
||||
width: parent.width
|
||||
placeholderText: I18n.tr("Enter text to encode")
|
||||
showClearButton: true
|
||||
focus: true
|
||||
onTextEdited: root.onTextChanged(text)
|
||||
Keys.onEscapePressed: event => {
|
||||
event.accepted = true;
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: qrContainer
|
||||
height: Math.min(parent.height - parent.spacing - modalTitle.height - textInput.height - parent.spacing * 4, 260)
|
||||
width: height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
opacity: 1
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 80
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: qrCodeImg
|
||||
anchors.fill: parent
|
||||
source: root.themedQrCodePath
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
cache: false
|
||||
visible: false
|
||||
|
||||
onSourceChanged: qrContainer.opacity = 0
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready)
|
||||
qrContainer.opacity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
source: qrCodeImg
|
||||
anchors.fill: qrCodeImg
|
||||
colorization: 1.0
|
||||
colorizationColor: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
visible: root.themedQrCodePath.length > 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("Save")
|
||||
iconName: "save"
|
||||
backgroundColor: Theme.surfaceContainer
|
||||
textColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
contentItem.saveBrowserLoader.active = true;
|
||||
if (contentItem.saveBrowserLoader.item) {
|
||||
contentItem.saveBrowserLoader.item.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("Copy")
|
||||
iconName: "content_copy"
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.onPrimary
|
||||
onClicked: {
|
||||
if (root.normalQrCodePath.length > 0)
|
||||
DMSService.sendRequest("clipboard.copyFile", {
|
||||
filePath: root.normalQrCodePath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ DankModal {
|
||||
property string promptToken: ""
|
||||
property string promptReason: ""
|
||||
property var promptFields: []
|
||||
property var promptHints: []
|
||||
property string promptSetting: ""
|
||||
|
||||
property bool isVpnPrompt: false
|
||||
@@ -40,17 +41,21 @@ DankModal {
|
||||
property var fieldsInfo: []
|
||||
property var secretValues: ({})
|
||||
|
||||
readonly property bool isCertificateChangedPrompt: promptReason === "server-certificate-changed"
|
||||
readonly property bool isCertificatePrompt: promptReason === "server-certificate" || isCertificateChangedPrompt
|
||||
readonly property string serverCertificateFingerprint: promptHints.length > 0 ? promptHints[0] : ""
|
||||
readonly property bool showUsernameField: requiresEnterprise && !isVpnPrompt && fieldsInfo.length === 0
|
||||
readonly property bool showPasswordField: fieldsInfo.length === 0
|
||||
readonly property bool showPasswordField: fieldsInfo.length === 0 && !isCertificatePrompt
|
||||
readonly property bool showAnonField: requiresEnterprise && !isVpnPrompt
|
||||
readonly property bool showDomainField: requiresEnterprise && !isVpnPrompt
|
||||
readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11"
|
||||
readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11" && !isCertificatePrompt
|
||||
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
readonly property int inputFieldWithSpacing: inputFieldHeight + Theme.spacingM
|
||||
readonly property int checkboxRowHeight: Theme.fontSizeMedium + Theme.spacingS
|
||||
readonly property int headerHeight: Theme.fontSizeLarge + Theme.fontSizeMedium + Theme.spacingM * 2
|
||||
readonly property int buttonRowHeight: 36 + Theme.spacingM
|
||||
readonly property int certificateWarningHeight: certificateWarningColumn.implicitHeight + Theme.spacingM * 2
|
||||
|
||||
property int calculatedHeight: {
|
||||
let h = headerHeight + buttonRowHeight + Theme.spacingL * 2;
|
||||
@@ -67,10 +72,16 @@ DankModal {
|
||||
h += inputFieldWithSpacing;
|
||||
if (showSavePasswordCheckbox)
|
||||
h += checkboxRowHeight;
|
||||
if (isCertificatePrompt)
|
||||
h += certificateWarningHeight + Theme.spacingM;
|
||||
return h;
|
||||
}
|
||||
|
||||
function focusFirstField() {
|
||||
if (isCertificatePrompt) {
|
||||
connectButton.forceActiveFocus();
|
||||
return;
|
||||
}
|
||||
if (fieldsInfo.length > 0) {
|
||||
if (dynamicFieldsRepeater.count > 0) {
|
||||
const firstItem = dynamicFieldsRepeater.itemAt(0);
|
||||
@@ -101,6 +112,7 @@ DankModal {
|
||||
promptToken = "";
|
||||
promptReason = "";
|
||||
promptFields = [];
|
||||
promptHints = [];
|
||||
promptSetting = "";
|
||||
isVpnPrompt = false;
|
||||
connectionName = "";
|
||||
@@ -127,6 +139,7 @@ DankModal {
|
||||
promptToken = "";
|
||||
promptReason = "";
|
||||
promptFields = [];
|
||||
promptHints = [];
|
||||
promptSetting = "";
|
||||
isVpnPrompt = false;
|
||||
connectionName = "";
|
||||
@@ -145,6 +158,7 @@ DankModal {
|
||||
promptToken = token;
|
||||
promptReason = reason;
|
||||
promptFields = fields || [];
|
||||
promptHints = hints || [];
|
||||
promptSetting = setting || "802-11-wireless-security";
|
||||
connectionType = connType || "802-11-wireless";
|
||||
connectionName = connName || ssid || "";
|
||||
@@ -324,6 +338,8 @@ DankModal {
|
||||
text: {
|
||||
if (promptReason === "pkcs11")
|
||||
return I18n.tr("Smartcard Authentication");
|
||||
if (isCertificatePrompt)
|
||||
return I18n.tr("Untrusted VPN certificate", "Title for VPN server certificate trust confirmation");
|
||||
if (isVpnPrompt)
|
||||
return I18n.tr("Connect to VPN");
|
||||
if (isHiddenNetwork)
|
||||
@@ -343,6 +359,8 @@ DankModal {
|
||||
text: {
|
||||
if (promptReason === "pkcs11")
|
||||
return I18n.tr("Enter PIN for ") + wifiPasswordSSID;
|
||||
if (isCertificatePrompt)
|
||||
return wifiPasswordSSID;
|
||||
if (fieldsInfo.length > 0)
|
||||
return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
|
||||
if (isVpnPrompt)
|
||||
@@ -383,6 +401,45 @@ DankModal {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: certificateWarningBox
|
||||
|
||||
readonly property color warningTone: isCertificateChangedPrompt ? Theme.error : Theme.warning
|
||||
|
||||
width: parent.width
|
||||
height: certificateWarningHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(warningTone, 0.12)
|
||||
border.color: Theme.withAlpha(warningTone, 0.5)
|
||||
border.width: 1
|
||||
visible: isCertificatePrompt
|
||||
|
||||
Column {
|
||||
id: certificateWarningColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: isCertificateChangedPrompt ? I18n.tr("The server certificate has changed since it was last trusted. Only continue if you recognize the new fingerprint.", "Warning shown when a trusted VPN server certificate no longer matches") : I18n.tr("Only continue if you recognize this server certificate fingerprint.", "Warning shown before trusting an unverified VPN server certificate")
|
||||
wrapMode: Text.Wrap
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: serverCertificateFingerprint
|
||||
wrapMode: Text.WrapAnywhere
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: certificateWarningBox.warningTone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: inputFieldHeight
|
||||
@@ -690,10 +747,15 @@ DankModal {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: connectButton
|
||||
|
||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
border.color: activeFocus ? Theme.surfaceText : "transparent"
|
||||
border.width: activeFocus ? 2 : 0
|
||||
activeFocusOnTab: true
|
||||
enabled: {
|
||||
if (fieldsInfo.length > 0) {
|
||||
for (var i = 0; i < fieldsInfo.length; i++) {
|
||||
@@ -705,6 +767,8 @@ DankModal {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isCertificatePrompt)
|
||||
return serverCertificateFingerprint.length > 0;
|
||||
if (isVpnPrompt)
|
||||
return passwordInput.text.length > 0;
|
||||
if (isHiddenNetwork)
|
||||
@@ -716,7 +780,7 @@ DankModal {
|
||||
StyledText {
|
||||
id: connectText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Connect")
|
||||
text: isCertificatePrompt ? I18n.tr("Trust", "Button that approves a VPN server certificate fingerprint") : I18n.tr("Connect")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
@@ -731,6 +795,22 @@ DankModal {
|
||||
onClicked: submitCredentialsAndClose()
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: event => {
|
||||
if (enabled)
|
||||
submitCredentialsAndClose();
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onEnterPressed: event => {
|
||||
if (enabled)
|
||||
submitCredentialsAndClose();
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onSpacePressed: event => {
|
||||
if (enabled)
|
||||
submitCredentialsAndClose();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
|
||||
@@ -25,29 +25,6 @@ Item {
|
||||
property bool effectActive: false
|
||||
property bool useNextForEffect: false
|
||||
|
||||
function getFillMode(modeName) {
|
||||
switch (modeName) {
|
||||
case "Stretch":
|
||||
return Image.Stretch;
|
||||
case "Fit":
|
||||
case "PreserveAspectFit":
|
||||
return Image.PreserveAspectFit;
|
||||
case "Fill":
|
||||
case "PreserveAspectCrop":
|
||||
return Image.PreserveAspectCrop;
|
||||
case "Tile":
|
||||
return Image.Tile;
|
||||
case "TileVertically":
|
||||
return Image.TileVertically;
|
||||
case "TileHorizontally":
|
||||
return Image.TileHorizontally;
|
||||
case "Pad":
|
||||
return Image.Pad;
|
||||
default:
|
||||
return Image.PreserveAspectCrop;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (initialSource && initialSource !== wallpaperSource && !(CompositorService.isNiri && SessionData.isSwitchingMode)) {
|
||||
currentWallpaper.source = initialSource;
|
||||
@@ -144,7 +121,7 @@ Item {
|
||||
smooth: true
|
||||
cache: true
|
||||
sourceSize: root.blurTextureSize
|
||||
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
|
||||
fillMode: Theme.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
@@ -166,7 +143,7 @@ Item {
|
||||
smooth: true
|
||||
cache: true
|
||||
sourceSize: root.blurTextureSize
|
||||
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
|
||||
fillMode: Theme.getFillMode(SessionData.getMonitorWallpaperFillMode(root.screenName))
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../Common/Format.js" as Format
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -188,16 +189,6 @@ Item {
|
||||
return DgopService.availableGpus.find(g => g.pciId === selectedGpuPciId);
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024)
|
||||
return bytes.toFixed(0) + "B";
|
||||
if (bytes < 1024 * 1024)
|
||||
return (bytes / 1024).toFixed(0) + "K";
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + "M";
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + "G";
|
||||
}
|
||||
|
||||
function formatMemKB(kb) {
|
||||
if (kb < 1024)
|
||||
return kb.toFixed(0) + "K";
|
||||
@@ -206,26 +197,18 @@ Item {
|
||||
return (kb / (1024 * 1024)).toFixed(1) + "G";
|
||||
}
|
||||
|
||||
function addToHistory(arr, val) {
|
||||
var newArr = arr.slice();
|
||||
newArr.push(val);
|
||||
if (newArr.length > historySize)
|
||||
newArr.shift();
|
||||
return newArr;
|
||||
}
|
||||
|
||||
function sampleData() {
|
||||
if (showCpuGraph)
|
||||
cpuHistory = addToHistory(cpuHistory, DgopService.cpuUsage);
|
||||
cpuHistory = Format.addToHistory(cpuHistory, DgopService.cpuUsage, historySize);
|
||||
if (showMemoryGraph)
|
||||
memHistory = addToHistory(memHistory, DgopService.memoryUsage);
|
||||
memHistory = Format.addToHistory(memHistory, DgopService.memoryUsage, historySize);
|
||||
if (showNetworkGraph) {
|
||||
netRxHistory = addToHistory(netRxHistory, DgopService.networkRxRate);
|
||||
netTxHistory = addToHistory(netTxHistory, DgopService.networkTxRate);
|
||||
netRxHistory = Format.addToHistory(netRxHistory, DgopService.networkRxRate, historySize);
|
||||
netTxHistory = Format.addToHistory(netTxHistory, DgopService.networkTxRate, historySize);
|
||||
}
|
||||
if (showDisk) {
|
||||
diskReadHistory = addToHistory(diskReadHistory, DgopService.diskReadRate);
|
||||
diskWriteHistory = addToHistory(diskWriteHistory, DgopService.diskWriteRate);
|
||||
diskReadHistory = Format.addToHistory(diskReadHistory, DgopService.diskReadRate, historySize);
|
||||
diskWriteHistory = Format.addToHistory(diskWriteHistory, DgopService.diskWriteRate, historySize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,7 +502,7 @@ Item {
|
||||
color: root.accentColor
|
||||
}
|
||||
StyledText {
|
||||
text: root.formatBytes(DgopService.networkRxRate) + "/s"
|
||||
text: Format.formatBytes(DgopService.networkRxRate) + "/s"
|
||||
isMonospace: true
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.textColor
|
||||
@@ -533,7 +516,7 @@ Item {
|
||||
color: root.dimColor
|
||||
}
|
||||
StyledText {
|
||||
text: root.formatBytes(DgopService.networkTxRate) + "/s"
|
||||
text: Format.formatBytes(DgopService.networkTxRate) + "/s"
|
||||
isMonospace: true
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.textColor
|
||||
@@ -552,7 +535,7 @@ Item {
|
||||
color: root.accentColor
|
||||
}
|
||||
StyledText {
|
||||
text: root.formatBytes(DgopService.diskReadRate) + "/s"
|
||||
text: Format.formatBytes(DgopService.diskReadRate) + "/s"
|
||||
isMonospace: true
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.textColor
|
||||
@@ -566,7 +549,7 @@ Item {
|
||||
color: root.dimColor
|
||||
}
|
||||
StyledText {
|
||||
text: root.formatBytes(DgopService.diskWriteRate) + "/s"
|
||||
text: Format.formatBytes(DgopService.diskWriteRate) + "/s"
|
||||
isMonospace: true
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.textColor
|
||||
|
||||
@@ -469,6 +469,8 @@ PluginComponent {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: peerMouseArea
|
||||
|
||||
z: -1
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Details
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property string iconName: ""
|
||||
property string text: ""
|
||||
property string secondaryText: ""
|
||||
property bool isActive: false
|
||||
property int widgetIndex: 0
|
||||
property var widgetData: null
|
||||
property bool editMode: false
|
||||
|
||||
signal clicked
|
||||
|
||||
width: parent ? parent.width : 200
|
||||
height: 60
|
||||
radius: {
|
||||
if (Theme.cornerRadius === 0)
|
||||
return 0;
|
||||
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
|
||||
}
|
||||
|
||||
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
||||
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
|
||||
readonly property color _tileRingActive: Theme.ccTileRing
|
||||
|
||||
color: isActive ? _tileBgActive : _tileBgInactive
|
||||
border.color: isActive ? _tileRingActive : Theme.outlineMedium
|
||||
border.width: isActive ? 1 : Theme.layerOutlineWidth
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2;
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse ? hoverTint(root.color) : Theme.withAlpha(hoverTint(root.color), 0)
|
||||
opacity: mouseArea.containsMouse ? 0.08 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL + 2
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: Theme.iconSize
|
||||
color: isActive ? Theme.ccTileActiveText : Theme.ccTileInactiveIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - Theme.iconSize - parent.spacing
|
||||
height: parent.height
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
Typography {
|
||||
width: parent.width
|
||||
text: root.text
|
||||
style: Typography.Style.Body
|
||||
color: isActive ? Theme.ccTileActiveText : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
Typography {
|
||||
width: parent.width
|
||||
text: root.secondaryText
|
||||
style: Typography.Style.Caption
|
||||
color: isActive ? Theme.ccTileActiveText : Theme.surfaceVariantText
|
||||
visible: text.length > 0
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: ripple
|
||||
cornerRadius: root.radius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: root.enabled
|
||||
onPressed: mouse => ripple.trigger(mouse.x, mouse.y)
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.ControlCenter.Details
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string expandedSection: ""
|
||||
property var expandedWidgetData: null
|
||||
|
||||
height: active ? 250 : 0
|
||||
visible: active
|
||||
|
||||
readonly property bool active: expandedSection !== ""
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingS
|
||||
sourceComponent: {
|
||||
if (!root.active)
|
||||
return null;
|
||||
|
||||
if (expandedSection.startsWith("diskUsage_")) {
|
||||
return diskUsageDetailComponent;
|
||||
}
|
||||
|
||||
switch (expandedSection) {
|
||||
case "wifi":
|
||||
return networkDetailComponent;
|
||||
case "bluetooth":
|
||||
return bluetoothDetailComponent;
|
||||
case "audioOutput":
|
||||
return audioOutputDetailComponent;
|
||||
case "audioInput":
|
||||
return audioInputDetailComponent;
|
||||
case "battery":
|
||||
return batteryDetailComponent;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: networkDetailComponent
|
||||
NetworkDetail {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: bluetoothDetailComponent
|
||||
BluetoothDetail {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: audioOutputDetailComponent
|
||||
AudioOutputDetail {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: audioInputDetailComponent
|
||||
AudioInputDetail {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: batteryDetailComponent
|
||||
BatteryDetail {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: diskUsageDetailComponent
|
||||
DiskUsageDetail {
|
||||
currentMountPath: root.expandedWidgetData?.mountPath || "/"
|
||||
instanceId: root.expandedWidgetData?.instanceId || ""
|
||||
|
||||
onMountPathChanged: newMountPath => {
|
||||
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
|
||||
const widgets = SettingsData.controlCenterWidgets || [];
|
||||
const newWidgets = widgets.map(w => {
|
||||
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
|
||||
const updatedWidget = Object.assign({}, w);
|
||||
updatedWidget.mountPath = newMountPath;
|
||||
return updatedWidget;
|
||||
}
|
||||
return w;
|
||||
});
|
||||
SettingsData.set("controlCenterWidgets", newWidgets);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property string iconName: ""
|
||||
property string text: ""
|
||||
|
||||
signal pressed
|
||||
|
||||
height: 34
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceVariant, 0.5)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: Theme.fontSizeSmall
|
||||
color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Typography {
|
||||
text: root.text
|
||||
style: Typography.Style.Button
|
||||
color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: ripple
|
||||
cornerRadius: root.radius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => {
|
||||
ripple.trigger(mouse.x, mouse.y);
|
||||
root.pressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import Quickshell.Services.Pipewire
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../../Common/QmlUtils.js" as QmlUtils
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@@ -150,17 +151,9 @@ Rectangle {
|
||||
|
||||
property int maxPinnedInputs: 3
|
||||
|
||||
function normalizePinList(value) {
|
||||
if (Array.isArray(value))
|
||||
return value.filter(v => v);
|
||||
if (typeof value === "string" && value.length > 0)
|
||||
return [value];
|
||||
return [];
|
||||
}
|
||||
|
||||
function getPinnedInputs() {
|
||||
const pins = SettingsData.audioInputDevicePins || {};
|
||||
return normalizePinList(pins["preferredInput"]);
|
||||
const pins = CacheData.audioInputDevicePins || {};
|
||||
return QmlUtils.normalizePinList(pins["preferredInput"]);
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -315,8 +308,8 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}));
|
||||
let pinnedList = audioContent.normalizePinList(pins["preferredInput"]);
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.audioInputDevicePins || {}));
|
||||
let pinnedList = QmlUtils.normalizePinList(pins["preferredInput"]);
|
||||
const pinIndex = pinnedList.indexOf(modelData.name);
|
||||
|
||||
if (pinIndex !== -1) {
|
||||
@@ -332,7 +325,7 @@ Rectangle {
|
||||
else
|
||||
delete pins["preferredInput"];
|
||||
|
||||
SettingsData.set("audioInputDevicePins", pins);
|
||||
CacheData.set("audioInputDevicePins", pins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Quickshell.Services.Pipewire
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../../Common/QmlUtils.js" as QmlUtils
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@@ -160,17 +161,9 @@ Rectangle {
|
||||
|
||||
property int maxPinnedOutputs: 3
|
||||
|
||||
function normalizePinList(value) {
|
||||
if (Array.isArray(value))
|
||||
return value.filter(v => v);
|
||||
if (typeof value === "string" && value.length > 0)
|
||||
return [value];
|
||||
return [];
|
||||
}
|
||||
|
||||
function getPinnedOutputs() {
|
||||
const pins = SettingsData.audioOutputDevicePins || {};
|
||||
return normalizePinList(pins["preferredOutput"]);
|
||||
const pins = CacheData.audioOutputDevicePins || {};
|
||||
return QmlUtils.normalizePinList(pins["preferredOutput"]);
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -324,8 +317,8 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}));
|
||||
let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]);
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.audioOutputDevicePins || {}));
|
||||
let pinnedList = QmlUtils.normalizePinList(pins["preferredOutput"]);
|
||||
const pinIndex = pinnedList.indexOf(modelData.name);
|
||||
|
||||
if (pinIndex !== -1) {
|
||||
@@ -341,7 +334,7 @@ Rectangle {
|
||||
else
|
||||
delete pins["preferredOutput"];
|
||||
|
||||
SettingsData.set("audioOutputDevicePins", pins);
|
||||
CacheData.set("audioOutputDevicePins", pins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Quickshell.Bluetooth
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../../Common/QmlUtils.js" as QmlUtils
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@@ -95,17 +96,9 @@ Rectangle {
|
||||
BluetoothService.updateDeviceCodec(deviceAddress, codecName);
|
||||
}
|
||||
|
||||
function normalizePinList(value) {
|
||||
if (Array.isArray(value))
|
||||
return value.filter(v => v);
|
||||
if (typeof value === "string" && value.length > 0)
|
||||
return [value];
|
||||
return [];
|
||||
}
|
||||
|
||||
function getPinnedDevices() {
|
||||
const pins = SettingsData.bluetoothDevicePins || {};
|
||||
return normalizePinList(pins["preferredDevice"]);
|
||||
const pins = CacheData.bluetoothDevicePins || {};
|
||||
return QmlUtils.normalizePinList(pins["preferredDevice"]);
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -395,8 +388,8 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {}));
|
||||
let pinnedList = root.normalizePinList(pins["preferredDevice"]);
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.bluetoothDevicePins || {}));
|
||||
let pinnedList = QmlUtils.normalizePinList(pins["preferredDevice"]);
|
||||
const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address);
|
||||
|
||||
if (pinIndex !== -1) {
|
||||
@@ -413,7 +406,7 @@ Rectangle {
|
||||
delete pins["preferredDevice"];
|
||||
}
|
||||
|
||||
SettingsData.set("bluetoothDevicePins", pins);
|
||||
CacheData.set("bluetoothDevicePins", pins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ Rectangle {
|
||||
|
||||
const pinKey = getScreenPinKey();
|
||||
if (pinKey.length > 0) {
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
const pinnedDevice = pins[pinKey];
|
||||
if (pinnedDevice && pinnedDevice.length > 0) {
|
||||
const found = devices.find(d => d.name === pinnedDevice);
|
||||
@@ -85,12 +85,12 @@ Rectangle {
|
||||
}
|
||||
const pinKey = getScreenPinKey();
|
||||
if (pinKey.length > 0) {
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
const existing = pins[pinKey];
|
||||
if (existing && existing !== deviceName) {
|
||||
const next = JSON.parse(JSON.stringify(pins));
|
||||
delete next[pinKey];
|
||||
SettingsData.set("brightnessDevicePins", next);
|
||||
CacheData.set("brightnessDevicePins", next);
|
||||
}
|
||||
}
|
||||
root.currentDeviceName = deviceName;
|
||||
@@ -106,7 +106,7 @@ Rectangle {
|
||||
const pinKey = getScreenPinKey();
|
||||
if (!pinKey || !deviceName)
|
||||
return false;
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
return pins[pinKey] === deviceName;
|
||||
}
|
||||
|
||||
@@ -114,13 +114,13 @@ Rectangle {
|
||||
const pinKey = getScreenPinKey();
|
||||
if (!pinKey || !deviceName)
|
||||
return;
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.brightnessDevicePins || {}));
|
||||
if (pins[pinKey] === deviceName) {
|
||||
delete pins[pinKey];
|
||||
} else {
|
||||
pins[pinKey] = deviceName;
|
||||
}
|
||||
SettingsData.set("brightnessDevicePins", pins);
|
||||
CacheData.set("brightnessDevicePins", pins);
|
||||
}
|
||||
|
||||
implicitHeight: {
|
||||
@@ -269,7 +269,7 @@ Rectangle {
|
||||
|
||||
readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName)
|
||||
readonly property bool devicePinnedHere: {
|
||||
SettingsData.brightnessDevicePins;
|
||||
CacheData.brightnessDevicePins;
|
||||
return root.isDevicePinnedToScreen(modelData ? modelData.name : "");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import "../../../Common/Format.js" as Format
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@@ -23,33 +24,8 @@ Rectangle {
|
||||
onTriggered: root.nowMs = Date.now()
|
||||
}
|
||||
|
||||
function _pad2(n) {
|
||||
return n < 10 ? "0" + n : "" + n;
|
||||
}
|
||||
|
||||
function formatUntil(ts) {
|
||||
if (!ts)
|
||||
return "";
|
||||
const d = new Date(ts);
|
||||
const use24h = (typeof SettingsData !== "undefined") ? SettingsData.use24HourClock : true;
|
||||
if (use24h)
|
||||
return _pad2(d.getHours()) + ":" + _pad2(d.getMinutes());
|
||||
const suffix = d.getHours() >= 12 ? "PM" : "AM";
|
||||
const h12 = ((d.getHours() + 11) % 12) + 1;
|
||||
return h12 + ":" + _pad2(d.getMinutes()) + " " + suffix;
|
||||
}
|
||||
|
||||
function formatRemaining(ms) {
|
||||
if (ms <= 0)
|
||||
return "";
|
||||
const totalMinutes = Math.ceil(ms / 60000);
|
||||
if (totalMinutes < 60)
|
||||
return I18n.tr("%1 min left").arg(totalMinutes);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const mins = totalMinutes - hours * 60;
|
||||
if (mins === 0)
|
||||
return I18n.tr("%1 h left").arg(hours);
|
||||
return I18n.tr("%1 h %2 m left").arg(hours).arg(mins);
|
||||
return Format.formatRemaining(ms, "", I18n.tr("%1 min left"), I18n.tr("%1 h left"), I18n.tr("%1 h %2 m left"));
|
||||
}
|
||||
|
||||
function minutesUntilTomorrowMorning() {
|
||||
@@ -125,7 +101,7 @@ Rectangle {
|
||||
if (SessionData.doNotDisturbUntil <= 0)
|
||||
return I18n.tr("On indefinitely");
|
||||
const remaining = Math.max(0, SessionData.doNotDisturbUntil - root.nowMs);
|
||||
return root.formatRemaining(remaining) + " · " + I18n.tr("until %1").arg(root.formatUntil(SessionData.doNotDisturbUntil));
|
||||
return root.formatRemaining(remaining) + " · " + I18n.tr("until %1").arg(Format.formatUntil(SessionData.doNotDisturbUntil, SettingsData.use24HourClock));
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modals
|
||||
import qs.Modals.Common
|
||||
import "../../../Common/QmlUtils.js" as QmlUtils
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@@ -40,7 +41,11 @@ Rectangle {
|
||||
property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0
|
||||
property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable
|
||||
property int maxPinnedNetworks: 3
|
||||
readonly property int hotspotContentHeight: currentPreferenceIndex === 1 && NetworkService.hotspotAvailable ? 56 + Theme.spacingS : 0
|
||||
// Hosting on the only wifi adapter with no ethernet uplink just drops connectivity,
|
||||
// so the hotspot row only shows where sharing can actually work (or is already on).
|
||||
readonly property bool hotspotRelevant: NetworkService.hotspotEnabled || NetworkService.hotspotActivating || NetworkService.hotspotBusy || NetworkService.ethernetConnected || (NetworkService.wifiDevices?.length ?? 0) > 1
|
||||
readonly property bool showHotspotRow: currentPreferenceIndex === 1 && NetworkService.hotspotAvailable && hotspotRelevant
|
||||
readonly property int hotspotContentHeight: showHotspotRow ? 56 + Theme.spacingS : 0
|
||||
|
||||
property var hotspotStartConfirm: ConfirmModal {}
|
||||
|
||||
@@ -66,17 +71,9 @@ Rectangle {
|
||||
PopoutService.openSettingsWithTab("network_wifi");
|
||||
}
|
||||
|
||||
function normalizePinList(value) {
|
||||
if (Array.isArray(value))
|
||||
return value.filter(v => v);
|
||||
if (typeof value === "string" && value.length > 0)
|
||||
return [value];
|
||||
return [];
|
||||
}
|
||||
|
||||
function getPinnedNetworks() {
|
||||
const pins = SettingsData.wifiNetworkPins || {};
|
||||
return normalizePinList(pins["preferredWifi"]);
|
||||
const pins = CacheData.wifiNetworkPins || {};
|
||||
return QmlUtils.normalizePinList(pins["preferredWifi"]);
|
||||
}
|
||||
|
||||
property int currentPreferenceIndex: {
|
||||
@@ -191,7 +188,7 @@ Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingM
|
||||
visible: currentPreferenceIndex === 1 && NetworkService.hotspotAvailable
|
||||
visible: root.showHotspotRow
|
||||
height: visible ? 56 : 0
|
||||
|
||||
Rectangle {
|
||||
@@ -907,8 +904,8 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
|
||||
let pinnedList = root.normalizePinList(pins["preferredWifi"]);
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
|
||||
let pinnedList = QmlUtils.normalizePinList(pins["preferredWifi"]);
|
||||
const pinIndex = pinnedList.indexOf(modelData.ssid);
|
||||
|
||||
if (pinIndex !== -1) {
|
||||
@@ -924,7 +921,7 @@ Rectangle {
|
||||
else
|
||||
delete pins["preferredWifi"];
|
||||
|
||||
SettingsData.set("wifiNetworkPins", pins);
|
||||
CacheData.set("wifiNetworkPins", pins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ Row {
|
||||
if (screenName && screenName.length > 0) {
|
||||
const screen = Quickshell.screens.find(s => s.name === screenName);
|
||||
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
const pinnedDevice = pins[pinKey];
|
||||
if (pinnedDevice && pinnedDevice.length > 0) {
|
||||
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property string iconName: ""
|
||||
property color iconColor: Theme.surfaceText
|
||||
property string labelText: ""
|
||||
property real value: 0.0
|
||||
property real maximumValue: 1.0
|
||||
property real minimumValue: 0.0
|
||||
|
||||
signal sliderValueChanged(real value)
|
||||
|
||||
width: parent ? parent.width : 200
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.nestedSurface
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: sliderContainer.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: Theme.iconSize
|
||||
color: root.iconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.labelText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: sliderContainer
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
width: 120
|
||||
height: parent.height - Theme.spacingS * 2
|
||||
|
||||
DankSlider {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
enabled: root.enabled
|
||||
minimum: Math.round(root.minimumValue * 100)
|
||||
maximum: Math.round(root.maximumValue * 100)
|
||||
value: Math.round(root.value * 100)
|
||||
trackColor: Theme.ccSliderTrackColor
|
||||
trackOpacity: Theme.ccSliderTrackOpacity
|
||||
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,6 @@ Rectangle {
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2;
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||
}
|
||||
|
||||
readonly property color _containerBg: Theme.ccPillInactiveBg
|
||||
|
||||
color: {
|
||||
@@ -61,7 +56,7 @@ Rectangle {
|
||||
radius: root.radius
|
||||
z: 0
|
||||
visible: false
|
||||
color: hoverTint(_containerBg)
|
||||
color: Theme.hoverTint(_containerBg)
|
||||
opacity: 0.08
|
||||
antialiasing: true
|
||||
Behavior on opacity {
|
||||
@@ -98,7 +93,7 @@ Rectangle {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: _tileRadius
|
||||
color: hoverTint(iconTile.color)
|
||||
color: Theme.hoverTint(iconTile.color)
|
||||
opacity: tileMouse.pressed ? 0.3 : (tileMouse.containsMouse ? 0.2 : 0.0)
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string title: ""
|
||||
property Component content: null
|
||||
property bool isVisible: true
|
||||
property int contentHeight: 300
|
||||
|
||||
width: parent ? parent.width : 400
|
||||
implicitHeight: isVisible ? contentHeight : 0
|
||||
height: implicitHeight
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.content
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,6 @@ Rectangle {
|
||||
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
|
||||
}
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2;
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||
}
|
||||
|
||||
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
||||
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
|
||||
readonly property color _tileRingActive: Theme.ccTileRing
|
||||
@@ -47,7 +42,7 @@ Rectangle {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: hoverTint(root.color)
|
||||
color: Theme.hoverTint(root.color)
|
||||
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
|
||||
@@ -16,11 +16,6 @@ Rectangle {
|
||||
height: 48
|
||||
radius: Theme.cornerRadius === 0 ? 0 : Theme.cornerRadius
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2;
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||
}
|
||||
|
||||
color: Theme.primary
|
||||
border.color: Theme.ccTileRing
|
||||
border.width: 1
|
||||
@@ -29,7 +24,7 @@ Rectangle {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: hoverTint(root.color)
|
||||
color: Theme.hoverTint(root.color)
|
||||
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
|
||||
@@ -25,11 +25,6 @@ Rectangle {
|
||||
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
|
||||
}
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2;
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||
}
|
||||
|
||||
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
||||
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
|
||||
readonly property color _tileRingActive: Theme.ccTileRing
|
||||
@@ -50,7 +45,7 @@ Rectangle {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: hoverTint(root.color)
|
||||
color: Theme.hoverTint(root.color)
|
||||
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
|
||||
@@ -34,11 +34,6 @@ Rectangle {
|
||||
height: 48
|
||||
radius: Theme.cornerRadius + 4
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2;
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||
}
|
||||
|
||||
readonly property color _tileBg: Theme.ccPillInactiveBg
|
||||
|
||||
color: mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBg
|
||||
@@ -50,7 +45,7 @@ Rectangle {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: hoverTint(root.color)
|
||||
color: Theme.hoverTint(root.color)
|
||||
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
|
||||
@@ -20,11 +20,6 @@ Rectangle {
|
||||
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
|
||||
}
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2;
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||
}
|
||||
|
||||
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
||||
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
|
||||
readonly property color _tileRingActive: Theme.ccTileRing
|
||||
@@ -45,7 +40,7 @@ Rectangle {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: hoverTint(root.color)
|
||||
color: Theme.hoverTint(root.color)
|
||||
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
|
||||
@@ -39,17 +39,12 @@ Rectangle {
|
||||
border.width: isActive ? 1 : Theme.layerOutlineWidth
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
|
||||
function hoverTint(base) {
|
||||
const factor = 1.2;
|
||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||
}
|
||||
|
||||
readonly property color _containerBg: Theme.ccPillInactiveBg
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse ? hoverTint(_containerBg) : Theme.withAlpha(_containerBg, 0)
|
||||
color: mouseArea.containsMouse ? Theme.hoverTint(_containerBg) : Theme.withAlpha(_containerBg, 0)
|
||||
opacity: mouseArea.containsMouse ? 0.08 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
|
||||
@@ -29,6 +29,8 @@ Item {
|
||||
property var centerWidgets: []
|
||||
property int totalWidgets: 0
|
||||
property real totalSize: 0
|
||||
property real contentStart: 0
|
||||
property real contentSize: 0
|
||||
|
||||
function updateLayout() {
|
||||
if (SettingsData.centeringMode === "geometric") {
|
||||
@@ -36,6 +38,25 @@ Item {
|
||||
} else {
|
||||
applyIndexLayout();
|
||||
}
|
||||
updateContentExtent();
|
||||
}
|
||||
|
||||
function updateContentExtent() {
|
||||
if (centerWidgets.length === 0) {
|
||||
contentStart = 0;
|
||||
contentSize = 0;
|
||||
return;
|
||||
}
|
||||
let start = Infinity;
|
||||
let end = -Infinity;
|
||||
for (const widget of centerWidgets) {
|
||||
const pos = isVertical ? widget.y : widget.x;
|
||||
const size = isVertical ? widget.height : widget.width;
|
||||
start = Math.min(start, pos);
|
||||
end = Math.max(end, pos + size);
|
||||
}
|
||||
contentStart = start;
|
||||
contentSize = end - start;
|
||||
}
|
||||
|
||||
function applyGeometricLayout() {
|
||||
|
||||
@@ -69,7 +69,62 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function triggerDashTab(tabId) {
|
||||
function dashSectionAnchor(section) {
|
||||
let item;
|
||||
switch (section) {
|
||||
case "left":
|
||||
item = barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection;
|
||||
break;
|
||||
case "right":
|
||||
item = barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection;
|
||||
break;
|
||||
default:
|
||||
item = barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection;
|
||||
}
|
||||
if (!item)
|
||||
return null;
|
||||
if (barWindow.isVertical)
|
||||
return {
|
||||
"pos": item.mapToItem(null, 0, item.height / 2),
|
||||
"width": item.height
|
||||
};
|
||||
return {
|
||||
"pos": item.mapToItem(null, 0, 0),
|
||||
"width": item.width
|
||||
};
|
||||
}
|
||||
|
||||
function positionDash(popout, position) {
|
||||
if (!popout.setTriggerPosition) {
|
||||
popout.triggerScreen = barWindow.screen;
|
||||
return "center";
|
||||
}
|
||||
|
||||
const explicit = position === "left" || position === "center" || position === "right";
|
||||
const section = explicit ? position : (clockButtonRef?.section || "center");
|
||||
const clockAnchor = clockButtonRef?.visualContent ? {
|
||||
"pos": clockButtonRef.visualContent.mapToItem(null, 0, 0),
|
||||
"width": clockButtonRef.visualWidth
|
||||
} : null;
|
||||
|
||||
let anchor;
|
||||
if (!explicit && section !== "center" && clockAnchor)
|
||||
anchor = clockAnchor;
|
||||
else
|
||||
anchor = dashSectionAnchor(section) || clockAnchor;
|
||||
|
||||
if (!anchor) {
|
||||
popout.triggerScreen = barWindow.screen;
|
||||
return section;
|
||||
}
|
||||
|
||||
const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1));
|
||||
const pos = SettingsData.getPopupTriggerPosition(anchor.pos, barWindow.screen, barWindow.effectiveBarThickness, anchor.width, barConfig?.spacing ?? 4, barPosition, barConfig);
|
||||
popout.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig);
|
||||
return section;
|
||||
}
|
||||
|
||||
function triggerDashTab(tabId, position) {
|
||||
const loader = PopoutService.dankDashPopoutLoader;
|
||||
if (!loader)
|
||||
return false;
|
||||
@@ -78,38 +133,7 @@ Item {
|
||||
return false;
|
||||
}
|
||||
|
||||
let section = "center";
|
||||
if (clockButtonRef && clockButtonRef.visualContent && loader.item.setTriggerPosition) {
|
||||
const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1));
|
||||
section = clockButtonRef.section || "center";
|
||||
|
||||
let triggerPos, triggerWidth;
|
||||
if (section === "center") {
|
||||
const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? topBarContent.vCenterSection : topBarContent.vCenterSection) : topBarContent.hCenterSection;
|
||||
if (centerSection) {
|
||||
if (barWindow.isVertical) {
|
||||
const centerY = centerSection.height / 2;
|
||||
triggerPos = centerSection.mapToItem(null, 0, centerY);
|
||||
triggerWidth = centerSection.height;
|
||||
} else {
|
||||
triggerPos = centerSection.mapToItem(null, 0, 0);
|
||||
triggerWidth = centerSection.width;
|
||||
}
|
||||
} else {
|
||||
triggerPos = clockButtonRef.visualContent.mapToItem(null, 0, 0);
|
||||
triggerWidth = clockButtonRef.visualWidth;
|
||||
}
|
||||
} else {
|
||||
triggerPos = clockButtonRef.visualContent.mapToItem(null, 0, 0);
|
||||
triggerWidth = clockButtonRef.visualWidth;
|
||||
}
|
||||
|
||||
const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, barConfig?.spacing ?? 4, barPosition, barConfig);
|
||||
loader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig);
|
||||
} else {
|
||||
loader.item.triggerScreen = barWindow.screen;
|
||||
}
|
||||
|
||||
const section = positionDash(loader.item, position);
|
||||
if (loader.item.requestTab)
|
||||
loader.item.requestTab(tabId);
|
||||
PopoutManager.requestPopout(loader.item, undefined, (barConfig?.id ?? "default") + "-" + section + "-" + tabId);
|
||||
@@ -808,16 +832,30 @@ Item {
|
||||
const pos = section.mapToItem(barWindow.hostWindow.contentItem, 0, 0);
|
||||
const implW = section.implicitWidth || 0;
|
||||
const implH = section.implicitHeight || 0;
|
||||
const contentSize = isCenter ? (section.contentSize || 0) : 0;
|
||||
|
||||
const offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0;
|
||||
const offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0);
|
||||
let offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0;
|
||||
let offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0);
|
||||
let w = implW;
|
||||
let h = implH;
|
||||
|
||||
// index centering lays content out asymmetrically; use the real extent
|
||||
if (contentSize > 0) {
|
||||
if (barWindow.isVertical) {
|
||||
offsetY = section.contentStart;
|
||||
h = contentSize;
|
||||
} else {
|
||||
offsetX = section.contentStart;
|
||||
w = contentSize;
|
||||
}
|
||||
}
|
||||
|
||||
const edgePad = 2;
|
||||
return {
|
||||
"x": pos.x + offsetX - edgePad,
|
||||
"y": pos.y + offsetY - edgePad,
|
||||
"w": implW + edgePad * 2,
|
||||
"h": implH + edgePad * 2
|
||||
"w": w + edgePad * 2,
|
||||
"h": h + edgePad * 2
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,10 @@ Item {
|
||||
readonly property real _barInsetPaddingRaw: SettingsData.barInsetPaddingSyncAll ? SettingsData.barInsetPaddingShared : (barConfig?.barInsetPadding ?? -1)
|
||||
readonly property real _barInsetPaddingAuto: _barIsVertical ? Theme.spacingXS : _edgeBaseMargin
|
||||
readonly property real _barInsetPadding: _barInsetPaddingRaw < 0 ? _barInsetPaddingAuto : _barInsetPaddingRaw
|
||||
// Connected-frame Bar Inset Padding: absolute free-end gap the hosted bar spans full-width into
|
||||
// (auto < 0 = frameThickness so widgets align with the interior cutout, 0 = edge-to-edge). The extra
|
||||
// beyond frameThickness is what an adjacent bar end adds on top of its corner alignment.
|
||||
readonly property real _frameInsetResolved: SettingsData.frameBarInsetPadding < 0 ? SettingsData.frameThickness : SettingsData.frameBarInsetPadding
|
||||
readonly property real _frameInsetExtra: Math.max(0, _frameInsetResolved - SettingsData.frameThickness)
|
||||
// Hosted bars span their edge fully; frameBarContentGap is the free-end gap measured from the
|
||||
// screen edge (auto = frameThickness, aligning widgets with the interior cutout).
|
||||
readonly property real _frameInsetResolved: SettingsData.frameBarContentGap
|
||||
readonly property real _frameInsetExtra: SettingsData.frameBarContentGapExtra
|
||||
|
||||
// Horizontal bars span the full width and own the corners; the perpendicular vertical bar
|
||||
// tucks in below/above. Where they meet, inset the corner widget so it centres in the
|
||||
@@ -274,22 +273,6 @@ Item {
|
||||
return ws.num !== -1 ? ws.num : ws.name;
|
||||
}
|
||||
|
||||
function escapeSwayWorkspaceName(name) {
|
||||
return String(name ?? "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
||||
}
|
||||
|
||||
function dispatchSwayWorkspace(ws) {
|
||||
if (!ws)
|
||||
return;
|
||||
try {
|
||||
if (ws.num !== undefined && ws.num !== -1) {
|
||||
I3.dispatch(`workspace number ${ws.num}`);
|
||||
} else if (ws.name) {
|
||||
I3.dispatch(`workspace "${escapeSwayWorkspaceName(ws.name)}"`);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function switchWorkspace(direction) {
|
||||
const realWorkspaces = getRealWorkspaces();
|
||||
if (realWorkspaces.length < 2) {
|
||||
@@ -334,7 +317,7 @@ Item {
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
|
||||
|
||||
if (nextIndex !== validIndex) {
|
||||
dispatchSwayWorkspace(realWorkspaces[nextIndex]);
|
||||
CompositorService.dispatchSwayWorkspace(realWorkspaces[nextIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,21 @@ PanelWindow {
|
||||
readonly property int barPos: body.barPos
|
||||
readonly property bool barRevealed: body.barRevealed
|
||||
|
||||
property alias controlCenterButtonRef: body.controlCenterButtonRef
|
||||
property alias clockButtonRef: body.clockButtonRef
|
||||
property alias systemUpdateButtonRef: body.systemUpdateButtonRef
|
||||
|
||||
function triggerSystemUpdate() {
|
||||
body.triggerSystemUpdate();
|
||||
}
|
||||
function triggerControlCenter() {
|
||||
body.triggerControlCenter();
|
||||
}
|
||||
function triggerDashTab(tabId) {
|
||||
body.triggerDashTab(tabId);
|
||||
function triggerDashTab(tabId, position) {
|
||||
return body.triggerDashTab(tabId, position);
|
||||
}
|
||||
function positionDash(popout, position) {
|
||||
return body.positionDash(popout, position);
|
||||
}
|
||||
function triggerWallpaperBrowser() {
|
||||
body.triggerWallpaperBrowser();
|
||||
|
||||
@@ -3,6 +3,7 @@ import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Details
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
|
||||
@@ -203,7 +203,7 @@ BasePill {
|
||||
const pinKey = getScreenPinKey();
|
||||
if (!pinKey)
|
||||
return "";
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
return pins[pinKey] || "";
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,11 @@ import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../../Common/Format.js" as Format
|
||||
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
function formatNetworkSpeed(bytesPerSec) {
|
||||
if (bytesPerSec < 1024) {
|
||||
return bytesPerSec.toFixed(0) + " B/s";
|
||||
} else if (bytesPerSec < 1024 * 1024) {
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s";
|
||||
} else if (bytesPerSec < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
|
||||
} else {
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s";
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
DgopService.addRef(["network"]);
|
||||
}
|
||||
@@ -97,7 +86,7 @@ BasePill {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.networkRxRate > 0 ? root.formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s"
|
||||
text: DgopService.networkRxRate > 0 ? Format.formatRate(DgopService.networkRxRate, 1) : "0 B/s"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -126,7 +115,7 @@ BasePill {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.networkTxRate > 0 ? root.formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s"
|
||||
text: DgopService.networkTxRate > 0 ? Format.formatRate(DgopService.networkTxRate, 1) : "0 B/s"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -35,44 +35,42 @@ BasePill {
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: (rx, ry) => {
|
||||
onRightClicked: {
|
||||
const screen = root.parentScreen || Screen;
|
||||
if (!screen)
|
||||
return;
|
||||
const globalPos = root.visualContent.mapToItem(null, 0, 0);
|
||||
|
||||
const isVertical = root.axis?.isVertical ?? false;
|
||||
const edge = root.axis?.edge ?? "top";
|
||||
const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS);
|
||||
const barOffset = root.barThickness + root.barSpacing + gap;
|
||||
const localPos = root.visualContent.mapToItem(null, root.visualContent.width / 2, root.visualContent.height / 2);
|
||||
|
||||
let anchorX;
|
||||
let anchorY;
|
||||
let anchorEdge;
|
||||
if (isVertical) {
|
||||
anchorY = globalPos.y - (screen.y || 0) + root.visualContent.height / 2;
|
||||
if (edge === "left") {
|
||||
anchorX = barOffset;
|
||||
anchorEdge = "top";
|
||||
} else {
|
||||
anchorX = screen.width - barOffset;
|
||||
anchorEdge = "top";
|
||||
}
|
||||
anchorX = edge === "left" ? barOffset : screen.width - barOffset;
|
||||
anchorY = localPos.y;
|
||||
} else {
|
||||
anchorX = globalPos.x - (screen.x || 0) + root.visualContent.width / 2;
|
||||
if (edge === "bottom") {
|
||||
anchorY = screen.height - barOffset;
|
||||
anchorEdge = "bottom";
|
||||
} else {
|
||||
anchorY = barOffset;
|
||||
anchorEdge = "top";
|
||||
}
|
||||
anchorX = localPos.x;
|
||||
anchorY = edge === "bottom" ? screen.height - barOffset : barOffset;
|
||||
}
|
||||
|
||||
dndPopupLoader.active = true;
|
||||
const popup = dndPopupLoader.item;
|
||||
if (!popup)
|
||||
return;
|
||||
popup.showAt(anchorX, anchorY, screen, anchorEdge);
|
||||
|
||||
popup.showAt(anchorX, anchorY, isVertical, edge, screen);
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.MiddleButton
|
||||
onPressed: mouse => {
|
||||
root.triggerRipple(this, mouse.x, mouse.y);
|
||||
SessionData.setDoNotDisturb(!SessionData.doNotDisturb);
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -228,22 +228,6 @@ Item {
|
||||
return ws.num !== -1 ? ws.num : ws.name;
|
||||
}
|
||||
|
||||
function escapeSwayWorkspaceName(name) {
|
||||
return String(name ?? "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
||||
}
|
||||
|
||||
function dispatchSwayWorkspace(ws) {
|
||||
if (!ws)
|
||||
return;
|
||||
try {
|
||||
if (ws.num !== undefined && ws.num !== -1) {
|
||||
I3.dispatch(`workspace number ${ws.num}`);
|
||||
} else if (ws.name) {
|
||||
I3.dispatch(`workspace "${escapeSwayWorkspaceName(ws.name)}"`);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function getSwayActiveWorkspace() {
|
||||
if (!root.screenName || SettingsData.workspaceFollowFocus) {
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||
@@ -711,7 +695,7 @@ Item {
|
||||
case "sway":
|
||||
case "scroll":
|
||||
case "miracle":
|
||||
dispatchSwayWorkspace(data);
|
||||
CompositorService.dispatchSwayWorkspace(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -818,7 +802,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatchSwayWorkspace(realWorkspaces[nextIndex]);
|
||||
CompositorService.dispatchSwayWorkspace(realWorkspaces[nextIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1486,7 +1470,7 @@ Item {
|
||||
} else if (root.isMango && modelData?.tag !== undefined) {
|
||||
MangoService.switchToTag(root.screenName, modelData.tag);
|
||||
} else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num !== undefined) {
|
||||
root.dispatchSwayWorkspace(modelData);
|
||||
CompositorService.dispatchSwayWorkspace(modelData);
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (CompositorService.isNiri) {
|
||||
|
||||
@@ -415,11 +415,24 @@ Item {
|
||||
}
|
||||
}
|
||||
onWheel: wheelEvent => {
|
||||
if (SettingsData.audioDeviceScrollVolumeEnabled && wheelEvent.x >= deviceMouseArea.width / 2) {
|
||||
AudioService.handleNodeVolumeWheel(modelData, wheelEvent);
|
||||
} else {
|
||||
if (!SettingsData.audioDeviceScrollVolumeEnabled || wheelEvent.x < deviceMouseArea.width / 2) {
|
||||
wheelEvent.accepted = false;
|
||||
return;
|
||||
}
|
||||
if (!modelData?.audio)
|
||||
return;
|
||||
SessionData.suppressOSDTemporarily();
|
||||
const delta = wheelEvent.angleDelta.y;
|
||||
if (delta === 0)
|
||||
return;
|
||||
const current = Math.round(modelData.audio.volume * 100);
|
||||
const maxVol = AudioService.getMaxVolumePercent(modelData);
|
||||
const newVolume = delta > 0 ? Math.min(maxVol, current + AudioService.wheelVolumeStep) : Math.max(0, current - AudioService.wheelVolumeStep);
|
||||
modelData.audio.muted = false;
|
||||
modelData.audio.volume = newVolume / 100;
|
||||
if (modelData === AudioService.sink)
|
||||
AudioService.playVolumeChangeSoundIfEnabled();
|
||||
wheelEvent.accepted = true;
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
|
||||
@@ -451,6 +451,9 @@ Item {
|
||||
height: width
|
||||
anchors.centerIn: parent
|
||||
activePlayer: root.activePlayer
|
||||
artUrl: TrackArtService.resolvedArtUrl
|
||||
accentColor: MediaAccentService.accent
|
||||
cavaService: CavaService
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,6 +525,10 @@ Item {
|
||||
height: 20
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
activePlayer: root.activePlayer
|
||||
stableLength: MprisController.activePlayerStableLength
|
||||
accentColor: MediaAccentService.accent
|
||||
accentTrackColor: MediaAccentService.accentTrack
|
||||
accentSubtleColor: MediaAccentService.accentSubtle
|
||||
isSeeking: root.isSeeking
|
||||
onIsSeekingChanged: root.isSeeking = isSeeking
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@ Card {
|
||||
height: 80
|
||||
anchors.centerIn: parent
|
||||
activePlayer: root.activePlayer
|
||||
artUrl: TrackArtService.resolvedArtUrl
|
||||
accentColor: MediaAccentService.accent
|
||||
cavaService: CavaService
|
||||
albumSize: 76
|
||||
animationScale: 1.05
|
||||
}
|
||||
@@ -114,6 +117,10 @@ Card {
|
||||
height: 20
|
||||
x: -2
|
||||
activePlayer: root.activePlayer
|
||||
stableLength: MprisController.activePlayerStableLength
|
||||
accentColor: MediaAccentService.accent
|
||||
accentTrackColor: MediaAccentService.accentTrack
|
||||
accentSubtleColor: MediaAccentService.accentSubtle
|
||||
isSeeking: root.isSeeking
|
||||
onIsSeekingChanged: root.isSeeking = isSeeking
|
||||
}
|
||||
|
||||
@@ -114,10 +114,6 @@ Item {
|
||||
readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin
|
||||
readonly property real joinedEdgeMargin: dockGeometry.joinedEdgeMargin
|
||||
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
|
||||
function px(v) {
|
||||
return Math.round(v * _dpr) / _dpr;
|
||||
}
|
||||
|
||||
DockGeometry {
|
||||
id: dockGeometry
|
||||
|
||||
@@ -397,8 +393,8 @@ Item {
|
||||
|
||||
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
|
||||
|
||||
readonly property real surfaceImplicitWidth: isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
readonly property real surfaceImplicitHeight: !isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
readonly property real surfaceImplicitWidth: isVertical ? (Theme.px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3, _dpr) + animationHeadroom) : 0
|
||||
readonly property real surfaceImplicitHeight: !isVertical ? (Theme.px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3, _dpr) + animationHeadroom) : 0
|
||||
|
||||
readonly property real blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
|
||||
readonly property real blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
|
||||
@@ -585,11 +581,11 @@ Item {
|
||||
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
|
||||
return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
|
||||
}
|
||||
return dock.reveal ? px(dockGeometry.motionThickness) : 1;
|
||||
return dock.reveal ? Theme.px(dockGeometry.motionThickness, _dpr) : 1;
|
||||
}
|
||||
width: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? px(dockGeometry.motionThickness) : 1;
|
||||
return dock.reveal ? Theme.px(dockGeometry.motionThickness, _dpr) : 1;
|
||||
}
|
||||
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
|
||||
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
|
||||
|
||||
@@ -19,17 +19,12 @@ QtObject {
|
||||
property real barSpacing: 0
|
||||
property real dpr: 1
|
||||
|
||||
function px(value) {
|
||||
return Math.round(value * dpr) / dpr;
|
||||
}
|
||||
|
||||
readonly property bool frameExclusionActive: CompositorService.frameWindowVisibleForScreen(screen)
|
||||
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screen)
|
||||
readonly property bool connectedBarActiveOnEdge: usesConnectedFrameChrome && !!screen && SettingsData.getActiveBarEdgesForScreen(screen).includes(edge)
|
||||
|
||||
readonly property real connectedJoinInset: {
|
||||
if (usesConnectedFrameChrome)
|
||||
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
|
||||
return SettingsData.frameEdgeReservation(screen, edge);
|
||||
if (frameExclusionActive)
|
||||
return SettingsData.frameEdgeInsetForSide(screen, edge);
|
||||
return 0;
|
||||
@@ -56,6 +51,6 @@ QtObject {
|
||||
|
||||
// Frame/bar edge exclusions already reserve the edge itself, so the dock
|
||||
// reservation covers only the dock body and user offset beyond that edge.
|
||||
readonly property real reserveZone: px(bodyThickness + reserveOffset + effectiveMargin)
|
||||
readonly property real reserveZone: Theme.px(bodyThickness + reserveOffset + effectiveMargin, dpr)
|
||||
readonly property bool shouldReserveSpace: dockVisible && !autoHide && barSpacing <= 0
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ Scope {
|
||||
}
|
||||
|
||||
function exclusionSizeForEdge(edge) {
|
||||
return root.barEdges.includes(edge) ? SettingsData.frameBarSize : SettingsData.frameThickness;
|
||||
return SettingsData.frameEdgeReservation(root.screen, edge);
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -262,10 +262,22 @@ PanelWindow {
|
||||
return false;
|
||||
}
|
||||
|
||||
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||
readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||
readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||
readonly property int cutoutTopInset: {
|
||||
SettingsData.barConfigs;
|
||||
return win._regionInt(SettingsData.frameEdgeReservation(win.targetScreen, "top"));
|
||||
}
|
||||
readonly property int cutoutBottomInset: {
|
||||
SettingsData.barConfigs;
|
||||
return win._regionInt(SettingsData.frameEdgeReservation(win.targetScreen, "bottom"));
|
||||
}
|
||||
readonly property int cutoutLeftInset: {
|
||||
SettingsData.barConfigs;
|
||||
return win._regionInt(SettingsData.frameEdgeReservation(win.targetScreen, "left"));
|
||||
}
|
||||
readonly property int cutoutRightInset: {
|
||||
SettingsData.barConfigs;
|
||||
return win._regionInt(SettingsData.frameEdgeReservation(win.targetScreen, "right"));
|
||||
}
|
||||
readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset)
|
||||
readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset)
|
||||
readonly property int cutoutRadius: {
|
||||
|
||||
@@ -66,9 +66,17 @@ Scope {
|
||||
IdleService.lockPowerOffRequested = false;
|
||||
}
|
||||
|
||||
// Avoid startup lock when using dms-greeter (#2952)
|
||||
function freshGreeterLogin() {
|
||||
const authTime = Number(Quickshell.env("DMS_GREETER_AUTH_TIME") || 0);
|
||||
if (!authTime)
|
||||
return false;
|
||||
return (Date.now() / 1000 - authTime) < 120;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
IdleService.lockComponent = this;
|
||||
if (SettingsData.lockAtStartup)
|
||||
if (SettingsData.lockAtStartup && !freshGreeterLogin())
|
||||
lock();
|
||||
}
|
||||
|
||||
|
||||
@@ -932,7 +932,9 @@ Item {
|
||||
pam.passwd.start();
|
||||
}
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
Keys.onPressed: event => handleKey(event)
|
||||
|
||||
function handleKey(event) {
|
||||
if (demoMode) {
|
||||
return;
|
||||
}
|
||||
@@ -1042,6 +1044,36 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Wayland IMEs commit unconsumed printable keys as text-input text
|
||||
// (ibus ibuswaylandim.c) instead of forwarding raw keys, so an active
|
||||
// text input must exist to receive them; the hidden-text hints put
|
||||
// fcitx5 into plain keyboard passthrough (CapabilityFlag::Password).
|
||||
// Raw keys stay in handleKey (#2950).
|
||||
TextInput {
|
||||
id: imeCommitSink
|
||||
|
||||
focus: true
|
||||
width: 1
|
||||
height: 1
|
||||
opacity: 0
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhHiddenText | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
|
||||
Keys.onPressed: event => {
|
||||
passwordField.handleKey(event);
|
||||
if (!event.accepted && (event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier)))
|
||||
event.accepted = true;
|
||||
}
|
||||
onTextChanged: {
|
||||
if (text.length === 0)
|
||||
return;
|
||||
const committed = text;
|
||||
text = "";
|
||||
if (demoMode || root.unlocking || pam.passwd.active)
|
||||
return;
|
||||
passwordField.insertText(committed);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!demoMode) {
|
||||
forceActiveFocus();
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../../Common/Format.js" as Format
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@@ -22,36 +23,8 @@ Rectangle {
|
||||
onTriggered: root.nowMs = Date.now()
|
||||
}
|
||||
|
||||
function _pad2(n) {
|
||||
return n < 10 ? "0" + n : "" + n;
|
||||
}
|
||||
|
||||
function formatRemaining(ms) {
|
||||
if (ms <= 0)
|
||||
return I18n.tr("Off");
|
||||
const totalMinutes = Math.ceil(ms / 60000);
|
||||
if (totalMinutes < 60)
|
||||
return I18n.tr("%1 min left").arg(totalMinutes);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const mins = totalMinutes - hours * 60;
|
||||
if (mins === 0)
|
||||
return I18n.tr("%1 h left").arg(hours);
|
||||
return I18n.tr("%1 h %2 m left").arg(hours).arg(mins);
|
||||
}
|
||||
|
||||
function formatUntilTimestamp(ts) {
|
||||
if (!ts)
|
||||
return "";
|
||||
const d = new Date(ts);
|
||||
const hours = d.getHours();
|
||||
const minutes = d.getMinutes();
|
||||
const use24h = (typeof SettingsData !== "undefined") ? SettingsData.use24HourClock : true;
|
||||
if (use24h) {
|
||||
return _pad2(hours) + ":" + _pad2(minutes);
|
||||
}
|
||||
const suffix = hours >= 12 ? "PM" : "AM";
|
||||
const h12 = ((hours + 11) % 12) + 1;
|
||||
return h12 + ":" + _pad2(minutes) + " " + suffix;
|
||||
return Format.formatRemaining(ms, I18n.tr("Off"), I18n.tr("%1 min left"), I18n.tr("%1 h left"), I18n.tr("%1 h %2 m left"));
|
||||
}
|
||||
|
||||
function minutesUntilTomorrowMorning() {
|
||||
@@ -149,7 +122,7 @@ Rectangle {
|
||||
visible: root.currentlyActive
|
||||
text: {
|
||||
if (SessionData.doNotDisturbUntil > 0) {
|
||||
return root.formatRemaining(root.currentRemainingMs) + " · " + I18n.tr("until %1").arg(root.formatUntilTimestamp(SessionData.doNotDisturbUntil));
|
||||
return root.formatRemaining(root.currentRemainingMs) + " · " + I18n.tr("until %1").arg(Format.formatUntil(SessionData.doNotDisturbUntil, SettingsData.use24HourClock));
|
||||
}
|
||||
return I18n.tr("On indefinitely");
|
||||
}
|
||||
|
||||
@@ -31,14 +31,16 @@ PanelWindow {
|
||||
}
|
||||
|
||||
property point anchorPos: Qt.point(0, 0)
|
||||
property string anchorEdge: "top"
|
||||
property bool isVertical: false
|
||||
property string edge: "top"
|
||||
visible: false
|
||||
|
||||
function showAt(x, y, targetScreen, edge) {
|
||||
function showAt(x, y, vertical, barEdge, targetScreen) {
|
||||
if (targetScreen)
|
||||
root.screen = targetScreen;
|
||||
anchorPos = Qt.point(x, y);
|
||||
anchorEdge = edge || "top";
|
||||
isVertical = vertical ?? false;
|
||||
edge = barEdge ?? "top";
|
||||
visible = true;
|
||||
}
|
||||
|
||||
@@ -66,21 +68,19 @@ PanelWindow {
|
||||
visible: root.visible
|
||||
|
||||
x: {
|
||||
const left = 10;
|
||||
const right = root.width - width - 10;
|
||||
const want = root.anchorPos.x - width / 2;
|
||||
return Math.max(left, Math.min(right, want));
|
||||
if (root.isVertical) {
|
||||
if (root.edge === "left")
|
||||
return Math.min(root.width - width - 10, root.anchorPos.x);
|
||||
return Math.max(10, root.anchorPos.x - width);
|
||||
}
|
||||
return Math.max(10, Math.min(root.width - width - 10, root.anchorPos.x - width / 2));
|
||||
}
|
||||
y: {
|
||||
switch (root.anchorEdge) {
|
||||
case "bottom":
|
||||
return Math.max(10, root.anchorPos.y - height);
|
||||
case "left":
|
||||
case "right":
|
||||
if (root.isVertical)
|
||||
return Math.max(10, Math.min(root.height - height - 10, root.anchorPos.y - height / 2));
|
||||
default:
|
||||
return Math.min(root.height - height - 10, root.anchorPos.y);
|
||||
}
|
||||
if (root.edge === "bottom")
|
||||
return Math.max(10, root.anchorPos.y - height);
|
||||
return Math.min(root.height - height - 10, root.anchorPos.y);
|
||||
}
|
||||
|
||||
onDismissed: root.closeMenu()
|
||||
|
||||
@@ -27,6 +27,36 @@ Rectangle {
|
||||
readonly property real collapsedContentHeight: iconSize + cardPadding
|
||||
readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight
|
||||
|
||||
function formatHistoryTime(timestamp) {
|
||||
NotificationService.timeUpdateTick;
|
||||
NotificationService.clockFormatChanged;
|
||||
const now = new Date();
|
||||
const date = new Date(timestamp);
|
||||
const diff = now.getTime() - timestamp;
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 1) {
|
||||
if (minutes < 1)
|
||||
return I18n.tr("now");
|
||||
return I18n.tr("%1m ago").arg(minutes);
|
||||
}
|
||||
const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const itemDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
const daysDiff = Math.floor((nowDate - itemDate) / (1000 * 60 * 60 * 24));
|
||||
const timeStr = SettingsData.use24HourClock ? date.toLocaleTimeString(Qt.locale(), "HH:mm") : date.toLocaleTimeString(Qt.locale(), "h:mm AP");
|
||||
if (daysDiff === 0)
|
||||
return timeStr;
|
||||
try {
|
||||
const localeName = (typeof I18n !== "undefined" && I18n.locale) ? I18n.locale().name : "en-US";
|
||||
const weekday = date.toLocaleDateString(localeName, {
|
||||
weekday: "long"
|
||||
});
|
||||
return weekday + ", " + timeStr;
|
||||
} catch (e) {
|
||||
return timeStr;
|
||||
}
|
||||
}
|
||||
|
||||
width: parent ? parent.width : 400
|
||||
height: baseCardHeight + contentItem.extraHeight
|
||||
radius: Theme.cornerRadius
|
||||
@@ -219,7 +249,7 @@ Rectangle {
|
||||
}
|
||||
StyledText {
|
||||
id: historyTimeText
|
||||
text: NotificationService.formatHistoryTime(historyItem.timestamp)
|
||||
text: root.formatHistoryTime(historyItem.timestamp)
|
||||
color: Theme.surfaceTextMedium
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Normal
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user