From d6d8a10c812846f738b8cda7ad2c7bc77501a356 Mon Sep 17 00:00:00 2001 From: kvasonaft Date: Fri, 17 Jul 2026 00:31:02 +0300 Subject: [PATCH] Address community catalog lint findings - Type loadData() result as Partial instead of implicit any - Schedule requestAnimationFrame on the view's owning window (contentEl.win) so timing works in popout windows - Drop !important from theme backgrounds; selector specificity already wins over Obsidian's .view-content rule - Adopt the declarative settings API (getSettingDefinitions + getControlValue/setControlValue) so settings appear in the settings search on Obsidian 1.13+; the imperative display() stays as a fallback for 1.7.2-1.12 Co-Authored-By: Claude Fable 5 --- src/main.ts | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++-- styles.css | 6 +- 2 files changed, 160 insertions(+), 9 deletions(-) diff --git a/src/main.ts b/src/main.ts index 500926e..4386d66 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,6 +25,7 @@ import { TFile, WorkspaceLeaf, } from "obsidian"; +import type { SettingDefinitionItem } from "obsidian"; // Internal identifiers of our two view types. const VIEW_TYPE_FB2 = "fb2-reader-view"; @@ -372,7 +373,9 @@ class Fb2View extends FileView { const pos = this.plugin.getPosition(path); if (!pos || pos.index <= 0) return; // By the next animation frame the browser has laid out all elements. - requestAnimationFrame(() => { + // contentEl.win is the window owning this view — the correct one + // when the reader lives in a popout window. + this.contentEl.win.requestAnimationFrame(() => { const blocks = this.getScrollBlocks(); const target = blocks[Math.min(pos.index, blocks.length - 1)]; target?.scrollIntoView({ block: "start" }); @@ -490,7 +493,9 @@ class Fb2View extends FileView { const deadline = performance.now() + 12; while (this.renderQueue.length) { if (performance.now() > deadline) { - requestAnimationFrame(() => { + // contentEl.win: schedule on the window owning this view, + // which matters when the reader lives in a popout window. + this.contentEl.win.requestAnimationFrame(() => { // A new render (or file close) may have started meanwhile. if (pass === this.renderPass) this.pumpRenderQueue(pass); }); @@ -802,7 +807,7 @@ export default class Fb2ReaderPlugin extends Plugin { // Load persisted data (data.json). Object.assign layers the stored // settings over the defaults, so fields added in a plugin update // still get values. - const stored = (await this.loadData()) ?? {}; + const stored = ((await this.loadData()) ?? {}) as Partial; this.data = { positions: stored.positions ?? {}, settings: Object.assign({}, DEFAULT_SETTINGS, stored.settings), @@ -961,21 +966,167 @@ export default class Fb2ReaderPlugin extends Plugin { // --------------------------------------------------------------------------- // Fb2SettingTab — the settings tab // -// Obsidian calls display() every time the user opens the plugin settings. -// Each control's onChange updates plugin.fb2Settings and calls -// plugin.saveSettings(), so changes apply immediately. +// On Obsidian 1.13+ the tab is rendered declaratively from +// getSettingDefinitions(), which also feeds the settings search. +// The imperative display()/render() pair below is the fallback for +// older versions and is not called when definitions are provided. // --------------------------------------------------------------------------- class Fb2SettingTab extends PluginSettingTab { private plugin: Fb2ReaderPlugin; // Render counter — guards against a race (see the comment in render). private renderToken = 0; + // The system font list is fetched once, asynchronously; the tab + // re-renders when it arrives (see fontDefinition). + private fontsRequested = false; constructor(app: App, plugin: Fb2ReaderPlugin) { super(app, plugin); this.plugin = plugin; } + // --- Declarative settings (Obsidian 1.13+) --- + + getSettingDefinitions(): SettingDefinitionItem[] { + return [ + { + name: "Theme", + desc: "Color scheme for the reading area.", + control: { + type: "dropdown", + key: "theme", + defaultValue: "", + options: { + "": "Same as Obsidian", + light: "Light", + dark: "Dark", + sepia: "Sepia", + }, + }, + }, + { + name: "Text color", + desc: "Color of the main book text. Default follows the theme.", + control: { + type: "dropdown", + key: "textColor", + defaultValue: "", + options: this.textColorOptions(), + }, + }, + this.fontDefinition(), + { + name: "Font size", + desc: "Book text size in pixels (8–72).", + control: { + type: "number", + key: "fontSize", + defaultValue: DEFAULT_SETTINGS.fontSize, + min: 8, + max: 72, + step: 1, + validate: (v) => + Number.isFinite(v) && v >= 8 && v <= 72 + ? undefined + : "Enter a number between 8 and 72.", + }, + }, + { + name: "Line height", + desc: "Line spacing multiplier (1–3), e.g. 1.65.", + control: { + type: "number", + key: "lineHeight", + defaultValue: DEFAULT_SETTINGS.lineHeight, + min: 1, + max: 3, + step: 0.05, + validate: (v) => + Number.isFinite(v) && v >= 1 && v <= 3 + ? undefined + : "Enter a number between 1 and 3.", + }, + }, + { + name: "Drop caps", + desc: "Large initial letter at the first paragraph of each chapter.", + control: { type: "toggle", key: "dropCaps", defaultValue: false }, + }, + { + name: "Reset to defaults", + action: () => { + Object.assign(this.plugin.fb2Settings, DEFAULT_SETTINGS); + this.plugin.saveSettings(); + this.update(); // re-render with the restored values + }, + }, + ]; + } + + getControlValue(key: string): unknown { + return this.plugin.fb2Settings[key as keyof Fb2Settings]; + } + + setControlValue(key: string, value: unknown): void { + if (typeof value === "string") value = value.trim(); + (this.plugin.fb2Settings as unknown as Record)[key] = + value; + this.plugin.saveSettings(); + } + + // Text color presets, plus the saved color as an extra option when it is + // not in the list (e.g. hand-edited in data.json), so the selection + // doesn't get lost. + private textColorOptions(): Record { + const options: Record = {}; + const current = this.plugin.fb2Settings.textColor; + if (current && !(current in TEXT_COLORS)) options[current] = current; + return Object.assign(options, TEXT_COLORS); + } + + // Font: a dropdown when the system font list is available, otherwise + // (no permission, unsupported platform) a plain text field. + // getSettingDefinitions is synchronous, so the first call kicks off the + // async font query and re-renders the tab once the list arrives. + private fontDefinition(): SettingDefinitionItem { + const fonts = cachedSystemFonts; + if (!fonts) { + if (!this.fontsRequested) { + this.fontsRequested = true; + void getSystemFonts().then((families) => { + if (families.length) this.update(); + }); + } + return { + name: "Font", + desc: + "Font family for book text. " + + "Leave empty to use the Obsidian theme font.", + control: { + type: "text", + key: "fontFamily", + placeholder: "Same as Obsidian", + }, + }; + } + const options: Record = { "": "Same as Obsidian" }; + const current = this.plugin.fb2Settings.fontFamily; + if (current && !fonts.includes(current)) options[current] = current; + for (const family of fonts) options[family] = family; + return { + name: "Font", + desc: "Font used for book text.", + control: { + type: "dropdown", + key: "fontFamily", + defaultValue: "", + options, + }, + }; + } + + // --- Imperative fallback for Obsidian older than 1.13 --- + display(): void { // render is async (it awaits the font list); fire and forget. void this.render(); diff --git a/styles.css b/styles.css index 8f2bbeb..0b86ccb 100644 --- a/styles.css +++ b/styles.css @@ -7,7 +7,7 @@ body.fb2-theme-dark .fb2-reader { --background-primary: #1e1e22; --background-secondary: #1e1e22; - background-color: #1e1e22 !important; + background-color: #1e1e22; color: #d6d6d6; --text-normal: #d6d6d6; --text-muted: #9c9ca4; @@ -21,7 +21,7 @@ body.fb2-theme-dark .fb2-reader { body.fb2-theme-light .fb2-reader { --background-primary: #fbfbf8; --background-secondary: #fbfbf8; - background-color: #fbfbf8 !important; + background-color: #fbfbf8; color: #232323; --text-normal: #232323; --text-muted: #6c6c68; @@ -35,7 +35,7 @@ body.fb2-theme-light .fb2-reader { body.fb2-theme-sepia .fb2-reader { --background-primary: #f4ecd8; --background-secondary: #f4ecd8; - background-color: #f4ecd8 !important; + background-color: #f4ecd8; color: #5b4636; --text-normal: #5b4636; --text-muted: #8a7a62;