Desktop: background store reads from the tray (no document.hidden skip, backoff), notifications by kind (rejected, approved, live, halted, crash, reviews) with switches, quiet hours and mute, click-through on next show; tray menu with apps and their states, read now, mute, updates, settings, tooltip summary and attention dot; tray_update command; tests

This commit is contained in:
CRTkafa 2026-10-22 22:24:05 +08:00
parent 5d8a0f5e9a
commit 03ef999dac
27 changed files with 1050 additions and 55 deletions

View file

@ -167,9 +167,20 @@ export function Settings({ onTheme: _onTheme, section }: { onTheme: () => void;
<Card id="set-notifications" title={t('set.notifications')} icon="bell" sub={t('set.notificationsSub')} className="set-card">
<div className="form">
<Toggle id="n-rejected" label={t('set.nRejected')} hint={t('set.nRejectedHint')} checked={s.notify.rejected} onChange={v => set(x => ({ ...x, notify: { ...x.notify, rejected: v } }))} />
<Toggle id="n-approved" label={t('set.nApproved')} hint={t('set.nApprovedHint')} checked={s.notify.approved} onChange={v => set(x => ({ ...x, notify: { ...x.notify, approved: v } }))} />
<Toggle id="n-live" label={t('set.nLive')} hint={t('set.nLiveHint')} checked={s.notify.live} onChange={v => set(x => ({ ...x, notify: { ...x.notify, live: v } }))} />
<Toggle id="n-review" label={t('set.nReview')} hint={t('set.nReviewHint')} checked={s.notify.reviewState} onChange={v => set(x => ({ ...x, notify: { ...x.notify, reviewState: v } }))} />
<Toggle id="n-low" label={t('set.nLow')} hint={t('set.nLowHint')} checked={s.notify.lowReviews} onChange={v => set(x => ({ ...x, notify: { ...x.notify, lowReviews: v } }))} />
<Toggle id="n-crash" label={t('set.nCrash')} hint={t('set.nCrashHint')} checked={s.notify.crash} onChange={v => set(x => ({ ...x, notify: { ...x.notify, crash: v } }))} />
<Toggle id="n-quiet" label={t('set.quiet')} hint={t('set.quietHint')} checked={s.notify.quietHours.enabled} onChange={v => set(x => ({ ...x, notify: { ...x.notify, quietHours: { ...x.notify.quietHours, enabled: v } } }))} />
{s.notify.quietHours.enabled && <div className="row" style={{ gap: 12 }}>
<label className="field"><span className="legend">{t('set.quietFrom')}</span><input type="time" className="input" style={{ width: 120 }} value={s.notify.quietHours.from} onChange={e => { const v = e.target.value; if (v) set(x => ({ ...x, notify: { ...x.notify, quietHours: { ...x.notify.quietHours, from: v } } })); }} /></label>
<label className="field"><span className="legend">{t('set.quietTo')}</span><input type="time" className="input" style={{ width: 120 }} value={s.notify.quietHours.to} onChange={e => { const v = e.target.value; if (v) set(x => ({ ...x, notify: { ...x.notify, quietHours: { ...x.notify.quietHours, to: v } } })); }} /></label>
</div>}
{d.muteUntil > Date.now() && <p className="small muted" role="status">{t('set.muted', { until: new Date(d.muteUntil).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) })}</p>}
<p className="small muted">{t('set.clickNote')}</p>
<div className="actions"><button className="btn btn-sm" onClick={() => d.setMute(d.muteUntil > Date.now() ? 0 : 60)}><Icon name="bell" />{d.muteUntil > Date.now() ? t('set.unmute', { until: new Date(d.muteUntil).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }) : t('set.muteHour')}</button></div>
<div className="actions"><button className="btn btn-sm" onClick={() => api.notify(t('set.testTitle'), t('set.testBody')).catch(e => setError(errorText(e)))}><Icon name="bell" />{t('set.testNotify')}</button></div>
</div>
</Card>