mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-07 14:08:29 -04:00
wallpaper: migrate auto-cycling scheduling to backend
This commit is contained in:
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
"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/wallpaper"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -56,6 +57,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(req.Method, "wallpaper.") {
|
||||||
|
if wallpaperManager == nil {
|
||||||
|
models.RespondError(conn, req.ID, "wallpaper manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wallpaper.HandleRequest(conn, req, wallpaperManager)
|
||||||
|
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")
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wallpaper"
|
||||||
"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"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const APIVersion = 26
|
const APIVersion = 27
|
||||||
|
|
||||||
var CLIVersion = "dev"
|
var CLIVersion = "dev"
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ var clipboardManager *clipboard.Manager
|
|||||||
var dbusManager *serverDbus.Manager
|
var dbusManager *serverDbus.Manager
|
||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
var themeModeManager *thememode.Manager
|
var themeModeManager *thememode.Manager
|
||||||
|
var wallpaperManager *wallpaper.Manager
|
||||||
var trayRecoveryManager *trayrecovery.Manager
|
var trayRecoveryManager *trayrecovery.Manager
|
||||||
var locationManager *location.Manager
|
var locationManager *location.Manager
|
||||||
var sysUpdateManager *sysupdate.Manager
|
var sysUpdateManager *sysupdate.Manager
|
||||||
@@ -347,6 +349,13 @@ func InitializeThemeModeManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitializeWallpaperManager() error {
|
||||||
|
wallpaperManager = wallpaper.NewManager()
|
||||||
|
|
||||||
|
log.Info("Wallpaper rotation scheduler initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func InitializeTrayRecoveryManager() error {
|
func InitializeTrayRecoveryManager() error {
|
||||||
manager, err := trayrecovery.NewManager()
|
manager, err := trayrecovery.NewManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -463,6 +472,10 @@ func getCapabilities() Capabilities {
|
|||||||
caps = append(caps, "theme.auto")
|
caps = append(caps, "theme.auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wallpaperManager != nil {
|
||||||
|
caps = append(caps, "wallpaper")
|
||||||
|
}
|
||||||
|
|
||||||
if dbusManager != nil {
|
if dbusManager != nil {
|
||||||
caps = append(caps, "dbus")
|
caps = append(caps, "dbus")
|
||||||
}
|
}
|
||||||
@@ -529,6 +542,10 @@ func getServerInfo() ServerInfo {
|
|||||||
caps = append(caps, "theme.auto")
|
caps = append(caps, "theme.auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wallpaperManager != nil {
|
||||||
|
caps = append(caps, "wallpaper")
|
||||||
|
}
|
||||||
|
|
||||||
if locationManager != nil {
|
if locationManager != nil {
|
||||||
caps = append(caps, "location")
|
caps = append(caps, "location")
|
||||||
}
|
}
|
||||||
@@ -841,6 +858,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldSubscribe("wallpaper") && wallpaperManager != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
wallpaperChan := wallpaperManager.Subscribe(clientID + "-wallpaper")
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer wallpaperManager.Unsubscribe(clientID + "-wallpaper")
|
||||||
|
|
||||||
|
initialState := wallpaperManager.GetState()
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "wallpaper", Data: initialState}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case state, ok := <-wallpaperChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "wallpaper", 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")
|
||||||
@@ -1286,6 +1335,9 @@ func cleanupManagers() {
|
|||||||
if themeModeManager != nil {
|
if themeModeManager != nil {
|
||||||
themeModeManager.Close()
|
themeModeManager.Close()
|
||||||
}
|
}
|
||||||
|
if wallpaperManager != nil {
|
||||||
|
wallpaperManager.Close()
|
||||||
|
}
|
||||||
if trayRecoveryManager != nil {
|
if trayRecoveryManager != nil {
|
||||||
trayRecoveryManager.Close()
|
trayRecoveryManager.Close()
|
||||||
}
|
}
|
||||||
@@ -1578,6 +1630,19 @@ func Start(printDocs bool) error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := InitializeWallpaperManager(); err != nil {
|
||||||
|
log.Warnf("Wallpaper scheduler unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
notifyCapabilityChange()
|
||||||
|
go func() {
|
||||||
|
<-loginctlReady
|
||||||
|
if loginctlManager == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wallpaperManager.WatchLoginctl(loginctlManager)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-loginctlReady
|
<-loginctlReady
|
||||||
if loginctlManager == nil {
|
if loginctlManager == nil {
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package wallpaper
|
||||||
|
|
||||||
|
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, "wallpaper manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Method {
|
||||||
|
case "wallpaper.getState":
|
||||||
|
handleGetState(conn, req, manager)
|
||||||
|
case "wallpaper.setConfig":
|
||||||
|
handleSetConfig(conn, req, manager)
|
||||||
|
case "wallpaper.trigger":
|
||||||
|
handleTrigger(conn, req, manager)
|
||||||
|
case "wallpaper.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 handleSetConfig(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
raw, ok := params.Any(req.Params, "config")
|
||||||
|
if !ok {
|
||||||
|
models.RespondError(conn, req.ID, "missing or invalid 'config' parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(raw)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetConfig(config)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "wallpaper schedule set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
manager.ResetSchedule(params.StringOpt(req.Params, "target", ""))
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "wallpaper schedule reset"})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type activeSchedule struct {
|
||||||
|
cfg ScheduleConfig
|
||||||
|
nextFire time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
config Config
|
||||||
|
configMutex sync.RWMutex
|
||||||
|
|
||||||
|
state *State
|
||||||
|
stateMutex sync.RWMutex
|
||||||
|
|
||||||
|
subscribers syncmap.Map[string, chan State]
|
||||||
|
|
||||||
|
stopChan chan struct{}
|
||||||
|
updateTrigger chan struct{}
|
||||||
|
resetReq chan string
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
m := &Manager{
|
||||||
|
config: Config{
|
||||||
|
Global: ScheduleConfig{Mode: "interval", IntervalSec: 300, Time: "06:00"},
|
||||||
|
Monitors: map[string]ScheduleConfig{},
|
||||||
|
},
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
updateTrigger: make(chan struct{}, 1),
|
||||||
|
resetReq: make(chan string, 8),
|
||||||
|
}
|
||||||
|
m.state = &State{Config: m.getConfig()}
|
||||||
|
|
||||||
|
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()}
|
||||||
|
}
|
||||||
|
return *m.state
|
||||||
|
}
|
||||||
|
|
||||||
|
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) SetConfig(config Config) {
|
||||||
|
if config.Monitors == nil {
|
||||||
|
config.Monitors = map[string]ScheduleConfig{}
|
||||||
|
}
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if reflect.DeepEqual(m.config, config) {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config = config
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ResetSchedule(target string) {
|
||||||
|
select {
|
||||||
|
case m.resetReq <- target:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) WatchLoginctl(lm *loginctl.Manager) {
|
||||||
|
ch := lm.Subscribe("wallpaper")
|
||||||
|
m.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer m.wg.Done()
|
||||||
|
defer lm.Unsubscribe("wallpaper")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
case state, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.PreparingForSleep {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) schedulerLoop() {
|
||||||
|
defer m.wg.Done()
|
||||||
|
|
||||||
|
schedules := map[string]*activeSchedule{}
|
||||||
|
resets := map[string]bool{}
|
||||||
|
var seq uint64
|
||||||
|
var timer *time.Timer
|
||||||
|
|
||||||
|
for {
|
||||||
|
now := time.Now()
|
||||||
|
config := m.getConfig()
|
||||||
|
active := activeSchedules(config)
|
||||||
|
|
||||||
|
for key := range schedules {
|
||||||
|
if _, ok := active[key]; !ok {
|
||||||
|
delete(schedules, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key, cfg := range active {
|
||||||
|
s, ok := schedules[key]
|
||||||
|
switch {
|
||||||
|
case !ok:
|
||||||
|
schedules[key] = &activeSchedule{cfg: cfg, nextFire: computeNext(now, cfg)}
|
||||||
|
case s.cfg != cfg || resets[key]:
|
||||||
|
s.cfg = cfg
|
||||||
|
s.nextFire = computeNext(now, cfg)
|
||||||
|
}
|
||||||
|
delete(resets, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dueKeys []string
|
||||||
|
for key, s := range schedules {
|
||||||
|
if !s.nextFire.After(now) {
|
||||||
|
dueKeys = append(dueKeys, key)
|
||||||
|
s.nextFire = computeNext(now, s.cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next, hasNext := soonest(schedules)
|
||||||
|
if len(dueKeys) == 0 {
|
||||||
|
m.setState(config, next, seq, "")
|
||||||
|
}
|
||||||
|
for _, key := range dueKeys {
|
||||||
|
seq++
|
||||||
|
m.setState(config, next, seq, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitDur := 24 * time.Hour
|
||||||
|
if hasNext {
|
||||||
|
waitDur = time.Until(next)
|
||||||
|
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()
|
||||||
|
case key := <-m.resetReq:
|
||||||
|
timer.Stop()
|
||||||
|
resets[key] = true
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) setState(config Config, next time.Time, seq uint64, target string) {
|
||||||
|
newState := State{Config: config, NextRotation: next, CycleSeq: seq, Target: target}
|
||||||
|
|
||||||
|
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 activeSchedules(config Config) map[string]ScheduleConfig {
|
||||||
|
out := map[string]ScheduleConfig{}
|
||||||
|
if config.PerMonitor {
|
||||||
|
for name, cfg := range config.Monitors {
|
||||||
|
if cfg.Enabled {
|
||||||
|
out[name] = cfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
if config.Global.Enabled {
|
||||||
|
out[""] = config.Global
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeNext(now time.Time, cfg ScheduleConfig) time.Time {
|
||||||
|
switch cfg.Mode {
|
||||||
|
case "time":
|
||||||
|
return nextDailyTime(now, cfg.Time)
|
||||||
|
default:
|
||||||
|
sec := cfg.IntervalSec
|
||||||
|
if sec < 1 {
|
||||||
|
sec = 1
|
||||||
|
}
|
||||||
|
return now.Add(time.Duration(sec) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextDailyTime(now time.Time, hhmm string) time.Time {
|
||||||
|
hour, minute, ok := parseHHMM(hhmm)
|
||||||
|
if !ok {
|
||||||
|
return now.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
|
||||||
|
if !next.After(now) {
|
||||||
|
next = next.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHHMM(hhmm string) (int, int, bool) {
|
||||||
|
parts := strings.Split(hhmm, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
hour, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil || hour < 0 || hour > 23 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
minute, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil || minute < 0 || minute > 59 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
return hour, minute, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func soonest(schedules map[string]*activeSchedule) (time.Time, bool) {
|
||||||
|
var best time.Time
|
||||||
|
found := false
|
||||||
|
for _, s := range schedules {
|
||||||
|
if !found || s.nextFire.Before(best) {
|
||||||
|
best = s.nextFire
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func statesEqual(a, b *State) bool {
|
||||||
|
switch {
|
||||||
|
case a == nil || b == nil:
|
||||||
|
return a == b
|
||||||
|
case a.CycleSeq != b.CycleSeq:
|
||||||
|
return false
|
||||||
|
case a.Target != b.Target:
|
||||||
|
return false
|
||||||
|
case !a.NextRotation.Equal(b.NextRotation):
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(a.Config, b.Config)
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseHHMM(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
in string
|
||||||
|
hour int
|
||||||
|
minute int
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"06:00", 6, 0, true},
|
||||||
|
{"23:59", 23, 59, true},
|
||||||
|
{"00:00", 0, 0, true},
|
||||||
|
{"24:00", 0, 0, false},
|
||||||
|
{"6:5", 6, 5, true},
|
||||||
|
{"bad", 0, 0, false},
|
||||||
|
{"12", 0, 0, false},
|
||||||
|
{"12:60", 0, 0, false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
hour, minute, ok := parseHHMM(c.in)
|
||||||
|
if ok != c.ok || (ok && (hour != c.hour || minute != c.minute)) {
|
||||||
|
t.Errorf("parseHHMM(%q) = (%d, %d, %v), want (%d, %d, %v)", c.in, hour, minute, ok, c.hour, c.minute, c.ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeNextInterval(t *testing.T) {
|
||||||
|
now := time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC)
|
||||||
|
got := computeNext(now, ScheduleConfig{Mode: "interval", IntervalSec: 300})
|
||||||
|
if want := now.Add(300 * time.Second); !got.Equal(want) {
|
||||||
|
t.Errorf("interval next = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
clamped := computeNext(now, ScheduleConfig{Mode: "interval", IntervalSec: 0})
|
||||||
|
if want := now.Add(time.Second); !clamped.Equal(want) {
|
||||||
|
t.Errorf("interval clamp = %v, want %v", clamped, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeNextTime(t *testing.T) {
|
||||||
|
now := time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
later := computeNext(now, ScheduleConfig{Mode: "time", Time: "18:00"})
|
||||||
|
if want := time.Date(2026, 6, 30, 18, 0, 0, 0, time.UTC); !later.Equal(want) {
|
||||||
|
t.Errorf("time next (today) = %v, want %v", later, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
tomorrow := computeNext(now, ScheduleConfig{Mode: "time", Time: "06:00"})
|
||||||
|
if want := time.Date(2026, 7, 1, 6, 0, 0, 0, time.UTC); !tomorrow.Equal(want) {
|
||||||
|
t.Errorf("time next (tomorrow) = %v, want %v", tomorrow, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActiveSchedules(t *testing.T) {
|
||||||
|
global := activeSchedules(Config{
|
||||||
|
Global: ScheduleConfig{Enabled: true, Mode: "interval", IntervalSec: 60},
|
||||||
|
})
|
||||||
|
if len(global) != 1 {
|
||||||
|
t.Fatalf("global active = %d, want 1", len(global))
|
||||||
|
}
|
||||||
|
if _, ok := global[""]; !ok {
|
||||||
|
t.Errorf("global active missing global key")
|
||||||
|
}
|
||||||
|
|
||||||
|
perMonitor := activeSchedules(Config{
|
||||||
|
PerMonitor: true,
|
||||||
|
Global: ScheduleConfig{Enabled: true},
|
||||||
|
Monitors: map[string]ScheduleConfig{
|
||||||
|
"DP-1": {Enabled: true, Mode: "interval", IntervalSec: 60},
|
||||||
|
"DP-2": {Enabled: false},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if len(perMonitor) != 1 {
|
||||||
|
t.Fatalf("per-monitor active = %d, want 1", len(perMonitor))
|
||||||
|
}
|
||||||
|
if _, ok := perMonitor["DP-1"]; !ok {
|
||||||
|
t.Errorf("per-monitor active missing DP-1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchedulerEmitsCycle(t *testing.T) {
|
||||||
|
m := NewManager()
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
sub := m.Subscribe("test")
|
||||||
|
defer m.Unsubscribe("test")
|
||||||
|
|
||||||
|
m.SetConfig(Config{Global: ScheduleConfig{Enabled: true, Mode: "interval", IntervalSec: 1}})
|
||||||
|
|
||||||
|
deadline := time.After(3 * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case state := <-sub:
|
||||||
|
if state.CycleSeq > 0 && state.Target == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-deadline:
|
||||||
|
t.Fatal("scheduler did not emit a cycle event within 3s")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ScheduleConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
IntervalSec int `json:"intervalSec"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PerMonitor bool `json:"perMonitor"`
|
||||||
|
Global ScheduleConfig `json:"global"`
|
||||||
|
Monitors map[string]ScheduleConfig `json:"monitors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Config Config `json:"config"`
|
||||||
|
NextRotation time.Time `json:"nextRotation"`
|
||||||
|
CycleSeq uint64 `json:"cycleSeq"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
}
|
||||||
@@ -74,6 +74,9 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
|
|||||||
|
|
||||||
if config.Enabled {
|
if config.Enabled {
|
||||||
m.post(func() {
|
m.post(func() {
|
||||||
|
if m.controlsInitialized {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Info("Gamma control enabled at startup")
|
log.Info("Gamma control enabled at startup")
|
||||||
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||||
m.availOutputsMu.RLock()
|
m.availOutputsMu.RLock()
|
||||||
@@ -184,13 +187,26 @@ func (m *Manager) setupRegistry() error {
|
|||||||
enabled := m.config.Enabled
|
enabled := m.config.Enabled
|
||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
|
|
||||||
if enabled && m.controlsInitialized {
|
if !enabled {
|
||||||
m.post(func() {
|
return
|
||||||
if err := m.addOutputControl(output); err != nil {
|
|
||||||
log.Warnf("Failed to add output control: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
m.post(func() {
|
||||||
|
if err := m.addOutputControl(output); err != nil {
|
||||||
|
log.Warnf("gamma: failed to add output control: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if m.controlsInitialized {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// All outputs had been torn down (monitor sleep/disconnect),
|
||||||
|
// clearing controlsInitialized. Mark it ready again so the
|
||||||
|
// gamma_size event that follows control creation drives the
|
||||||
|
// reapply, instead of waiting for a manual toggle. No explicit
|
||||||
|
// apply here: applyGamma's dedup would suppress a no-change
|
||||||
|
// write anyway, and the new control isn't ready until gamma_size.
|
||||||
|
log.Info("gamma: output returned, re-establishing controls")
|
||||||
|
m.controlsInitialized = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -129,9 +129,13 @@ Variants {
|
|||||||
property bool useNextForEffect: false
|
property bool useNextForEffect: false
|
||||||
property string pendingWallpaper: ""
|
property string pendingWallpaper: ""
|
||||||
property string _deferredSource: ""
|
property string _deferredSource: ""
|
||||||
|
// Held true from the moment a wallpaper change is initiated until it
|
||||||
|
// settles, so the paused render loop wakes and drives the async load
|
||||||
|
// and transition even when nothing else marks the window dirty.
|
||||||
|
property bool changePending: false
|
||||||
readonly property bool overviewBlurActive: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && currentWallpaper.source !== ""
|
readonly property bool overviewBlurActive: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && currentWallpaper.source !== ""
|
||||||
readonly property var backingWindow: Window.window
|
readonly property var backingWindow: Window.window
|
||||||
readonly property bool renderActive: !source || effectActive || overviewBlurActive || pendingWallpaper !== "" || _deferredSource !== "" || frameAnim.running || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
|
readonly property bool renderActive: !source || effectActive || overviewBlurActive || pendingWallpaper !== "" || _deferredSource !== "" || changePending || frameAnim.running || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
|
||||||
property int _settleFrames: 3
|
property int _settleFrames: 3
|
||||||
|
|
||||||
function invalidate() {
|
function invalidate() {
|
||||||
@@ -265,6 +269,7 @@ Variants {
|
|||||||
root.useNextForEffect = false;
|
root.useNextForEffect = false;
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
root.transitionProgress = 0.0;
|
root.transitionProgress = 0.0;
|
||||||
|
root.changePending = false;
|
||||||
currentWallpaper.layer.enabled = false;
|
currentWallpaper.layer.enabled = false;
|
||||||
nextWallpaper.layer.enabled = false;
|
nextWallpaper.layer.enabled = false;
|
||||||
nextWallpaper.source = "";
|
nextWallpaper.source = "";
|
||||||
@@ -502,6 +507,9 @@ Variants {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
root.changePending = true;
|
||||||
|
invalidate();
|
||||||
|
|
||||||
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
|
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
|
||||||
|
|
||||||
if (!isInitialized || !currentWallpaper.source) {
|
if (!isInitialized || !currentWallpaper.source) {
|
||||||
@@ -688,6 +696,9 @@ Variants {
|
|||||||
if (status === Image.Ready) {
|
if (status === Image.Ready) {
|
||||||
imageMetrics.capture(implicitWidth, implicitHeight);
|
imageMetrics.capture(implicitWidth, implicitHeight);
|
||||||
}
|
}
|
||||||
|
if (status === Image.Ready || status === Image.Error) {
|
||||||
|
root.changePending = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSourceChanged: {
|
onSourceChanged: {
|
||||||
@@ -890,6 +901,8 @@ Variants {
|
|||||||
property real imageHeight2: modelData.height
|
property real imageHeight2: modelData.height
|
||||||
property real screenWidth: modelData.width
|
property real screenWidth: modelData.width
|
||||||
property real screenHeight: modelData.height
|
property real screenHeight: modelData.height
|
||||||
|
property real scrollX: 50
|
||||||
|
property real scrollY: 50
|
||||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_fade.frag.qsb")
|
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_fade.frag.qsb")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1048,6 +1061,7 @@ Variants {
|
|||||||
currentWallpaper.layer.enabled = false;
|
currentWallpaper.layer.enabled = false;
|
||||||
nextWallpaper.layer.enabled = false;
|
nextWallpaper.layer.enabled = false;
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
|
root.changePending = false;
|
||||||
|
|
||||||
if (!root.pendingWallpaper)
|
if (!root.pendingWallpaper)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Singleton {
|
|||||||
signal evdevStateUpdate(var data)
|
signal evdevStateUpdate(var data)
|
||||||
signal gammaStateUpdate(var data)
|
signal gammaStateUpdate(var data)
|
||||||
signal themeAutoStateUpdate(var data)
|
signal themeAutoStateUpdate(var data)
|
||||||
|
signal wallpaperCycleUpdate(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)
|
||||||
@@ -68,7 +69,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", "theme.auto", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location", "sysupdate"]
|
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location", "sysupdate"]
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (socketPath && socketPath.length > 0) {
|
if (socketPath && socketPath.length > 0) {
|
||||||
@@ -371,6 +372,8 @@ Singleton {
|
|||||||
gammaStateUpdate(data);
|
gammaStateUpdate(data);
|
||||||
} else if (service === "theme.auto") {
|
} else if (service === "theme.auto") {
|
||||||
themeAutoStateUpdate(data);
|
themeAutoStateUpdate(data);
|
||||||
|
} else if (service === "wallpaper") {
|
||||||
|
wallpaperCycleUpdate(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) {
|
||||||
|
|||||||
@@ -22,62 +22,37 @@ Singleton {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readonly property bool shouldPauseCycling: fullscreenShowing || SessionService.locked
|
readonly property bool shouldPauseCycling: fullscreenShowing || SessionService.locked
|
||||||
property string cachedCyclingTime: SessionData.wallpaperCyclingTime
|
readonly property bool serverSchedulingAvailable: DMSService.capabilities.includes("wallpaper")
|
||||||
property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
|
property real lastCycleSeq: -1
|
||||||
property string lastTimeCheck: ""
|
|
||||||
property var monitorTimers: ({})
|
|
||||||
property var monitorLastTimeChecks: ({})
|
|
||||||
property var monitorProcesses: ({})
|
property var monitorProcesses: ({})
|
||||||
|
|
||||||
Component {
|
Connections {
|
||||||
id: monitorTimerComponent
|
target: DMSService
|
||||||
Timer {
|
|
||||||
property string targetScreen: ""
|
function onWallpaperCycleUpdate(data) {
|
||||||
running: false
|
if (!data)
|
||||||
repeat: true
|
return;
|
||||||
onTriggered: {
|
const seq = data.cycleSeq || 0;
|
||||||
if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "" && !WallpaperCyclingService.shouldPauseCycling) {
|
if (lastCycleSeq < 0) {
|
||||||
WallpaperCyclingService.cycleNextForMonitor(targetScreen);
|
lastCycleSeq = seq;
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
if (seq <= lastCycleSeq)
|
||||||
|
return;
|
||||||
|
lastCycleSeq = seq;
|
||||||
|
if (shouldPauseCycling)
|
||||||
|
return;
|
||||||
|
const target = data.target || "";
|
||||||
|
if (target === "") {
|
||||||
|
cycleToNextWallpaper();
|
||||||
|
} else {
|
||||||
|
cycleNextForMonitor(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
function onCapabilitiesReceived() {
|
||||||
id: monitorProcessComponent
|
lastCycleSeq = -1;
|
||||||
Process {
|
updateCyclingState();
|
||||||
property string targetScreenName: ""
|
|
||||||
property string currentWallpaper: ""
|
|
||||||
property bool goToPrevious: false
|
|
||||||
running: false
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (text && text.trim()) {
|
|
||||||
const files = text.trim().split('\n').filter(file => file.length > 0);
|
|
||||||
if (files.length <= 1)
|
|
||||||
return;
|
|
||||||
const wallpaperList = files.sort();
|
|
||||||
const currentPath = currentWallpaper;
|
|
||||||
let currentIndex = wallpaperList.findIndex(path => path === currentPath);
|
|
||||||
if (currentIndex === -1)
|
|
||||||
currentIndex = 0;
|
|
||||||
let targetIndex;
|
|
||||||
if (goToPrevious) {
|
|
||||||
targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1;
|
|
||||||
} else {
|
|
||||||
targetIndex = (currentIndex + 1) % wallpaperList.length;
|
|
||||||
}
|
|
||||||
const targetWallpaper = wallpaperList[targetIndex];
|
|
||||||
if (targetWallpaper && targetWallpaper !== currentPath) {
|
|
||||||
if (targetScreenName) {
|
|
||||||
SessionData.setMonitorWallpaper(targetScreenName, targetWallpaper);
|
|
||||||
} else {
|
|
||||||
SessionData.setWallpaper(targetWallpaper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,17 +68,11 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onWallpaperCyclingIntervalChanged() {
|
function onWallpaperCyclingIntervalChanged() {
|
||||||
cachedCyclingInterval = SessionData.wallpaperCyclingInterval;
|
updateCyclingState();
|
||||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
|
||||||
updateCyclingState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWallpaperCyclingTimeChanged() {
|
function onWallpaperCyclingTimeChanged() {
|
||||||
cachedCyclingTime = SessionData.wallpaperCyclingTime;
|
updateCyclingState();
|
||||||
if (SessionData.wallpaperCyclingMode === "time") {
|
|
||||||
updateCyclingState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPerMonitorWallpaperChanged() {
|
function onPerMonitorWallpaperChanged() {
|
||||||
@@ -119,225 +88,117 @@ Singleton {
|
|||||||
target: SessionService
|
target: SessionService
|
||||||
|
|
||||||
function onSessionUnlocked() {
|
function onSessionUnlocked() {
|
||||||
if (SessionData.wallpaperCyclingEnabled || SessionData.perMonitorWallpaper) {
|
updateCyclingState();
|
||||||
updateCyclingState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCyclingState() {
|
function updateCyclingState() {
|
||||||
if (SessionData.perMonitorWallpaper) {
|
cyclingActive = serverSchedulingAvailable && (SessionData.wallpaperCyclingEnabled || SessionData.perMonitorWallpaper);
|
||||||
stopCycling();
|
pushConfigToServer();
|
||||||
updatePerMonitorCycling();
|
|
||||||
} else if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath) {
|
|
||||||
startCycling();
|
|
||||||
stopAllMonitorCycling();
|
|
||||||
} else {
|
|
||||||
stopCycling();
|
|
||||||
stopAllMonitorCycling();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePerMonitorCycling() {
|
function buildServerConfig() {
|
||||||
if (typeof Quickshell === "undefined")
|
var monitors = {};
|
||||||
|
if (SessionData.perMonitorWallpaper && typeof Quickshell !== "undefined") {
|
||||||
|
var screens = Quickshell.screens;
|
||||||
|
for (var i = 0; i < screens.length; i++) {
|
||||||
|
var name = screens[i].name;
|
||||||
|
var s = SessionData.getMonitorCyclingSettings(name);
|
||||||
|
var wp = SessionData.getMonitorWallpaper(name);
|
||||||
|
monitors[name] = {
|
||||||
|
"enabled": !!(s.enabled && wp && !wp.startsWith("#")),
|
||||||
|
"mode": s.mode || "interval",
|
||||||
|
"intervalSec": s.interval || 300,
|
||||||
|
"time": s.time || "06:00"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"perMonitor": SessionData.perMonitorWallpaper,
|
||||||
|
"global": {
|
||||||
|
"enabled": !!(SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath),
|
||||||
|
"mode": SessionData.wallpaperCyclingMode,
|
||||||
|
"intervalSec": SessionData.wallpaperCyclingInterval,
|
||||||
|
"time": SessionData.wallpaperCyclingTime
|
||||||
|
},
|
||||||
|
"monitors": monitors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushConfigToServer() {
|
||||||
|
if (!serverSchedulingAvailable)
|
||||||
return;
|
return;
|
||||||
var screens = Quickshell.screens;
|
DMSService.sendRequest("wallpaper.setConfig", {
|
||||||
for (var i = 0; i < screens.length; i++) {
|
"config": buildServerConfig()
|
||||||
var screenName = screens[i].name;
|
}, null);
|
||||||
var settings = SessionData.getMonitorCyclingSettings(screenName);
|
|
||||||
var wallpaper = SessionData.getMonitorWallpaper(screenName);
|
|
||||||
|
|
||||||
if (settings.enabled && wallpaper && !wallpaper.startsWith("#")) {
|
|
||||||
startMonitorCycling(screenName, settings);
|
|
||||||
} else {
|
|
||||||
stopMonitorCycling(screenName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopAllMonitorCycling() {
|
function findCommand(wallpaperDir) {
|
||||||
var screenNames = Object.keys(monitorTimers);
|
return ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
||||||
for (var i = 0; i < screenNames.length; i++) {
|
|
||||||
stopMonitorCycling(screenNames[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCycling() {
|
function monitorProcessFor(screenName) {
|
||||||
switch (SessionData.wallpaperCyclingMode) {
|
|
||||||
case "interval":
|
|
||||||
lastTimeCheck = "";
|
|
||||||
intervalTimer.interval = cachedCyclingInterval * 1000;
|
|
||||||
intervalTimer.start();
|
|
||||||
cyclingActive = true;
|
|
||||||
break;
|
|
||||||
case "time":
|
|
||||||
intervalTimer.stop();
|
|
||||||
cyclingActive = true;
|
|
||||||
checkTimeBasedCycling();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopCycling() {
|
|
||||||
intervalTimer.stop();
|
|
||||||
cyclingActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startMonitorCycling(screenName, settings) {
|
|
||||||
switch (settings.mode) {
|
|
||||||
case "interval":
|
|
||||||
{
|
|
||||||
var newChecks = Object.assign({}, monitorLastTimeChecks);
|
|
||||||
delete newChecks[screenName];
|
|
||||||
monitorLastTimeChecks = newChecks;
|
|
||||||
|
|
||||||
var timer = monitorTimers[screenName];
|
|
||||||
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
|
|
||||||
var newTimers = Object.assign({}, monitorTimers);
|
|
||||||
var newTimer = monitorTimerComponent.createObject(root);
|
|
||||||
newTimer.targetScreen = screenName;
|
|
||||||
newTimers[screenName] = newTimer;
|
|
||||||
monitorTimers = newTimers;
|
|
||||||
timer = newTimer;
|
|
||||||
}
|
|
||||||
if (timer) {
|
|
||||||
timer.interval = settings.interval * 1000;
|
|
||||||
timer.start();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "time":
|
|
||||||
{
|
|
||||||
var existingTimer = monitorTimers[screenName];
|
|
||||||
if (existingTimer) {
|
|
||||||
existingTimer.stop();
|
|
||||||
existingTimer.destroy();
|
|
||||||
var newTimers = Object.assign({}, monitorTimers);
|
|
||||||
delete newTimers[screenName];
|
|
||||||
monitorTimers = newTimers;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newChecks = Object.assign({}, monitorLastTimeChecks);
|
|
||||||
newChecks[screenName] = "";
|
|
||||||
monitorLastTimeChecks = newChecks;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopMonitorCycling(screenName) {
|
|
||||||
var timer = monitorTimers[screenName];
|
|
||||||
if (timer) {
|
|
||||||
timer.stop();
|
|
||||||
timer.destroy();
|
|
||||||
var newTimers = Object.assign({}, monitorTimers);
|
|
||||||
delete newTimers[screenName];
|
|
||||||
monitorTimers = newTimers;
|
|
||||||
}
|
|
||||||
|
|
||||||
var process = monitorProcesses[screenName];
|
var process = monitorProcesses[screenName];
|
||||||
if (process) {
|
if (process)
|
||||||
process.destroy();
|
return process;
|
||||||
var newProcesses = Object.assign({}, monitorProcesses);
|
var newProcesses = Object.assign({}, monitorProcesses);
|
||||||
delete newProcesses[screenName];
|
process = monitorProcessComponent.createObject(root);
|
||||||
monitorProcesses = newProcesses;
|
newProcesses[screenName] = process;
|
||||||
|
monitorProcesses = newProcesses;
|
||||||
|
return process;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycle(screenName, wallpaperPath, goToPrevious) {
|
||||||
|
const currentWallpaper = wallpaperPath || SessionData.wallpaperPath;
|
||||||
|
if (!currentWallpaper)
|
||||||
|
return;
|
||||||
|
const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
|
||||||
|
|
||||||
|
if (screenName && monitorProcessComponent.status === Component.Ready) {
|
||||||
|
var process = monitorProcessFor(screenName);
|
||||||
|
process.command = findCommand(wallpaperDir);
|
||||||
|
process.targetScreenName = screenName;
|
||||||
|
process.currentWallpaper = currentWallpaper;
|
||||||
|
process.goToPrevious = goToPrevious;
|
||||||
|
process.running = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newChecks = Object.assign({}, monitorLastTimeChecks);
|
var globalProcess = goToPrevious ? prevCyclingProcess : cyclingProcess;
|
||||||
delete newChecks[screenName];
|
globalProcess.command = findCommand(wallpaperDir);
|
||||||
monitorLastTimeChecks = newChecks;
|
globalProcess.targetScreenName = screenName || "";
|
||||||
|
globalProcess.currentWallpaper = currentWallpaper;
|
||||||
|
globalProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleToNextWallpaper(screenName, wallpaperPath) {
|
function cycleToNextWallpaper(screenName, wallpaperPath) {
|
||||||
const currentWallpaper = wallpaperPath || SessionData.wallpaperPath;
|
cycle(screenName, wallpaperPath, false);
|
||||||
if (!currentWallpaper)
|
|
||||||
return;
|
|
||||||
const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
|
|
||||||
|
|
||||||
if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) {
|
|
||||||
// Use per-monitor process
|
|
||||||
var process = monitorProcesses[screenName];
|
|
||||||
if (!process) {
|
|
||||||
var newProcesses = Object.assign({}, monitorProcesses);
|
|
||||||
var newProcess = monitorProcessComponent.createObject(root);
|
|
||||||
newProcesses[screenName] = newProcess;
|
|
||||||
monitorProcesses = newProcesses;
|
|
||||||
process = newProcess;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process) {
|
|
||||||
process.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
|
||||||
process.targetScreenName = screenName;
|
|
||||||
process.currentWallpaper = currentWallpaper;
|
|
||||||
process.goToPrevious = false;
|
|
||||||
process.running = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use global process for fallback
|
|
||||||
cyclingProcess.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
|
||||||
cyclingProcess.targetScreenName = screenName || "";
|
|
||||||
cyclingProcess.currentWallpaper = currentWallpaper;
|
|
||||||
cyclingProcess.running = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleToPrevWallpaper(screenName, wallpaperPath) {
|
function cycleToPrevWallpaper(screenName, wallpaperPath) {
|
||||||
const currentWallpaper = wallpaperPath || SessionData.wallpaperPath;
|
cycle(screenName, wallpaperPath, true);
|
||||||
if (!currentWallpaper)
|
}
|
||||||
|
|
||||||
|
function resetScheduleAfterManual() {
|
||||||
|
if (!serverSchedulingAvailable)
|
||||||
return;
|
return;
|
||||||
const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
|
DMSService.sendRequest("wallpaper.trigger", {
|
||||||
|
"target": ""
|
||||||
if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) {
|
}, null);
|
||||||
// Use per-monitor process (same as next, but with prev flag)
|
|
||||||
var process = monitorProcesses[screenName];
|
|
||||||
if (!process) {
|
|
||||||
var newProcesses = Object.assign({}, monitorProcesses);
|
|
||||||
var newProcess = monitorProcessComponent.createObject(root);
|
|
||||||
newProcesses[screenName] = newProcess;
|
|
||||||
monitorProcesses = newProcesses;
|
|
||||||
process = newProcess;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process) {
|
|
||||||
process.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
|
||||||
process.targetScreenName = screenName;
|
|
||||||
process.currentWallpaper = currentWallpaper;
|
|
||||||
process.goToPrevious = true;
|
|
||||||
process.running = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use global process for fallback
|
|
||||||
prevCyclingProcess.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
|
||||||
prevCyclingProcess.targetScreenName = screenName || "";
|
|
||||||
prevCyclingProcess.currentWallpaper = currentWallpaper;
|
|
||||||
prevCyclingProcess.running = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleNextManually() {
|
function cycleNextManually() {
|
||||||
if (SessionData.wallpaperPath) {
|
if (!SessionData.wallpaperPath)
|
||||||
cycleToNextWallpaper();
|
return;
|
||||||
// Restart timers if cycling is active
|
cycleToNextWallpaper();
|
||||||
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
|
resetScheduleAfterManual();
|
||||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
|
||||||
intervalTimer.interval = cachedCyclingInterval * 1000;
|
|
||||||
intervalTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cyclePrevManually() {
|
function cyclePrevManually() {
|
||||||
if (SessionData.wallpaperPath) {
|
if (!SessionData.wallpaperPath)
|
||||||
cycleToPrevWallpaper();
|
return;
|
||||||
// Restart timers if cycling is active
|
cycleToPrevWallpaper();
|
||||||
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
|
resetScheduleAfterManual();
|
||||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
|
||||||
intervalTimer.interval = cachedCyclingInterval * 1000;
|
|
||||||
intervalTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleNextForMonitor(screenName) {
|
function cycleNextForMonitor(screenName) {
|
||||||
@@ -358,138 +219,66 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkTimeBasedCycling() {
|
function applyCycledWallpaper(text, currentPath, targetScreenName, goToPrevious) {
|
||||||
if (shouldPauseCycling)
|
if (!text || !text.trim())
|
||||||
return;
|
return;
|
||||||
const currentTime = Qt.formatTime(systemClock.date, "hh:mm");
|
const files = text.trim().split('\n').filter(file => file.length > 0);
|
||||||
|
if (files.length <= 1)
|
||||||
|
return;
|
||||||
|
const wallpaperList = files.sort();
|
||||||
|
let currentIndex = wallpaperList.findIndex(path => path === currentPath);
|
||||||
|
if (currentIndex === -1)
|
||||||
|
currentIndex = 0;
|
||||||
|
|
||||||
if (!SessionData.perMonitorWallpaper) {
|
let targetIndex;
|
||||||
if (currentTime === cachedCyclingTime && currentTime !== lastTimeCheck) {
|
if (goToPrevious) {
|
||||||
lastTimeCheck = currentTime;
|
targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1;
|
||||||
cycleToNextWallpaper();
|
|
||||||
} else if (currentTime !== cachedCyclingTime) {
|
|
||||||
lastTimeCheck = "";
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
checkPerMonitorTimeBasedCycling(currentTime);
|
targetIndex = (currentIndex + 1) % wallpaperList.length;
|
||||||
}
|
}
|
||||||
}
|
const targetWallpaper = wallpaperList[targetIndex];
|
||||||
|
if (!targetWallpaper || targetWallpaper === currentPath)
|
||||||
function checkPerMonitorTimeBasedCycling(currentTime) {
|
|
||||||
if (typeof Quickshell === "undefined")
|
|
||||||
return;
|
return;
|
||||||
var screens = Quickshell.screens;
|
|
||||||
for (var i = 0; i < screens.length; i++) {
|
|
||||||
var screenName = screens[i].name;
|
|
||||||
var settings = SessionData.getMonitorCyclingSettings(screenName);
|
|
||||||
var wallpaper = SessionData.getMonitorWallpaper(screenName);
|
|
||||||
|
|
||||||
if (settings.enabled && settings.mode === "time" && wallpaper && !wallpaper.startsWith("#")) {
|
if (targetScreenName) {
|
||||||
var lastCheck = monitorLastTimeChecks[screenName] || "";
|
SessionData.setMonitorWallpaper(targetScreenName, targetWallpaper);
|
||||||
|
} else {
|
||||||
if (currentTime === settings.time && currentTime !== lastCheck) {
|
SessionData.setWallpaper(targetWallpaper);
|
||||||
var newChecks = Object.assign({}, monitorLastTimeChecks);
|
|
||||||
newChecks[screenName] = currentTime;
|
|
||||||
monitorLastTimeChecks = newChecks;
|
|
||||||
cycleNextForMonitor(screenName);
|
|
||||||
} else if (currentTime !== settings.time) {
|
|
||||||
var newChecks = Object.assign({}, monitorLastTimeChecks);
|
|
||||||
newChecks[screenName] = "";
|
|
||||||
monitorLastTimeChecks = newChecks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Component {
|
||||||
id: intervalTimer
|
id: monitorProcessComponent
|
||||||
interval: cachedCyclingInterval * 1000
|
Process {
|
||||||
running: false
|
property string targetScreenName: ""
|
||||||
repeat: true
|
property string currentWallpaper: ""
|
||||||
onTriggered: {
|
property bool goToPrevious: false
|
||||||
if (shouldPauseCycling)
|
running: false
|
||||||
return;
|
stdout: StdioCollector {
|
||||||
cycleToNextWallpaper();
|
onStreamFinished: root.applyCycledWallpaper(text, currentWallpaper, targetScreenName, goToPrevious)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemClock {
|
|
||||||
id: systemClock
|
|
||||||
precision: SystemClock.Minutes
|
|
||||||
onDateChanged: {
|
|
||||||
if ((SessionData.wallpaperCyclingMode === "time" && cyclingActive) || SessionData.perMonitorWallpaper) {
|
|
||||||
checkTimeBasedCycling();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: cyclingProcess
|
id: cyclingProcess
|
||||||
|
|
||||||
property string targetScreenName: ""
|
property string targetScreenName: ""
|
||||||
property string currentWallpaper: ""
|
property string currentWallpaper: ""
|
||||||
|
property bool goToPrevious: false
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: root.applyCycledWallpaper(text, cyclingProcess.currentWallpaper, cyclingProcess.targetScreenName, cyclingProcess.goToPrevious)
|
||||||
if (text && text.trim()) {
|
|
||||||
const files = text.trim().split('\n').filter(file => file.length > 0);
|
|
||||||
if (files.length <= 1)
|
|
||||||
return;
|
|
||||||
const wallpaperList = files.sort();
|
|
||||||
const currentPath = cyclingProcess.currentWallpaper;
|
|
||||||
let currentIndex = wallpaperList.findIndex(path => path === currentPath);
|
|
||||||
if (currentIndex === -1)
|
|
||||||
currentIndex = 0;
|
|
||||||
|
|
||||||
const nextIndex = (currentIndex + 1) % wallpaperList.length;
|
|
||||||
const nextWallpaper = wallpaperList[nextIndex];
|
|
||||||
|
|
||||||
if (nextWallpaper && nextWallpaper !== currentPath) {
|
|
||||||
if (cyclingProcess.targetScreenName) {
|
|
||||||
SessionData.setMonitorWallpaper(cyclingProcess.targetScreenName, nextWallpaper);
|
|
||||||
} else {
|
|
||||||
SessionData.setWallpaper(nextWallpaper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: prevCyclingProcess
|
id: prevCyclingProcess
|
||||||
|
|
||||||
property string targetScreenName: ""
|
property string targetScreenName: ""
|
||||||
property string currentWallpaper: ""
|
property string currentWallpaper: ""
|
||||||
|
property bool goToPrevious: true
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: root.applyCycledWallpaper(text, prevCyclingProcess.currentWallpaper, prevCyclingProcess.targetScreenName, prevCyclingProcess.goToPrevious)
|
||||||
if (text && text.trim()) {
|
|
||||||
const files = text.trim().split('\n').filter(file => file.length > 0);
|
|
||||||
if (files.length <= 1)
|
|
||||||
return;
|
|
||||||
const wallpaperList = files.sort();
|
|
||||||
const currentPath = prevCyclingProcess.currentWallpaper;
|
|
||||||
let currentIndex = wallpaperList.findIndex(path => path === currentPath);
|
|
||||||
if (currentIndex === -1)
|
|
||||||
currentIndex = 0;
|
|
||||||
|
|
||||||
const prevIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1;
|
|
||||||
const prevWallpaper = wallpaperList[prevIndex];
|
|
||||||
|
|
||||||
if (prevWallpaper && prevWallpaper !== currentPath) {
|
|
||||||
if (prevCyclingProcess.targetScreenName) {
|
|
||||||
SessionData.setMonitorWallpaper(prevCyclingProcess.targetScreenName, prevWallpaper);
|
|
||||||
} else {
|
|
||||||
SessionData.setWallpaper(prevWallpaper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user