diff --git a/.trellis/tasks/04-18-04-18-finish-windows-desktop/task.json b/.trellis/tasks/04-18-04-18-finish-windows-desktop/task.json index 2538ac4..1ea5ea0 100644 --- a/.trellis/tasks/04-18-04-18-finish-windows-desktop/task.json +++ b/.trellis/tasks/04-18-04-18-finish-windows-desktop/task.json @@ -3,19 +3,19 @@ "name": "04-18-finish-windows-desktop", "title": "Finish Windows desktop integration", "description": "", - "status": "planning", - "dev_type": null, - "scope": null, + "status": "completed", + "dev_type": "feature", + "scope": "desktop", "package": null, "priority": "P2", "creator": "OpenCode", "assignee": "OpenCode", "createdAt": "2026-04-18", - "completedAt": null, - "branch": null, + "completedAt": "2026-04-20", + "branch": "master", "base_branch": "master", "worktree_path": null, - "current_phase": 0, + "current_phase": 6, "next_action": [ { "phase": 1, @@ -47,7 +47,32 @@ "subtasks": [], "children": [], "parent": null, - "relatedFiles": [], - "notes": "", - "meta": {} -} \ No newline at end of file + "relatedFiles": [ + "build/windows/icon.ico", + "frontend/assets.go", + "frontend/src/App.tsx", + "frontend/src/api.ts", + "frontend/src/i18n/locales/en.json", + "frontend/src/i18n/locales/zh-CN.json", + "frontend/src/styles.css", + "internal/desktop/app.go", + "internal/desktop/runtime_wails.go", + "internal/desktop/tray_wails.go", + "internal/desktop/tray_language.go", + "internal/desktop/tray_language_windows.go", + "internal/desktop/wails.go" + ], + "notes": "Finished the Windows desktop integration pass. Reworked tray startup onto a stable systray event loop with icon embedding, click-to-open behavior, localized menu labels, and Windows system-language fallback. Replaced desktop/window branding assets, wired repository opening through the system browser, and refreshed the providers and aliases list/detail panels into a denser, product-style UI with updated i18n strings.", + "meta": { + "tests": [ + "npm run build", + "go test -tags desktop_wails ./internal/desktop/..." + ], + "desktopAreas": [ + "windows tray", + "window icon and branding", + "providers panel", + "aliases panel" + ] + } +} diff --git a/frontend/assets.go b/frontend/assets.go index aa767c3..4fffae6 100644 --- a/frontend/assets.go +++ b/frontend/assets.go @@ -2,12 +2,65 @@ package frontend import ( "embed" + "encoding/json" + "fmt" "io/fs" + "sync" ) //go:embed dist/* var assets embed.FS +//go:embed src/i18n/locales/*.json +var localeAssets embed.FS + +var ( + localeCache = map[string]map[string]any{} + localeCacheMu sync.RWMutex +) + func DistFS() (fs.FS, error) { return fs.Sub(assets, "dist") } + +func LocaleValue(language string, path ...string) (string, bool, error) { + locale, err := loadLocale(language) + if err != nil { + return "", false, err + } + var current any = locale + for _, key := range path { + next, ok := current.(map[string]any) + if !ok { + return "", false, nil + } + current, ok = next[key] + if !ok { + return "", false, nil + } + } + value, ok := current.(string) + return value, ok, nil +} + +func loadLocale(language string) (map[string]any, error) { + localeCacheMu.RLock() + if locale, ok := localeCache[language]; ok { + localeCacheMu.RUnlock() + return locale, nil + } + localeCacheMu.RUnlock() + + data, err := localeAssets.ReadFile(fmt.Sprintf("src/i18n/locales/%s.json", language)) + if err != nil { + return nil, err + } + var locale map[string]any + if err := json.Unmarshal(data, &locale); err != nil { + return nil, err + } + localeCacheMu.Lock() + localeCache[language] = locale + localeCacheMu.Unlock() + return locale, nil +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 21125d8..fe22835 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,6 +20,7 @@ import { saveDesktopPrefs, saveProxySettings, saveProvider, + openExternalURL, setAliasTargetState, setProviderState, startProxy, @@ -27,6 +28,7 @@ import { unbindAliasTarget, } from './api' import i18n, { resolveLanguagePreference } from './i18n' +import githubMark from './assets/GitHub_Invertocat_Black_Clearspace.png' import type { AliasTargetInput, AliasView, @@ -85,6 +87,7 @@ type ConfirmIntent = | { kind: 'unbind-target'; alias: string; provider: string; model: string } const tabs: TabKey[] = ['overview', 'providers', 'aliases', 'log', 'network', 'sync', 'settings'] +const GITHUB_REPOSITORY_URL = 'https://github.com/Apale7/opencode-provider-switch' const emptyPrefs: DesktopPrefsView = { launchAtLogin: false, @@ -1105,6 +1108,13 @@ export default function App() { : t('confirm.unbindTargetTitle') : '' + function openRepository() { + if (!GITHUB_REPOSITORY_URL) { + return + } + void openExternalURL(GITHUB_REPOSITORY_URL) + } + const confirmMessage = confirmIntent ? confirmIntent.kind === 'delete-provider' ? t('messages.confirmDeleteProvider', { id: confirmIntent.id }) @@ -1131,8 +1141,19 @@ export default function App() {