mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-11 13:31:09 +08:00
Merge branch 'fix/settings-switches-notice'
This commit is contained in:
commit
dc6cbf5b2a
7 files changed
+49
-20
No files matched your search
@@ -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>;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user