262 lines
5.5 KiB
Go
262 lines
5.5 KiB
Go
|
|
package display
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||
|
|
)
|
||
|
|
|
||
|
|
// WindowState holds the persisted state of a window.
|
||
|
|
type WindowState struct {
|
||
|
|
X int `json:"x"`
|
||
|
|
Y int `json:"y"`
|
||
|
|
Width int `json:"width"`
|
||
|
|
Height int `json:"height"`
|
||
|
|
Maximized bool `json:"maximized"`
|
||
|
|
Screen string `json:"screen,omitempty"` // Screen identifier for multi-monitor
|
||
|
|
URL string `json:"url,omitempty"` // Last URL/route
|
||
|
|
UpdatedAt int64 `json:"updatedAt"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// WindowStateManager handles saving and restoring window positions.
|
||
|
|
type WindowStateManager struct {
|
||
|
|
states map[string]*WindowState
|
||
|
|
filePath string
|
||
|
|
mu sync.RWMutex
|
||
|
|
dirty bool
|
||
|
|
saveTimer *time.Timer
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewWindowStateManager creates a new window state manager.
|
||
|
|
// It loads existing state from the config directory.
|
||
|
|
func NewWindowStateManager() *WindowStateManager {
|
||
|
|
m := &WindowStateManager{
|
||
|
|
states: make(map[string]*WindowState),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine config path
|
||
|
|
configDir, err := os.UserConfigDir()
|
||
|
|
if err != nil {
|
||
|
|
configDir = "."
|
||
|
|
}
|
||
|
|
m.filePath = filepath.Join(configDir, "Core", "window_state.json")
|
||
|
|
|
||
|
|
// Ensure directory exists
|
||
|
|
os.MkdirAll(filepath.Dir(m.filePath), 0755)
|
||
|
|
|
||
|
|
// Load existing state
|
||
|
|
m.load()
|
||
|
|
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
// load reads window states from disk.
|
||
|
|
func (m *WindowStateManager) load() error {
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
|
||
|
|
data, err := os.ReadFile(m.filePath)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil // No saved state yet
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
return json.Unmarshal(data, &m.states)
|
||
|
|
}
|
||
|
|
|
||
|
|
// save writes window states to disk.
|
||
|
|
func (m *WindowStateManager) save() error {
|
||
|
|
m.mu.RLock()
|
||
|
|
data, err := json.MarshalIndent(m.states, "", " ")
|
||
|
|
m.mu.RUnlock()
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
return os.WriteFile(m.filePath, data, 0644)
|
||
|
|
}
|
||
|
|
|
||
|
|
// scheduleSave debounces saves to avoid excessive disk writes.
|
||
|
|
func (m *WindowStateManager) scheduleSave() {
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
|
||
|
|
m.dirty = true
|
||
|
|
|
||
|
|
// Cancel existing timer
|
||
|
|
if m.saveTimer != nil {
|
||
|
|
m.saveTimer.Stop()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Schedule save after 500ms of no changes
|
||
|
|
m.saveTimer = time.AfterFunc(500*time.Millisecond, func() {
|
||
|
|
m.mu.Lock()
|
||
|
|
if m.dirty {
|
||
|
|
m.dirty = false
|
||
|
|
m.mu.Unlock()
|
||
|
|
m.save()
|
||
|
|
} else {
|
||
|
|
m.mu.Unlock()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetState returns the saved state for a window, or nil if none.
|
||
|
|
func (m *WindowStateManager) GetState(name string) *WindowState {
|
||
|
|
m.mu.RLock()
|
||
|
|
defer m.mu.RUnlock()
|
||
|
|
return m.states[name]
|
||
|
|
}
|
||
|
|
|
||
|
|
// SetState saves the state for a window.
|
||
|
|
func (m *WindowStateManager) SetState(name string, state *WindowState) {
|
||
|
|
m.mu.Lock()
|
||
|
|
state.UpdatedAt = time.Now().Unix()
|
||
|
|
m.states[name] = state
|
||
|
|
m.mu.Unlock()
|
||
|
|
|
||
|
|
m.scheduleSave()
|
||
|
|
}
|
||
|
|
|
||
|
|
// UpdatePosition updates just the position of a window.
|
||
|
|
func (m *WindowStateManager) UpdatePosition(name string, x, y int) {
|
||
|
|
m.mu.Lock()
|
||
|
|
state, ok := m.states[name]
|
||
|
|
if !ok {
|
||
|
|
state = &WindowState{}
|
||
|
|
m.states[name] = state
|
||
|
|
}
|
||
|
|
state.X = x
|
||
|
|
state.Y = y
|
||
|
|
state.UpdatedAt = time.Now().Unix()
|
||
|
|
m.mu.Unlock()
|
||
|
|
|
||
|
|
m.scheduleSave()
|
||
|
|
}
|
||
|
|
|
||
|
|
// UpdateSize updates just the size of a window.
|
||
|
|
func (m *WindowStateManager) UpdateSize(name string, width, height int) {
|
||
|
|
m.mu.Lock()
|
||
|
|
state, ok := m.states[name]
|
||
|
|
if !ok {
|
||
|
|
state = &WindowState{}
|
||
|
|
m.states[name] = state
|
||
|
|
}
|
||
|
|
state.Width = width
|
||
|
|
state.Height = height
|
||
|
|
state.UpdatedAt = time.Now().Unix()
|
||
|
|
m.mu.Unlock()
|
||
|
|
|
||
|
|
m.scheduleSave()
|
||
|
|
}
|
||
|
|
|
||
|
|
// UpdateMaximized updates the maximized state of a window.
|
||
|
|
func (m *WindowStateManager) UpdateMaximized(name string, maximized bool) {
|
||
|
|
m.mu.Lock()
|
||
|
|
state, ok := m.states[name]
|
||
|
|
if !ok {
|
||
|
|
state = &WindowState{}
|
||
|
|
m.states[name] = state
|
||
|
|
}
|
||
|
|
state.Maximized = maximized
|
||
|
|
state.UpdatedAt = time.Now().Unix()
|
||
|
|
m.mu.Unlock()
|
||
|
|
|
||
|
|
m.scheduleSave()
|
||
|
|
}
|
||
|
|
|
||
|
|
// CaptureState captures the current state from a window.
|
||
|
|
func (m *WindowStateManager) CaptureState(name string, window *application.WebviewWindow) {
|
||
|
|
if window == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
x, y := window.Position()
|
||
|
|
width, height := window.Size()
|
||
|
|
|
||
|
|
m.mu.Lock()
|
||
|
|
state, ok := m.states[name]
|
||
|
|
if !ok {
|
||
|
|
state = &WindowState{}
|
||
|
|
m.states[name] = state
|
||
|
|
}
|
||
|
|
state.X = x
|
||
|
|
state.Y = y
|
||
|
|
state.Width = width
|
||
|
|
state.Height = height
|
||
|
|
state.Maximized = window.IsMaximised()
|
||
|
|
state.UpdatedAt = time.Now().Unix()
|
||
|
|
m.mu.Unlock()
|
||
|
|
|
||
|
|
m.scheduleSave()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ApplyState applies saved state to window options.
|
||
|
|
// Returns the modified options with position/size restored.
|
||
|
|
func (m *WindowStateManager) ApplyState(opts application.WebviewWindowOptions) application.WebviewWindowOptions {
|
||
|
|
state := m.GetState(opts.Name)
|
||
|
|
if state == nil {
|
||
|
|
return opts
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only apply if we have valid saved dimensions
|
||
|
|
if state.Width > 0 && state.Height > 0 {
|
||
|
|
opts.Width = state.Width
|
||
|
|
opts.Height = state.Height
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply position (check for reasonable values)
|
||
|
|
if state.X != 0 || state.Y != 0 {
|
||
|
|
opts.X = state.X
|
||
|
|
opts.Y = state.Y
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply maximized state
|
||
|
|
if state.Maximized {
|
||
|
|
opts.StartState = application.WindowStateMaximised
|
||
|
|
}
|
||
|
|
|
||
|
|
return opts
|
||
|
|
}
|
||
|
|
|
||
|
|
// ForceSync immediately saves all state to disk.
|
||
|
|
func (m *WindowStateManager) ForceSync() error {
|
||
|
|
m.mu.Lock()
|
||
|
|
if m.saveTimer != nil {
|
||
|
|
m.saveTimer.Stop()
|
||
|
|
m.saveTimer = nil
|
||
|
|
}
|
||
|
|
m.dirty = false
|
||
|
|
m.mu.Unlock()
|
||
|
|
|
||
|
|
return m.save()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear removes all saved window states.
|
||
|
|
func (m *WindowStateManager) Clear() error {
|
||
|
|
m.mu.Lock()
|
||
|
|
m.states = make(map[string]*WindowState)
|
||
|
|
m.mu.Unlock()
|
||
|
|
|
||
|
|
return m.save()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ListStates returns all saved window names.
|
||
|
|
func (m *WindowStateManager) ListStates() []string {
|
||
|
|
m.mu.RLock()
|
||
|
|
defer m.mu.RUnlock()
|
||
|
|
|
||
|
|
names := make([]string, 0, len(m.states))
|
||
|
|
for name := range m.states {
|
||
|
|
names = append(names, name)
|
||
|
|
}
|
||
|
|
return names
|
||
|
|
}
|