fix(desktop): finish Windows shell polish
Stabilize the tray icon and locale handling on Windows while aligning provider and alias management panels with the refreshed desktop branding.
This commit is contained in:
parent
b70f49cbef
commit
8fc72e87e4
@ -3,19 +3,19 @@
|
|||||||
"name": "04-18-finish-windows-desktop",
|
"name": "04-18-finish-windows-desktop",
|
||||||
"title": "Finish Windows desktop integration",
|
"title": "Finish Windows desktop integration",
|
||||||
"description": "",
|
"description": "",
|
||||||
"status": "planning",
|
"status": "completed",
|
||||||
"dev_type": null,
|
"dev_type": "feature",
|
||||||
"scope": null,
|
"scope": "desktop",
|
||||||
"package": null,
|
"package": null,
|
||||||
"priority": "P2",
|
"priority": "P2",
|
||||||
"creator": "OpenCode",
|
"creator": "OpenCode",
|
||||||
"assignee": "OpenCode",
|
"assignee": "OpenCode",
|
||||||
"createdAt": "2026-04-18",
|
"createdAt": "2026-04-18",
|
||||||
"completedAt": null,
|
"completedAt": "2026-04-20",
|
||||||
"branch": null,
|
"branch": "master",
|
||||||
"base_branch": "master",
|
"base_branch": "master",
|
||||||
"worktree_path": null,
|
"worktree_path": null,
|
||||||
"current_phase": 0,
|
"current_phase": 6,
|
||||||
"next_action": [
|
"next_action": [
|
||||||
{
|
{
|
||||||
"phase": 1,
|
"phase": 1,
|
||||||
@ -47,7 +47,32 @@
|
|||||||
"subtasks": [],
|
"subtasks": [],
|
||||||
"children": [],
|
"children": [],
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"relatedFiles": [],
|
"relatedFiles": [
|
||||||
"notes": "",
|
"build/windows/icon.ico",
|
||||||
"meta": {}
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,12 +2,65 @@ package frontend
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
var assets embed.FS
|
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) {
|
func DistFS() (fs.FS, error) {
|
||||||
return fs.Sub(assets, "dist")
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
saveDesktopPrefs,
|
saveDesktopPrefs,
|
||||||
saveProxySettings,
|
saveProxySettings,
|
||||||
saveProvider,
|
saveProvider,
|
||||||
|
openExternalURL,
|
||||||
setAliasTargetState,
|
setAliasTargetState,
|
||||||
setProviderState,
|
setProviderState,
|
||||||
startProxy,
|
startProxy,
|
||||||
@ -27,6 +28,7 @@ import {
|
|||||||
unbindAliasTarget,
|
unbindAliasTarget,
|
||||||
} from './api'
|
} from './api'
|
||||||
import i18n, { resolveLanguagePreference } from './i18n'
|
import i18n, { resolveLanguagePreference } from './i18n'
|
||||||
|
import githubMark from './assets/GitHub_Invertocat_Black_Clearspace.png'
|
||||||
import type {
|
import type {
|
||||||
AliasTargetInput,
|
AliasTargetInput,
|
||||||
AliasView,
|
AliasView,
|
||||||
@ -85,6 +87,7 @@ type ConfirmIntent =
|
|||||||
| { kind: 'unbind-target'; alias: string; provider: string; model: string }
|
| { kind: 'unbind-target'; alias: string; provider: string; model: string }
|
||||||
|
|
||||||
const tabs: TabKey[] = ['overview', 'providers', 'aliases', 'log', 'network', 'sync', 'settings']
|
const tabs: TabKey[] = ['overview', 'providers', 'aliases', 'log', 'network', 'sync', 'settings']
|
||||||
|
const GITHUB_REPOSITORY_URL = 'https://github.com/Apale7/opencode-provider-switch'
|
||||||
|
|
||||||
const emptyPrefs: DesktopPrefsView = {
|
const emptyPrefs: DesktopPrefsView = {
|
||||||
launchAtLogin: false,
|
launchAtLogin: false,
|
||||||
@ -1105,6 +1108,13 @@ export default function App() {
|
|||||||
: t('confirm.unbindTargetTitle')
|
: t('confirm.unbindTargetTitle')
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
|
function openRepository() {
|
||||||
|
if (!GITHUB_REPOSITORY_URL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void openExternalURL(GITHUB_REPOSITORY_URL)
|
||||||
|
}
|
||||||
|
|
||||||
const confirmMessage = confirmIntent
|
const confirmMessage = confirmIntent
|
||||||
? confirmIntent.kind === 'delete-provider'
|
? confirmIntent.kind === 'delete-provider'
|
||||||
? t('messages.confirmDeleteProvider', { id: confirmIntent.id })
|
? t('messages.confirmDeleteProvider', { id: confirmIntent.id })
|
||||||
@ -1131,8 +1141,19 @@ export default function App() {
|
|||||||
<aside className="sidebar">
|
<aside className="sidebar">
|
||||||
<div className="sidebar-brand">
|
<div className="sidebar-brand">
|
||||||
<div className="sidebar-brand-line" />
|
<div className="sidebar-brand-line" />
|
||||||
<p className="eyebrow">{t('app.eyebrow')}</p>
|
<div className="sidebar-brand-header">
|
||||||
<h1>{t('app.brand')}</h1>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="brand-github"
|
||||||
|
onClick={openRepository}
|
||||||
|
disabled={!GITHUB_REPOSITORY_URL}
|
||||||
|
aria-label="Open project repository"
|
||||||
|
title={GITHUB_REPOSITORY_URL || 'Set GITHUB_REPOSITORY_URL in App.tsx'}
|
||||||
|
>
|
||||||
|
<img src={githubMark} alt="" />
|
||||||
|
</button>
|
||||||
|
<h1>{t('app.brand')}</h1>
|
||||||
|
</div>
|
||||||
<div className="brand-meta">
|
<div className="brand-meta">
|
||||||
<span className="badge">v{meta.version || t('app.dev')}</span>
|
<span className="badge">v{meta.version || t('app.dev')}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -1282,51 +1303,58 @@ export default function App() {
|
|||||||
: t('providers.subtitle')}
|
: t('providers.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="toolbar toolbar-end">
|
<div className="list-header-actions">
|
||||||
<span className="subtle">
|
<span className="subtle list-status-text">
|
||||||
{providerStatus ||
|
{providerStatus ||
|
||||||
t('providers.listCount', { shown: filteredProviders.length, total: providers.length })}
|
t('providers.listCount', { shown: filteredProviders.length, total: providers.length })}
|
||||||
</span>
|
</span>
|
||||||
{providerDetailOpen ? (
|
<div className="toolbar list-toolbar-actions">
|
||||||
<button type="button" onClick={closeProviderDetail}>
|
{providerDetailOpen ? (
|
||||||
{t('actions.close')}
|
<button type="button" onClick={closeProviderDetail}>
|
||||||
|
{t('actions.close')}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button type="button" className="primary" onClick={openProviderCreateModal}>
|
||||||
|
{t('actions.newProvider')}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
<button type="button" onClick={openProviderImportModal}>
|
||||||
<button type="button" className="primary" onClick={openProviderCreateModal}>
|
{t('actions.import')}
|
||||||
{t('actions.newProvider')}
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<button type="button" onClick={openProviderImportModal}>
|
|
||||||
{t('actions.import')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="list-toolbar">
|
<div className="list-toolbar">
|
||||||
<label>
|
<div className="filter-group filter-group-search">
|
||||||
<span>{t('providers.search')}</span>
|
<span className="filter-group-label">{t('providers.search')}</span>
|
||||||
<input
|
<label className="search-field">
|
||||||
type="text"
|
<input
|
||||||
value={providerQuery}
|
type="text"
|
||||||
onChange={(event) => setProviderQuery(event.target.value)}
|
value={providerQuery}
|
||||||
placeholder={t('providers.searchPlaceholder')}
|
onChange={(event) => setProviderQuery(event.target.value)}
|
||||||
/>
|
placeholder={t('providers.searchPlaceholder')}
|
||||||
</label>
|
/>
|
||||||
<label>
|
</label>
|
||||||
<span>{t('providers.filter')}</span>
|
</div>
|
||||||
<select
|
<div className="filter-group filter-group-select">
|
||||||
value={providerFilter}
|
<span className="filter-group-label">{t('providers.filter')}</span>
|
||||||
onChange={(event) => setProviderFilter(event.target.value as FilterState)}
|
<label>
|
||||||
>
|
<select
|
||||||
<option value="all">{t('providers.filterAll')}</option>
|
value={providerFilter}
|
||||||
<option value="enabled">{t('providers.filterEnabled')}</option>
|
onChange={(event) => setProviderFilter(event.target.value as FilterState)}
|
||||||
<option value="disabled">{t('providers.filterDisabled')}</option>
|
>
|
||||||
</select>
|
<option value="all">{t('providers.filterAll')}</option>
|
||||||
</label>
|
<option value="enabled">{t('providers.filterEnabled')}</option>
|
||||||
|
<option value="disabled">{t('providers.filterDisabled')}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scroll-list compact-list">
|
<div className="scroll-list compact-list">
|
||||||
{providers.length === 0 ? (
|
{providers.length === 0 ? (
|
||||||
<article className="empty-card">
|
<article className="empty-card">
|
||||||
|
<div className="empty-illustration" aria-hidden="true">◌</div>
|
||||||
<span className="empty-kicker">{t('providers.title')}</span>
|
<span className="empty-kicker">{t('providers.title')}</span>
|
||||||
<h4>{t('providers.empty')}</h4>
|
<h4>{t('providers.empty')}</h4>
|
||||||
<p className="subtle">{t('providers.emptyHint')}</p>
|
<p className="subtle">{t('providers.emptyHint')}</p>
|
||||||
@ -1350,22 +1378,36 @@ export default function App() {
|
|||||||
<button
|
<button
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={`compact-row ${providerDetailOpen && selectedProviderId === provider.id ? 'active' : ''}`}
|
className={`resource-card ${providerDetailOpen && selectedProviderId === provider.id ? 'active' : ''}`}
|
||||||
onClick={() => onEditProvider(provider)}
|
onClick={() => onEditProvider(provider)}
|
||||||
>
|
>
|
||||||
<div className="compact-row-main">
|
<div className="resource-card-top">
|
||||||
<div className="compact-row-titleblock">
|
<div className="resource-card-heading">
|
||||||
<strong>{provider.name || provider.id}</strong>
|
<div className="resource-card-titlewrap">
|
||||||
<code>{provider.id}</code>
|
<strong className="resource-card-title">{provider.name || provider.id}</strong>
|
||||||
|
<code className="resource-card-code">{provider.id}</code>
|
||||||
|
</div>
|
||||||
|
<p className="resource-card-subtitle">{provider.baseUrl}</p>
|
||||||
|
</div>
|
||||||
|
<div className="resource-card-side">
|
||||||
|
<span className={`badge status-badge ${provider.disabled ? 'idle' : 'live'}`}>
|
||||||
|
{provider.disabled ? t('status.disabled') : t('status.enabled')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={`badge ${provider.disabled ? 'idle' : 'live'}`}>
|
|
||||||
{provider.disabled ? t('status.disabled') : t('status.enabled')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="compact-row-summary compact-row-summary-grid">
|
<div className="resource-card-meta">
|
||||||
<span>{provider.baseUrl}</span>
|
<div className="resource-meta-item resource-meta-route">
|
||||||
<span>{t('providers.modelsCount', { count: provider.models?.length || 0 })}</span>
|
<span className="resource-meta-label">{t('providers.cardBaseUrl')}</span>
|
||||||
<span>{provider.apiKeyMasked || t('providers.apiKeyNotSet')}</span>
|
<span className="resource-meta-value">{provider.baseUrl}</span>
|
||||||
|
</div>
|
||||||
|
<div className="resource-meta-item">
|
||||||
|
<span className="resource-meta-label">{t('providers.cardModels')}</span>
|
||||||
|
<span className="resource-meta-value">{t('providers.modelsCount', { count: provider.models?.length || 0 })}</span>
|
||||||
|
</div>
|
||||||
|
<div className="resource-meta-item">
|
||||||
|
<span className="resource-meta-label">{t('providers.cardApiKey')}</span>
|
||||||
|
<span className="resource-meta-value">{provider.apiKeyMasked || t('providers.apiKeyNotSet')}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@ -1390,39 +1432,44 @@ export default function App() {
|
|||||||
: t('aliases.subtitle')}
|
: t('aliases.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="toolbar toolbar-end">
|
<div className="list-header-actions">
|
||||||
<span className="subtle">
|
<span className="subtle list-status-text">
|
||||||
{aliasStatus || t('aliases.listCount', { shown: filteredAliases.length, total: aliases.length })}
|
{aliasStatus || t('aliases.listCount', { shown: filteredAliases.length, total: aliases.length })}
|
||||||
</span>
|
</span>
|
||||||
{aliasDetailOpen ? (
|
<div className="toolbar list-toolbar-actions">
|
||||||
<button type="button" onClick={closeAliasDetail}>
|
{aliasDetailOpen ? (
|
||||||
{t('actions.close')}
|
<button type="button" onClick={closeAliasDetail}>
|
||||||
|
{t('actions.close')}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button type="button" className="primary" onClick={openAliasCreateModal}>
|
||||||
|
{t('actions.newAlias')}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
<button type="button" onClick={() => openAliasTargetModal()}>
|
||||||
<button type="button" className="primary" onClick={openAliasCreateModal}>
|
{t('actions.bind')}
|
||||||
{t('actions.newAlias')}
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<button type="button" onClick={() => openAliasTargetModal()}>
|
|
||||||
{t('actions.bind')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="list-toolbar list-toolbar-single">
|
<div className="list-toolbar list-toolbar-single">
|
||||||
<label>
|
<div className="filter-group filter-group-search">
|
||||||
<span>{t('aliases.search')}</span>
|
<span className="filter-group-label">{t('aliases.search')}</span>
|
||||||
<input
|
<label className="search-field">
|
||||||
type="text"
|
<input
|
||||||
value={aliasQuery}
|
type="text"
|
||||||
onChange={(event) => setAliasQuery(event.target.value)}
|
value={aliasQuery}
|
||||||
placeholder={t('aliases.searchPlaceholder')}
|
onChange={(event) => setAliasQuery(event.target.value)}
|
||||||
/>
|
placeholder={t('aliases.searchPlaceholder')}
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scroll-list compact-list">
|
<div className="scroll-list compact-list">
|
||||||
{aliases.length === 0 ? (
|
{aliases.length === 0 ? (
|
||||||
<article className="empty-card">
|
<article className="empty-card">
|
||||||
|
<div className="empty-illustration" aria-hidden="true">◎</div>
|
||||||
<span className="empty-kicker">{t('aliases.title')}</span>
|
<span className="empty-kicker">{t('aliases.title')}</span>
|
||||||
<h4>{t('aliases.empty')}</h4>
|
<h4>{t('aliases.empty')}</h4>
|
||||||
<p className="subtle">{t('aliases.emptyHint')}</p>
|
<p className="subtle">{t('aliases.emptyHint')}</p>
|
||||||
@ -1446,22 +1493,42 @@ export default function App() {
|
|||||||
<button
|
<button
|
||||||
key={alias.alias}
|
key={alias.alias}
|
||||||
type="button"
|
type="button"
|
||||||
className={`compact-row ${aliasDetailOpen && selectedAliasId === alias.alias ? 'active' : ''}`}
|
className={`resource-card ${aliasDetailOpen && selectedAliasId === alias.alias ? 'active' : ''}`}
|
||||||
onClick={() => onEditAlias(alias)}
|
onClick={() => onEditAlias(alias)}
|
||||||
>
|
>
|
||||||
<div className="compact-row-main">
|
<div className="resource-card-top">
|
||||||
<div className="compact-row-titleblock">
|
<div className="resource-card-heading">
|
||||||
<strong>{alias.displayName || alias.alias}</strong>
|
<div className="resource-card-titlewrap">
|
||||||
<code>{alias.alias}</code>
|
<strong className="resource-card-title">{alias.displayName || alias.alias}</strong>
|
||||||
|
<code className="resource-card-code">{alias.alias}</code>
|
||||||
|
</div>
|
||||||
|
<p className="resource-card-subtitle">
|
||||||
|
{alias.targets[0]
|
||||||
|
? `${alias.targets[0].provider}/${alias.targets[0].model}`
|
||||||
|
: t('aliases.noTargets')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="resource-card-side">
|
||||||
|
<span className={`badge status-badge ${alias.enabled ? 'live' : 'idle'}`}>
|
||||||
|
{alias.enabled ? t('status.enabled') : t('status.disabled')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={`badge ${alias.enabled ? 'live' : 'idle'}`}>
|
|
||||||
{alias.enabled ? t('status.enabled') : t('status.disabled')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="compact-row-summary compact-row-summary-grid">
|
<div className="resource-card-meta">
|
||||||
<span>{t('aliases.routable', { available: alias.availableTargetCount, total: alias.targetCount })}</span>
|
<div className="resource-meta-item resource-meta-route">
|
||||||
<span>{t('aliases.targetsCount', { count: alias.targets.length })}</span>
|
<span className="resource-meta-label">{t('aliases.cardRoute')}</span>
|
||||||
<span>{alias.targets[0] ? `${alias.targets[0].provider}/${alias.targets[0].model}` : t('aliases.noTargets')}</span>
|
<span className="resource-meta-value">{t('aliases.routable', { available: alias.availableTargetCount, total: alias.targetCount })}</span>
|
||||||
|
</div>
|
||||||
|
<div className="resource-meta-item">
|
||||||
|
<span className="resource-meta-label">{t('aliases.cardTargets')}</span>
|
||||||
|
<span className="resource-meta-value">{t('aliases.targetsCount', { count: alias.targets.length })}</span>
|
||||||
|
</div>
|
||||||
|
<div className="resource-meta-item">
|
||||||
|
<span className="resource-meta-label">{t('aliases.cardPrimary')}</span>
|
||||||
|
<span className="resource-meta-value">
|
||||||
|
{alias.targets[0] ? `${alias.targets[0].provider}/${alias.targets[0].model}` : t('aliases.noTargets')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@ -2047,86 +2114,114 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="stack-blocks" onSubmit={(event) => void onSaveProvider(event)}>
|
<form className="stack-blocks" onSubmit={(event) => void onSaveProvider(event)}>
|
||||||
<div className="inline-meta compact-inline-meta">
|
<section className="detail-hero detail-hero-grid">
|
||||||
<div>
|
<div className="detail-hero-card detail-hero-primary">
|
||||||
<span className="meta-label">{t('providers.baseUrl')}</span>
|
<span className="meta-label">{t('providers.name')}</span>
|
||||||
<strong>{selectedProvider?.baseUrl || providerForm.baseUrl || '-'}</strong>
|
<strong>{providerForm.name || providerForm.id || t('providers.formCreateTitle')}</strong>
|
||||||
|
<p className="subtle detail-hero-subtle">{selectedProvider?.baseUrl || providerForm.baseUrl || '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="detail-hero-card">
|
||||||
|
<span className="meta-label">{t('status.status')}</span>
|
||||||
|
<strong>{providerForm.disabled ? t('status.disabled') : t('status.enabled')}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="detail-hero-card">
|
||||||
<span className="meta-label">{t('providers.models')}</span>
|
<span className="meta-label">{t('providers.models')}</span>
|
||||||
<strong>{selectedProvider?.models?.join(', ') || t('providers.modelsNone')}</strong>
|
<strong>{t('providers.modelsCount', { count: selectedProvider?.models?.length || 0 })}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<label>
|
|
||||||
<span>{t('providers.id')}</span>
|
<section className="detail-section">
|
||||||
<input
|
<div className="detail-section-header">
|
||||||
type="text"
|
<div>
|
||||||
value={providerForm.id}
|
<h4>{t('providers.detailBasicsTitle')}</h4>
|
||||||
onChange={(event) => setProviderForm((current) => ({ ...current, id: event.target.value }))}
|
<p className="subtle">{t('providers.detailHint')}</p>
|
||||||
placeholder={t('providers.placeholderId')}
|
</div>
|
||||||
/>
|
</div>
|
||||||
</label>
|
<div className="detail-form-grid">
|
||||||
<label>
|
<label>
|
||||||
<span>{t('providers.name')}</span>
|
<span>{t('providers.id')}</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={providerForm.name}
|
value={providerForm.id}
|
||||||
onChange={(event) => setProviderForm((current) => ({ ...current, name: event.target.value }))}
|
onChange={(event) => setProviderForm((current) => ({ ...current, id: event.target.value }))}
|
||||||
placeholder={t('providers.placeholderName')}
|
placeholder={t('providers.placeholderId')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>{t('providers.baseUrl')}</span>
|
<span>{t('providers.name')}</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={providerForm.baseUrl}
|
value={providerForm.name}
|
||||||
onChange={(event) => setProviderForm((current) => ({ ...current, baseUrl: event.target.value }))}
|
onChange={(event) => setProviderForm((current) => ({ ...current, name: event.target.value }))}
|
||||||
placeholder={t('providers.placeholderBaseUrl')}
|
placeholder={t('providers.placeholderName')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label className="detail-form-span">
|
||||||
<span>{t('providers.apiKey')}</span>
|
<span>{t('providers.baseUrl')}</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={providerForm.apiKey}
|
value={providerForm.baseUrl}
|
||||||
onChange={(event) => setProviderForm((current) => ({ ...current, apiKey: event.target.value }))}
|
onChange={(event) => setProviderForm((current) => ({ ...current, baseUrl: event.target.value }))}
|
||||||
placeholder={t('providers.placeholderApiKey')}
|
placeholder={t('providers.placeholderBaseUrl')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label className="detail-form-span">
|
||||||
<span>{t('providers.headers')}</span>
|
<span>{t('providers.apiKey')}</span>
|
||||||
<textarea
|
<input
|
||||||
value={providerForm.headersText}
|
type="text"
|
||||||
onChange={(event) => setProviderForm((current) => ({ ...current, headersText: event.target.value }))}
|
value={providerForm.apiKey}
|
||||||
placeholder={t('providers.placeholderHeaders')}
|
onChange={(event) => setProviderForm((current) => ({ ...current, apiKey: event.target.value }))}
|
||||||
rows={4}
|
placeholder={t('providers.placeholderApiKey')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="checkbox-row">
|
<label className="detail-form-span">
|
||||||
<input
|
<span>{t('providers.headers')}</span>
|
||||||
type="checkbox"
|
<textarea
|
||||||
checked={providerForm.disabled}
|
value={providerForm.headersText}
|
||||||
onChange={(event) => setProviderForm((current) => ({ ...current, disabled: event.target.checked }))}
|
onChange={(event) => setProviderForm((current) => ({ ...current, headersText: event.target.value }))}
|
||||||
/>
|
placeholder={t('providers.placeholderHeaders')}
|
||||||
<span>{t('providers.saveDisabled')}</span>
|
rows={4}
|
||||||
</label>
|
/>
|
||||||
<label className="checkbox-row">
|
</label>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
</section>
|
||||||
checked={providerForm.skipModels}
|
|
||||||
onChange={(event) => setProviderForm((current) => ({ ...current, skipModels: event.target.checked }))}
|
<section className="detail-section">
|
||||||
/>
|
<div className="detail-section-header">
|
||||||
<span>{t('providers.skipModels')}</span>
|
<div>
|
||||||
</label>
|
<h4>{t('providers.detailTogglesTitle')}</h4>
|
||||||
<label className="checkbox-row">
|
<p className="subtle">{selectedProvider?.models?.join(', ') || t('providers.modelsNone')}</p>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
</div>
|
||||||
checked={providerForm.clearHeaders}
|
<div className="toggle-grid">
|
||||||
onChange={(event) => setProviderForm((current) => ({ ...current, clearHeaders: event.target.checked }))}
|
<label className="checkbox-row checkbox-card">
|
||||||
/>
|
<input
|
||||||
<span>{t('providers.clearHeaders')}</span>
|
type="checkbox"
|
||||||
</label>
|
checked={providerForm.disabled}
|
||||||
<div className="toolbar">
|
onChange={(event) => setProviderForm((current) => ({ ...current, disabled: event.target.checked }))}
|
||||||
|
/>
|
||||||
|
<span>{t('providers.saveDisabled')}</span>
|
||||||
|
</label>
|
||||||
|
<label className="checkbox-row checkbox-card">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={providerForm.skipModels}
|
||||||
|
onChange={(event) => setProviderForm((current) => ({ ...current, skipModels: event.target.checked }))}
|
||||||
|
/>
|
||||||
|
<span>{t('providers.skipModels')}</span>
|
||||||
|
</label>
|
||||||
|
<label className="checkbox-row checkbox-card detail-form-span">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={providerForm.clearHeaders}
|
||||||
|
onChange={(event) => setProviderForm((current) => ({ ...current, clearHeaders: event.target.checked }))}
|
||||||
|
/>
|
||||||
|
<span>{t('providers.clearHeaders')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="toolbar detail-actions">
|
||||||
<button type="submit" className="primary">
|
<button type="submit" className="primary">
|
||||||
{t('actions.save')}
|
{t('actions.save')}
|
||||||
</button>
|
</button>
|
||||||
@ -2139,9 +2234,9 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
resetProviderForm()
|
resetProviderForm()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('actions.reset')}
|
{t('actions.reset')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -2255,34 +2350,63 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stack-blocks">
|
<div className="stack-blocks">
|
||||||
<form className="stack" onSubmit={(event) => void onSaveAlias(event)}>
|
<section className="detail-hero detail-hero-grid">
|
||||||
<label>
|
<div className="detail-hero-card detail-hero-primary">
|
||||||
<span>{t('aliases.alias')}</span>
|
<span className="meta-label">{t('aliases.alias')}</span>
|
||||||
<input
|
<strong>{aliasForm.displayName || aliasForm.alias || t('aliases.formCreateTitle')}</strong>
|
||||||
type="text"
|
<p className="subtle detail-hero-subtle">{aliasForm.alias || '-'}</p>
|
||||||
value={aliasForm.alias}
|
</div>
|
||||||
onChange={(event) => setAliasForm((current) => ({ ...current, alias: event.target.value }))}
|
<div className="detail-hero-card">
|
||||||
placeholder={t('aliases.placeholderAlias')}
|
<span className="meta-label">{t('status.status')}</span>
|
||||||
/>
|
<strong>{aliasForm.disabled ? t('status.disabled') : t('status.enabled')}</strong>
|
||||||
</label>
|
</div>
|
||||||
<label>
|
<div className="detail-hero-card">
|
||||||
<span>{t('aliases.displayName')}</span>
|
<span className="meta-label">{t('aliases.targets')}</span>
|
||||||
<input
|
<strong>{t('aliases.targetsCount', { count: selectedAlias?.targets.length || 0 })}</strong>
|
||||||
type="text"
|
</div>
|
||||||
value={aliasForm.displayName}
|
</section>
|
||||||
onChange={(event) => setAliasForm((current) => ({ ...current, displayName: event.target.value }))}
|
|
||||||
placeholder={t('aliases.placeholderDisplayName')}
|
<form className="stack-blocks" onSubmit={(event) => void onSaveAlias(event)}>
|
||||||
/>
|
<section className="detail-section">
|
||||||
</label>
|
<div className="detail-section-header">
|
||||||
<label className="checkbox-row">
|
<div>
|
||||||
<input
|
<h4>{t('aliases.detailBasicsTitle')}</h4>
|
||||||
type="checkbox"
|
<p className="subtle">{t('aliases.detailHint')}</p>
|
||||||
checked={aliasForm.disabled}
|
</div>
|
||||||
onChange={(event) => setAliasForm((current) => ({ ...current, disabled: event.target.checked }))}
|
</div>
|
||||||
/>
|
<div className="detail-form-grid">
|
||||||
<span>{t('aliases.createDisabled')}</span>
|
<label>
|
||||||
</label>
|
<span>{t('aliases.alias')}</span>
|
||||||
<div className="toolbar">
|
<input
|
||||||
|
type="text"
|
||||||
|
value={aliasForm.alias}
|
||||||
|
onChange={(event) => setAliasForm((current) => ({ ...current, alias: event.target.value }))}
|
||||||
|
placeholder={t('aliases.placeholderAlias')}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>{t('aliases.displayName')}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={aliasForm.displayName}
|
||||||
|
onChange={(event) => setAliasForm((current) => ({ ...current, displayName: event.target.value }))}
|
||||||
|
placeholder={t('aliases.placeholderDisplayName')}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="toggle-grid">
|
||||||
|
<label className="checkbox-row checkbox-card detail-form-span">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aliasForm.disabled}
|
||||||
|
onChange={(event) => setAliasForm((current) => ({ ...current, disabled: event.target.checked }))}
|
||||||
|
/>
|
||||||
|
<span>{t('aliases.createDisabled')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="toolbar detail-actions">
|
||||||
<button type="submit" className="primary">
|
<button type="submit" className="primary">
|
||||||
{t('actions.save')}
|
{t('actions.save')}
|
||||||
</button>
|
</button>
|
||||||
@ -2301,20 +2425,32 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<section className="subpanel">
|
<section className="detail-section detail-targets-section">
|
||||||
<div className="subpanel-header">
|
<div className="detail-section-header">
|
||||||
<h4>{t('aliases.targets')}</h4>
|
<div>
|
||||||
|
<h4>{t('aliases.targets')}</h4>
|
||||||
|
<p className="subtle">{t('aliases.detailTargetsHint')}</p>
|
||||||
|
</div>
|
||||||
{selectedAlias ? <span className="subtle">{t('aliases.targetsCount', { count: selectedAlias.targets.length })}</span> : null}
|
{selectedAlias ? <span className="subtle">{t('aliases.targetsCount', { count: selectedAlias.targets.length })}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="target-list target-list-compact">
|
<div className="target-list target-list-compact">
|
||||||
{!selectedAlias || selectedAlias.targets.length === 0 ? <p className="subtle">{t('aliases.noTargets')}</p> : null}
|
{!selectedAlias || selectedAlias.targets.length === 0 ? (
|
||||||
|
<article className="empty-card compact-empty detail-empty-card">
|
||||||
|
<div className="empty-illustration" aria-hidden="true">◎</div>
|
||||||
|
<h4>{t('aliases.noTargets')}</h4>
|
||||||
|
<p className="subtle">{t('aliases.emptyHint')}</p>
|
||||||
|
</article>
|
||||||
|
) : null}
|
||||||
{selectedAlias?.targets.map((target) => (
|
{selectedAlias?.targets.map((target) => (
|
||||||
<div className="target-card" key={`${selectedAlias.alias}-${target.provider}-${target.model}`}>
|
<div className="target-card" key={`${selectedAlias.alias}-${target.provider}-${target.model}`}>
|
||||||
<div className="target-card-main">
|
<div className="target-card-main">
|
||||||
<code>
|
<div className="target-card-copy">
|
||||||
{target.provider}/{target.model}
|
<code>
|
||||||
</code>
|
{target.provider}/{target.model}
|
||||||
<span className={`badge ${target.enabled ? 'live' : 'idle'}`}>
|
</code>
|
||||||
|
<span className="subtle target-card-subtitle">{selectedAlias.alias}</span>
|
||||||
|
</div>
|
||||||
|
<span className={`badge status-badge ${target.enabled ? 'live' : 'idle'}`}>
|
||||||
{target.enabled ? t('status.enabled') : t('status.disabled')}
|
{target.enabled ? t('status.enabled') : t('status.disabled')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -62,6 +62,18 @@ export async function getMeta(): Promise<MetaView> {
|
|||||||
return http<MetaView>('/api/meta')
|
return http<MetaView>('/api/meta')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function openExternalURL(url: string): Promise<void> {
|
||||||
|
if (isWails()) {
|
||||||
|
await bridge().OpenExternalURL(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const target = url.trim()
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.open(target, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
|
||||||
export function getOverview(): Promise<Overview> {
|
export function getOverview(): Promise<Overview> {
|
||||||
return isWails() ? bridge().Overview() : http<Overview>('/api/overview')
|
return isWails() ? bridge().Overview() : http<Overview>('/api/overview')
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
frontend/src/assets/GitHub_Invertocat_Black_Clearspace.png
Normal file
BIN
frontend/src/assets/GitHub_Invertocat_Black_Clearspace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
61
frontend/src/assets/icon.svg
Normal file
61
frontend/src/assets/icon.svg
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||||
|
<defs>
|
||||||
|
<!-- 渐变定义 -->
|
||||||
|
<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#1E1E2E" />
|
||||||
|
<stop offset="100%" stop-color="#0F0F16" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="primary-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#00F0FF" />
|
||||||
|
<stop offset="100%" stop-color="#7000FF" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- 发光滤镜 -->
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="8" result="blur" />
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- 文字专属轻度发光滤镜 -->
|
||||||
|
<filter id="text-glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 背景底板 (圆角矩形,适合Windows应用) -->
|
||||||
|
<rect x="16" y="16" width="224" height="224" rx="50" fill="url(#bg-gradient)" stroke="#2A2A3C" stroke-width="4"/>
|
||||||
|
|
||||||
|
<!-- ================= 主体图形 (整体中心上移至 Y=110) ================= -->
|
||||||
|
|
||||||
|
<!-- 左侧输入节点 (代表多个大模型上游) -->
|
||||||
|
<circle cx="60" cy="62" r="10" fill="#00F0FF" opacity="0.8"/>
|
||||||
|
<circle cx="50" cy="110" r="12" fill="#7000FF" opacity="0.8"/>
|
||||||
|
<circle cx="60" cy="158" r="10" fill="#00F0FF" opacity="0.8"/>
|
||||||
|
|
||||||
|
<!-- 连接线 (代表流式转发与故障切换) -->
|
||||||
|
<path d="M 70 62 Q 110 62 128 110" fill="none" stroke="#00F0FF" stroke-width="4" stroke-dasharray="4 4" opacity="0.6"/>
|
||||||
|
<path d="M 62 110 L 128 110" fill="none" stroke="#7000FF" stroke-width="6" opacity="0.8"/>
|
||||||
|
<path d="M 70 158 Q 110 158 128 110" fill="none" stroke="#00F0FF" stroke-width="4" stroke-dasharray="4 4" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- 中心核心/切换器 (代表 OC Switch 本身) -->
|
||||||
|
<circle cx="128" cy="110" r="32" fill="url(#primary-gradient)" filter="url(#glow)"/>
|
||||||
|
<circle cx="128" cy="110" r="24" fill="#1E1E2E"/>
|
||||||
|
|
||||||
|
<!-- 中心 Switch 拨动形态 (代表控制台) -->
|
||||||
|
<rect x="112" y="100" width="32" height="20" rx="10" fill="#2A2A3C"/>
|
||||||
|
<circle cx="132" cy="110" r="6" fill="#00F0FF"/>
|
||||||
|
|
||||||
|
<!-- 右侧单输出 (代表单一稳定的聚合模型别名) -->
|
||||||
|
<path d="M 160 110 L 206 110" fill="none" stroke="url(#primary-gradient)" stroke-width="8" stroke-linecap="round" filter="url(#glow)"/>
|
||||||
|
<polygon points="196,100 212,110 196,120" fill="#7000FF" filter="url(#glow)"/>
|
||||||
|
|
||||||
|
<!-- ================= 底部文字排版 ================= -->
|
||||||
|
<!-- 使用系统无衬线粗体字,现代感强 -->
|
||||||
|
<text x="128" y="200" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" font-size="26" font-weight="900" text-anchor="middle" letter-spacing="2">
|
||||||
|
<!-- OC 部分青色高亮发光 -->
|
||||||
|
<tspan fill="#00F0FF" filter="url(#text-glow)">OC</tspan>
|
||||||
|
<!-- SWITCH 部分纯白对比 -->
|
||||||
|
<tspan fill="#FFFFFF"> SWITCH</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
11
frontend/src/env.d.ts
vendored
11
frontend/src/env.d.ts
vendored
@ -1,11 +1,22 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
go?: {
|
go?: {
|
||||||
desktop?: {
|
desktop?: {
|
||||||
App?: {
|
App?: {
|
||||||
Meta: () => Promise<Record<string, string>>
|
Meta: () => Promise<Record<string, string>>
|
||||||
|
OpenExternalURL: (url: string) => Promise<void>
|
||||||
Overview: () => Promise<import('./types').Overview>
|
Overview: () => Promise<import('./types').Overview>
|
||||||
ExportConfig: () => Promise<import('./types').ConfigExportView>
|
ExportConfig: () => Promise<import('./types').ConfigExportView>
|
||||||
ImportConfig: (input: import('./types').ConfigImportInput) => Promise<import('./types').ConfigImportResult>
|
ImportConfig: (input: import('./types').ConfigImportInput) => Promise<import('./types').ConfigImportResult>
|
||||||
|
|||||||
@ -7,6 +7,25 @@
|
|||||||
"loadingConfig": "loading config",
|
"loadingConfig": "loading config",
|
||||||
"dev": "dev"
|
"dev": "dev"
|
||||||
},
|
},
|
||||||
|
"tray": {
|
||||||
|
"appTitle": "ocswitch",
|
||||||
|
"appTooltip": "ocswitch desktop",
|
||||||
|
"proxyChecking": "Proxy: checking...",
|
||||||
|
"proxyStatusHint": "Current proxy status",
|
||||||
|
"proxyUnavailable": "Proxy: unavailable",
|
||||||
|
"proxyRunning": "Proxy: running (%s)",
|
||||||
|
"proxyStopped": "Proxy: stopped (%s)",
|
||||||
|
"openWindow": "Open window",
|
||||||
|
"openWindowHint": "Show desktop window",
|
||||||
|
"hideWindow": "Hide window",
|
||||||
|
"hideWindowHint": "Hide desktop window",
|
||||||
|
"startProxy": "Start proxy",
|
||||||
|
"startProxyHint": "Start local proxy",
|
||||||
|
"stopProxy": "Stop proxy",
|
||||||
|
"stopProxyHint": "Stop local proxy",
|
||||||
|
"quit": "Quit",
|
||||||
|
"quitHint": "Exit application"
|
||||||
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"overview": { "title": "Overview", "description": "Status, actions, and environment" },
|
"overview": { "title": "Overview", "description": "Status, actions, and environment" },
|
||||||
"providers": { "title": "Providers", "description": "Manage upstream endpoints" },
|
"providers": { "title": "Providers", "description": "Manage upstream endpoints" },
|
||||||
@ -133,6 +152,11 @@
|
|||||||
"overwrite": "Overwrite existing providers",
|
"overwrite": "Overwrite existing providers",
|
||||||
"apiKeyMasked": "API key",
|
"apiKeyMasked": "API key",
|
||||||
"apiKeyNotSet": "not set",
|
"apiKeyNotSet": "not set",
|
||||||
|
"cardBaseUrl": "URL",
|
||||||
|
"cardModels": "Models",
|
||||||
|
"cardApiKey": "API key",
|
||||||
|
"detailBasicsTitle": "Basics",
|
||||||
|
"detailTogglesTitle": "Behavior",
|
||||||
"headersNone": "none",
|
"headersNone": "none",
|
||||||
"modelsNone": "none",
|
"modelsNone": "none",
|
||||||
"modelsCount": "{{count}} models",
|
"modelsCount": "{{count}} models",
|
||||||
@ -185,6 +209,11 @@
|
|||||||
"targets": "Targets",
|
"targets": "Targets",
|
||||||
"targetsCount": "{{count}} targets",
|
"targetsCount": "{{count}} targets",
|
||||||
"routable": "{{available}}/{{total}} routable",
|
"routable": "{{available}}/{{total}} routable",
|
||||||
|
"cardRoute": "Route",
|
||||||
|
"cardTargets": "Targets",
|
||||||
|
"cardPrimary": "Primary",
|
||||||
|
"detailBasicsTitle": "Alias details",
|
||||||
|
"detailTargetsHint": "Manage routable provider/model targets for this alias.",
|
||||||
"statusSaved": "Saved alias {{alias}}",
|
"statusSaved": "Saved alias {{alias}}",
|
||||||
"statusEditing": "Editing alias {{alias}}",
|
"statusEditing": "Editing alias {{alias}}",
|
||||||
"statusDeleting": "Deleting alias {{alias}}...",
|
"statusDeleting": "Deleting alias {{alias}}...",
|
||||||
|
|||||||
@ -7,6 +7,25 @@
|
|||||||
"loadingConfig": "正在读取配置",
|
"loadingConfig": "正在读取配置",
|
||||||
"dev": "dev"
|
"dev": "dev"
|
||||||
},
|
},
|
||||||
|
"tray": {
|
||||||
|
"appTitle": "ocswitch",
|
||||||
|
"appTooltip": "ocswitch desktop",
|
||||||
|
"proxyChecking": "代理:检查中...",
|
||||||
|
"proxyStatusHint": "当前代理状态",
|
||||||
|
"proxyUnavailable": "代理:不可用",
|
||||||
|
"proxyRunning": "代理:运行中(%s)",
|
||||||
|
"proxyStopped": "代理:已停止(%s)",
|
||||||
|
"openWindow": "打开主面板",
|
||||||
|
"openWindowHint": "显示桌面主窗口",
|
||||||
|
"hideWindow": "隐藏窗口",
|
||||||
|
"hideWindowHint": "隐藏桌面主窗口",
|
||||||
|
"startProxy": "启动代理",
|
||||||
|
"startProxyHint": "启动本地代理",
|
||||||
|
"stopProxy": "停止代理",
|
||||||
|
"stopProxyHint": "停止本地代理",
|
||||||
|
"quit": "退出",
|
||||||
|
"quitHint": "退出应用"
|
||||||
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"overview": { "title": "概览", "description": "状态、操作与环境信息" },
|
"overview": { "title": "概览", "description": "状态、操作与环境信息" },
|
||||||
"providers": { "title": "提供商", "description": "管理上游端点" },
|
"providers": { "title": "提供商", "description": "管理上游端点" },
|
||||||
@ -133,6 +152,11 @@
|
|||||||
"overwrite": "覆盖现有提供商",
|
"overwrite": "覆盖现有提供商",
|
||||||
"apiKeyMasked": "API key",
|
"apiKeyMasked": "API key",
|
||||||
"apiKeyNotSet": "未设置",
|
"apiKeyNotSet": "未设置",
|
||||||
|
"cardBaseUrl": "地址",
|
||||||
|
"cardModels": "模型",
|
||||||
|
"cardApiKey": "密钥",
|
||||||
|
"detailBasicsTitle": "基础信息",
|
||||||
|
"detailTogglesTitle": "行为选项",
|
||||||
"headersNone": "无",
|
"headersNone": "无",
|
||||||
"modelsNone": "无",
|
"modelsNone": "无",
|
||||||
"modelsCount": "{{count}} 个模型",
|
"modelsCount": "{{count}} 个模型",
|
||||||
@ -185,6 +209,11 @@
|
|||||||
"targets": "目标",
|
"targets": "目标",
|
||||||
"targetsCount": "{{count}} 个目标",
|
"targetsCount": "{{count}} 个目标",
|
||||||
"routable": "可路由 {{available}}/{{total}}",
|
"routable": "可路由 {{available}}/{{total}}",
|
||||||
|
"cardRoute": "路由",
|
||||||
|
"cardTargets": "目标数",
|
||||||
|
"cardPrimary": "主目标",
|
||||||
|
"detailBasicsTitle": "别名信息",
|
||||||
|
"detailTargetsHint": "为当前别名维护可路由的 provider/model 目标。",
|
||||||
"statusSaved": "已保存别名 {{alias}}",
|
"statusSaved": "已保存别名 {{alias}}",
|
||||||
"statusEditing": "正在编辑别名 {{alias}}",
|
"statusEditing": "正在编辑别名 {{alias}}",
|
||||||
"statusDeleting": "正在删除别名 {{alias}}...",
|
"statusDeleting": "正在删除别名 {{alias}}...",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei UI', Inter, ui-sans-serif, system-ui, -apple-system,
|
||||||
|
BlinkMacSystemFont, sans-serif;
|
||||||
--bg: #f4f7fc;
|
--bg: #f4f7fc;
|
||||||
--bg-elevated: rgba(255, 255, 255, 0.72);
|
--bg-elevated: rgba(255, 255, 255, 0.72);
|
||||||
--bg-grid: rgba(10, 34, 66, 0.05);
|
--bg-grid: rgba(10, 34, 66, 0.05);
|
||||||
@ -158,11 +159,11 @@ button.primary {
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
box-shadow: 0 18px 34px rgba(0, 102, 255, 0.22);
|
box-shadow: 0 8px 18px rgba(0, 102, 255, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='dark'] button.primary {
|
html[data-theme='dark'] button.primary {
|
||||||
box-shadow: 0 20px 38px rgba(30, 112, 255, 0.28);
|
box-shadow: 0 10px 24px rgba(30, 112, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
@ -241,6 +242,7 @@ h4 {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 224px minmax(0, 1fr);
|
grid-template-columns: 224px minmax(0, 1fr);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
min-width: 1120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell.shell-locked,
|
.app-shell.shell-locked,
|
||||||
@ -279,8 +281,8 @@ html[data-theme='dark'] .sidebar {
|
|||||||
.sidebar-brand {
|
.sidebar-brand {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.55rem;
|
gap: 0.65rem;
|
||||||
padding: 1rem;
|
padding: 1.1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +304,36 @@ html[data-theme='dark'] .sidebar {
|
|||||||
background: linear-gradient(90deg, var(--accent), var(--accent-strong));
|
background: linear-gradient(90deg, var(--accent), var(--accent-strong));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-brand-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-github {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0.35rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--panel-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-github img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-github:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
.brand-meta {
|
.brand-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -326,7 +358,10 @@ dt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-brand h1 {
|
.sidebar-brand h1 {
|
||||||
font-size: 1.1rem;
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtle {
|
.subtle {
|
||||||
@ -348,10 +383,10 @@ dt {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 42px;
|
min-height: 40px;
|
||||||
justify-items: start;
|
justify-items: start;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0.7rem 0.85rem 0.7rem 1rem;
|
padding: 0.62rem 0.8rem 0.62rem 0.95rem;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -397,6 +432,19 @@ html[data-theme='dark'] .nav-item.active .nav-title {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge,
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
.subtle,
|
||||||
|
.nav-title,
|
||||||
|
.meta-label,
|
||||||
|
dt,
|
||||||
|
dd {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-header,
|
.panel-header,
|
||||||
.toolbar,
|
.toolbar,
|
||||||
.item-heading,
|
.item-heading,
|
||||||
@ -410,7 +458,12 @@ html[data-theme='dark'] .nav-item.active .nav-title {
|
|||||||
|
|
||||||
.panel-header,
|
.panel-header,
|
||||||
.subpanel-header {
|
.subpanel-header {
|
||||||
margin-bottom: 1.1rem;
|
margin-bottom: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header h3,
|
||||||
|
.subpanel-header h4 {
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stack,
|
.stack,
|
||||||
@ -434,14 +487,14 @@ html[data-theme='dark'] .nav-item.active .nav-title {
|
|||||||
.workspace {
|
.workspace {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.15rem;
|
gap: 1rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 1.5rem;
|
padding: 1.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-layout {
|
.tab-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 0.9rem;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,7 +518,7 @@ html[data-theme='dark'] .nav-item.active .nav-title {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
padding: 1.3rem;
|
padding: 1.15rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -493,9 +546,9 @@ html[data-theme='dark'] .nav-item.active .nav-title {
|
|||||||
|
|
||||||
.trace-row {
|
.trace-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.7rem;
|
gap: 0.6rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem;
|
padding: 0.9rem;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background: var(--panel-muted);
|
background: var(--panel-muted);
|
||||||
@ -590,7 +643,7 @@ html[data-theme='dark'] .nav-item.active .nav-title {
|
|||||||
.stat-card {
|
.stat-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 1rem;
|
padding: 0.92rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card::after {
|
.stat-card::after {
|
||||||
@ -616,13 +669,13 @@ html[data-theme='dark'] .nav-item.active .nav-title {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
min-height: 32px;
|
min-height: 28px;
|
||||||
padding: 0.36rem 0.7rem;
|
padding: 0.22rem 0.58rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: var(--accent-strong);
|
color: var(--accent-strong);
|
||||||
font-size: 0.78rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
@ -638,13 +691,13 @@ html[data-theme='dark'] .badge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge.live {
|
.badge.live {
|
||||||
background: var(--success-soft);
|
background: color-mix(in srgb, var(--success-soft) 94%, var(--panel));
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.idle {
|
.badge.idle {
|
||||||
background: rgba(148, 163, 184, 0.14);
|
background: color-mix(in srgb, var(--panel-muted) 82%, var(--panel));
|
||||||
color: var(--text-muted);
|
color: color-mix(in srgb, var(--text-muted) 92%, var(--text));
|
||||||
}
|
}
|
||||||
|
|
||||||
button.danger {
|
button.danger {
|
||||||
@ -699,11 +752,13 @@ html[data-theme='dark'] button.ghost-danger:hover {
|
|||||||
|
|
||||||
.list-column {
|
.list-column {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-toolbar {
|
.list-toolbar {
|
||||||
padding: 0.1rem 0;
|
padding: 0;
|
||||||
|
gap: 0.9rem;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-toolbar > label,
|
.list-toolbar > label,
|
||||||
@ -720,69 +775,194 @@ html[data-theme='dark'] button.ghost-danger:hover {
|
|||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-header-actions {
|
||||||
|
display: grid;
|
||||||
|
justify-items: end;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-toolbar-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-status-text {
|
||||||
|
max-width: 28rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field {
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding: 0.85rem 0.95rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: color-mix(in srgb, var(--panel-soft) 86%, var(--panel));
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group-search {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group-select {
|
||||||
|
flex: 0 0 min(240px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group label {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group input,
|
||||||
|
.filter-group select {
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
.scroll-list {
|
.scroll-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.9rem;
|
gap: 0.8rem;
|
||||||
max-height: calc(100vh - 18rem);
|
max-height: calc(100vh - 18rem);
|
||||||
padding-right: 0.25rem;
|
padding-right: 0.25rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-list {
|
.compact-list {
|
||||||
gap: 0.7rem;
|
gap: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-row {
|
.resource-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.88rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.9rem 1rem;
|
padding: 1rem 1.05rem;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 18px;
|
border-radius: 20px;
|
||||||
background: var(--panel-muted);
|
background: var(--panel);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
transition:
|
||||||
|
border-color 160ms ease,
|
||||||
|
background 160ms ease,
|
||||||
|
box-shadow 160ms ease,
|
||||||
|
transform 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-row:hover,
|
.resource-card:hover {
|
||||||
.compact-row.active {
|
border-color: var(--border-strong);
|
||||||
border-color: var(--accent);
|
box-shadow: var(--shadow-md);
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-row-main,
|
.resource-card.active {
|
||||||
.compact-row-summary {
|
border-color: var(--accent);
|
||||||
display: flex;
|
background: color-mix(in srgb, var(--accent-softer) 68%, var(--panel));
|
||||||
align-items: center;
|
box-shadow: var(--shadow-sm);
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-row-titleblock {
|
.resource-card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-card-heading {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.15rem;
|
gap: 0.38rem;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-card-titlewrap {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.16rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-row-titleblock code,
|
.resource-card-title {
|
||||||
.compact-row-summary span {
|
color: var(--text);
|
||||||
color: var(--text-muted);
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-row-titleblock strong,
|
.resource-card-code {
|
||||||
.compact-row-titleblock code,
|
color: var(--text-muted);
|
||||||
.compact-row-summary span {
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-card-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-card-title,
|
||||||
|
.resource-card-code,
|
||||||
|
.resource-card-subtitle,
|
||||||
|
.resource-meta-value {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-row-summary-grid {
|
.resource-card-side {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 0.18rem 0.58rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-card-meta {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.5fr) repeat(2, minmax(110px, auto));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
align-items: center;
|
gap: 0.7rem;
|
||||||
font-size: 0.88rem;
|
}
|
||||||
|
|
||||||
|
.resource-meta-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.26rem;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.72rem 0.82rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--panel-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-meta-route {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-meta-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.73rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-meta-value {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-panel {
|
.detail-panel {
|
||||||
@ -805,7 +985,7 @@ html[data-theme='dark'] button.ghost-danger:hover {
|
|||||||
width: min(100%, 720px);
|
width: min(100%, 720px);
|
||||||
height: calc(100vh - 2rem);
|
height: calc(100vh - 2rem);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 1.3rem;
|
padding: 1.2rem;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 28px;
|
border-radius: 28px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
@ -817,9 +997,91 @@ html[data-theme='dark'] button.ghost-danger:hover {
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
|
background: linear-gradient(180deg, var(--panel) 78%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-hero {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-hero-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-hero-card,
|
||||||
|
.detail-section {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--panel-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-hero-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.95rem 1rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-hero-card strong {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-hero-primary {
|
||||||
|
background: linear-gradient(135deg, color-mix(in srgb, var(--accent-softer) 72%, var(--panel)) 0%, var(--panel-soft) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-hero-subtle {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section-header p {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-form-grid,
|
||||||
|
.toggle-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-form-span {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-card {
|
||||||
|
min-height: 58px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-targets-section {
|
||||||
|
background: linear-gradient(180deg, color-mix(in srgb, var(--panel-soft) 92%, var(--panel)) 0%, var(--panel) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
.compact-inline-meta {
|
.compact-inline-meta {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
@ -843,8 +1105,8 @@ html[data-theme='dark'] button.ghost-danger:hover {
|
|||||||
|
|
||||||
.item-card {
|
.item-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 0.85rem;
|
||||||
padding: 1.05rem;
|
padding: 0.96rem;
|
||||||
transition:
|
transition:
|
||||||
border-color 160ms ease,
|
border-color 160ms ease,
|
||||||
background 160ms ease,
|
background 160ms ease,
|
||||||
@ -917,6 +1179,18 @@ html[data-theme='dark'] button.ghost-danger:hover {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] .resource-card {
|
||||||
|
background: color-mix(in srgb, var(--panel) 94%, #09111f);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] .resource-card.active {
|
||||||
|
background: color-mix(in srgb, var(--accent-soft) 34%, var(--panel));
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] .resource-meta-item {
|
||||||
|
background: color-mix(in srgb, var(--panel-muted) 86%, #09111f);
|
||||||
|
}
|
||||||
|
|
||||||
.item-grid > div,
|
.item-grid > div,
|
||||||
.info-grid > div {
|
.info-grid > div {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -941,6 +1215,9 @@ dd {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.9rem 0.95rem;
|
padding: 0.9rem 0.95rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: var(--panel);
|
||||||
transition:
|
transition:
|
||||||
border-color 160ms ease,
|
border-color 160ms ease,
|
||||||
background 160ms ease,
|
background 160ms ease,
|
||||||
@ -961,6 +1238,23 @@ dd {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.target-card-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-card-copy code,
|
||||||
|
.target-card-subtitle {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-card-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.target-list-compact {
|
.target-list-compact {
|
||||||
gap: 0.7rem;
|
gap: 0.7rem;
|
||||||
}
|
}
|
||||||
@ -1106,7 +1400,7 @@ input[type='file']::file-selector-button {
|
|||||||
width: min(100%, 680px);
|
width: min(100%, 680px);
|
||||||
max-height: calc(100vh - 3rem);
|
max-height: calc(100vh - 3rem);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 1.15rem;
|
padding: 1.05rem;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
@ -1133,8 +1427,8 @@ input[type='file']::file-selector-button {
|
|||||||
|
|
||||||
.empty-card {
|
.empty-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.8rem;
|
gap: 0.7rem;
|
||||||
padding: 1.2rem;
|
padding: 1.05rem;
|
||||||
border: 1px dashed var(--border-strong);
|
border: 1px dashed var(--border-strong);
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
background: linear-gradient(135deg, var(--panel-soft), var(--panel-muted));
|
background: linear-gradient(135deg, var(--panel-soft), var(--panel-muted));
|
||||||
@ -1142,14 +1436,30 @@ input[type='file']::file-selector-button {
|
|||||||
align-content: center;
|
align-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-illustration {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: color-mix(in srgb, var(--accent-soft) 88%, var(--panel));
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-empty-card {
|
||||||
|
min-height: 0;
|
||||||
|
background: linear-gradient(135deg, color-mix(in srgb, var(--accent-softer) 62%, var(--panel)) 0%, var(--panel-soft) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
.compact-empty {
|
.compact-empty {
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-kicker {
|
.empty-kicker {
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1208,9 +1518,23 @@ dd {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] .filter-group,
|
||||||
|
html[data-theme='dark'] .detail-section,
|
||||||
|
html[data-theme='dark'] .detail-hero-card,
|
||||||
|
html[data-theme='dark'] .target-card,
|
||||||
|
html[data-theme='dark'] .checkbox-card {
|
||||||
|
background: color-mix(in srgb, var(--panel-soft) 88%, #09111f);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] .detail-hero-primary,
|
||||||
|
html[data-theme='dark'] .detail-empty-card {
|
||||||
|
background: linear-gradient(135deg, color-mix(in srgb, var(--accent-soft) 40%, var(--panel)) 0%, color-mix(in srgb, var(--panel-soft) 92%, #09111f) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1240px) {
|
@media (max-width: 1240px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@ -1221,7 +1545,7 @@ dd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-list {
|
.nav-list {
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(160px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-layout,
|
.overview-layout,
|
||||||
@ -1263,13 +1587,14 @@ dd {
|
|||||||
.panel-header,
|
.panel-header,
|
||||||
.toolbar,
|
.toolbar,
|
||||||
.card-actions,
|
.card-actions,
|
||||||
|
.detail-section-header,
|
||||||
.subpanel-header,
|
.subpanel-header,
|
||||||
.list-toolbar,
|
.list-toolbar,
|
||||||
.target-card,
|
.target-card,
|
||||||
.target-card-main,
|
.target-card-main,
|
||||||
.item-heading,
|
.item-heading,
|
||||||
.item-summary,
|
.item-summary,
|
||||||
.compact-row-main,
|
.resource-card-top,
|
||||||
.inline-meta {
|
.inline-meta {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@ -1287,14 +1612,31 @@ dd {
|
|||||||
.settings-layout,
|
.settings-layout,
|
||||||
.stats-grid,
|
.stats-grid,
|
||||||
.card-grid-list,
|
.card-grid-list,
|
||||||
|
.detail-hero-grid,
|
||||||
.info-grid,
|
.info-grid,
|
||||||
.item-grid,
|
.item-grid,
|
||||||
.action-grid,
|
.action-grid,
|
||||||
.split-view,
|
.split-view,
|
||||||
.compact-row-summary-grid {
|
.detail-form-grid,
|
||||||
|
.toggle-grid,
|
||||||
|
.resource-card-meta {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-header-actions {
|
||||||
|
justify-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-status-text {
|
||||||
|
max-width: none;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-toolbar-actions,
|
||||||
|
.resource-card-side {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.scroll-list {
|
.scroll-list {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|||||||
2
frontend/wailsjs/go/desktop/App.d.ts
vendored
2
frontend/wailsjs/go/desktop/App.d.ts
vendored
@ -30,6 +30,8 @@ export function ImportProviders(arg1:app.ProviderImportInput):Promise<app.Provid
|
|||||||
|
|
||||||
export function Meta():Promise<Record<string, string>>;
|
export function Meta():Promise<Record<string, string>>;
|
||||||
|
|
||||||
|
export function OpenExternalURL(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function Overview():Promise<app.Overview>;
|
export function Overview():Promise<app.Overview>;
|
||||||
|
|
||||||
export function PreviewSync(arg1:app.SyncInput):Promise<app.SyncPreview>;
|
export function PreviewSync(arg1:app.SyncInput):Promise<app.SyncPreview>;
|
||||||
|
|||||||
@ -54,6 +54,10 @@ export function Meta() {
|
|||||||
return window['go']['desktop']['App']['Meta']();
|
return window['go']['desktop']['App']['Meta']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OpenExternalURL(arg1) {
|
||||||
|
return window['go']['desktop']['App']['OpenExternalURL'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function Overview() {
|
export function Overview() {
|
||||||
return window['go']['desktop']['App']['Overview']();
|
return window['go']['desktop']['App']['Overview']();
|
||||||
}
|
}
|
||||||
|
|||||||
10
go.mod
10
go.mod
@ -3,7 +3,7 @@ module github.com/Apale7/opencode-provider-switch
|
|||||||
go 1.22.2
|
go 1.22.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/getlantern/systray v1.2.2
|
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/tidwall/jsonc v0.3.2
|
github.com/tidwall/jsonc v0.3.2
|
||||||
github.com/wailsapp/wails/v2 v2.12.0
|
github.com/wailsapp/wails/v2 v2.12.0
|
||||||
@ -13,14 +13,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-stack/stack v1.8.0 // indirect
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
@ -34,7 +27,6 @@ require (
|
|||||||
github.com/leaanthony/u v1.1.1 // indirect
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
|||||||
28
go.sum
28
go.sum
@ -1,29 +1,14 @@
|
|||||||
|
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI=
|
||||||
|
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
|
||||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
|
||||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@ -48,8 +33,6 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
|
|||||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
|
||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
|
||||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
@ -58,8 +41,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@ -72,13 +53,10 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc=
|
github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc=
|
||||||
@ -101,7 +79,6 @@ golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@ -114,7 +91,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Apale7/opencode-provider-switch/internal/app"
|
"github.com/Apale7/opencode-provider-switch/internal/app"
|
||||||
@ -140,6 +141,14 @@ func (a *App) Meta() map[string]string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) OpenExternalURL(rawURL string) error {
|
||||||
|
url := strings.TrimSpace(rawURL)
|
||||||
|
if url == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return openExternalURL(a.callContext(), url)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) Overview() (app.Overview, error) {
|
func (a *App) Overview() (app.Overview, error) {
|
||||||
return a.bindings.GetOverview(a.callContext())
|
return a.bindings.GetOverview(a.callContext())
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
internal/desktop/assets/icon.ico
Normal file
BIN
internal/desktop/assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
@ -25,6 +25,11 @@ func quitWindow(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openExternalURL(ctx context.Context, url string) error {
|
||||||
|
_ = ctx
|
||||||
|
return openBrowser(url)
|
||||||
|
}
|
||||||
|
|
||||||
func initDesktopNotifications(ctx context.Context) error {
|
func initDesktopNotifications(ctx context.Context) error {
|
||||||
_ = ctx
|
_ = ctx
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -4,6 +4,7 @@ package desktop
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
@ -21,6 +22,11 @@ func hideWindow(ctx context.Context) error {
|
|||||||
|
|
||||||
func showWindow(ctx context.Context) error {
|
func showWindow(ctx context.Context) error {
|
||||||
wruntime.Show(ctx)
|
wruntime.Show(ctx)
|
||||||
|
wruntime.WindowShow(ctx)
|
||||||
|
wruntime.WindowUnminimise(ctx)
|
||||||
|
wruntime.WindowSetAlwaysOnTop(ctx, true)
|
||||||
|
time.Sleep(80 * time.Millisecond)
|
||||||
|
wruntime.WindowSetAlwaysOnTop(ctx, false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,6 +35,11 @@ func quitWindow(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openExternalURL(ctx context.Context, url string) error {
|
||||||
|
wruntime.BrowserOpenURL(ctx, url)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func initDesktopNotifications(ctx context.Context) error {
|
func initDesktopNotifications(ctx context.Context) error {
|
||||||
return wruntime.InitializeNotifications(ctx)
|
return wruntime.InitializeNotifications(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
36
internal/desktop/tray_language.go
Normal file
36
internal/desktop/tray_language.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//go:build desktop_wails
|
||||||
|
|
||||||
|
package desktop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var systemTrayLanguage = detectSystemTrayLanguage
|
||||||
|
|
||||||
|
func trayLanguage(preference string) string {
|
||||||
|
if language := normalizeTrayLanguage(strings.TrimSpace(preference)); language != "" {
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
if language := normalizeTrayLanguage(systemTrayLanguage()); language != "" {
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
for _, value := range []string{os.Getenv("LC_ALL"), os.Getenv("LANG"), os.Getenv("LANGUAGE")} {
|
||||||
|
if language := normalizeTrayLanguage(value); language != "" {
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "en-US"
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeTrayLanguage(value string) string {
|
||||||
|
lower := strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if strings.HasPrefix(lower, "zh") {
|
||||||
|
return "zh-CN"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(lower, "en") {
|
||||||
|
return "en-US"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
7
internal/desktop/tray_language_other.go
Normal file
7
internal/desktop/tray_language_other.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//go:build desktop_wails && !windows
|
||||||
|
|
||||||
|
package desktop
|
||||||
|
|
||||||
|
func detectSystemTrayLanguage() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
47
internal/desktop/tray_language_test.go
Normal file
47
internal/desktop/tray_language_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//go:build desktop_wails
|
||||||
|
|
||||||
|
package desktop
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTrayLanguageHonorsExplicitPreference(t *testing.T) {
|
||||||
|
original := systemTrayLanguage
|
||||||
|
systemTrayLanguage = func() string { return "en-US" }
|
||||||
|
t.Cleanup(func() { systemTrayLanguage = original })
|
||||||
|
|
||||||
|
t.Setenv("LC_ALL", "")
|
||||||
|
t.Setenv("LANG", "")
|
||||||
|
t.Setenv("LANGUAGE", "")
|
||||||
|
|
||||||
|
if got := trayLanguage("zh-CN"); got != "zh-CN" {
|
||||||
|
t.Fatalf("trayLanguage(zh-CN) = %q, want zh-CN", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrayLanguageUsesSystemLanguageBeforeEnv(t *testing.T) {
|
||||||
|
original := systemTrayLanguage
|
||||||
|
systemTrayLanguage = func() string { return "zh-CN" }
|
||||||
|
t.Cleanup(func() { systemTrayLanguage = original })
|
||||||
|
|
||||||
|
t.Setenv("LC_ALL", "en_US.UTF-8")
|
||||||
|
t.Setenv("LANG", "")
|
||||||
|
t.Setenv("LANGUAGE", "")
|
||||||
|
|
||||||
|
if got := trayLanguage("system"); got != "zh-CN" {
|
||||||
|
t.Fatalf("trayLanguage(system) = %q, want zh-CN", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrayLanguageFallsBackToEnv(t *testing.T) {
|
||||||
|
original := systemTrayLanguage
|
||||||
|
systemTrayLanguage = func() string { return "" }
|
||||||
|
t.Cleanup(func() { systemTrayLanguage = original })
|
||||||
|
|
||||||
|
t.Setenv("LC_ALL", "")
|
||||||
|
t.Setenv("LANG", "zh_CN.UTF-8")
|
||||||
|
t.Setenv("LANGUAGE", "")
|
||||||
|
|
||||||
|
if got := trayLanguage("system"); got != "zh-CN" {
|
||||||
|
t.Fatalf("trayLanguage(system) = %q, want zh-CN", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
internal/desktop/tray_language_windows.go
Normal file
20
internal/desktop/tray_language_windows.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
//go:build desktop_wails && windows
|
||||||
|
|
||||||
|
package desktop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func detectSystemTrayLanguage() string {
|
||||||
|
const localeNameMaxLength = 85
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
proc := kernel32.NewProc("GetUserDefaultLocaleName")
|
||||||
|
var buffer [localeNameMaxLength]uint16
|
||||||
|
result, _, _ := proc.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)))
|
||||||
|
if result == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return syscall.UTF16ToString(buffer[:])
|
||||||
|
}
|
||||||
@ -4,14 +4,21 @@ package desktop
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"fyne.io/systray"
|
||||||
|
frontendassets "github.com/Apale7/opencode-provider-switch/frontend"
|
||||||
"github.com/Apale7/opencode-provider-switch/internal/app"
|
"github.com/Apale7/opencode-provider-switch/internal/app"
|
||||||
"github.com/getlantern/systray"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed assets/icon.ico
|
||||||
|
var trayIcon []byte
|
||||||
|
|
||||||
// Tray wires resident-mode controls into a native system tray.
|
// Tray wires resident-mode controls into a native system tray.
|
||||||
type Tray struct {
|
type Tray struct {
|
||||||
service *app.Service
|
service *app.Service
|
||||||
@ -19,8 +26,10 @@ type Tray struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
prefs app.DesktopPrefsView
|
prefs app.DesktopPrefsView
|
||||||
|
language string
|
||||||
registered bool
|
registered bool
|
||||||
ready bool
|
ready bool
|
||||||
|
running bool
|
||||||
quitting bool
|
quitting bool
|
||||||
|
|
||||||
statusItem *systray.MenuItem
|
statusItem *systray.MenuItem
|
||||||
@ -44,7 +53,12 @@ func (t *Tray) Attach(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
t.registered = true
|
t.registered = true
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
systray.Register(t.onReady, nil)
|
|
||||||
|
go func() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
systray.Run(t.onReady, t.onExit)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tray) Detach() {
|
func (t *Tray) Detach() {
|
||||||
@ -63,7 +77,30 @@ func (t *Tray) Sync(ctx context.Context, prefs app.DesktopPrefsView) {
|
|||||||
t.ctx = ctx
|
t.ctx = ctx
|
||||||
}
|
}
|
||||||
t.prefs = prefs
|
t.prefs = prefs
|
||||||
|
t.language = trayLanguage(prefs.Language)
|
||||||
|
ready := t.ready
|
||||||
|
statusItem := t.statusItem
|
||||||
|
showItem := t.showItem
|
||||||
|
hideItem := t.hideItem
|
||||||
|
startItem := t.startItem
|
||||||
|
stopItem := t.stopItem
|
||||||
|
quitItem := t.quitItem
|
||||||
|
labels := t.trayLabelsLocked()
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
|
if ready && statusItem != nil && showItem != nil && hideItem != nil && startItem != nil && stopItem != nil && quitItem != nil {
|
||||||
|
systray.SetTitle(labels.appTitle)
|
||||||
|
systray.SetTooltip(labels.appTooltip)
|
||||||
|
showItem.SetTitle(labels.openWindow)
|
||||||
|
showItem.SetTooltip(labels.openWindowHint)
|
||||||
|
hideItem.SetTitle(labels.hideWindow)
|
||||||
|
hideItem.SetTooltip(labels.hideWindowHint)
|
||||||
|
startItem.SetTitle(labels.startProxy)
|
||||||
|
startItem.SetTooltip(labels.startProxyHint)
|
||||||
|
stopItem.SetTitle(labels.stopProxy)
|
||||||
|
stopItem.SetTooltip(labels.stopProxyHint)
|
||||||
|
quitItem.SetTitle(labels.quit)
|
||||||
|
quitItem.SetTooltip(labels.quitHint)
|
||||||
|
}
|
||||||
t.refresh()
|
t.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,27 +126,49 @@ func (t *Tray) BeforeClose(ctx context.Context) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tray) onReady() {
|
func (t *Tray) onReady() {
|
||||||
systray.SetTitle("ocswitch")
|
labels := t.labels()
|
||||||
systray.SetTooltip("ocswitch desktop")
|
if len(trayIcon) > 0 {
|
||||||
|
systray.SetIcon(trayIcon)
|
||||||
|
}
|
||||||
|
systray.SetTitle(labels.appTitle)
|
||||||
|
systray.SetTooltip(labels.appTooltip)
|
||||||
|
systray.SetOnTapped(func() {
|
||||||
|
_ = t.withContext(showWindow)
|
||||||
|
})
|
||||||
|
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
t.ready = true
|
t.ready = true
|
||||||
t.statusItem = systray.AddMenuItem("Proxy: checking...", "Current proxy status")
|
t.running = true
|
||||||
|
t.statusItem = systray.AddMenuItem(labels.proxyChecking, labels.proxyStatusHint)
|
||||||
t.statusItem.Disable()
|
t.statusItem.Disable()
|
||||||
systray.AddSeparator()
|
systray.AddSeparator()
|
||||||
t.showItem = systray.AddMenuItem("Open window", "Show desktop window")
|
t.showItem = systray.AddMenuItem(labels.openWindow, labels.openWindowHint)
|
||||||
t.hideItem = systray.AddMenuItem("Hide window", "Hide desktop window")
|
t.hideItem = systray.AddMenuItem(labels.hideWindow, labels.hideWindowHint)
|
||||||
systray.AddSeparator()
|
systray.AddSeparator()
|
||||||
t.startItem = systray.AddMenuItem("Start proxy", "Start local proxy")
|
t.startItem = systray.AddMenuItem(labels.startProxy, labels.startProxyHint)
|
||||||
t.stopItem = systray.AddMenuItem("Stop proxy", "Stop local proxy")
|
t.stopItem = systray.AddMenuItem(labels.stopProxy, labels.stopProxyHint)
|
||||||
systray.AddSeparator()
|
systray.AddSeparator()
|
||||||
t.quitItem = systray.AddMenuItem("Quit", "Exit application")
|
t.quitItem = systray.AddMenuItem(labels.quit, labels.quitHint)
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
|
|
||||||
go t.loop()
|
go t.loop()
|
||||||
t.refresh()
|
t.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tray) onExit() {
|
||||||
|
t.mu.Lock()
|
||||||
|
t.ready = false
|
||||||
|
t.running = false
|
||||||
|
t.registered = false
|
||||||
|
t.statusItem = nil
|
||||||
|
t.showItem = nil
|
||||||
|
t.hideItem = nil
|
||||||
|
t.startItem = nil
|
||||||
|
t.stopItem = nil
|
||||||
|
t.quitItem = nil
|
||||||
|
t.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Tray) loop() {
|
func (t *Tray) loop() {
|
||||||
for {
|
for {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
@ -118,7 +177,11 @@ func (t *Tray) loop() {
|
|||||||
startItem := t.startItem
|
startItem := t.startItem
|
||||||
stopItem := t.stopItem
|
stopItem := t.stopItem
|
||||||
quitItem := t.quitItem
|
quitItem := t.quitItem
|
||||||
|
running := t.running
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
|
if !running || showItem == nil || hideItem == nil || startItem == nil || stopItem == nil || quitItem == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-showItem.ClickedCh:
|
case <-showItem.ClickedCh:
|
||||||
@ -183,6 +246,7 @@ func (t *Tray) refresh() {
|
|||||||
statusItem := t.statusItem
|
statusItem := t.statusItem
|
||||||
startItem := t.startItem
|
startItem := t.startItem
|
||||||
stopItem := t.stopItem
|
stopItem := t.stopItem
|
||||||
|
labels := t.trayLabelsLocked()
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
if !ready || statusItem == nil || startItem == nil || stopItem == nil || t.service == nil {
|
if !ready || statusItem == nil || startItem == nil || stopItem == nil || t.service == nil {
|
||||||
return
|
return
|
||||||
@ -190,20 +254,76 @@ func (t *Tray) refresh() {
|
|||||||
|
|
||||||
status, err := t.service.GetProxyStatus(context.Background())
|
status, err := t.service.GetProxyStatus(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
statusItem.SetTitle("Proxy: unavailable")
|
statusItem.SetTitle(labels.proxyUnavailable)
|
||||||
startItem.Enable()
|
startItem.Enable()
|
||||||
stopItem.Disable()
|
stopItem.Disable()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.Running {
|
if status.Running {
|
||||||
statusItem.SetTitle(fmt.Sprintf("Proxy: running (%s)", status.BindAddress))
|
statusItem.SetTitle(fmt.Sprintf(labels.proxyRunning, status.BindAddress))
|
||||||
startItem.Disable()
|
startItem.Disable()
|
||||||
stopItem.Enable()
|
stopItem.Enable()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
statusItem.SetTitle(fmt.Sprintf("Proxy: stopped (%s)", status.BindAddress))
|
statusItem.SetTitle(fmt.Sprintf(labels.proxyStopped, status.BindAddress))
|
||||||
startItem.Enable()
|
startItem.Enable()
|
||||||
stopItem.Disable()
|
stopItem.Disable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tray) labels() trayLabels {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
return t.trayLabelsLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tray) trayLabelsLocked() trayLabels {
|
||||||
|
return trayLabels{
|
||||||
|
appTitle: trayLocaleString(t.language, "tray", "appTitle", "ocswitch"),
|
||||||
|
appTooltip: trayLocaleString(t.language, "tray", "appTooltip", "ocswitch desktop"),
|
||||||
|
proxyChecking: trayLocaleString(t.language, "tray", "proxyChecking", "Proxy: checking..."),
|
||||||
|
proxyStatusHint: trayLocaleString(t.language, "tray", "proxyStatusHint", "Current proxy status"),
|
||||||
|
proxyUnavailable: trayLocaleString(t.language, "tray", "proxyUnavailable", "Proxy: unavailable"),
|
||||||
|
proxyRunning: trayLocaleString(t.language, "tray", "proxyRunning", "Proxy: running (%s)"),
|
||||||
|
proxyStopped: trayLocaleString(t.language, "tray", "proxyStopped", "Proxy: stopped (%s)"),
|
||||||
|
openWindow: trayLocaleString(t.language, "tray", "openWindow", "Open window"),
|
||||||
|
openWindowHint: trayLocaleString(t.language, "tray", "openWindowHint", "Show desktop window"),
|
||||||
|
hideWindow: trayLocaleString(t.language, "tray", "hideWindow", "Hide window"),
|
||||||
|
hideWindowHint: trayLocaleString(t.language, "tray", "hideWindowHint", "Hide desktop window"),
|
||||||
|
startProxy: trayLocaleString(t.language, "tray", "startProxy", "Start proxy"),
|
||||||
|
startProxyHint: trayLocaleString(t.language, "tray", "startProxyHint", "Start local proxy"),
|
||||||
|
stopProxy: trayLocaleString(t.language, "tray", "stopProxy", "Stop proxy"),
|
||||||
|
stopProxyHint: trayLocaleString(t.language, "tray", "stopProxyHint", "Stop local proxy"),
|
||||||
|
quit: trayLocaleString(t.language, "tray", "quit", "Quit"),
|
||||||
|
quitHint: trayLocaleString(t.language, "tray", "quitHint", "Exit application"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func trayLocaleString(language string, key string, field string, fallback string) string {
|
||||||
|
value, ok, err := frontendassets.LocaleValue(language, key, field)
|
||||||
|
if err == nil && ok && strings.TrimSpace(value) != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
type trayLabels struct {
|
||||||
|
appTitle string
|
||||||
|
appTooltip string
|
||||||
|
proxyChecking string
|
||||||
|
proxyStatusHint string
|
||||||
|
proxyUnavailable string
|
||||||
|
proxyRunning string
|
||||||
|
proxyStopped string
|
||||||
|
openWindow string
|
||||||
|
openWindowHint string
|
||||||
|
hideWindow string
|
||||||
|
hideWindowHint string
|
||||||
|
startProxy string
|
||||||
|
startProxyHint string
|
||||||
|
stopProxy string
|
||||||
|
stopProxyHint string
|
||||||
|
quit string
|
||||||
|
quitHint string
|
||||||
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/linux"
|
"github.com/wailsapp/wails/v2/pkg/options/linux"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunWails(configPath string, version string) error {
|
func RunWails(configPath string, version string) error {
|
||||||
@ -27,7 +28,7 @@ func RunWails(configPath string, version string) error {
|
|||||||
Title: "ocswitch desktop",
|
Title: "ocswitch desktop",
|
||||||
Width: 1280,
|
Width: 1280,
|
||||||
Height: 880,
|
Height: 880,
|
||||||
MinWidth: 980,
|
MinWidth: 1120,
|
||||||
MinHeight: 720,
|
MinHeight: 720,
|
||||||
AssetServer: &assetserver.Options{
|
AssetServer: &assetserver.Options{
|
||||||
Assets: mustFS(assets),
|
Assets: mustFS(assets),
|
||||||
@ -45,6 +46,9 @@ func RunWails(configPath string, version string) error {
|
|||||||
Linux: &linux.Options{
|
Linux: &linux.Options{
|
||||||
ProgramName: "ocswitch-desktop",
|
ProgramName: "ocswitch-desktop",
|
||||||
},
|
},
|
||||||
|
Windows: &windows.Options{
|
||||||
|
DisableWindowIcon: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user