1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

Compare commits

...

10 Commits

Author SHA1 Message Date
purian23
80025804ab theme: Tweaks to Auto Color Mode 2026-01-24 20:43:45 -05:00
bbedward
028d3b4e61 workspaces: fix index numbers with show apps on vBar + animation 2026-01-24 20:31:45 -05:00
purian23
9cce5ccfe6 autoThemeMode: Add transition time & layout update 2026-01-24 19:33:37 -05:00
purian23
a260b8060e Merge branch 'master' into auto-theme 2026-01-24 18:19:13 -05:00
purian23
f945307232 cleanup: Auto theme switcher 2026-01-24 17:48:34 -05:00
bbedward
8f44d52cb2 launcher v2: allow categories in plugins 2026-01-24 16:58:55 -05:00
purian23
3413cb7b89 feat: Create new Auto theme mode based on region / time of day 2026-01-24 16:38:45 -05:00
bbedward
4e3b24ffbb settings: migrate vpnLastConnected to session
fixes #1488
2026-01-24 16:08:15 -05:00
bbedward
03cfa55e0b ipc: ass toast IPCs
fixes #964
2026-01-24 12:53:51 -05:00
bbedward
a887e60f40 keybinds: fix MangoWC config traversal in provider
fixes #1464
2026-01-24 12:23:59 -05:00
23 changed files with 2171 additions and 148 deletions

View File

@@ -502,17 +502,17 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
p.dmsProcessed = true p.dmsProcessed = true
} }
fullPath := sourcePath expanded, err := utils.ExpandPath(sourcePath)
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil { if err != nil {
return return
} }
includedBinds, err := p.parseFileWithSource(expanded) fullPath := expanded
if !filepath.IsAbs(expanded) {
fullPath = filepath.Join(baseDir, expanded)
}
includedBinds, err := p.parseFileWithSource(fullPath)
if err != nil { if err != nil {
return return
} }
@@ -521,33 +521,10 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
} }
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding { func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
data, err := os.ReadFile(dmsBindsPath) keybinds, err := p.parseFileWithSource(dmsBindsPath)
if err != nil { if err != nil {
return nil return nil
} }
prevSource := p.currentSource
p.currentSource = dmsBindsPath
var keybinds []MangoWCKeyBinding
lines := strings.Split(string(data), "\n")
for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.getKeybindAtLineContent(line, lineNum)
if kb == nil {
continue
}
kb.Source = dmsBindsPath
p.addBind(kb)
keybinds = append(keybinds, *kb)
}
p.currentSource = prevSource
p.dmsProcessed = true p.dmsProcessed = true
return keybinds return keybinds
} }

View File

@@ -19,6 +19,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins" serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes" serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
@@ -44,6 +45,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
if strings.HasPrefix(req.Method, "theme.auto.") {
if themeModeManager == nil {
models.RespondError(conn, req.ID, "theme mode manager not initialized")
return
}
thememode.HandleRequest(conn, req, themeModeManager)
return
}
if strings.HasPrefix(req.Method, "loginctl.") { if strings.HasPrefix(req.Method, "loginctl.") {
if loginctlManager == nil { if loginctlManager == nil {
models.RespondError(conn, req.ID, "loginctl manager not initialized") models.RespondError(conn, req.ID, "loginctl manager not initialized")

View File

@@ -28,6 +28,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
@@ -68,6 +69,7 @@ var evdevManager *evdev.Manager
var clipboardManager *clipboard.Manager var clipboardManager *clipboard.Manager
var dbusManager *serverDbus.Manager var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager
const dbusClientID = "dms-dbus-client" const dbusClientID = "dms-dbus-client"
@@ -380,6 +382,14 @@ func InitializeDbusManager() error {
return nil return nil
} }
func InitializeThemeModeManager() error {
manager := thememode.NewManager()
themeModeManager = manager
log.Info("Theme mode automation manager initialized")
return nil
}
func handleConnection(conn net.Conn) { func handleConnection(conn net.Conn) {
defer conn.Close() defer conn.Close()
@@ -457,6 +467,10 @@ func getCapabilities() Capabilities {
caps = append(caps, "clipboard") caps = append(caps, "clipboard")
} }
if themeModeManager != nil {
caps = append(caps, "theme.auto")
}
if dbusManager != nil { if dbusManager != nil {
caps = append(caps, "dbus") caps = append(caps, "dbus")
} }
@@ -519,6 +533,10 @@ func getServerInfo() ServerInfo {
caps = append(caps, "clipboard") caps = append(caps, "clipboard")
} }
if themeModeManager != nil {
caps = append(caps, "theme.auto")
}
if dbusManager != nil { if dbusManager != nil {
caps = append(caps, "dbus") caps = append(caps, "dbus")
} }
@@ -791,6 +809,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("theme.auto") && themeModeManager != nil {
wg.Add(1)
themeAutoChan := themeModeManager.Subscribe(clientID + "-theme-auto")
go func() {
defer wg.Done()
defer themeModeManager.Unsubscribe(clientID + "-theme-auto")
initialState := themeModeManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "theme.auto", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-themeAutoChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "theme.auto", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("bluetooth") && bluezManager != nil { if shouldSubscribe("bluetooth") && bluezManager != nil {
wg.Add(1) wg.Add(1)
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth") bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
@@ -1251,6 +1301,9 @@ func cleanupManagers() {
if dbusManager != nil { if dbusManager != nil {
dbusManager.Close() dbusManager.Close()
} }
if themeModeManager != nil {
themeModeManager.Close()
}
if wlContext != nil { if wlContext != nil {
wlContext.Close() wlContext.Close()
} }
@@ -1346,6 +1399,15 @@ func Start(printDocs bool) error {
log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)") log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)")
log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)") log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)")
log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)") log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)")
log.Info("Theme automation:")
log.Info(" theme.auto.getState - Get current theme automation state")
log.Info(" theme.auto.setEnabled - Enable/disable theme automation (params: enabled)")
log.Info(" theme.auto.setMode - Set automation mode (params: mode [time|location])")
log.Info(" theme.auto.setSchedule - Set time schedule (params: startHour, startMinute, endHour, endMinute)")
log.Info(" theme.auto.setLocation - Set location (params: latitude, longitude)")
log.Info(" theme.auto.setUseIPLocation - Use IP location (params: use)")
log.Info(" theme.auto.trigger - Trigger immediate re-evaluation")
log.Info(" theme.auto.subscribe - Subscribe to theme automation state changes (streaming)")
log.Info("Bluetooth:") log.Info("Bluetooth:")
log.Info(" bluetooth.getState - Get current bluetooth state") log.Info(" bluetooth.getState - Get current bluetooth state")
log.Info(" bluetooth.startDiscovery - Start device discovery") log.Info(" bluetooth.startDiscovery - Start device discovery")
@@ -1503,6 +1565,12 @@ func Start(printDocs bool) error {
log.Debugf("WlrOutput manager unavailable: %v", err) log.Debugf("WlrOutput manager unavailable: %v", err)
} }
if err := InitializeThemeModeManager(); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
fatalErrChan := make(chan error, 1) fatalErrChan := make(chan error, 1)
if wlrOutputManager != nil { if wlrOutputManager != nil {
go func() { go func() {

View File

@@ -0,0 +1,154 @@
package thememode
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "theme mode manager not initialized")
return
}
switch req.Method {
case "theme.auto.getState":
handleGetState(conn, req, manager)
case "theme.auto.setEnabled":
handleSetEnabled(conn, req, manager)
case "theme.auto.setMode":
handleSetMode(conn, req, manager)
case "theme.auto.setSchedule":
handleSetSchedule(conn, req, manager)
case "theme.auto.setLocation":
handleSetLocation(conn, req, manager)
case "theme.auto.setUseIPLocation":
handleSetUseIPLocation(conn, req, manager)
case "theme.auto.trigger":
handleTrigger(conn, req, manager)
case "theme.auto.subscribe":
handleSubscribe(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
enabled, err := params.Bool(req.Params, "enabled")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetEnabled(enabled)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto enabled set"})
}
func handleSetMode(conn net.Conn, req models.Request, manager *Manager) {
mode, err := params.String(req.Params, "mode")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if mode != "time" && mode != "location" {
models.RespondError(conn, req.ID, "invalid mode")
return
}
manager.SetMode(mode)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto mode set"})
}
func handleSetSchedule(conn net.Conn, req models.Request, manager *Manager) {
startHour, err := params.Int(req.Params, "startHour")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
startMinute, err := params.Int(req.Params, "startMinute")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
endHour, err := params.Int(req.Params, "endHour")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
endMinute, err := params.Int(req.Params, "endMinute")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := manager.ValidateSchedule(startHour, startMinute, endHour, endMinute); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetSchedule(startHour, startMinute, endHour, endMinute)
models.Respond(conn, req.ID, manager.GetState())
}
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
lat, err := params.Float(req.Params, "latitude")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
lon, err := params.Float(req.Params, "longitude")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetLocation(lat, lon)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto location set"})
}
func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
use, err := params.Bool(req.Params, "use")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetUseIPLocation(use)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto IP location set"})
}
func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
manager.TriggerUpdate()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto update triggered"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
initialState := manager.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
return
}
for state := range stateChan {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
Result: &state,
}); err != nil {
return
}
}
}

View File

@@ -0,0 +1,432 @@
package thememode
import (
"errors"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
const (
defaultStartHour = 18
defaultStartMinute = 0
defaultEndHour = 6
defaultEndMinute = 0
defaultElevationTwilight = -6.0
defaultElevationDaylight = 3.0
)
type Manager struct {
config Config
configMutex sync.RWMutex
state *State
stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan State]
locationMutex sync.RWMutex
cachedIPLat *float64
cachedIPLon *float64
stopChan chan struct{}
updateTrigger chan struct{}
wg sync.WaitGroup
}
func NewManager() *Manager {
m := &Manager{
config: Config{
Enabled: false,
Mode: "time",
StartHour: defaultStartHour,
StartMinute: defaultStartMinute,
EndHour: defaultEndHour,
EndMinute: defaultEndMinute,
ElevationTwilight: defaultElevationTwilight,
ElevationDaylight: defaultElevationDaylight,
},
stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1),
}
m.updateState(time.Now())
m.wg.Add(1)
go m.schedulerLoop()
return m
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{Config: m.getConfig()}
}
stateCopy := *m.state
return stateCopy
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) SetEnabled(enabled bool) {
m.configMutex.Lock()
if m.config.Enabled == enabled {
m.configMutex.Unlock()
return
}
m.config.Enabled = enabled
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetMode(mode string) {
m.configMutex.Lock()
if m.config.Mode == mode {
m.configMutex.Unlock()
return
}
m.config.Mode = mode
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetSchedule(startHour, startMinute, endHour, endMinute int) {
m.configMutex.Lock()
changed := m.config.StartHour != startHour ||
m.config.StartMinute != startMinute ||
m.config.EndHour != endHour ||
m.config.EndMinute != endMinute
if !changed {
m.configMutex.Unlock()
return
}
m.config.StartHour = startHour
m.config.StartMinute = startMinute
m.config.EndHour = endHour
m.config.EndMinute = endMinute
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetLocation(lat, lon float64) {
m.configMutex.Lock()
if m.config.Latitude != nil && m.config.Longitude != nil &&
*m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation {
m.configMutex.Unlock()
return
}
m.config.Latitude = &lat
m.config.Longitude = &lon
m.config.UseIPLocation = false
m.configMutex.Unlock()
m.locationMutex.Lock()
m.cachedIPLat = nil
m.cachedIPLon = nil
m.locationMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetUseIPLocation(use bool) {
m.configMutex.Lock()
if m.config.UseIPLocation == use {
m.configMutex.Unlock()
return
}
m.config.UseIPLocation = use
if use {
m.config.Latitude = nil
m.config.Longitude = nil
}
m.configMutex.Unlock()
if use {
m.locationMutex.Lock()
m.cachedIPLat = nil
m.cachedIPLon = nil
m.locationMutex.Unlock()
}
m.TriggerUpdate()
}
func (m *Manager) TriggerUpdate() {
select {
case m.updateTrigger <- struct{}{}:
default:
}
}
func (m *Manager) Close() {
select {
case <-m.stopChan:
return
default:
close(m.stopChan)
}
m.wg.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
}
func (m *Manager) schedulerLoop() {
defer m.wg.Done()
var timer *time.Timer
for {
config := m.getConfig()
now := time.Now()
var isLight bool
var next time.Time
if config.Enabled {
isLight, next = m.computeSchedule(now, config)
} else {
m.stateMutex.RLock()
if m.state != nil {
isLight = m.state.IsLight
}
m.stateMutex.RUnlock()
next = now.Add(24 * time.Hour)
}
m.updateStateWithValues(config, isLight, next)
waitDur := time.Until(next)
if !config.Enabled {
waitDur = 24 * time.Hour
}
if waitDur < time.Second {
waitDur = time.Second
}
if timer != nil {
timer.Stop()
}
timer = time.NewTimer(waitDur)
select {
case <-m.stopChan:
timer.Stop()
return
case <-m.updateTrigger:
timer.Stop()
continue
case <-timer.C:
continue
}
}
}
func (m *Manager) updateState(now time.Time) {
config := m.getConfig()
var isLight bool
var next time.Time
if config.Enabled {
isLight, next = m.computeSchedule(now, config)
} else {
m.stateMutex.RLock()
if m.state != nil {
isLight = m.state.IsLight
}
m.stateMutex.RUnlock()
next = now.Add(24 * time.Hour)
}
m.updateStateWithValues(config, isLight, next)
}
func (m *Manager) updateStateWithValues(config Config, isLight bool, next time.Time) {
newState := State{
Config: config,
IsLight: isLight,
NextTransition: next,
}
m.stateMutex.Lock()
if m.state != nil && statesEqual(m.state, &newState) {
m.stateMutex.Unlock()
return
}
m.state = &newState
m.stateMutex.Unlock()
m.notifySubscribers()
}
func (m *Manager) notifySubscribers() {
state := m.GetState()
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- state:
default:
}
return true
})
}
func (m *Manager) getConfig() Config {
m.configMutex.RLock()
defer m.configMutex.RUnlock()
return m.config
}
func (m *Manager) getLocation(config Config) (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude
}
if !config.UseIPLocation {
return nil, nil
}
m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil {
lat, lon := m.cachedIPLat, m.cachedIPLon
m.locationMutex.RUnlock()
return lat, lon
}
m.locationMutex.RUnlock()
lat, lon, err := wayland.FetchIPLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = lat
m.cachedIPLon = lon
m.locationMutex.Unlock()
return lat, lon
}
func statesEqual(a, b *State) bool {
if a == nil || b == nil {
return a == b
}
if a.IsLight != b.IsLight || !a.NextTransition.Equal(b.NextTransition) {
return false
}
return a.Config == b.Config
}
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
if config.Mode == "location" {
return m.computeLocationSchedule(now, config)
}
return computeTimeSchedule(now, config)
}
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
startMinutes := config.StartHour*60 + config.StartMinute
endMinutes := config.EndHour*60 + config.EndMinute
currentMinutes := now.Hour()*60 + now.Minute()
startTime := time.Date(now.Year(), now.Month(), now.Day(), config.StartHour, config.StartMinute, 0, 0, now.Location())
endTime := time.Date(now.Year(), now.Month(), now.Day(), config.EndHour, config.EndMinute, 0, 0, now.Location())
if startMinutes == endMinutes {
next := startTime
if !next.After(now) {
next = next.Add(24 * time.Hour)
}
return true, next
}
if startMinutes < endMinutes {
if currentMinutes < startMinutes {
return true, startTime
}
if currentMinutes >= endMinutes {
return true, startTime.Add(24 * time.Hour)
}
return false, endTime
}
if currentMinutes >= startMinutes {
return false, endTime.Add(24 * time.Hour)
}
if currentMinutes < endMinutes {
return false, endTime
}
return true, startTime
}
func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, time.Time) {
lat, lon := m.getLocation(config)
if lat == nil || lon == nil {
currentIsLight := false
m.stateMutex.RLock()
if m.state != nil {
currentIsLight = m.state.IsLight
}
m.stateMutex.RUnlock()
return currentIsLight, now.Add(10 * time.Minute)
}
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
if cond != wayland.SunNormal {
if cond == wayland.SunMidnightSun {
return true, startOfNextDay(now)
}
return false, startOfNextDay(now)
}
if now.Before(times.Sunrise) {
return false, times.Sunrise
}
if now.Before(times.Sunset) {
return true, times.Sunset
}
nextDay := startOfNextDay(now)
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
if nextCond != wayland.SunNormal {
if nextCond == wayland.SunMidnightSun {
return true, startOfNextDay(nextDay)
}
return false, startOfNextDay(nextDay)
}
return false, nextTimes.Sunrise
}
func startOfNextDay(t time.Time) time.Time {
next := t.Add(24 * time.Hour)
return time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
}
func validateHourMinute(hour, minute int) bool {
if hour < 0 || hour > 23 {
return false
}
if minute < 0 || minute > 59 {
return false
}
return true
}
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
if !validateHourMinute(startHour, startMinute) || !validateHourMinute(endHour, endMinute) {
return errInvalidTime
}
return nil
}
var errInvalidTime = errors.New("invalid schedule time")

View File

@@ -0,0 +1,23 @@
package thememode
import "time"
type Config struct {
Enabled bool `json:"enabled"`
Mode string `json:"mode"`
StartHour int `json:"startHour"`
StartMinute int `json:"startMinute"`
EndHour int `json:"endHour"`
EndMinute int `json:"endMinute"`
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
UseIPLocation bool `json:"useIPLocation"`
ElevationTwilight float64 `json:"elevationTwilight"`
ElevationDaylight float64 `json:"elevationDaylight"`
}
type State struct {
Config Config `json:"config"`
IsLight bool `json:"isLight"`
NextTransition time.Time `json:"nextTransition"`
}

View File

@@ -626,6 +626,7 @@ func (m *Manager) schedulerLoop() {
m.schedule.calcDay = time.Time{} m.schedule.calcDay = time.Time{}
m.scheduleMutex.Unlock() m.scheduleMutex.Unlock()
m.recalcSchedule(time.Now()) m.recalcSchedule(time.Now())
m.updateStateFromSchedule()
m.configMutex.RLock() m.configMutex.RLock()
enabled := m.config.Enabled enabled := m.config.Enabled
m.configMutex.RUnlock() m.configMutex.RUnlock()

View File

@@ -13,7 +13,7 @@ import "settings/SessionStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property int sessionConfigVersion: 2 readonly property int sessionConfigVersion: 3
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
property bool _parseError: false property bool _parseError: false
@@ -82,6 +82,15 @@ Singleton {
property bool nightModeUseIPLocation: false property bool nightModeUseIPLocation: false
property string nightModeLocationProvider: "" property string nightModeLocationProvider: ""
property bool themeModeAutoEnabled: false
property string themeModeAutoMode: "time"
property int themeModeStartHour: 18
property int themeModeStartMinute: 0
property int themeModeEndHour: 6
property int themeModeEndMinute: 0
property bool themeModeShareGammaSettings: true
property string themeModeNextTransition: ""
property var pinnedApps: [] property var pinnedApps: []
property var barPinnedApps: [] property var barPinnedApps: []
property int dockLauncherPosition: 0 property int dockLauncherPosition: 0
@@ -109,6 +118,8 @@ Singleton {
property var appOverrides: ({}) property var appOverrides: ({})
property bool searchAppActions: true property bool searchAppActions: true
property string vpnLastConnected: ""
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
loadSettings(); loadSettings();
@@ -172,7 +183,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg); console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
} }
} }
@@ -186,14 +197,10 @@ Singleton {
_isReadOnly = !writable; _isReadOnly = !writable;
if (_isReadOnly) { if (_isReadOnly) {
_hasUnsavedChanges = _checkForUnsavedChanges(); _hasUnsavedChanges = _checkForUnsavedChanges();
if (!wasReadOnly)
console.info("SessionData: session.json is now read-only");
} else { } else {
_loadedSessionSnapshot = getCurrentSessionJson(); _loadedSessionSnapshot = getCurrentSessionJson();
_hasUnsavedChanges = false; _hasUnsavedChanges = false;
if (wasReadOnly) if (wasReadOnly && _pendingMigration)
console.info("SessionData: session.json is now writable");
if (_pendingMigration)
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2)); settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
} }
_pendingMigration = null; _pendingMigration = null;
@@ -255,7 +262,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg); console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
} }
} }
@@ -273,7 +280,6 @@ Singleton {
} }
function migrateFromUndefinedToV1(settings) { function migrateFromUndefinedToV1(settings) {
console.info("SessionData: Migrating configuration from undefined to version 1");
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
if (settings.acMonitorTimeout !== undefined) { if (settings.acMonitorTimeout !== undefined) {
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout); SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout);
@@ -448,7 +454,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found:", screenName); console.warn("SessionData: Screen not found");
return; return;
} }
@@ -545,7 +551,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found:", screenName); console.warn("SessionData: Screen not found");
return; return;
} }
@@ -583,7 +589,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found:", screenName); console.warn("SessionData: Screen not found");
return; return;
} }
@@ -621,7 +627,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found:", screenName); console.warn("SessionData: Screen not found");
return; return;
} }
@@ -659,7 +665,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found:", screenName); console.warn("SessionData: Screen not found");
return; return;
} }
@@ -702,7 +708,6 @@ Singleton {
} }
function setNightModeAutoEnabled(enabled) { function setNightModeAutoEnabled(enabled) {
console.log("SessionData: Setting nightModeAutoEnabled to", enabled);
nightModeAutoEnabled = enabled; nightModeAutoEnabled = enabled;
saveSettings(); saveSettings();
} }
@@ -738,13 +743,11 @@ Singleton {
} }
function setLatitude(lat) { function setLatitude(lat) {
console.log("SessionData: Setting latitude to", lat);
latitude = lat; latitude = lat;
saveSettings(); saveSettings();
} }
function setLongitude(lng) { function setLongitude(lng) {
console.log("SessionData: Setting longitude to", lng);
longitude = lng; longitude = lng;
saveSettings(); saveSettings();
} }
@@ -754,6 +757,41 @@ Singleton {
saveSettings(); saveSettings();
} }
function setThemeModeAutoEnabled(enabled) {
themeModeAutoEnabled = enabled;
saveSettings();
}
function setThemeModeAutoMode(mode) {
themeModeAutoMode = mode;
saveSettings();
}
function setThemeModeStartHour(hour) {
themeModeStartHour = hour;
saveSettings();
}
function setThemeModeStartMinute(minute) {
themeModeStartMinute = minute;
saveSettings();
}
function setThemeModeEndHour(hour) {
themeModeEndHour = hour;
saveSettings();
}
function setThemeModeEndMinute(minute) {
themeModeEndMinute = minute;
saveSettings();
}
function setThemeModeShareGammaSettings(share) {
themeModeShareGammaSettings = share;
saveSettings();
}
function setPinnedApps(apps) { function setPinnedApps(apps) {
pinnedApps = apps; pinnedApps = apps;
saveSettings(); saveSettings();
@@ -1003,6 +1041,11 @@ Singleton {
saveSettings(); saveSettings();
} }
function setVpnLastConnected(uuid) {
vpnLastConnected = uuid || "";
saveSettings();
}
function syncWallpaperForCurrentMode() { function syncWallpaperForCurrentMode() {
if (!perModeWallpaper) if (!perModeWallpaper)
return; return;

View File

@@ -292,13 +292,13 @@ Singleton {
property string _legacyWeatherLocation: "New York, NY" property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060" property string _legacyWeatherCoordinates: "40.7128,-74.0060"
property string _legacyVpnLastConnected: ""
readonly property string weatherLocation: SessionData.weatherLocation readonly property string weatherLocation: SessionData.weatherLocation
readonly property string weatherCoordinates: SessionData.weatherCoordinates readonly property string weatherCoordinates: SessionData.weatherCoordinates
property bool useAutoLocation: false property bool useAutoLocation: false
property bool weatherEnabled: true property bool weatherEnabled: true
property string networkPreference: "auto" property string networkPreference: "auto"
property string vpnLastConnected: ""
property string iconTheme: "System Default" property string iconTheme: "System Default"
property var availableIconThemes: ["System Default"] property var availableIconThemes: ["System Default"]
@@ -1078,6 +1078,11 @@ Singleton {
_legacyWeatherLocation = obj.weatherLocation; _legacyWeatherLocation = obj.weatherLocation;
if (obj?.weatherCoordinates !== undefined) if (obj?.weatherCoordinates !== undefined)
_legacyWeatherCoordinates = obj.weatherCoordinates; _legacyWeatherCoordinates = obj.weatherCoordinates;
if (obj?.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
_legacyVpnLastConnected = obj.vpnLastConnected;
SessionData.vpnLastConnected = _legacyVpnLastConnected;
SessionData.saveSettings();
}
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
_hasLoaded = true; _hasLoaded = true;
@@ -2311,6 +2316,11 @@ Singleton {
_legacyWeatherLocation = obj.weatherLocation; _legacyWeatherLocation = obj.weatherLocation;
if (obj.weatherCoordinates !== undefined) if (obj.weatherCoordinates !== undefined)
_legacyWeatherCoordinates = obj.weatherCoordinates; _legacyWeatherCoordinates = obj.weatherCoordinates;
if (obj.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
_legacyVpnLastConnected = obj.vpnLastConnected;
SessionData.vpnLastConnected = _legacyVpnLastConnected;
SessionData.saveSettings();
}
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
_hasLoaded = true; _hasLoaded = true;

View File

@@ -94,6 +94,9 @@ Singleton {
property var matugenColors: ({}) property var matugenColors: ({})
property var _pendingGenerateParams: null property var _pendingGenerateParams: null
property bool themeModeAutomationActive: false
property bool dmsServiceWasDisconnected: true
readonly property var dank16: { readonly property var dank16: {
const raw = matugenColors?.dank16; const raw = matugenColors?.dank16;
if (!raw) if (!raw)
@@ -176,6 +179,237 @@ Singleton {
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) { if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
switchTheme(SettingsData.currentThemeName, false, false); switchTheme(SettingsData.currentThemeName, false, false);
} }
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
enabled: typeof DMSService !== "undefined" && typeof SessionData !== "undefined"
function onLoginctlEvent(event) {
if (!SessionData.themeModeAutoEnabled) return;
if (event.event === "unlock" || event.event === "resume") {
if (!themeAutoBackendAvailable()) {
root.evaluateThemeMode();
return;
}
DMSService.sendRequest("theme.auto.trigger", {});
}
}
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) {
console.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) {
console.warn("Theme automation: Failed to set location", locationResponse.error);
}
});
}
});
} else {
console.warn("Theme automation: No location configured");
}
}
}
} }
function applyGreeterTheme(themeName) { function applyGreeterTheme(themeName) {
@@ -491,7 +725,9 @@ Singleton {
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0 property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
function screenTransition() { function screenTransition() {
CompositorService.isNiri && NiriService.doScreenTransition(); if (CompositorService.isNiri) {
NiriService.doScreenTransition();
}
} }
function switchTheme(themeName, savePrefs = true, enableTransition = true) { function switchTheme(themeName, savePrefs = true, enableTransition = true) {
@@ -543,8 +779,10 @@ Singleton {
} }
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) {
SessionData.setLightMode(light); SessionData.setLightMode(light);
}
if (!isGreeterMode) { if (!isGreeterMode) {
// Skip with matugen because, our script runner will do it. // Skip with matugen because, our script runner will do it.
if (!matugenAvailable) { if (!matugenAvailable) {
@@ -552,6 +790,7 @@ Singleton {
} }
generateSystemThemesFromCurrentTheme(); generateSystemThemesFromCurrentTheme();
} }
} }
function toggleLightMode(savePrefs = true) { function toggleLightMode(savePrefs = true) {
@@ -1233,7 +1472,7 @@ Singleton {
return `#${invR}${invG}${invB}`; return `#${invR}${invG}${invB}`;
} }
property string baseLogoColor: { property var baseLogoColor: {
if (typeof SettingsData === "undefined") if (typeof SettingsData === "undefined")
return ""; return "";
const colorOverride = SettingsData.launcherLogoColorOverride; const colorOverride = SettingsData.launcherLogoColorOverride;
@@ -1246,7 +1485,7 @@ Singleton {
return colorOverride; return colorOverride;
} }
property string effectiveLogoColor: { property var effectiveLogoColor: {
if (typeof SettingsData === "undefined") if (typeof SettingsData === "undefined")
return ""; return "";
@@ -1453,4 +1692,297 @@ Singleton {
root.switchTheme(defaultTheme, true, false); 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) {
console.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) {
console.warn("Theme automation: Waiting for IP location from backend");
} else {
console.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) {
console.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) {
console.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});
}
}
} }

View File

@@ -35,6 +35,14 @@ var SPEC = {
nightModeUseIPLocation: { def: false }, nightModeUseIPLocation: { def: false },
nightModeLocationProvider: { def: "" }, nightModeLocationProvider: { def: "" },
themeModeAutoEnabled: { def: false },
themeModeAutoMode: { def: "time" },
themeModeStartHour: { def: 18 },
themeModeStartMinute: { def: 0 },
themeModeEndHour: { def: 6 },
themeModeEndMinute: { def: 0 },
themeModeShareGammaSettings: { def: true },
weatherLocation: { def: "New York, NY" }, weatherLocation: { def: "New York, NY" },
weatherCoordinates: { def: "40.7128,-74.0060" }, weatherCoordinates: { def: "40.7128,-74.0060" },
@@ -61,7 +69,9 @@ var SPEC = {
hiddenApps: { def: [] }, hiddenApps: { def: [] },
appOverrides: { def: {} }, appOverrides: { def: {} },
searchAppActions: { def: true } searchAppActions: { def: true },
vpnLastConnected: { def: "" }
}; };
function getValidKeys() { function getValidKeys() {

View File

@@ -1,6 +1,6 @@
.pragma library .pragma library
.import "./SessionSpec.js" as SpecModule .import "./SessionSpec.js" as SpecModule
function parse(root, jsonObj) { function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC; var SPEC = SpecModule.SPEC;
@@ -68,6 +68,11 @@ function migrateToVersion(obj, targetVersion, settingsData) {
session.configVersion = 2; session.configVersion = 2;
} }
if (currentVersion < 3) {
console.info("SessionData: Migrating session to version 3");
session.configVersion = 3;
}
return session; return session;
} }

View File

@@ -79,16 +79,18 @@ var SPEC = {
privacyShowCameraIcon: { def: false }, privacyShowCameraIcon: { def: false },
privacyShowScreenShareIcon: { def: false }, privacyShowScreenShareIcon: { def: false },
controlCenterWidgets: { def: [ controlCenterWidgets: {
{ id: "volumeSlider", enabled: true, width: 50 }, def: [
{ id: "brightnessSlider", enabled: true, width: 50 }, { id: "volumeSlider", enabled: true, width: 50 },
{ id: "wifi", enabled: true, width: 50 }, { id: "brightnessSlider", enabled: true, width: 50 },
{ id: "bluetooth", enabled: true, width: 50 }, { id: "wifi", enabled: true, width: 50 },
{ id: "audioOutput", enabled: true, width: 50 }, { id: "bluetooth", enabled: true, width: 50 },
{ id: "audioInput", enabled: true, width: 50 }, { id: "audioOutput", enabled: true, width: 50 },
{ id: "nightMode", enabled: true, width: 50 }, { id: "audioInput", enabled: true, width: 50 },
{ id: "darkMode", enabled: true, width: 50 } { id: "nightMode", enabled: true, width: 50 },
]}, { id: "darkMode", enabled: true, width: 50 }
]
},
showWorkspaceIndex: { def: false }, showWorkspaceIndex: { def: false },
showWorkspaceName: { def: false }, showWorkspaceName: { def: false },
@@ -119,13 +121,15 @@ var SPEC = {
keyboardLayoutNameCompactMode: { def: false }, keyboardLayoutNameCompactMode: { def: false },
runningAppsCurrentWorkspace: { def: false }, runningAppsCurrentWorkspace: { def: false },
runningAppsGroupByApp: { def: false }, runningAppsGroupByApp: { def: false },
appIdSubstitutions: { def: [ appIdSubstitutions: {
{ pattern: "Spotify", replacement: "spotify", type: "exact" }, def: [
{ pattern: "beepertexts", replacement: "beeper", type: "exact" }, { pattern: "Spotify", replacement: "spotify", type: "exact" },
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" }, { pattern: "beepertexts", replacement: "beeper", type: "exact" },
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" }, { pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" } { pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
]}, { pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
]
},
centeringMode: { def: "index" }, centeringMode: { def: "index" },
clockDateFormat: { def: "" }, clockDateFormat: { def: "" },
lockDateFormat: { def: "" }, lockDateFormat: { def: "" },
@@ -153,7 +157,6 @@ var SPEC = {
weatherEnabled: { def: true }, weatherEnabled: { def: true },
networkPreference: { def: "auto" }, networkPreference: { def: "auto" },
vpnLastConnected: { def: "" },
iconTheme: { def: "System Default", onChange: "applyStoredIconTheme" }, iconTheme: { def: "System Default", onChange: "applyStoredIconTheme" },
availableIconThemes: { def: ["System Default"], persist: false }, availableIconThemes: { def: ["System Default"], persist: false },
@@ -306,7 +309,7 @@ var SPEC = {
osdAlwaysShowValue: { def: false }, osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 }, osdPosition: { def: 5 },
osdVolumeEnabled: { def: true }, osdVolumeEnabled: { def: true },
osdMediaVolumeEnabled : { def: true }, osdMediaVolumeEnabled: { def: true },
osdBrightnessEnabled: { def: true }, osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true }, osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true }, osdMicMuteEnabled: { def: true },
@@ -337,52 +340,54 @@ var SPEC = {
niriOutputSettings: { def: {} }, niriOutputSettings: { def: {} },
hyprlandOutputSettings: { def: {} }, hyprlandOutputSettings: { def: {} },
barConfigs: { def: [{ barConfigs: {
id: "default", def: [{
name: "Main Bar", id: "default",
enabled: true, name: "Main Bar",
position: 0, enabled: true,
screenPreferences: ["all"], position: 0,
showOnLastDisplay: true, screenPreferences: ["all"],
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"], showOnLastDisplay: true,
centerWidgets: ["music", "clock", "weather"], leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"], centerWidgets: ["music", "clock", "weather"],
spacing: 4, rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
innerPadding: 4, spacing: 4,
bottomGap: 0, innerPadding: 4,
transparency: 1.0, bottomGap: 0,
widgetTransparency: 1.0, transparency: 1.0,
squareCorners: false, widgetTransparency: 1.0,
noBackground: false, squareCorners: false,
gothCornersEnabled: false, noBackground: false,
gothCornerRadiusOverride: false, gothCornersEnabled: false,
gothCornerRadiusValue: 12, gothCornerRadiusOverride: false,
borderEnabled: false, gothCornerRadiusValue: 12,
borderColor: "surfaceText", borderEnabled: false,
borderOpacity: 1.0, borderColor: "surfaceText",
borderThickness: 1, borderOpacity: 1.0,
widgetOutlineEnabled: false, borderThickness: 1,
widgetOutlineColor: "primary", widgetOutlineEnabled: false,
widgetOutlineOpacity: 1.0, widgetOutlineColor: "primary",
widgetOutlineThickness: 1, widgetOutlineOpacity: 1.0,
fontScale: 1.0, widgetOutlineThickness: 1,
autoHide: false, fontScale: 1.0,
autoHideDelay: 250, autoHide: false,
showOnWindowsOpen: false, autoHideDelay: 250,
openOnOverview: false, showOnWindowsOpen: false,
visible: true, openOnOverview: false,
popupGapsAuto: true, visible: true,
popupGapsManual: 4, popupGapsAuto: true,
maximizeDetection: true, popupGapsManual: 4,
scrollEnabled: true, maximizeDetection: true,
scrollXBehavior: "column", scrollEnabled: true,
scrollYBehavior: "workspace", scrollXBehavior: "column",
shadowIntensity: 0, scrollYBehavior: "workspace",
shadowOpacity: 60, shadowIntensity: 0,
shadowColorMode: "text", shadowOpacity: 60,
shadowCustomColor: "#000000", shadowColorMode: "text",
clickThrough: false shadowCustomColor: "#000000",
}], onChange: "updateBarConfigs" }, clickThrough: false
}], onChange: "updateBarConfigs"
},
desktopClockEnabled: { def: false }, desktopClockEnabled: { def: false },
desktopClockStyle: { def: "analog" }, desktopClockStyle: { def: "analog" },
@@ -437,7 +442,7 @@ var SPEC = {
}; };
function getValidKeys() { function getValidKeys() {
return Object.keys(SPEC).filter(function(k) { return SPEC[k].persist !== false; }).concat(["configVersion"]); return Object.keys(SPEC).filter(function (k) { return SPEC[k].persist !== false; }).concat(["configVersion"]);
} }
function set(root, key, value, saveFn, hooks) { function set(root, key, value, saveFn, hooks) {

View File

@@ -1,6 +1,6 @@
.pragma library .pragma library
.import "./SettingsSpec.js" as SpecModule .import "./SettingsSpec.js" as SpecModule
function parse(root, jsonObj) { function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC; var SPEC = SpecModule.SPEC;

View File

@@ -1114,6 +1114,79 @@ Item {
target: "spotlight" target: "spotlight"
} }
IpcHandler {
function info(message: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showInfo(message);
return "TOAST_INFO_SUCCESS";
}
function infoWith(message: string, details: string, command: string, category: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showInfo(message, details, command, category);
return "TOAST_INFO_SUCCESS";
}
function warn(message: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showWarning(message);
return "TOAST_WARN_SUCCESS";
}
function warnWith(message: string, details: string, command: string, category: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showWarning(message, details, command, category);
return "TOAST_WARN_SUCCESS";
}
function error(message: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showError(message);
return "TOAST_ERROR_SUCCESS";
}
function errorWith(message: string, details: string, command: string, category: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showError(message, details, command, category);
return "TOAST_ERROR_SUCCESS";
}
function hide(): string {
ToastService.hideToast();
return "TOAST_HIDE_SUCCESS";
}
function dismiss(category: string): string {
if (!category)
return "ERROR: No category specified";
ToastService.dismissCategory(category);
return "TOAST_DISMISS_SUCCESS";
}
function status(): string {
if (!ToastService.toastVisible)
return "hidden";
const levels = ["info", "warn", "error"];
return `visible:${levels[ToastService.currentLevel]}:${ToastService.currentMessage}`;
}
target: "toast"
}
IpcHandler { IpcHandler {
function open(): string { function open(): string {
FirstLaunchService.showWelcome(); FirstLaunchService.showWelcome();

View File

@@ -48,9 +48,14 @@ Item {
Connections { Connections {
target: PluginService target: PluginService
function onRequestLauncherUpdate(pluginId) { function onRequestLauncherUpdate(pluginId) {
if (activePluginId === pluginId || searchQuery) { if (activePluginId === pluginId) {
if (activePluginCategories.length <= 1)
loadPluginCategories(pluginId);
performSearch(); performSearch();
return;
} }
if (searchQuery)
performSearch();
} }
} }
@@ -133,6 +138,8 @@ Item {
property string pluginFilter: "" property string pluginFilter: ""
property string activePluginName: "" property string activePluginName: ""
property var activePluginCategories: []
property string activePluginCategory: ""
function getSectionViewMode(sectionId) { function getSectionViewMode(sectionId) {
if (sectionId === "browse_plugins") if (sectionId === "browse_plugins")
@@ -307,10 +314,33 @@ Item {
isSearching = false; isSearching = false;
activePluginId = ""; activePluginId = "";
activePluginName = ""; activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
pluginFilter = ""; pluginFilter = "";
collapsedSections = {}; collapsedSections = {};
} }
function loadPluginCategories(pluginId) {
if (!pluginId) {
activePluginCategories = [];
activePluginCategory = "";
return;
}
const categories = AppSearchService.getPluginLauncherCategories(pluginId);
activePluginCategories = categories;
activePluginCategory = "";
AppSearchService.setPluginLauncherCategory(pluginId, "");
}
function setActivePluginCategory(categoryId) {
if (activePluginCategory === categoryId)
return;
activePluginCategory = categoryId;
AppSearchService.setPluginLauncherCategory(activePluginId, categoryId);
performSearch();
}
function clearPluginFilter() { function clearPluginFilter() {
if (pluginFilter) { if (pluginFilter) {
pluginFilter = ""; pluginFilter = "";
@@ -342,6 +372,8 @@ Item {
if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) { if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) {
activePluginId = ""; activePluginId = "";
activePluginName = ""; activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
clearActivePluginViewPreference(); clearActivePluginViewPreference();
sections = cachedSections.map(function (s) { sections = cachedSections.map(function (s) {
var copy = Object.assign({}, s, { var copy = Object.assign({}, s, {
@@ -363,10 +395,14 @@ Item {
var triggerMatch = detectTrigger(searchQuery); var triggerMatch = detectTrigger(searchQuery);
if (triggerMatch.pluginId) { if (triggerMatch.pluginId) {
var pluginChanged = activePluginId !== triggerMatch.pluginId;
activePluginId = triggerMatch.pluginId; activePluginId = triggerMatch.pluginId;
activePluginName = getPluginName(triggerMatch.pluginId, triggerMatch.isBuiltIn); activePluginName = getPluginName(triggerMatch.pluginId, triggerMatch.isBuiltIn);
applyActivePluginViewPreference(triggerMatch.pluginId, triggerMatch.isBuiltIn); applyActivePluginViewPreference(triggerMatch.pluginId, triggerMatch.isBuiltIn);
if (pluginChanged && !triggerMatch.isBuiltIn)
loadPluginCategories(triggerMatch.pluginId);
var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query); var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query);
allItems = allItems.concat(pluginItems); allItems = allItems.concat(pluginItems);
@@ -401,6 +437,8 @@ Item {
activePluginId = ""; activePluginId = "";
activePluginName = ""; activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
clearActivePluginViewPreference(); clearActivePluginViewPreference();
if (searchMode === "files") { if (searchMode === "files") {

View File

@@ -483,9 +483,64 @@ FocusScope {
} }
} }
Row {
id: categoryRow
width: parent.width
height: controller.activePluginCategories.length > 0 ? 36 : 0
visible: controller.activePluginCategories.length > 0
spacing: Theme.spacingS
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
DankDropdown {
id: categoryDropdown
width: Math.min(200, parent.width)
compactMode: true
dropdownWidth: 200
popupWidth: 240
maxPopupHeight: 300
enableFuzzySearch: controller.activePluginCategories.length > 8
currentValue: {
const cats = controller.activePluginCategories;
const current = controller.activePluginCategory;
if (!current)
return cats.length > 0 ? cats[0].name : "";
for (let i = 0; i < cats.length; i++) {
if (cats[i].id === current)
return cats[i].name;
}
return cats.length > 0 ? cats[0].name : "";
}
options: {
const cats = controller.activePluginCategories;
const names = [];
for (let i = 0; i < cats.length; i++)
names.push(cats[i].name);
return names;
}
onValueChanged: value => {
const cats = controller.activePluginCategories;
for (let i = 0; i < cats.length; i++) {
if (cats[i].name === value) {
controller.setActivePluginCategory(cats[i].id);
return;
}
}
}
}
}
Item { Item {
width: parent.width width: parent.width
height: parent.height - searchField.height - actionPanel.height - Theme.spacingXS * 2 height: parent.height - searchField.height - categoryRow.height - actionPanel.height - Theme.spacingXS * (categoryRow.visible ? 3 : 2)
opacity: root.parentModal?.isClosing ? 0 : 1 opacity: root.parentModal?.isClosing ? 0 : 1
ResultsList { ResultsList {

View File

@@ -508,18 +508,17 @@ Item {
function getRealWorkspaces() { function getRealWorkspaces() {
return root.workspaceList.filter(ws => { return root.workspaceList.filter(ws => {
if (useExtWorkspace) if (useExtWorkspace)
return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden; return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden;
if (CompositorService.isNiri) if (CompositorService.isNiri)
return ws && ws.idx !== -1; return ws && ws.idx !== -1;
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return ws && ws.id !== -1; return ws && ws.id !== -1;
if (CompositorService.isDwl) if (CompositorService.isDwl)
return ws && ws.tag !== -1; return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll)
return ws && ws.num !== -1; return ws && ws.num !== -1;
return ws !== -1; return ws !== -1;
}); });
} }
@@ -864,6 +863,60 @@ Item {
property bool loadedHasIcon: false property bool loadedHasIcon: false
property var loadedIcons: [] property var loadedIcons: []
readonly property int stableIconCount: {
if (!SettingsData.showWorkspaceApps || isPlaceholder)
return 0;
let targetWorkspaceId;
if (root.useExtWorkspace) {
targetWorkspaceId = modelData?.id || modelData?.name;
} else if (CompositorService.isNiri) {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isHyprland) {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isDwl) {
targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) {
targetWorkspaceId = modelData?.num;
}
if (targetWorkspaceId === undefined || targetWorkspaceId === null)
return 0;
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels;
const seen = {};
let groupedCount = 0;
let totalCount = 0;
for (let i = 0; i < wins.length; i++) {
const w = wins[i];
if (!w)
continue;
let winWs = null;
if (CompositorService.isNiri) {
winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) {
winWs = w.workspace?.num;
} else if (CompositorService.isHyprland) {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w);
winWs = hyprToplevel?.workspace?.id;
}
if (winWs !== targetWorkspaceId)
continue;
totalCount++;
const appKey = w.app_id || w.appId || w.class || w.windowClass || "unknown";
if (!seen[appKey]) {
seen[appKey] = true;
groupedCount++;
}
}
return SettingsData.groupWorkspaceApps ? groupedCount : totalCount;
}
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5)
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== "" readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
@@ -872,27 +925,29 @@ Item {
readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0 readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0
readonly property real iconsExtraWidth: { readonly property real iconsExtraWidth: {
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) { if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons); const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0); return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
} }
return 0; return 0;
} }
readonly property real iconsExtraHeight: { readonly property real iconsExtraHeight: {
if (root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) { if (root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons); const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0); return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
} }
return 0; return 0;
} }
readonly property real visualWidth: { readonly property real visualWidth: {
if (contentImplicitWidth <= 0) return baseWidth + iconsExtraWidth; if (contentImplicitWidth <= 0)
return baseWidth + iconsExtraWidth;
const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS; const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS;
return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding); return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding);
} }
readonly property real visualHeight: { readonly property real visualHeight: {
if (contentImplicitHeight <= 0) return baseHeight + iconsExtraHeight; if (contentImplicitHeight <= 0)
return baseHeight + iconsExtraHeight;
const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS; const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS;
return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding); return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding);
} }
@@ -1080,6 +1135,20 @@ Item {
width: root.isVertical ? root.widgetHeight : visualWidth width: root.isVertical ? root.widgetHeight : visualWidth
height: root.isVertical ? visualHeight : root.widgetHeight height: root.isVertical ? visualHeight : root.widgetHeight
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Rectangle { Rectangle {
id: focusedBorderRing id: focusedBorderRing
x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2 x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
@@ -1349,6 +1418,15 @@ Item {
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
} }
StyledText {
visible: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon
anchors.horizontalCenter: parent.horizontalCenter
text: root.getWorkspaceIndex(modelData, index)
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons) values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)

View File

@@ -125,6 +125,19 @@ Item {
return Theme.warning; return Theme.warning;
} }
function formatThemeAutoTime(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 "";
}
}
Component.onCompleted: { Component.onCompleted: {
SettingsData.detectAvailableIconThemes(); SettingsData.detectAvailableIconThemes();
SettingsData.detectAvailableCursorThemes(); SettingsData.detectAvailableCursorThemes();
@@ -152,7 +165,6 @@ Item {
} }
themeColorsTab.templateDetection = detection; themeColorsTab.templateDetection = detection;
} catch (e) { } catch (e) {
console.warn("ThemeColorsTab: Failed to parse template check:", e);
} }
} }
} }
@@ -962,6 +974,453 @@ Item {
} }
} }
SettingsCard {
tab: "theme"
tags: ["automatic", "color", "mode", "schedule", "sunrise", "sunset"]
title: I18n.tr("Automatic Color Mode")
settingKey: "automaticColorMode"
iconName: "schedule"
Column {
width: parent.width
spacing: Theme.spacingM
DankToggle {
id: themeModeAutoToggle
width: parent.width
text: I18n.tr("Enable Automatic Switching")
description: I18n.tr("Automatically switch between light and dark modes based on time or sunrise/sunset")
checked: SessionData.themeModeAutoEnabled
onToggled: checked => {
SessionData.setThemeModeAutoEnabled(checked);
}
Connections {
target: SessionData
function onThemeModeAutoEnabledChanged() {
themeModeAutoToggle.checked = SessionData.themeModeAutoEnabled;
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.themeModeAutoEnabled
DankToggle {
width: parent.width
text: I18n.tr("Share Gamma Control Settings")
description: I18n.tr("Use the same time and location settings as gamma control")
checked: SessionData.themeModeShareGammaSettings
onToggled: checked => {
SessionData.setThemeModeShareGammaSettings(checked);
}
}
Item {
width: parent.width
height: 45 + Theme.spacingM
DankTabBar {
id: themeModeTabBar
width: 200
height: 45
anchors.horizontalCenter: parent.horizontalCenter
model: [
{ "text": "Time", "icon": "access_time" },
{ "text": "Location", "icon": "place" }
]
Component.onCompleted: {
currentIndex = SessionData.themeModeAutoMode === "location" ? 1 : 0;
Qt.callLater(updateIndicator);
}
onTabClicked: index => {
SessionData.setThemeModeAutoMode(index === 1 ? "location" : "time");
currentIndex = index;
}
Connections {
target: SessionData
function onThemeModeAutoModeChanged() {
themeModeTabBar.currentIndex = SessionData.themeModeAutoMode === "location" ? 1 : 0;
Qt.callLater(themeModeTabBar.updateIndicator);
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.themeModeAutoMode === "time" && !SessionData.themeModeShareGammaSettings
Column {
spacing: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingM
StyledText {
text: ""
width: 80
height: 20
}
StyledText {
text: I18n.tr("Hour")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 70
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: I18n.tr("Minute")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 70
horizontalAlignment: Text.AlignHCenter
}
}
Row {
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Dark Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 80
height: 40
verticalAlignment: Text.AlignVCenter
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeStartHour.toString()
options: {
var hours = [];
for (var i = 0; i < 24; i++) hours.push(i.toString());
return hours;
}
onValueChanged: value => {
SessionData.setThemeModeStartHour(parseInt(value));
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeStartMinute.toString().padStart(2, '0')
options: {
var minutes = [];
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'));
}
return minutes;
}
onValueChanged: value => {
SessionData.setThemeModeStartMinute(parseInt(value));
}
}
}
Row {
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Light Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 80
height: 40
verticalAlignment: Text.AlignVCenter
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeEndHour.toString()
options: {
var hours = [];
for (var i = 0; i < 24; i++) hours.push(i.toString());
return hours;
}
onValueChanged: value => {
SessionData.setThemeModeEndHour(parseInt(value));
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeEndMinute.toString().padStart(2, '0')
options: {
var minutes = [];
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'));
}
return minutes;
}
onValueChanged: value => {
SessionData.setThemeModeEndMinute(parseInt(value));
}
}
}
}
StyledText {
text: SessionData.isLightMode ? I18n.tr("Light mode will be active from Light Start to Dark Start") : I18n.tr("Dark mode will be active from Dark Start to Light Start")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.themeModeAutoMode === "location" && !SessionData.themeModeShareGammaSettings
DankToggle {
id: themeModeIpLocationToggle
width: parent.width
text: I18n.tr("Use IP Location")
description: I18n.tr("Automatically detect location based on IP address")
checked: SessionData.nightModeUseIPLocation || false
onToggled: checked => {
SessionData.setNightModeUseIPLocation(checked);
}
Connections {
target: SessionData
function onNightModeUseIPLocationChanged() {
themeModeIpLocationToggle.checked = SessionData.nightModeUseIPLocation;
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: !SessionData.nightModeUseIPLocation
StyledText {
text: I18n.tr("Manual Coordinates")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
Row {
spacing: Theme.spacingL
anchors.horizontalCenter: parent.horizontalCenter
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Latitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.latitude.toString()
placeholderText: "0.0"
onEditingFinished: {
const lat = parseFloat(text);
if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) {
SessionData.setLatitude(lat);
}
}
}
}
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Longitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.longitude.toString()
placeholderText: "0.0"
onEditingFinished: {
const lon = parseFloat(text);
if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) {
SessionData.setLongitude(lon);
}
}
}
}
}
StyledText {
text: I18n.tr("Uses sunrise/sunset times to automatically adjust theme mode based on your location.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
StyledText {
text: SessionData.isLightMode ? I18n.tr("Light mode will be active from sunrise to sunset") : I18n.tr("Dark mode will be active from sunset to sunrise")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
visible: SessionData.nightModeUseIPLocation || (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0)
}
}
StyledText {
width: parent.width
text: I18n.tr("Using shared settings from Gamma Control")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
visible: SessionData.themeModeShareGammaSettings
}
Rectangle {
width: parent.width
height: statusRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Row {
id: statusRow
anchors.centerIn: parent
spacing: Theme.spacingL
width: parent.width - Theme.spacingM * 2
Column {
spacing: 2
width: (parent.width - Theme.spacingL * 2) / 3
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: 8
height: 8
radius: 4
color: SessionData.themeModeAutoEnabled ? Theme.success : Theme.error
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Automation")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
}
StyledText {
text: SessionData.themeModeAutoEnabled ? I18n.tr("Enabled") : I18n.tr("Disabled")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
Column {
spacing: 2
width: (parent.width - Theme.spacingL * 2) / 3
anchors.verticalCenter: parent.verticalCenter
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingS
DankIcon {
name: SessionData.isLightMode ? "light_mode" : "dark_mode"
size: Theme.iconSizeMedium
color: SessionData.isLightMode ? "#FFA726" : "#7E57C2"
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: SessionData.isLightMode ? I18n.tr("Light Mode") : I18n.tr("Dark Mode")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: I18n.tr("Active")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
Column {
spacing: 2
width: (parent.width - Theme.spacingL * 2) / 3
anchors.verticalCenter: parent.verticalCenter
visible: SessionData.themeModeAutoEnabled && SessionData.themeModeNextTransition
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "schedule"
size: Theme.iconSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Next Transition")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: themeColorsTab.formatThemeAutoTime(SessionData.themeModeNextTransition)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
}
}
}
}
}
SettingsCard { SettingsCard {
tab: "theme" tab: "theme"
tags: ["light", "dark", "mode", "appearance"] tags: ["light", "dark", "mode", "appearance"]

View File

@@ -884,4 +884,52 @@ Singleton {
return allItems; return allItems;
} }
function getPluginLauncherCategories(pluginId) {
if (typeof PluginService === "undefined")
return [];
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return [];
if (typeof instance.getCategories !== "function")
return [];
try {
return instance.getCategories() || [];
} catch (e) {
console.warn("AppSearchService: Error getting categories from plugin", pluginId, ":", e);
return [];
}
}
function setPluginLauncherCategory(pluginId, categoryId) {
if (typeof PluginService === "undefined")
return;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return;
if (typeof instance.setCategory !== "function")
return;
try {
instance.setCategory(categoryId);
} catch (e) {
console.warn("AppSearchService: Error setting category on plugin", pluginId, ":", e);
}
}
function pluginHasCategories(pluginId) {
if (typeof PluginService === "undefined")
return false;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return false;
return typeof instance.getCategories === "function";
}
} }

View File

@@ -130,7 +130,7 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
root.userPreference = SettingsData.networkPreference; root.userPreference = SettingsData.networkPreference;
lastConnectedVpnUuid = SettingsData.vpnLastConnected || ""; lastConnectedVpnUuid = SessionData.vpnLastConnected || "";
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
checkDMSCapabilities(); checkDMSCapabilities();
} }
@@ -293,7 +293,7 @@ Singleton {
if (vpnConnected && activeUuid) { if (vpnConnected && activeUuid) {
lastConnectedVpnUuid = activeUuid; lastConnectedVpnUuid = activeUuid;
SettingsData.set("vpnLastConnected", activeUuid); SessionData.setVpnLastConnected(activeUuid);
} }
if (vpnIsBusy) { if (vpnIsBusy) {

View File

@@ -56,6 +56,7 @@ Singleton {
signal wlrOutputStateUpdate(var data) signal wlrOutputStateUpdate(var data)
signal evdevStateUpdate(var data) signal evdevStateUpdate(var data)
signal gammaStateUpdate(var data) signal gammaStateUpdate(var data)
signal themeAutoStateUpdate(var data)
signal openUrlRequested(string url) signal openUrlRequested(string url)
signal appPickerRequested(var data) signal appPickerRequested(var data)
signal screensaverStateUpdate(var data) signal screensaverStateUpdate(var data)
@@ -64,7 +65,7 @@ Singleton {
property bool screensaverInhibited: false property bool screensaverInhibited: false
property var screensaverInhibitors: [] property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus"] property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus"]
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
@@ -304,7 +305,7 @@ Singleton {
excludeServices = [excludeServices]; excludeServices = [excludeServices];
} }
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"]; const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"];
const filtered = allServices.filter(s => !excludeServices.includes(s)); const filtered = allServices.filter(s => !excludeServices.includes(s));
subscribe(filtered); subscribe(filtered);
} }
@@ -373,6 +374,8 @@ Singleton {
evdevStateUpdate(data); evdevStateUpdate(data);
} else if (service === "gamma") { } else if (service === "gamma") {
gammaStateUpdate(data); gammaStateUpdate(data);
} else if (service === "theme.auto") {
themeAutoStateUpdate(data);
} else if (service === "browser.open_requested") { } else if (service === "browser.open_requested") {
if (data.target) { if (data.target) {
if (data.requestType === "url" || !data.requestType) { if (data.requestType === "url" || !data.requestType) {

View File

@@ -396,7 +396,6 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
root.currentValue = delegateRoot.modelData;
root.valueChanged(delegateRoot.modelData); root.valueChanged(delegateRoot.modelData);
dropdownMenu.close(); dropdownMenu.close();
} }