mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
10 Commits
816819bf9f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80025804ab | ||
|
|
028d3b4e61 | ||
|
|
9cce5ccfe6 | ||
|
|
a260b8060e | ||
|
|
f945307232 | ||
|
|
8f44d52cb2 | ||
|
|
3413cb7b89 | ||
|
|
4e3b24ffbb | ||
|
|
03cfa55e0b | ||
|
|
a887e60f40 |
@@ -502,17 +502,17 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
||||
p.dmsProcessed = true
|
||||
}
|
||||
|
||||
fullPath := sourcePath
|
||||
if !filepath.IsAbs(sourcePath) {
|
||||
fullPath = filepath.Join(baseDir, sourcePath)
|
||||
}
|
||||
|
||||
expanded, err := utils.ExpandPath(fullPath)
|
||||
expanded, err := utils.ExpandPath(sourcePath)
|
||||
if err != nil {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@@ -521,33 +521,10 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
|
||||
data, err := os.ReadFile(dmsBindsPath)
|
||||
keybinds, err := p.parseFileWithSource(dmsBindsPath)
|
||||
if err != 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
|
||||
return keybinds
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
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"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||
@@ -44,6 +45,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
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 loginctlManager == nil {
|
||||
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"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/wlcontext"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||
@@ -68,6 +69,7 @@ var evdevManager *evdev.Manager
|
||||
var clipboardManager *clipboard.Manager
|
||||
var dbusManager *serverDbus.Manager
|
||||
var wlContext *wlcontext.SharedContext
|
||||
var themeModeManager *thememode.Manager
|
||||
|
||||
const dbusClientID = "dms-dbus-client"
|
||||
|
||||
@@ -380,6 +382,14 @@ func InitializeDbusManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeThemeModeManager() error {
|
||||
manager := thememode.NewManager()
|
||||
themeModeManager = manager
|
||||
|
||||
log.Info("Theme mode automation manager initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -457,6 +467,10 @@ func getCapabilities() Capabilities {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
if themeModeManager != nil {
|
||||
caps = append(caps, "theme.auto")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
@@ -519,6 +533,10 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "clipboard")
|
||||
}
|
||||
|
||||
if themeModeManager != nil {
|
||||
caps = append(caps, "theme.auto")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
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 {
|
||||
wg.Add(1)
|
||||
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
|
||||
@@ -1251,6 +1301,9 @@ func cleanupManagers() {
|
||||
if dbusManager != nil {
|
||||
dbusManager.Close()
|
||||
}
|
||||
if themeModeManager != nil {
|
||||
themeModeManager.Close()
|
||||
}
|
||||
if wlContext != nil {
|
||||
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.setEnabled - Enable/disable gamma control (params: enabled)")
|
||||
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.getState - Get current bluetooth state")
|
||||
log.Info(" bluetooth.startDiscovery - Start device discovery")
|
||||
@@ -1503,6 +1565,12 @@ func Start(printDocs bool) error {
|
||||
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)
|
||||
if wlrOutputManager != nil {
|
||||
go func() {
|
||||
|
||||
154
core/internal/server/thememode/handlers.go
Normal file
154
core/internal/server/thememode/handlers.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
432
core/internal/server/thememode/manager.go
Normal file
432
core/internal/server/thememode/manager.go
Normal 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")
|
||||
23
core/internal/server/thememode/types.go
Normal file
23
core/internal/server/thememode/types.go
Normal 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"`
|
||||
}
|
||||
@@ -626,6 +626,7 @@ func (m *Manager) schedulerLoop() {
|
||||
m.schedule.calcDay = time.Time{}
|
||||
m.scheduleMutex.Unlock()
|
||||
m.recalcSchedule(time.Now())
|
||||
m.updateStateFromSchedule()
|
||||
m.configMutex.RLock()
|
||||
enabled := m.config.Enabled
|
||||
m.configMutex.RUnlock()
|
||||
|
||||
@@ -13,7 +13,7 @@ import "settings/SessionStore.js" as Store
|
||||
Singleton {
|
||||
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"
|
||||
property bool _parseError: false
|
||||
@@ -82,6 +82,15 @@ Singleton {
|
||||
property bool nightModeUseIPLocation: false
|
||||
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 barPinnedApps: []
|
||||
property int dockLauncherPosition: 0
|
||||
@@ -109,6 +118,8 @@ Singleton {
|
||||
property var appOverrides: ({})
|
||||
property bool searchAppActions: true
|
||||
|
||||
property string vpnLastConnected: ""
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
loadSettings();
|
||||
@@ -172,7 +183,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -186,14 +197,10 @@ Singleton {
|
||||
_isReadOnly = !writable;
|
||||
if (_isReadOnly) {
|
||||
_hasUnsavedChanges = _checkForUnsavedChanges();
|
||||
if (!wasReadOnly)
|
||||
console.info("SessionData: session.json is now read-only");
|
||||
} else {
|
||||
_loadedSessionSnapshot = getCurrentSessionJson();
|
||||
_hasUnsavedChanges = false;
|
||||
if (wasReadOnly)
|
||||
console.info("SessionData: session.json is now writable");
|
||||
if (_pendingMigration)
|
||||
if (wasReadOnly && _pendingMigration)
|
||||
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
|
||||
}
|
||||
_pendingMigration = null;
|
||||
@@ -255,7 +262,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -273,7 +280,6 @@ Singleton {
|
||||
}
|
||||
|
||||
function migrateFromUndefinedToV1(settings) {
|
||||
console.info("SessionData: Migrating configuration from undefined to version 1");
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
if (settings.acMonitorTimeout !== undefined) {
|
||||
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout);
|
||||
@@ -448,7 +454,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
console.warn("SessionData: Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -545,7 +551,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
console.warn("SessionData: Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -583,7 +589,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
console.warn("SessionData: Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -621,7 +627,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
console.warn("SessionData: Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -659,7 +665,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found:", screenName);
|
||||
console.warn("SessionData: Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -702,7 +708,6 @@ Singleton {
|
||||
}
|
||||
|
||||
function setNightModeAutoEnabled(enabled) {
|
||||
console.log("SessionData: Setting nightModeAutoEnabled to", enabled);
|
||||
nightModeAutoEnabled = enabled;
|
||||
saveSettings();
|
||||
}
|
||||
@@ -738,13 +743,11 @@ Singleton {
|
||||
}
|
||||
|
||||
function setLatitude(lat) {
|
||||
console.log("SessionData: Setting latitude to", lat);
|
||||
latitude = lat;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setLongitude(lng) {
|
||||
console.log("SessionData: Setting longitude to", lng);
|
||||
longitude = lng;
|
||||
saveSettings();
|
||||
}
|
||||
@@ -754,6 +757,41 @@ Singleton {
|
||||
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) {
|
||||
pinnedApps = apps;
|
||||
saveSettings();
|
||||
@@ -1003,6 +1041,11 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setVpnLastConnected(uuid) {
|
||||
vpnLastConnected = uuid || "";
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncWallpaperForCurrentMode() {
|
||||
if (!perModeWallpaper)
|
||||
return;
|
||||
|
||||
@@ -292,13 +292,13 @@ Singleton {
|
||||
|
||||
property string _legacyWeatherLocation: "New York, NY"
|
||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||
property string _legacyVpnLastConnected: ""
|
||||
readonly property string weatherLocation: SessionData.weatherLocation
|
||||
readonly property string weatherCoordinates: SessionData.weatherCoordinates
|
||||
property bool useAutoLocation: false
|
||||
property bool weatherEnabled: true
|
||||
|
||||
property string networkPreference: "auto"
|
||||
property string vpnLastConnected: ""
|
||||
|
||||
property string iconTheme: "System Default"
|
||||
property var availableIconThemes: ["System Default"]
|
||||
@@ -1078,6 +1078,11 @@ Singleton {
|
||||
_legacyWeatherLocation = obj.weatherLocation;
|
||||
if (obj?.weatherCoordinates !== undefined)
|
||||
_legacyWeatherCoordinates = obj.weatherCoordinates;
|
||||
if (obj?.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
|
||||
_legacyVpnLastConnected = obj.vpnLastConnected;
|
||||
SessionData.vpnLastConnected = _legacyVpnLastConnected;
|
||||
SessionData.saveSettings();
|
||||
}
|
||||
|
||||
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
|
||||
_hasLoaded = true;
|
||||
@@ -2311,6 +2316,11 @@ Singleton {
|
||||
_legacyWeatherLocation = obj.weatherLocation;
|
||||
if (obj.weatherCoordinates !== undefined)
|
||||
_legacyWeatherCoordinates = obj.weatherCoordinates;
|
||||
if (obj.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
|
||||
_legacyVpnLastConnected = obj.vpnLastConnected;
|
||||
SessionData.vpnLastConnected = _legacyVpnLastConnected;
|
||||
SessionData.saveSettings();
|
||||
}
|
||||
|
||||
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
|
||||
_hasLoaded = true;
|
||||
|
||||
@@ -94,6 +94,9 @@ Singleton {
|
||||
property var matugenColors: ({})
|
||||
property var _pendingGenerateParams: null
|
||||
|
||||
property bool themeModeAutomationActive: false
|
||||
property bool dmsServiceWasDisconnected: true
|
||||
|
||||
readonly property var dank16: {
|
||||
const raw = matugenColors?.dank16;
|
||||
if (!raw)
|
||||
@@ -176,6 +179,237 @@ Singleton {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
||||
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) {
|
||||
@@ -491,7 +725,9 @@ Singleton {
|
||||
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
|
||||
|
||||
function screenTransition() {
|
||||
CompositorService.isNiri && NiriService.doScreenTransition();
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.doScreenTransition();
|
||||
}
|
||||
}
|
||||
|
||||
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
|
||||
@@ -543,8 +779,10 @@ Singleton {
|
||||
}
|
||||
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
||||
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
|
||||
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) {
|
||||
SessionData.setLightMode(light);
|
||||
}
|
||||
|
||||
if (!isGreeterMode) {
|
||||
// Skip with matugen because, our script runner will do it.
|
||||
if (!matugenAvailable) {
|
||||
@@ -552,6 +790,7 @@ Singleton {
|
||||
}
|
||||
generateSystemThemesFromCurrentTheme();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toggleLightMode(savePrefs = true) {
|
||||
@@ -1233,7 +1472,7 @@ Singleton {
|
||||
return `#${invR}${invG}${invB}`;
|
||||
}
|
||||
|
||||
property string baseLogoColor: {
|
||||
property var baseLogoColor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return "";
|
||||
const colorOverride = SettingsData.launcherLogoColorOverride;
|
||||
@@ -1246,7 +1485,7 @@ Singleton {
|
||||
return colorOverride;
|
||||
}
|
||||
|
||||
property string effectiveLogoColor: {
|
||||
property var effectiveLogoColor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return "";
|
||||
|
||||
@@ -1453,4 +1692,297 @@ 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) {
|
||||
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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@ var SPEC = {
|
||||
nightModeUseIPLocation: { def: false },
|
||||
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" },
|
||||
weatherCoordinates: { def: "40.7128,-74.0060" },
|
||||
|
||||
@@ -61,7 +69,9 @@ var SPEC = {
|
||||
|
||||
hiddenApps: { def: [] },
|
||||
appOverrides: { def: {} },
|
||||
searchAppActions: { def: true }
|
||||
searchAppActions: { def: true },
|
||||
|
||||
vpnLastConnected: { def: "" }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
|
||||
@@ -68,6 +68,11 @@ function migrateToVersion(obj, targetVersion, settingsData) {
|
||||
session.configVersion = 2;
|
||||
}
|
||||
|
||||
if (currentVersion < 3) {
|
||||
console.info("SessionData: Migrating session to version 3");
|
||||
session.configVersion = 3;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,8 @@ var SPEC = {
|
||||
privacyShowCameraIcon: { def: false },
|
||||
privacyShowScreenShareIcon: { def: false },
|
||||
|
||||
controlCenterWidgets: { def: [
|
||||
controlCenterWidgets: {
|
||||
def: [
|
||||
{ id: "volumeSlider", enabled: true, width: 50 },
|
||||
{ id: "brightnessSlider", enabled: true, width: 50 },
|
||||
{ id: "wifi", enabled: true, width: 50 },
|
||||
@@ -88,7 +89,8 @@ var SPEC = {
|
||||
{ id: "audioInput", enabled: true, width: 50 },
|
||||
{ id: "nightMode", enabled: true, width: 50 },
|
||||
{ id: "darkMode", enabled: true, width: 50 }
|
||||
]},
|
||||
]
|
||||
},
|
||||
|
||||
showWorkspaceIndex: { def: false },
|
||||
showWorkspaceName: { def: false },
|
||||
@@ -119,13 +121,15 @@ var SPEC = {
|
||||
keyboardLayoutNameCompactMode: { def: false },
|
||||
runningAppsCurrentWorkspace: { def: false },
|
||||
runningAppsGroupByApp: { def: false },
|
||||
appIdSubstitutions: { def: [
|
||||
appIdSubstitutions: {
|
||||
def: [
|
||||
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
|
||||
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
|
||||
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
|
||||
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
|
||||
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
|
||||
]},
|
||||
]
|
||||
},
|
||||
centeringMode: { def: "index" },
|
||||
clockDateFormat: { def: "" },
|
||||
lockDateFormat: { def: "" },
|
||||
@@ -153,7 +157,6 @@ var SPEC = {
|
||||
weatherEnabled: { def: true },
|
||||
|
||||
networkPreference: { def: "auto" },
|
||||
vpnLastConnected: { def: "" },
|
||||
|
||||
iconTheme: { def: "System Default", onChange: "applyStoredIconTheme" },
|
||||
availableIconThemes: { def: ["System Default"], persist: false },
|
||||
@@ -337,7 +340,8 @@ var SPEC = {
|
||||
niriOutputSettings: { def: {} },
|
||||
hyprlandOutputSettings: { def: {} },
|
||||
|
||||
barConfigs: { def: [{
|
||||
barConfigs: {
|
||||
def: [{
|
||||
id: "default",
|
||||
name: "Main Bar",
|
||||
enabled: true,
|
||||
@@ -382,7 +386,8 @@ var SPEC = {
|
||||
shadowColorMode: "text",
|
||||
shadowCustomColor: "#000000",
|
||||
clickThrough: false
|
||||
}], onChange: "updateBarConfigs" },
|
||||
}], onChange: "updateBarConfigs"
|
||||
},
|
||||
|
||||
desktopClockEnabled: { def: false },
|
||||
desktopClockStyle: { def: "analog" },
|
||||
|
||||
@@ -1114,6 +1114,79 @@ Item {
|
||||
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 {
|
||||
function open(): string {
|
||||
FirstLaunchService.showWelcome();
|
||||
|
||||
@@ -48,9 +48,14 @@ Item {
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onRequestLauncherUpdate(pluginId) {
|
||||
if (activePluginId === pluginId || searchQuery) {
|
||||
if (activePluginId === pluginId) {
|
||||
if (activePluginCategories.length <= 1)
|
||||
loadPluginCategories(pluginId);
|
||||
performSearch();
|
||||
return;
|
||||
}
|
||||
if (searchQuery)
|
||||
performSearch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +138,8 @@ Item {
|
||||
|
||||
property string pluginFilter: ""
|
||||
property string activePluginName: ""
|
||||
property var activePluginCategories: []
|
||||
property string activePluginCategory: ""
|
||||
|
||||
function getSectionViewMode(sectionId) {
|
||||
if (sectionId === "browse_plugins")
|
||||
@@ -307,10 +314,33 @@ Item {
|
||||
isSearching = false;
|
||||
activePluginId = "";
|
||||
activePluginName = "";
|
||||
activePluginCategories = [];
|
||||
activePluginCategory = "";
|
||||
pluginFilter = "";
|
||||
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() {
|
||||
if (pluginFilter) {
|
||||
pluginFilter = "";
|
||||
@@ -342,6 +372,8 @@ Item {
|
||||
if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) {
|
||||
activePluginId = "";
|
||||
activePluginName = "";
|
||||
activePluginCategories = [];
|
||||
activePluginCategory = "";
|
||||
clearActivePluginViewPreference();
|
||||
sections = cachedSections.map(function (s) {
|
||||
var copy = Object.assign({}, s, {
|
||||
@@ -363,10 +395,14 @@ Item {
|
||||
|
||||
var triggerMatch = detectTrigger(searchQuery);
|
||||
if (triggerMatch.pluginId) {
|
||||
var pluginChanged = activePluginId !== triggerMatch.pluginId;
|
||||
activePluginId = triggerMatch.pluginId;
|
||||
activePluginName = getPluginName(triggerMatch.pluginId, triggerMatch.isBuiltIn);
|
||||
applyActivePluginViewPreference(triggerMatch.pluginId, triggerMatch.isBuiltIn);
|
||||
|
||||
if (pluginChanged && !triggerMatch.isBuiltIn)
|
||||
loadPluginCategories(triggerMatch.pluginId);
|
||||
|
||||
var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query);
|
||||
allItems = allItems.concat(pluginItems);
|
||||
|
||||
@@ -401,6 +437,8 @@ Item {
|
||||
|
||||
activePluginId = "";
|
||||
activePluginName = "";
|
||||
activePluginCategories = [];
|
||||
activePluginCategory = "";
|
||||
clearActivePluginViewPreference();
|
||||
|
||||
if (searchMode === "files") {
|
||||
|
||||
@@ -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 {
|
||||
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
|
||||
|
||||
ResultsList {
|
||||
|
||||
@@ -519,7 +519,6 @@ Item {
|
||||
if (CompositorService.isSway || CompositorService.isScroll)
|
||||
return ws && ws.num !== -1;
|
||||
return ws !== -1;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -864,6 +863,60 @@ Item {
|
||||
property bool loadedHasIcon: false
|
||||
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 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 !== ""
|
||||
@@ -872,27 +925,29 @@ Item {
|
||||
readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0
|
||||
|
||||
readonly property real iconsExtraWidth: {
|
||||
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons);
|
||||
if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
|
||||
const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
|
||||
return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
readonly property real iconsExtraHeight: {
|
||||
if (root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons);
|
||||
if (root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
|
||||
const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
|
||||
return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
readonly property real visualWidth: {
|
||||
if (contentImplicitWidth <= 0) return baseWidth + iconsExtraWidth;
|
||||
if (contentImplicitWidth <= 0)
|
||||
return baseWidth + iconsExtraWidth;
|
||||
const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS;
|
||||
return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding);
|
||||
}
|
||||
readonly property real visualHeight: {
|
||||
if (contentImplicitHeight <= 0) return baseHeight + iconsExtraHeight;
|
||||
if (contentImplicitHeight <= 0)
|
||||
return baseHeight + iconsExtraHeight;
|
||||
const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS;
|
||||
return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding);
|
||||
}
|
||||
@@ -1080,6 +1135,20 @@ Item {
|
||||
width: root.isVertical ? root.widgetHeight : visualWidth
|
||||
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 {
|
||||
id: focusedBorderRing
|
||||
x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
|
||||
@@ -1349,6 +1418,15 @@ Item {
|
||||
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 {
|
||||
model: ScriptModel {
|
||||
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
||||
|
||||
@@ -125,6 +125,19 @@ Item {
|
||||
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: {
|
||||
SettingsData.detectAvailableIconThemes();
|
||||
SettingsData.detectAvailableCursorThemes();
|
||||
@@ -152,7 +165,6 @@ Item {
|
||||
}
|
||||
themeColorsTab.templateDetection = detection;
|
||||
} 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 {
|
||||
tab: "theme"
|
||||
tags: ["light", "dark", "mode", "appearance"]
|
||||
|
||||
@@ -884,4 +884,52 @@ Singleton {
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ Singleton {
|
||||
|
||||
Component.onCompleted: {
|
||||
root.userPreference = SettingsData.networkPreference;
|
||||
lastConnectedVpnUuid = SettingsData.vpnLastConnected || "";
|
||||
lastConnectedVpnUuid = SessionData.vpnLastConnected || "";
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
checkDMSCapabilities();
|
||||
}
|
||||
@@ -293,7 +293,7 @@ Singleton {
|
||||
|
||||
if (vpnConnected && activeUuid) {
|
||||
lastConnectedVpnUuid = activeUuid;
|
||||
SettingsData.set("vpnLastConnected", activeUuid);
|
||||
SessionData.setVpnLastConnected(activeUuid);
|
||||
}
|
||||
|
||||
if (vpnIsBusy) {
|
||||
|
||||
@@ -56,6 +56,7 @@ Singleton {
|
||||
signal wlrOutputStateUpdate(var data)
|
||||
signal evdevStateUpdate(var data)
|
||||
signal gammaStateUpdate(var data)
|
||||
signal themeAutoStateUpdate(var data)
|
||||
signal openUrlRequested(string url)
|
||||
signal appPickerRequested(var data)
|
||||
signal screensaverStateUpdate(var data)
|
||||
@@ -64,7 +65,7 @@ Singleton {
|
||||
property bool screensaverInhibited: false
|
||||
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: {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
@@ -304,7 +305,7 @@ Singleton {
|
||||
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));
|
||||
subscribe(filtered);
|
||||
}
|
||||
@@ -373,6 +374,8 @@ Singleton {
|
||||
evdevStateUpdate(data);
|
||||
} else if (service === "gamma") {
|
||||
gammaStateUpdate(data);
|
||||
} else if (service === "theme.auto") {
|
||||
themeAutoStateUpdate(data);
|
||||
} else if (service === "browser.open_requested") {
|
||||
if (data.target) {
|
||||
if (data.requestType === "url" || !data.requestType) {
|
||||
|
||||
@@ -396,7 +396,6 @@ Item {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.currentValue = delegateRoot.modelData;
|
||||
root.valueChanged(delegateRoot.modelData);
|
||||
dropdownMenu.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user