desktop dev update channel: every push to main builds dev bundles (<next patch>-dev.<UTC>), the VDS signs and publishes dev/latest.json (newest 5); app channel setting stable|dev, background updater at start / 15 min / resume / channel switch, deferred while in use, no downgrade, autostart rewritten on every start, card shows version and channel
This commit is contained in:
parent
4dff7d9b2e
commit
39b2f73ad7
25 changed files with 732 additions and 47 deletions
|
|
@ -36,7 +36,8 @@ export interface Settings {
|
|||
cache: { keepDays: 0 | 7 | 30 | 90 | 365 };
|
||||
/** reviewState: other state changes (submitted, in review); rejected / approved / live: own toggles; crash also covers a halted rollout. quietHours: no notification between from and to (HH:MM, may cross midnight). */
|
||||
notify: { reviewState: boolean; rejected: boolean; approved: boolean; live: boolean; lowReviews: boolean; crash: boolean; quietHours: { enabled: boolean; from: string; to: string } };
|
||||
channel: 'stable' | 'beta';
|
||||
/** Update channel: stable (npm releases) or dev (every push to main); beta is kept for saved states. */
|
||||
channel: 'stable' | 'dev' | 'beta';
|
||||
}
|
||||
export const DEFAULT_SETTINGS: Settings = { refresh: { autoMinutes: 0, onLaunch: false, parallel: 4, icons: true, autoRead: true, everyMinutes: 15 }, cache: { keepDays: 30 }, notify: { reviewState: true, rejected: true, approved: true, live: true, lowReviews: true, crash: true, quietHours: { enabled: false, from: '23:00', to: '08:00' } }, channel: 'stable' };
|
||||
export interface Workspace { studios: Studio[]; overrides: Overrides; settings: Settings }
|
||||
|
|
@ -47,7 +48,7 @@ export interface CacheEntry<T = unknown> { value: T; at: string }
|
|||
export interface DownloadInfo { tool: 'ffmpeg' | 'chrome'; available: boolean; version: string | null; size: number | null; license: string; licenseUrl: string; source: string; installed: boolean; folder: string }
|
||||
export interface DownloadEvent { job: string; tool: string; stage: 'resolve' | 'download' | 'verify' | 'unpack' | 'install' | 'done' | 'error' | 'cancelled'; received: number; total: number | null; message?: string }
|
||||
export type { IconResult as Icon };
|
||||
export interface UpdateInfo { current: string; channel: 'stable' | 'beta'; available: string | null; notes: string | null; date: string | null }
|
||||
export interface UpdateInfo { current: string; channel: 'stable' | 'dev' | 'beta'; available: string | null; notes: string | null; date: string | null }
|
||||
|
||||
/** What a failed command rejects with (sidecar.rs `Fail`). */
|
||||
export interface Fail { code: number; message: string; data?: unknown }
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export function Card({ host, lang }: { host: CardHost; lang: CardLang }) {
|
|||
</Section>
|
||||
)}
|
||||
</div>
|
||||
{s?.build && <footer className="tc-foot" data-testid="card-build">{T('build', { version: s.build.version, channel: T(s.build.channel === 'dev' ? 'chDev' : s.build.channel === 'beta' ? 'chBeta' : 'chStable') })}</footer>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function TrayCardBridge() {
|
|||
asc: ios ? ascUrl(ios.ascAppId) : null, play: st?.app?.platforms.android || a.android ? playUrlFor(d, a) : null,
|
||||
};
|
||||
});
|
||||
return cardSnapshot(inputs, { at: d.live?.value.at ?? d.portfolio?.at ?? null, busy: all.busy, muted: d.muteUntil > Date.now() });
|
||||
return cardSnapshot(inputs, { at: d.live?.value.at ?? d.portfolio?.at ?? null, busy: all.busy, muted: d.muteUntil > Date.now(), build: d.info ? { version: d.info.version, channel: d.settings.channel } : null });
|
||||
}, [visible, d, extra, all.busy]);
|
||||
|
||||
const last = useRef('');
|
||||
|
|
|
|||
|
|
@ -56,3 +56,4 @@ body.tc-page.tc-in .tc { animation: tc-in 120ms ease-out; }
|
|||
.tc-stars { display: inline-flex; gap: 1px; align-items: center; color: var(--warn-solid); }
|
||||
.tc-rname { font-weight: var(--ds-w-strong); font-size: var(--ds-t-meta); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.tc-rtext { grid-column: 1 / -1; font-size: var(--ds-t-meta); color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.tc-foot { padding: 6px 12px 8px; color: var(--muted); font-size: var(--ds-t-micro); border-top: 1px solid var(--ds-stroke); font-variant-numeric: tabular-nums; }
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export interface CardSnapshot {
|
|||
health: CardHealth[];
|
||||
/** Apps with something on the card, for the quick actions (console links). */
|
||||
links: CardApp[];
|
||||
/** This app's version and update channel (Settings > Updates), shown at the card's foot. */
|
||||
build?: { version: string; channel: string } | null;
|
||||
}
|
||||
|
||||
export interface CardInput {
|
||||
|
|
@ -48,7 +50,7 @@ export function healthAlerts(h: HealthSnapshot | null | undefined): ('crash' | '
|
|||
return out;
|
||||
}
|
||||
|
||||
export function cardSnapshot(apps: CardInput[], o: { at: string | null; busy: boolean; muted: boolean; now?: number }): CardSnapshot {
|
||||
export function cardSnapshot(apps: CardInput[], o: { at: string | null; busy: boolean; muted: boolean; now?: number; build?: { version: string; channel: string } | null }): CardSnapshot {
|
||||
const now = o.now ?? Date.now();
|
||||
const rejections: CardRejection[] = [], releases: CardRelease[] = [], low: CardLowReview[] = [], health: CardHealth[] = [];
|
||||
const touched = new Set<string>();
|
||||
|
|
@ -82,5 +84,6 @@ export function cardSnapshot(apps: CardInput[], o: { at: string | null; busy: bo
|
|||
rejections: rejections.slice(0, MAX_ROWS), releases: releases.slice(0, MAX_ROWS),
|
||||
reviews: { recent, low: low.slice(0, MAX_LOW) }, health: health.slice(0, MAX_ROWS),
|
||||
links: apps.filter(a => touched.has(a.key)).map(a => ({ key: a.key, name: a.name, hue: a.hue, asc: a.asc ?? null, play: a.play ?? null })),
|
||||
build: o.build ?? null,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ export const en = {
|
|||
refresh: 'Refresh',
|
||||
refreshing: 'Reading…',
|
||||
openMain: 'Open storebound',
|
||||
build: 'storebound {version} · {channel} channel',
|
||||
chStable: 'stable',
|
||||
chDev: 'dev',
|
||||
chBeta: 'beta',
|
||||
more: 'More',
|
||||
mute: 'Mute notifications for 1 hour',
|
||||
unmute: 'Unmute notifications',
|
||||
|
|
@ -70,6 +74,10 @@ export const tr: Record<CardKey, string> = {
|
|||
refresh: 'Yenile',
|
||||
refreshing: 'Okunuyor…',
|
||||
openMain: 'storebound\'u aç',
|
||||
build: 'storebound {version} · {channel} kanalı',
|
||||
chStable: 'kararlı',
|
||||
chDev: 'geliştirme',
|
||||
chBeta: 'beta',
|
||||
more: 'Diğer',
|
||||
mute: 'Bildirimleri 1 saat sustur',
|
||||
unmute: 'Bildirimleri aç',
|
||||
|
|
|
|||
|
|
@ -974,7 +974,9 @@ export const en = {
|
|||
'set.channel': 'Channel',
|
||||
'set.stable': 'Stable',
|
||||
'set.beta': 'Beta',
|
||||
'set.updatesText': 'Updates come from storebound.dev and are signed; the app checks the signature before it replaces anything, and installs only when you press Install. Nothing is checked in the background.',
|
||||
'set.dev': 'Dev (every change)',
|
||||
'set.devToStable': 'This is the dev build {version}: it stays until a stable release is newer (no downgrade).',
|
||||
'set.updatesText': 'Updates come from storebound.dev and are signed; the app checks the signature before it replaces anything. It looks for a newer version of the chosen channel at start, every 15 minutes and after sleep, and installs it in the background (not while you work in this window). Stable follows the npm releases; Dev gets every change on main.',
|
||||
'set.updateCheck': 'Check for updates',
|
||||
'set.updateChecking': 'Checking…',
|
||||
'set.updateNone': 'storebound {version} is the newest version on the {channel} channel.',
|
||||
|
|
|
|||
|
|
@ -976,7 +976,9 @@ export const tr: Record<keyof typeof en, string> = {
|
|||
'set.channel': 'Kanal',
|
||||
'set.stable': 'Kararlı',
|
||||
'set.beta': 'Beta',
|
||||
'set.updatesText': 'Güncellemeler storebound.dev\'den imzalı gelir; uygulama bir şeyi değiştirmeden önce imzayı denetler ve yalnızca siz Yükle\'ye bastığınızda yükler. Arka planda denetim yapılmaz.',
|
||||
'set.dev': 'Geliştirme (her değişiklik)',
|
||||
'set.devToStable': 'Bu, geliştirme sürümü {version}: daha yeni bir kararlı sürüm çıkana kadar kalır (eski sürüme dönülmez).',
|
||||
'set.updatesText': 'Güncellemeler storebound.dev\'den imzalı gelir; uygulama bir şeyi değiştirmeden önce imzayı denetler. Seçili kanalda daha yeni bir sürümü açılışta, 15 dakikada bir ve uykudan sonra arar, arka planda yükler (siz bu pencerede çalışırken değil). Kararlı, npm sürümlerini izler; Geliştirme, main\'deki her değişikliği alır.',
|
||||
'set.updateCheck': 'Güncellemeleri denetle',
|
||||
'set.updateChecking': 'Denetleniyor…',
|
||||
'set.updateNone': 'storebound {version}, {channel} kanalındaki en yeni sürüm.',
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function normalizeSettings(v: unknown): Settings {
|
|||
lowReviews: pick(n.lowReviews, d.notify.lowReviews, isBool), crash: pick(n.crash, d.notify.crash, isBool),
|
||||
quietHours: { enabled: pick(q.enabled, d.notify.quietHours.enabled, isBool), from: pick(q.from, d.notify.quietHours.from, isHm), to: pick(q.to, d.notify.quietHours.to, isHm) },
|
||||
},
|
||||
channel: v.channel === 'beta' ? 'beta' : 'stable',
|
||||
channel: v.channel === 'dev' || v.channel === 'beta' ? v.channel : 'stable',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -257,7 +257,8 @@ export function Settings({ onTheme: _onTheme, section }: { onTheme: () => void;
|
|||
<Section id="set-updates" title={t('set.updates')} icon="download" className="set-card">
|
||||
<div className="form">
|
||||
<div className="field"><span className="legend">{t('set.channel')}</span>
|
||||
<Seg value={s.channel} label={t('set.channel')} onChange={channel => set(x => ({ ...x, channel }))} options={[['stable', t('set.stable')], ['beta', t('set.beta')]]} /></div>
|
||||
<Seg value={s.channel} label={t('set.channel')} onChange={channel => set(x => ({ ...x, channel }))} options={[['stable', t('set.stable')], ['dev', t('set.dev')]]} /></div>
|
||||
{s.channel === 'stable' && /-dev\./.test(d.info?.version ?? '') && <p className="small muted">{t('set.devToStable', { version: d.info!.version })}</p>}
|
||||
<Updates />
|
||||
<p className="small muted">{t('set.updatesText')}</p>
|
||||
</div>
|
||||
|
|
@ -279,7 +280,7 @@ export function Settings({ onTheme: _onTheme, section }: { onTheme: () => void;
|
|||
);
|
||||
}
|
||||
|
||||
/** Settings > Updates: checks on click only; installs after the host's native dialog (the signature is checked there). */
|
||||
/** Settings > Updates: Check asks the channel now; Install runs after the host's native dialog (the signature is checked there). The host also checks and installs in the background (lib.rs auto_update). */
|
||||
function Updates() {
|
||||
const [info, setInfo] = useState<UpdateInfo | null>(null);
|
||||
const [busy, setBusy] = useState<'check' | 'install' | null>(null);
|
||||
|
|
@ -294,7 +295,7 @@ function Updates() {
|
|||
setBusy('install'); setError(null);
|
||||
try { await api.updateInstall(version); } catch (e) { setError(errorText(e)); } finally { setBusy(null); setGot(null); }
|
||||
};
|
||||
const channel = info ? t(info.channel === 'beta' ? 'set.beta' : 'set.stable') : '';
|
||||
const channel = info ? t(info.channel === 'beta' ? 'set.beta' : info.channel === 'dev' ? 'set.dev' : 'set.stable') : '';
|
||||
return (
|
||||
<div className="stack" style={{ gap: 8 }}>
|
||||
{error && <Notice tone="bad">{error}</Notice>}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue