feat: add reader settings state and stylesheet generator
Settings are validated on read, not only on write: several go straight onto foliate's renderer as custom-element attributes, where a bad value wedges the layout silently rather than throwing. Unknown keys are stripped and missing ones filled from the defaults, so settings added later do not invalidate what a reader already has stored. The stylesheet is built as foliate's [before, after] pair. The paginator prepends the first to the section's <head> and appends the second, so the book's own CSS overrides the first and loses to the second. Typography goes in the first — a book that styles its own headings still gets to, and font size lands on <html> without !important so rem/em headings scale rather than being flattened. Colour goes in the second, because most EPUBs set a body background and would otherwise leave the page white against a dark UI. Font choices are reused from the app theme's FONT_STACKS rather than duplicated, so the reader cannot drift from the rest of the UI. Rename the ambient declaration to foliate-js.d.ts: as foliate.d.ts beside foliate.ts, TypeScript takes it for that file's emitted declaration, drops it from the program, and every $foliate import fails with TS2307.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { FONT_STACKS } from '$lib/theme/presets';
|
||||
import { readerSettingsSchema, type ReaderSettings } from '$lib/schema/reader';
|
||||
|
||||
export const READER_SETTINGS_STORAGE_KEY = 'chitai:reader-settings';
|
||||
|
||||
/** Georgia — the app's own title face, and a reasonable default for long prose. */
|
||||
export const DEFAULT_READER_SETTINGS: ReaderSettings = {
|
||||
fontFamily: FONT_STACKS[1].value,
|
||||
fontSize: 18,
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.6,
|
||||
letterSpacing: 0,
|
||||
margin: 48,
|
||||
gap: 6,
|
||||
maxInlineSize: 720,
|
||||
maxColumnCount: 2,
|
||||
flow: 'paginated',
|
||||
justify: true,
|
||||
hyphenate: true
|
||||
};
|
||||
|
||||
/** Reused from the app theme: system stacks only, so nothing silently falls back. */
|
||||
export const READER_FONT_OPTIONS = FONT_STACKS;
|
||||
|
||||
/** Slider bounds, kept next to the schema they mirror. */
|
||||
export const READER_BOUNDS = {
|
||||
fontSize: { min: 12, max: 32, step: 1 },
|
||||
fontWeight: { min: 300, max: 700, step: 100 },
|
||||
lineHeight: { min: 1, max: 2.5, step: 0.05 },
|
||||
letterSpacing: { min: -0.05, max: 0.2, step: 0.01 },
|
||||
margin: { min: 0, max: 120, step: 4 },
|
||||
gap: { min: 0, max: 15, step: 1 },
|
||||
maxInlineSize: { min: 480, max: 1400, step: 20 }
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Reads stored settings, falling back to defaults on anything unusable.
|
||||
*
|
||||
* Unknown keys are stripped and missing ones filled, so settings added in a
|
||||
* later release do not invalidate what a reader already has stored.
|
||||
*/
|
||||
export function loadReaderSettings(): ReaderSettings {
|
||||
try {
|
||||
const raw = localStorage.getItem(READER_SETTINGS_STORAGE_KEY);
|
||||
if (!raw) return DEFAULT_READER_SETTINGS;
|
||||
const parsed = readerSettingsSchema.safeParse({
|
||||
...DEFAULT_READER_SETTINGS,
|
||||
...JSON.parse(raw)
|
||||
});
|
||||
return parsed.success ? parsed.data : DEFAULT_READER_SETTINGS;
|
||||
} catch {
|
||||
return DEFAULT_READER_SETTINGS;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveReaderSettings(settings: ReaderSettings) {
|
||||
try {
|
||||
localStorage.setItem(READER_SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch (error) {
|
||||
// A full quota must not stop anyone reading.
|
||||
console.warn('Could not save reader settings', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The half of the settings that foliate takes as renderer attributes.
|
||||
*
|
||||
* The renderer has no JS property API — these must be set with setAttribute.
|
||||
* Note there is no `margin` shorthand upstream, and no `spread`: on a reflowable
|
||||
* book a two-page spread is max-column-count 2.
|
||||
*/
|
||||
export function toRendererAttributes(s: ReaderSettings): Record<string, string> {
|
||||
return {
|
||||
flow: s.flow,
|
||||
gap: `${s.gap}%`,
|
||||
'margin-top': `${s.margin}px`,
|
||||
'margin-bottom': `${s.margin}px`,
|
||||
'margin-left': `${s.margin}px`,
|
||||
'margin-right': `${s.margin}px`,
|
||||
'max-inline-size': `${s.maxInlineSize}px`,
|
||||
'max-column-count': String(s.maxColumnCount)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user