Merge branch 'fix/settings-switches-notice'

This commit is contained in:
hectorqin committed 2026-11-04 13:24:16 +00:00
commit dc6cbf5b2a
7 files changed
+49 -20

No files matched your search

+4
View File
@@ -142,6 +142,10 @@
* plain sub-labels, not sticky bars. */
.settings-page .shelf-settings .section-title { position: static; margin: 22px 0 10px; padding: 0; border: 0; background: none; color: var(--color-muted); }
.settings-page .shelf-settings .segmented { width: 100%; }
/* The page's generic button rule (min-height, padding, a square radius, no background)
* outranks the toolkit switch; give the switch back its pill (ui.md: one on/off control). */
.settings-page .shelf-settings .switch { min-height: 0; padding: 0; border: 0; border-radius: var(--radius-pill); background: var(--reader-line); }
.settings-page .shelf-settings .switch[aria-pressed='true'] { background: var(--reader-fill); }
.settings-page .settings-link {
display: flex;
@@ -1,5 +1,6 @@
import { Icon } from '../../../ui/icon.tsx';
import { useState } from 'react';
import { useRef } from 'react';
import { notify } from '../../../ui/notifications.ts';
import {
appThemes, paperIds, readerPapers, resolveAppTheme, resolveReadingTheme, saveThemePreference, themes,
type AppThemeId, type ReaderPaper, type ThemeId, type ThemeMode, type ThemePreference,
@@ -68,12 +69,13 @@ export function MediaThemeSettings({ scope, onReaderSettings }: { scope: string;
const { preference, systemDark } = useThemeState(scope);
const paper = useSettingsStore(state => state.settings.paper);
const paperFollowsDark = useSettingsStore(state => state.settings.paperFollowsDark);
const [error, setError] = useState(''), [saved, setSaved] = useState(false);
// One notice for the panel: a run of taps updates it in place instead of stacking (ui.md §3.6).
const notice = useRef(Symbol('theme-notice'));
const report = (message: string, kind: 'success' | 'error') => notify(message, { kind }, notice.current);
const following = preference.mode === 'system';
const resolved = resolveAppTheme(preference, systemDark);
function save(patch: Partial<ThemePreference>) {
setError(''); setSaved(false);
try { saveThemePreference(scope, patch); setSaved(true); } catch (cause) { setError(cause instanceof Error ? cause.message : '主题保存失败'); }
try { saveThemePreference(scope, patch); report('主题已保存', 'success'); } catch (cause) { report(cause instanceof Error ? cause.message : '主题保存失败', 'error'); }
}
// Leaving follow-system keeps what is on screen now, so the switch never makes the page jump.
const toggleFollow = () => save(following ? { mode: resolved.mode, theme: resolved.theme } : { mode: 'system' });
@@ -106,9 +108,8 @@ export function MediaThemeSettings({ scope, onReaderSettings }: { scope: string;
{onReaderSettings && <>
<h2>阅读纸色</h2>
<ReadingPapers themeId={resolveReadingTheme(preference, systemDark).theme} settings={{ paper, paperFollowsDark }} onChange={patch => { onReaderSettings(patch); setSaved(true); setError(''); }} />
<ReadingPapers themeId={resolveReadingTheme(preference, systemDark).theme} settings={{ paper, paperFollowsDark }} onChange={patch => { onReaderSettings(patch); report('主题已保存', 'success'); }} />
<p className="theme-settings-note">纸色只改阅读页的底色和文字,工具栏仍用阅读主题;也可在阅读页的“外观”里切换。纸色按设备保存。</p>
</>}
{saved && <p role="status">主题已保存</p>}{error && <p className="media-error" role="alert">{error}</p>}
</section>;
}
+1 -1
View File
@@ -86,7 +86,7 @@
.media-screen .media-settings {max-width:760px;margin-inline:auto}
.media-screen .media-settings-panel,.media-screen .media-form {max-width:760px;margin-inline:auto}
.media-screen .media-setting-field,.media-screen .media-settings-check {min-height:56px;padding:12px 0;border-bottom:1px solid var(--border)}
/* Boolean preferences wear the same switch as the reader's sheets (.sheet .switch): one control for "on/off" app-wide. */
/* Boolean preferences wear the same switch as the reader's sheets (.switch): one control for "on/off" app-wide. */
.media-screen .media-settings-check input[role=switch] {appearance:none;-webkit-appearance:none;position:relative;flex:0 0 auto;width:2.7rem;height:1.6rem;min-height:1.6rem;margin:0;padding:0;border:0;border-radius:var(--radius-pill);background:var(--color-line);cursor:pointer;transition:background var(--motion-duration) var(--motion-ease)}
.media-screen .media-settings-check input[role=switch]::after {content:'';position:absolute;top:.2rem;inset-inline-start:.2rem;width:1.2rem;height:1.2rem;border-radius:50%;background:var(--color-surface);box-shadow:0 1px 2px rgb(0 0 0 / .25);transition:inset-inline-start var(--motion-duration) var(--motion-ease)}
.media-screen .media-settings-check input[role=switch]:checked {background:var(--color-brand)}
+10 -10
View File
@@ -1830,21 +1830,21 @@ book-content {
margin-top: 0.85rem;
}
/* ---- shelf settings sheet ---- */
/* ---- switch rows (SwitchRow: sheets and the 偏好设置 page) ---- */
.sheet .switch-field {
.switch-field {
flex-direction: row;
align-items: center;
gap: 0.75rem;
}
.sheet .switch-text {
.switch-text {
flex: 1 1 auto;
min-width: 0;
font-size: var(--text-ui);
}
.sheet .switch-text .muted {
.switch-text .muted {
font-size: var(--text-meta);
margin-top: 0.15rem;
}
@@ -1857,7 +1857,7 @@ book-content {
* this on" — a separate class would be a second one, and the two would eventually
* disagree.
*/
.sheet .switch {
.switch {
flex: 0 0 auto;
position: relative;
width: 2.7rem;
@@ -1869,7 +1869,7 @@ book-content {
transition: background var(--motion-duration) var(--motion-ease);
}
.sheet .switch::after {
.switch::after {
content: '';
position: absolute;
top: 0.2rem;
@@ -1882,11 +1882,11 @@ book-content {
transition: inset-inline-start var(--motion-duration) var(--motion-ease);
}
.sheet .switch[aria-pressed='true'] {
.switch[aria-pressed='true'] {
background: var(--reader-fill);
}
.sheet .switch[aria-pressed='true']::after {
.switch[aria-pressed='true']::after {
inset-inline-start: 1.3rem;
}
@@ -1901,8 +1901,8 @@ book-content {
animation: none;
}
.sheet .switch,
.sheet .switch::after {
.switch,
.switch::after {
transition: none;
}
+1 -1
View File
@@ -165,7 +165,7 @@
/* A switch row: label and hint take the width, the switch sits on the trailing edge.
The row is the 44px hit target (ui.md §3.3); SwitchRow forwards a press on the words. */
.sheet .switch-field {
.field.switch-field {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
+5 -2
View File
@@ -7,6 +7,7 @@ import {
resolveReaderSurface, resolveReadingTheme, saveThemePreference, themeSummary,
} from '../src/shared/theme/theme.ts';
import { MediaThemeSettings } from '../src/features/media/components/theme-settings.tsx';
import { noticeText } from './helpers/notices.ts';
const root = document.createElement('div');
document.body.append(root);
@@ -135,7 +136,8 @@ describe('外观与主题', () => {
await click('青绿');
expect(readThemePreference('a').system).toEqual({ light: 'green', dark: 'green' });
expect(pressed('青绿')).toBe('true');
expect(root.querySelector('[role=status]')?.textContent).toBe('主题已保存');
expect(root.querySelector('[role=status]')).toBeNull();
expect(noticeText()).toBe('主题已保存');
});
it('leaving follow-system keeps what is on screen, then offers light or dark', async () => {
@@ -164,7 +166,8 @@ describe('外观与主题', () => {
await act(async () => render(<MediaThemeSettings scope="a" />, root));
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('full'); });
await click('雾蓝');
expect(root.querySelector('[role=alert]')?.textContent).toContain('无法保存主题');
expect(root.querySelector('[role=alert]')).toBeNull();
expect(document.querySelector('.notyf [role=alert]')?.textContent).toContain('无法保存主题');
expect(readThemePreference('a').system.light).toBe('warm');
});
});
+21
View File
@@ -315,6 +315,7 @@ async function runMediaReview(browser, fixture, checks, errors) {
['media-folders', '/#/media/video/folders/review-lib', '文件夹浏览', '.media-folders'],
// 我的: every preference is a section of one page; administration is one page per job.
['preferences', '/#/me/settings', '偏好设置', '.settings-section'],
['preferences-shelf', '/#/me/settings?section=shelf', '偏好设置', '.settings-section .switch-field'],
['preferences-playback', '/#/me/settings?section=playback', '偏好设置', '.settings-section'],
['account', '/#/me/account', '账号与连接', '.media-account-panel'],
['admin-services', '/#/me/admin/services', '服务配置', '.business-settings'],
@@ -333,6 +334,26 @@ async function runMediaReview(browser, fixture, checks, errors) {
await capture(page, name, checks, errors);
}
// 偏好设置 is a page, not a sheet: the shelf's switches must still be drawn there, and a
// theme change reports through the floating notice rather than a line in the page (§3.6).
await page.goto(`${origin}/#/me/settings?section=shelf`);
await waitMediaHeading(page, '偏好设置');
const switches = await page.locator('.settings-section .switch').evaluateAll(nodes => nodes.map(node => {
const box = node.getBoundingClientRect();
const style = getComputedStyle(node);
return { width: box.width, height: box.height, background: style.backgroundColor, radius: parseFloat(style.borderTopLeftRadius) };
}));
assert.equal(switches.length, 2, 'shelf preferences should show 显示作者 and 显示阅读进度');
for (const box of switches) {
assert.ok(box.width >= 40 && box.height >= 24 && box.height < 32 && box.radius >= box.height / 2, `settings switch is not the toolkit pill: ${JSON.stringify(box)}`);
assert.notEqual(box.background, 'rgba(0, 0, 0, 0)', 'settings switch has no track colour');
}
await page.goto(`${origin}/#/me/settings`);
await waitMediaHeading(page, '偏好设置');
await page.locator('.theme-grid [aria-pressed="false"]').first().click();
await page.locator('.notyf .global-notice').filter({ hasText: '主题已保存' }).waitFor({ state: 'visible' });
assert.equal(await page.locator('.settings-section [role=status]').count(), 0, 'theme feedback must not be inline');
// The browse fixture includes one playable, partially watched video. Keep
// this assertion close to the route sweep so the recent continuation card
// cannot silently disappear when fixture data or rendering changes.