refactor(display): remove extracted clipboard/dialog/notification/theme/screen code
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
335d93d05c
commit
bba743d2cb
10 changed files with 17 additions and 670 deletions
|
|
@ -1,61 +0,0 @@
|
|||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// ClipboardContentType represents the type of content in the clipboard.
|
||||
type ClipboardContentType string
|
||||
|
||||
const (
|
||||
ClipboardText ClipboardContentType = "text"
|
||||
ClipboardImage ClipboardContentType = "image"
|
||||
ClipboardHTML ClipboardContentType = "html"
|
||||
)
|
||||
|
||||
// ClipboardContent holds clipboard data.
|
||||
type ClipboardContent struct {
|
||||
Type ClipboardContentType `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
HTML string `json:"html,omitempty"`
|
||||
}
|
||||
|
||||
// ReadClipboard reads text content from the system clipboard.
|
||||
func (s *Service) ReadClipboard() (string, error) {
|
||||
app := application.Get()
|
||||
if app == nil || app.Clipboard == nil {
|
||||
return "", fmt.Errorf("application or clipboard not available")
|
||||
}
|
||||
|
||||
text, ok := app.Clipboard.Text()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to read clipboard")
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// WriteClipboard writes text content to the system clipboard.
|
||||
func (s *Service) WriteClipboard(text string) error {
|
||||
app := application.Get()
|
||||
if app == nil || app.Clipboard == nil {
|
||||
return fmt.Errorf("application or clipboard not available")
|
||||
}
|
||||
|
||||
if !app.Clipboard.SetText(text) {
|
||||
return fmt.Errorf("failed to write to clipboard")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasClipboard checks if the clipboard has content.
|
||||
func (s *Service) HasClipboard() bool {
|
||||
text, err := s.ReadClipboard()
|
||||
return err == nil && text != ""
|
||||
}
|
||||
|
||||
// ClearClipboard clears the clipboard by setting empty text.
|
||||
func (s *Service) ClearClipboard() error {
|
||||
return s.WriteClipboard("")
|
||||
}
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// FileFilter represents a file type filter for dialogs.
|
||||
type FileFilter struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
Pattern string `json:"pattern"`
|
||||
Extensions []string `json:"extensions,omitempty"`
|
||||
}
|
||||
|
||||
// OpenFileOptions contains options for the open file dialog.
|
||||
type OpenFileOptions struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
DefaultDirectory string `json:"defaultDirectory,omitempty"`
|
||||
DefaultFilename string `json:"defaultFilename,omitempty"`
|
||||
Filters []FileFilter `json:"filters,omitempty"`
|
||||
AllowMultiple bool `json:"allowMultiple,omitempty"`
|
||||
}
|
||||
|
||||
// SaveFileOptions contains options for the save file dialog.
|
||||
type SaveFileOptions struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
DefaultDirectory string `json:"defaultDirectory,omitempty"`
|
||||
DefaultFilename string `json:"defaultFilename,omitempty"`
|
||||
Filters []FileFilter `json:"filters,omitempty"`
|
||||
}
|
||||
|
||||
// OpenDirectoryOptions contains options for the directory picker.
|
||||
type OpenDirectoryOptions struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
DefaultDirectory string `json:"defaultDirectory,omitempty"`
|
||||
AllowMultiple bool `json:"allowMultiple,omitempty"`
|
||||
}
|
||||
|
||||
// OpenFileDialog shows a file open dialog and returns selected path(s).
|
||||
func (s *Service) OpenFileDialog(opts OpenFileOptions) ([]string, error) {
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return nil, fmt.Errorf("application not available")
|
||||
}
|
||||
|
||||
dialog := app.Dialog.OpenFile()
|
||||
|
||||
if opts.Title != "" {
|
||||
dialog.SetTitle(opts.Title)
|
||||
}
|
||||
if opts.DefaultDirectory != "" {
|
||||
dialog.SetDirectory(opts.DefaultDirectory)
|
||||
}
|
||||
|
||||
// Add filters
|
||||
for _, f := range opts.Filters {
|
||||
dialog.AddFilter(f.DisplayName, f.Pattern)
|
||||
}
|
||||
|
||||
if opts.AllowMultiple {
|
||||
dialog.CanChooseFiles(true)
|
||||
// Use PromptForMultipleSelection for multiple files
|
||||
paths, err := dialog.PromptForMultipleSelection()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialog error: %w", err)
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// Single selection
|
||||
path, err := dialog.PromptForSingleSelection()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialog error: %w", err)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
return []string{path}, nil
|
||||
}
|
||||
|
||||
// OpenSingleFileDialog shows a file open dialog for a single file.
|
||||
func (s *Service) OpenSingleFileDialog(opts OpenFileOptions) (string, error) {
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return "", fmt.Errorf("application not available")
|
||||
}
|
||||
|
||||
dialog := app.Dialog.OpenFile()
|
||||
|
||||
if opts.Title != "" {
|
||||
dialog.SetTitle(opts.Title)
|
||||
}
|
||||
if opts.DefaultDirectory != "" {
|
||||
dialog.SetDirectory(opts.DefaultDirectory)
|
||||
}
|
||||
|
||||
for _, f := range opts.Filters {
|
||||
dialog.AddFilter(f.DisplayName, f.Pattern)
|
||||
}
|
||||
|
||||
path, err := dialog.PromptForSingleSelection()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dialog error: %w", err)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// SaveFileDialog shows a save file dialog and returns the selected path.
|
||||
func (s *Service) SaveFileDialog(opts SaveFileOptions) (string, error) {
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return "", fmt.Errorf("application not available")
|
||||
}
|
||||
|
||||
dialog := app.Dialog.SaveFile()
|
||||
|
||||
if opts.DefaultDirectory != "" {
|
||||
dialog.SetDirectory(opts.DefaultDirectory)
|
||||
}
|
||||
if opts.DefaultFilename != "" {
|
||||
dialog.SetFilename(opts.DefaultFilename)
|
||||
}
|
||||
|
||||
for _, f := range opts.Filters {
|
||||
dialog.AddFilter(f.DisplayName, f.Pattern)
|
||||
}
|
||||
|
||||
path, err := dialog.PromptForSingleSelection()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dialog error: %w", err)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// OpenDirectoryDialog shows a directory picker.
|
||||
func (s *Service) OpenDirectoryDialog(opts OpenDirectoryOptions) (string, error) {
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return "", fmt.Errorf("application not available")
|
||||
}
|
||||
|
||||
// Use OpenFile dialog with directory selection
|
||||
dialog := app.Dialog.OpenFile()
|
||||
dialog.CanChooseDirectories(true)
|
||||
dialog.CanChooseFiles(false)
|
||||
|
||||
if opts.Title != "" {
|
||||
dialog.SetTitle(opts.Title)
|
||||
}
|
||||
if opts.DefaultDirectory != "" {
|
||||
dialog.SetDirectory(opts.DefaultDirectory)
|
||||
}
|
||||
|
||||
path, err := dialog.PromptForSingleSelection()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dialog error: %w", err)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// ConfirmDialog shows a confirmation dialog and returns the user's choice.
|
||||
func (s *Service) ConfirmDialog(title, message string) (bool, error) {
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return false, fmt.Errorf("application not available")
|
||||
}
|
||||
|
||||
dialog := app.Dialog.Question()
|
||||
dialog.SetTitle(title)
|
||||
dialog.SetMessage(message)
|
||||
dialog.AddButton("Yes").SetAsDefault()
|
||||
dialog.AddButton("No")
|
||||
|
||||
dialog.Show()
|
||||
// Note: Wails v3 Question dialog Show() doesn't return a value
|
||||
// The button callbacks would need to be used for async handling
|
||||
// For now, return true as we showed the dialog
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// PromptDialog shows an input prompt dialog.
|
||||
// Note: Wails v3 doesn't have a native prompt dialog, so this uses a question dialog.
|
||||
func (s *Service) PromptDialog(title, message string) (string, bool, error) {
|
||||
// Wails v3 doesn't have a native text input dialog
|
||||
// For now, return an error suggesting to use webview-based input
|
||||
return "", false, fmt.Errorf("text input dialogs not supported natively; use webview-based input instead")
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"forge.lthn.ai/core/go-config"
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"forge.lthn.ai/core/gui/pkg/dialog"
|
||||
"forge.lthn.ai/core/gui/pkg/environment"
|
||||
"forge.lthn.ai/core/gui/pkg/menu"
|
||||
"forge.lthn.ai/core/gui/pkg/notification"
|
||||
|
|
@ -16,7 +17,6 @@ import (
|
|||
"forge.lthn.ai/core/gui/pkg/systray"
|
||||
"forge.lthn.ai/core/gui/pkg/window"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||
)
|
||||
|
||||
// Options holds configuration for the display service.
|
||||
|
|
@ -35,8 +35,7 @@ type Service struct {
|
|||
config Options
|
||||
configData map[string]map[string]any
|
||||
cfg *config.Config // go-config instance for file persistence
|
||||
notifier *notifications.NotificationService
|
||||
events *WSEventManager
|
||||
events *WSEventManager
|
||||
}
|
||||
|
||||
// New is the constructor for the display service.
|
||||
|
|
@ -77,8 +76,7 @@ func (s *Service) OnStartup(ctx context.Context) error {
|
|||
// Initialise Wails wrappers if app is available (nil in tests)
|
||||
if s.wailsApp != nil {
|
||||
s.app = newWailsApp(s.wailsApp)
|
||||
s.events = NewWSEventManager(newWailsEventSource(s.wailsApp))
|
||||
s.events.SetupWindowEventListeners()
|
||||
s.events = NewWSEventManager()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -165,8 +163,18 @@ func (s *Service) handleTrayAction(actionID string) {
|
|||
case "close-desktop":
|
||||
// Hide all windows — future: add TaskHideWindow
|
||||
case "env-info":
|
||||
if s.app != nil {
|
||||
s.ShowEnvironmentDialog()
|
||||
// Query environment info via IPC and show as dialog
|
||||
result, handled, _ := s.Core().QUERY(environment.QueryInfo{})
|
||||
if handled {
|
||||
info := result.(environment.EnvironmentInfo)
|
||||
details := fmt.Sprintf("OS: %s\nArch: %s\nPlatform: %s %s",
|
||||
info.OS, info.Arch, info.Platform.Name, info.Platform.Version)
|
||||
_, _, _ = s.Core().PERFORM(dialog.TaskMessageDialog{
|
||||
Opts: dialog.MessageDialogOptions{
|
||||
Type: dialog.DialogInfo, Title: "Environment",
|
||||
Message: details, Buttons: []string{"OK"},
|
||||
},
|
||||
})
|
||||
}
|
||||
case "quit":
|
||||
if s.app != nil {
|
||||
|
|
@ -619,171 +627,6 @@ func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error {
|
|||
return ws.Manager().ApplyWorkflow(workflow, ws.Manager().List(), 1920, 1080)
|
||||
}
|
||||
|
||||
// --- Screen queries (remain in display — use application.Get() directly) ---
|
||||
|
||||
// ScreenInfo contains information about a display screen.
|
||||
type ScreenInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Primary bool `json:"primary"`
|
||||
}
|
||||
|
||||
// WorkArea represents usable screen space.
|
||||
type WorkArea struct {
|
||||
ScreenID string `json:"screenId"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
// GetScreens returns information about all available screens.
|
||||
func (s *Service) GetScreens() []ScreenInfo {
|
||||
app := application.Get()
|
||||
if app == nil || app.Screen == nil {
|
||||
return nil
|
||||
}
|
||||
screens := app.Screen.GetAll()
|
||||
if screens == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]ScreenInfo, 0, len(screens))
|
||||
for _, screen := range screens {
|
||||
result = append(result, ScreenInfo{
|
||||
ID: screen.ID,
|
||||
Name: screen.Name,
|
||||
X: screen.Bounds.X,
|
||||
Y: screen.Bounds.Y,
|
||||
Width: screen.Bounds.Width,
|
||||
Height: screen.Bounds.Height,
|
||||
Primary: screen.IsPrimary,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetWorkAreas returns the usable work area for all screens.
|
||||
func (s *Service) GetWorkAreas() []WorkArea {
|
||||
app := application.Get()
|
||||
if app == nil || app.Screen == nil {
|
||||
return nil
|
||||
}
|
||||
screens := app.Screen.GetAll()
|
||||
if screens == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]WorkArea, 0, len(screens))
|
||||
for _, screen := range screens {
|
||||
result = append(result, WorkArea{
|
||||
ScreenID: screen.ID,
|
||||
X: screen.WorkArea.X,
|
||||
Y: screen.WorkArea.Y,
|
||||
Width: screen.WorkArea.Width,
|
||||
Height: screen.WorkArea.Height,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetPrimaryScreen returns information about the primary screen.
|
||||
func (s *Service) GetPrimaryScreen() (*ScreenInfo, error) {
|
||||
app := application.Get()
|
||||
if app == nil || app.Screen == nil {
|
||||
return nil, fmt.Errorf("screen service not available")
|
||||
}
|
||||
screens := app.Screen.GetAll()
|
||||
for _, screen := range screens {
|
||||
if screen.IsPrimary {
|
||||
return &ScreenInfo{
|
||||
ID: screen.ID, Name: screen.Name,
|
||||
X: screen.Bounds.X, Y: screen.Bounds.Y,
|
||||
Width: screen.Bounds.Width, Height: screen.Bounds.Height,
|
||||
Primary: true,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no primary screen found")
|
||||
}
|
||||
|
||||
// GetScreen returns information about a specific screen by ID.
|
||||
func (s *Service) GetScreen(id string) (*ScreenInfo, error) {
|
||||
app := application.Get()
|
||||
if app == nil || app.Screen == nil {
|
||||
return nil, fmt.Errorf("screen service not available")
|
||||
}
|
||||
screens := app.Screen.GetAll()
|
||||
for _, screen := range screens {
|
||||
if screen.ID == id {
|
||||
return &ScreenInfo{
|
||||
ID: screen.ID, Name: screen.Name,
|
||||
X: screen.Bounds.X, Y: screen.Bounds.Y,
|
||||
Width: screen.Bounds.Width, Height: screen.Bounds.Height,
|
||||
Primary: screen.IsPrimary,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("screen not found: %s", id)
|
||||
}
|
||||
|
||||
// GetScreenAtPoint returns the screen containing a specific point.
|
||||
func (s *Service) GetScreenAtPoint(x, y int) (*ScreenInfo, error) {
|
||||
app := application.Get()
|
||||
if app == nil || app.Screen == nil {
|
||||
return nil, fmt.Errorf("screen service not available")
|
||||
}
|
||||
screens := app.Screen.GetAll()
|
||||
for _, screen := range screens {
|
||||
bounds := screen.Bounds
|
||||
if x >= bounds.X && x < bounds.X+bounds.Width &&
|
||||
y >= bounds.Y && y < bounds.Y+bounds.Height {
|
||||
return &ScreenInfo{
|
||||
ID: screen.ID, Name: screen.Name,
|
||||
X: bounds.X, Y: bounds.Y,
|
||||
Width: bounds.Width, Height: bounds.Height,
|
||||
Primary: screen.IsPrimary,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no screen found at point (%d, %d)", x, y)
|
||||
}
|
||||
|
||||
// GetScreenForWindow returns the screen containing a specific window.
|
||||
func (s *Service) GetScreenForWindow(name string) (*ScreenInfo, error) {
|
||||
info, err := s.GetWindowInfo(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
centerX := info.X + info.Width/2
|
||||
centerY := info.Y + info.Height/2
|
||||
return s.GetScreenAtPoint(centerX, centerY)
|
||||
}
|
||||
|
||||
// ShowEnvironmentDialog displays environment information.
|
||||
func (s *Service) ShowEnvironmentDialog() {
|
||||
envInfo := s.app.Env().Info()
|
||||
details := "Environment Information:\n\n"
|
||||
details += fmt.Sprintf("Operating System: %s\n", envInfo.OS)
|
||||
details += fmt.Sprintf("Architecture: %s\n", envInfo.Arch)
|
||||
details += fmt.Sprintf("Debug Mode: %t\n\n", envInfo.Debug)
|
||||
details += fmt.Sprintf("Dark Mode: %t\n\n", s.app.Env().IsDarkMode())
|
||||
details += "Platform Information:"
|
||||
for key, value := range envInfo.PlatformInfo {
|
||||
details += fmt.Sprintf("\n%s: %v", key, value)
|
||||
}
|
||||
if envInfo.OSInfo != nil {
|
||||
details += fmt.Sprintf("\n\nOS Details:\nName: %s\nVersion: %s",
|
||||
envInfo.OSInfo.Name, envInfo.OSInfo.Version)
|
||||
}
|
||||
dialog := s.app.Dialog().Info()
|
||||
dialog.SetTitle("Environment Information")
|
||||
dialog.SetMessage(details)
|
||||
dialog.Show()
|
||||
}
|
||||
|
||||
// GetEventManager returns the event manager for WebSocket event subscriptions.
|
||||
func (s *Service) GetEventManager() *WSEventManager {
|
||||
return s.events
|
||||
|
|
|
|||
|
|
@ -432,25 +432,13 @@ func TestHandleListWorkspaces_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWSEventManager_Good(t *testing.T) {
|
||||
es := newMockEventSource()
|
||||
em := NewWSEventManager(es)
|
||||
em := NewWSEventManager()
|
||||
defer em.Close()
|
||||
|
||||
assert.NotNil(t, em)
|
||||
assert.Equal(t, 0, em.ConnectedClients())
|
||||
}
|
||||
|
||||
func TestWSEventManager_SetupWindowEventListeners_Good(t *testing.T) {
|
||||
es := newMockEventSource()
|
||||
em := NewWSEventManager(es)
|
||||
defer em.Close()
|
||||
|
||||
em.SetupWindowEventListeners()
|
||||
|
||||
// Verify theme handler was registered
|
||||
assert.Len(t, es.themeHandlers, 1)
|
||||
}
|
||||
|
||||
// --- Config file loading tests ---
|
||||
|
||||
func TestLoadConfig_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ type WSEventManager struct {
|
|||
upgrader websocket.Upgrader
|
||||
clients map[*websocket.Conn]*clientState
|
||||
mu sync.RWMutex
|
||||
eventSource EventSource
|
||||
nextSubID int
|
||||
eventBuffer chan Event
|
||||
}
|
||||
|
|
@ -59,8 +58,7 @@ type clientState struct {
|
|||
}
|
||||
|
||||
// NewWSEventManager creates a new event manager.
|
||||
// It accepts an EventSource for theme change events instead of using application.Get() directly.
|
||||
func NewWSEventManager(es EventSource) *WSEventManager {
|
||||
func NewWSEventManager() *WSEventManager {
|
||||
em := &WSEventManager{
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
|
|
@ -70,7 +68,6 @@ func NewWSEventManager(es EventSource) *WSEventManager {
|
|||
WriteBufferSize: 1024,
|
||||
},
|
||||
clients: make(map[*websocket.Conn]*clientState),
|
||||
eventSource: es,
|
||||
eventBuffer: make(chan Event, 100),
|
||||
}
|
||||
|
||||
|
|
@ -305,26 +302,6 @@ func (em *WSEventManager) Close() {
|
|||
close(em.eventBuffer)
|
||||
}
|
||||
|
||||
// SetupWindowEventListeners registers listeners for application-level events.
|
||||
// Uses EventSource instead of application.Get() directly.
|
||||
func (em *WSEventManager) SetupWindowEventListeners() {
|
||||
if em.eventSource != nil {
|
||||
em.eventSource.OnThemeChange(func(isDark bool) {
|
||||
theme := "light"
|
||||
if isDark {
|
||||
theme = "dark"
|
||||
}
|
||||
em.Emit(Event{
|
||||
Type: EventThemeChange,
|
||||
Data: map[string]any{
|
||||
"isDark": isDark,
|
||||
"theme": theme,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AttachWindowListeners attaches event listeners to a specific window.
|
||||
// Accepts window.PlatformWindow instead of *application.WebviewWindow.
|
||||
func (em *WSEventManager) AttachWindowListeners(pw window.PlatformWindow) {
|
||||
|
|
|
|||
|
|
@ -76,19 +76,3 @@ func (ev *wailsEventManager) Emit(name string, data ...any) bool {
|
|||
return ev.app.Event.Emit(name, data...)
|
||||
}
|
||||
|
||||
// wailsEventSource implements EventSource using a Wails app.
|
||||
type wailsEventSource struct{ app *application.App }
|
||||
|
||||
func newWailsEventSource(app *application.App) EventSource {
|
||||
return &wailsEventSource{app: app}
|
||||
}
|
||||
|
||||
func (es *wailsEventSource) OnThemeChange(handler func(isDark bool)) func() {
|
||||
return es.app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(_ *application.ApplicationEvent) {
|
||||
handler(es.app.Env.IsDarkMode())
|
||||
})
|
||||
}
|
||||
|
||||
func (es *wailsEventSource) Emit(name string, data ...any) bool {
|
||||
return es.app.Event.Emit(name, data...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1 @@
|
|||
package display
|
||||
|
||||
// mockEventSource implements EventSource for testing.
|
||||
type mockEventSource struct {
|
||||
themeHandlers []func(isDark bool)
|
||||
}
|
||||
|
||||
func newMockEventSource() *mockEventSource {
|
||||
return &mockEventSource{}
|
||||
}
|
||||
|
||||
func (m *mockEventSource) OnThemeChange(handler func(isDark bool)) func() {
|
||||
m.themeHandlers = append(m.themeHandlers, handler)
|
||||
return func() {}
|
||||
}
|
||||
|
||||
func (m *mockEventSource) Emit(name string, data ...any) bool {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||
)
|
||||
|
||||
// NotificationOptions contains options for showing a notification.
|
||||
type NotificationOptions struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
}
|
||||
|
||||
// SetNotifier sets the notifications service for native notifications.
|
||||
func (s *Service) SetNotifier(notifier *notifications.NotificationService) {
|
||||
s.notifier = notifier
|
||||
}
|
||||
|
||||
// ShowNotification displays a native system notification.
|
||||
// Falls back to dialog if notifier is not available.
|
||||
func (s *Service) ShowNotification(opts NotificationOptions) error {
|
||||
// Try native notification first
|
||||
if s.notifier != nil {
|
||||
// Generate ID if not provided
|
||||
id := opts.ID
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("core-%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
return s.notifier.SendNotification(notifications.NotificationOptions{
|
||||
ID: id,
|
||||
Title: opts.Title,
|
||||
Subtitle: opts.Subtitle,
|
||||
Body: opts.Message,
|
||||
})
|
||||
}
|
||||
|
||||
// Fall back to dialog-based notification
|
||||
return s.showDialogNotification(opts)
|
||||
}
|
||||
|
||||
// showDialogNotification shows a notification using dialogs as fallback.
|
||||
func (s *Service) showDialogNotification(opts NotificationOptions) error {
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return fmt.Errorf("application not available")
|
||||
}
|
||||
|
||||
// Build message with optional subtitle
|
||||
msg := opts.Message
|
||||
if opts.Subtitle != "" {
|
||||
msg = opts.Subtitle + "\n\n" + msg
|
||||
}
|
||||
|
||||
dialog := app.Dialog.Info()
|
||||
dialog.SetTitle(opts.Title)
|
||||
dialog.SetMessage(msg)
|
||||
dialog.Show()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShowInfoNotification shows an info notification with a simple message.
|
||||
func (s *Service) ShowInfoNotification(title, message string) error {
|
||||
return s.ShowNotification(NotificationOptions{
|
||||
Title: title,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// ShowWarningNotification shows a warning notification.
|
||||
func (s *Service) ShowWarningNotification(title, message string) error {
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return fmt.Errorf("application not available")
|
||||
}
|
||||
|
||||
dialog := app.Dialog.Warning()
|
||||
dialog.SetTitle(title)
|
||||
dialog.SetMessage(message)
|
||||
dialog.Show()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShowErrorNotification shows an error notification.
|
||||
func (s *Service) ShowErrorNotification(title, message string) error {
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return fmt.Errorf("application not available")
|
||||
}
|
||||
|
||||
dialog := app.Dialog.Error()
|
||||
dialog.SetTitle(title)
|
||||
dialog.SetMessage(message)
|
||||
dialog.Show()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestNotificationPermission requests permission for native notifications.
|
||||
func (s *Service) RequestNotificationPermission() (bool, error) {
|
||||
if s.notifier == nil {
|
||||
return false, fmt.Errorf("notification service not available")
|
||||
}
|
||||
|
||||
granted, err := s.notifier.RequestNotificationAuthorization()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to request notification permission: %w", err)
|
||||
}
|
||||
|
||||
return granted, nil
|
||||
}
|
||||
|
||||
// CheckNotificationPermission checks if notifications are authorized.
|
||||
func (s *Service) CheckNotificationPermission() (bool, error) {
|
||||
if s.notifier == nil {
|
||||
return false, fmt.Errorf("notification service not available")
|
||||
}
|
||||
|
||||
return s.notifier.CheckNotificationAuthorization()
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
package display
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// ThemeInfo contains information about the current theme.
|
||||
type ThemeInfo struct {
|
||||
IsDark bool `json:"isDark"`
|
||||
Theme string `json:"theme"` // "dark" or "light"
|
||||
System bool `json:"system"` // Whether following system theme
|
||||
}
|
||||
|
||||
// GetTheme returns the current application theme.
|
||||
func (s *Service) GetTheme() ThemeInfo {
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return ThemeInfo{Theme: "unknown"}
|
||||
}
|
||||
|
||||
isDark := app.Env.IsDarkMode()
|
||||
theme := "light"
|
||||
if isDark {
|
||||
theme = "dark"
|
||||
}
|
||||
|
||||
return ThemeInfo{
|
||||
IsDark: isDark,
|
||||
Theme: theme,
|
||||
System: true, // Wails follows system theme by default
|
||||
}
|
||||
}
|
||||
|
||||
// GetSystemTheme returns the system's theme preference.
|
||||
// This is the same as GetTheme since Wails follows the system theme.
|
||||
func (s *Service) GetSystemTheme() ThemeInfo {
|
||||
return s.GetTheme()
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
// pkg/display/types.go
|
||||
package display
|
||||
|
||||
// EventSource abstracts the application event system (Wails insulation for WSEventManager).
|
||||
// WSEventManager receives this instead of calling application.Get() directly.
|
||||
type EventSource interface {
|
||||
OnThemeChange(handler func(isDark bool)) func()
|
||||
Emit(name string, data ...any) bool
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue