From 68dd789fbf1f89013593eeb225af68ee1e810bdf Mon Sep 17 00:00:00 2001 From: sudacode Date: Wed, 2 Sep 2026 23:17:20 -0700 Subject: [PATCH] feat(anime): remember the default Anime Browser source - Add `anime.defaultSource` with installed-source fallback - Let the source picker save a source or All sources as default --- changes/anime-browser.md | 2 +- config.example.jsonc | 1 + docs-site/anime-browser.md | 23 ++++-- docs-site/configuration.md | 6 +- docs-site/public/config.example.jsonc | 1 + src/animeui/animeui.ts | 48 ++++++++++- src/animeui/index.html | 15 +++- src/animeui/style.css | 11 +++ .../definitions/defaults-integrations.ts | 1 + .../definitions/options-integrations.ts | 7 ++ src/config/resolve/integrations.test.ts | 12 +++ src/config/resolve/integrations.ts | 12 +++ src/main.ts | 3 + .../runtime/anime-browser-ipc-handlers.ts | 3 + ...ime-browser-runtime-default-source.test.ts | 81 +++++++++++++++++++ .../runtime/anime-browser-runtime-deps.ts | 7 ++ src/main/runtime/anime-browser-runtime.ts | 33 +++++++- src/preload-anime-browser-api.ts | 2 + src/shared/ipc/contracts.ts | 1 + src/types/anime-browser.ts | 4 + src/types/config.ts | 1 + src/types/integrations.ts | 5 ++ 22 files changed, 260 insertions(+), 19 deletions(-) create mode 100644 src/main/runtime/anime-browser-runtime-default-source.test.ts diff --git a/changes/anime-browser.md b/changes/anime-browser.md index 9554dcfb..57d1037f 100644 --- a/changes/anime-browser.md +++ b/changes/anime-browser.md @@ -10,7 +10,7 @@ area: anime - Repository URLs only need to be an https URL to a `.json` index; the file name is not restricted to `index.min.json`. - The source picker offers "All sources", which searches every installed source at once. Results stream in as each source answers, with per-source progress in the status bar. Results are tagged with their source, failures do not blank the grid, and **Load more** appends later pages without duplicating streamed entries. - The Extensions tab opens with an Installed section listing every extension on disk with Remove, including ones added by hand or whose repository has since been removed. It compares APK and repository version codes, enables Update only for newer builds, marks current extensions as Up to date, and offers Update all when multiple updates are waiting. -- Added `anime.repos`, `anime.extensionsDir`, and `anime.preferredQuality` config keys. SubMiner ships no extension repositories and performs no discovery. +- Added `anime.repos`, `anime.extensionsDir`, `anime.preferredQuality`, and `anime.defaultSource` config keys; a star beside the Source picker saves the current source (or All sources) as the one the browser opens on. SubMiner ships no extension repositories and performs no discovery. - The settings app now groups Anime Browser config under an **Aniyomi** section in Integrations. A new `anime.autoOpenJimaku` option pauses newly loaded Anime Browser episodes, closes the in-player browser, opens Jimaku with the episode details filled in, and resumes after a subtitle loads or the modal closes without overriding playback that was already paused. - Anime playback targets Japanese audio: dub-labelled entries are skipped when the source offers an alternative, `alang` prefers Japanese, and the source's own audio and subtitle tracks are loaded into mpv (Japanese selected) instead of being discarded, so all of them can be switched from mpv's track menu. - The primary subtitle slot stays reserved for Japanese: a source that only has, say, English subtitles gets them added with a normalized language tag (`English` → `en`) but not selected, so the regular `secondarySub` auto-load can route them to the secondary slot instead. diff --git a/config.example.jsonc b/config.example.jsonc index cf408b98..b1477155 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -633,6 +633,7 @@ "extensionsDir": "", // Directory holding Aniyomi extension .apk files. Empty uses /anime-extensions. "repos": [], // Extension repository index URLs (any https .json index, e.g. https://.../index.min.json). Empty by default; SubMiner ships no repositories. "preferredQuality": "", // Preferred stream quality label, matched as a substring (for example: 1080). Empty uses the source order. + "defaultSource": "", // Source the Anime Browser selects when it opens: a source id (:) or "all" for every installed source. Empty selects the first installed source. The star beside the Source picker writes this value. "bridgeDir": "" // Directory holding an M-Extension-Server bundle (java runtime plus server jar) to run instead of the copy SubMiner downloads. Empty checks the package-manager install (Arch: mangatan-extension-server), then /anime-bridge. }, // Anime browser sources. SubMiner ships no extension repositories and bundles no sources; diff --git a/docs-site/anime-browser.md b/docs-site/anime-browser.md index 9e9433b7..f5cd2d15 100644 --- a/docs-site/anime-browser.md +++ b/docs-site/anime-browser.md @@ -116,6 +116,14 @@ missing. An extension that fails to load is skipped rather than blocking the others, so one bad APK will not hide the rest. +### Default source + +The browser opens on the first installed source. To open on a different one, +pick it in the **Source** picker and press the star beside it; the choice is +written to `anime.defaultSource` and applies the next time a browser window or +the in-player modal opens. **All sources** can be the default too. A default +that is no longer installed falls back to the first source. + ## Searching every source at once With more than one source installed, the **Source** picker gains an @@ -200,13 +208,14 @@ there is nowhere to write them and the status bar says so. ## Settings -| Key | Purpose | -| ------------------------ | ----------------------------------------------------------------------- | -| `anime.autoOpenJimaku` | Pause new episodes and open Jimaku for Japanese subtitles. | -| `anime.repos` | Repository index URLs. Empty by default. | -| `anime.extensionsDir` | Where APKs are read from. Empty uses `/anime-extensions`. | -| `anime.preferredQuality` | Preferred stream label, matched as a substring (for example `1080`). | -| `anime.bridgeDir` | A bridge bundle to run instead of the downloaded one. Empty by default. | +| Key | Purpose | +| ------------------------ | ------------------------------------------------------------------------ | +| `anime.autoOpenJimaku` | Pause new episodes and open Jimaku for Japanese subtitles. | +| `anime.repos` | Repository index URLs. Empty by default. | +| `anime.extensionsDir` | Where APKs are read from. Empty uses `/anime-extensions`. | +| `anime.preferredQuality` | Preferred stream label, matched as a substring (for example `1080`). | +| `anime.defaultSource` | Source the browser opens on: a source id or `all`. Empty uses the first. | +| `anime.bridgeDir` | A bridge bundle to run instead of the downloaded one. Empty by default. | Enable `anime.autoOpenJimaku` to hand each newly loaded Anime Browser episode to Jimaku. SubMiner pauses playback, closes the in-player browser if it is open, diff --git a/docs-site/configuration.md b/docs-site/configuration.md index 938b6f93..9c01abac 100644 --- a/docs-site/configuration.md +++ b/docs-site/configuration.md @@ -1053,7 +1053,7 @@ This example is intentionally compact. The option table below documents availabl | `metadata.pattern` | string | Format pattern for metadata: `%f`=filename, `%F`=filename+ext, `%t`=time, `%T`=time with milliseconds, `
`=newline | | `isLapis` | object | Lapis/shared sentence-card config: `{ enabled, sentenceCardModel }`. Sentence/audio field names are fixed to `Sentence` and `SentenceAudio`. | | `isKiku` | object | Kiku-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }` (shared sentence/audio/model settings are inherited from `isLapis`) | -| `isSenren` | object | Senren-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }`. Merges duplicates using Senren's scene-switching markup. Mutually exclusive with `isKiku.enabled`. | +| `isSenren` | object | Senren-only config: `{ enabled, fieldGrouping, deleteDuplicateInAuto }`. Merges duplicates using Senren's scene-switching markup. Mutually exclusive with `isKiku.enabled`. | `ankiConnect.ai` only controls feature-local enablement plus optional `model` / `systemPrompt` overrides. API key resolution, base URL, and timeout live under the shared top-level [`ai`](#shared-ai-provider) config. @@ -1168,7 +1168,8 @@ Sources for the [anime browser](/anime-browser). SubMiner ships no extension rep "autoOpenJimaku": false, "extensionsDir": "", "repos": [], - "preferredQuality": "" + "preferredQuality": "", + "defaultSource": "" } } ``` @@ -1179,6 +1180,7 @@ Sources for the [anime browser](/anime-browser). SubMiner ships no extension rep | `anime.extensionsDir` | `string` | `""` | Directory holding Aniyomi extension `.apk` files. Empty uses `/anime-extensions`. | | `anime.repos` | `string[]` | `[]` | Extension repository index URLs. Any `https` URL ending in `.json` works; `index.min.json` is only the common name. | | `anime.preferredQuality` | `string` | `""` | Preferred stream quality label, matched as a substring (for example `1080`). Empty keeps the source's own order. A Japanese-audio entry always outranks a higher-quality dub. | +| `anime.defaultSource` | `string` | `""` | Source the Anime Browser selects when it opens: a source id (`:`) or `all` for every installed source. Empty selects the first installed source. The star beside the Source picker writes this value. | | `anime.bridgeDir` | `string` | `""` | Directory holding an M-Extension-Server bundle (Java runtime plus server jar) to run instead of the downloaded copy. Empty checks the package-manager install first, then `/anime-bridge`. See [the bridge](anime-browser.md#the-bridge). | Repositories added from the browser's Extensions tab are written back to `anime.repos`, so the list can also be kept in a dotfile. Changes apply the next time the anime browser opens. diff --git a/docs-site/public/config.example.jsonc b/docs-site/public/config.example.jsonc index cf408b98..b1477155 100644 --- a/docs-site/public/config.example.jsonc +++ b/docs-site/public/config.example.jsonc @@ -633,6 +633,7 @@ "extensionsDir": "", // Directory holding Aniyomi extension .apk files. Empty uses /anime-extensions. "repos": [], // Extension repository index URLs (any https .json index, e.g. https://.../index.min.json). Empty by default; SubMiner ships no repositories. "preferredQuality": "", // Preferred stream quality label, matched as a substring (for example: 1080). Empty uses the source order. + "defaultSource": "", // Source the Anime Browser selects when it opens: a source id (:) or "all" for every installed source. Empty selects the first installed source. The star beside the Source picker writes this value. "bridgeDir": "" // Directory holding an M-Extension-Server bundle (java runtime plus server jar) to run instead of the copy SubMiner downloads. Empty checks the package-manager install (Arch: mangatan-extension-server), then /anime-bridge. }, // Anime browser sources. SubMiner ships no extension repositories and bundles no sources; diff --git a/src/animeui/animeui.ts b/src/animeui/animeui.ts index d1e8336a..3e0a6433 100644 --- a/src/animeui/animeui.ts +++ b/src/animeui/animeui.ts @@ -50,6 +50,7 @@ const searchForm = el('search-form'); const searchInput = el('search-input'); const searchButton = el('search-button'); const sourceSelect = el('source-select'); +const sourceDefaultButton = el('source-default'); const grid = el('grid'); const gridEmpty = el('grid-empty'); const loadMoreButton = el('load-more'); @@ -70,6 +71,8 @@ const settingsTitle = el('settings-title'); /** Last source accepted by the main process, used to roll back a rejected change. */ let selectedSourceId: string | null = null; +/** Configured `anime.defaultSource`, so the star reflects the picker's current value. */ +let defaultSourceId: string | null = null; /* ---------- tabs ---------- */ @@ -168,7 +171,11 @@ function renderBridgeState(state: AnimeBrowserBridgeState): void { * searches them together. That entry only earns its place with more than one * source installed. */ -function renderSources(sources: AnimeBrowserSource[], selectedId: string | null): void { +function renderSources( + sources: AnimeBrowserSource[], + selectedId: string | null, + defaultId: string | null, +): void { const options: HTMLOptionElement[] = []; if (sources.length > 1) { @@ -190,6 +197,23 @@ function renderSources(sources: AnimeBrowserSource[], selectedId: string | null) sourceSelect.replaceChildren(...options); sourceSelect.disabled = options.length <= 1; selectedSourceId = selectedId; + defaultSourceId = defaultId; + renderDefaultSourceButton(); +} + +/** + * The star is lit while the picker shows the configured default. With one + * source or none there is nothing to choose between, so it stays hidden. + */ +function renderDefaultSourceButton(): void { + const isDefault = sourceSelect.value !== '' && sourceSelect.value === defaultSourceId; + sourceDefaultButton.hidden = sourceSelect.options.length <= 1; + sourceDefaultButton.disabled = isDefault; + sourceDefaultButton.setAttribute('aria-pressed', String(isDefault)); + sourceDefaultButton.textContent = isDefault ? '\u2605' : '\u2606'; + sourceDefaultButton.title = isDefault + ? 'The browser opens on this source' + : 'Open the browser on this source'; } function searchingAllSources(): boolean { @@ -402,7 +426,7 @@ async function openSettings(): Promise { async function refreshSources(): Promise { const snapshot = await api.getSnapshot(); - renderSources(snapshot.sources, snapshot.selectedSourceId); + renderSources(snapshot.sources, snapshot.selectedSourceId, snapshot.defaultSourceId); } const extensions = createExtensionsPanel({ api, setStatus, onSourcesChanged: refreshSources }); @@ -429,6 +453,7 @@ sourceSelect.addEventListener('change', () => { try { await api.selectSource(requestedSourceId); selectedSourceId = requestedSourceId; + renderDefaultSourceButton(); // Settings belong to the source, so reload them rather than showing stale fields. if (currentView === 'settings') await openSettings(); await runSearch(searchInput.value.trim()); @@ -439,6 +464,21 @@ sourceSelect.addEventListener('change', () => { })(); }); +sourceDefaultButton.addEventListener('click', () => { + void (async () => { + const sourceId = sourceSelect.value; + try { + await api.setDefaultSource(sourceId); + defaultSourceId = sourceId; + renderDefaultSourceButton(); + const label = sourceSelect.selectedOptions[0]?.textContent ?? sourceId; + setStatus(`${label} is now the default source.`, 'ok'); + } catch (error) { + setStatus(describe(error), 'error'); + } + })(); +}); + loadMoreButton.addEventListener('click', () => void loadNextPage()); bannerUpdate.addEventListener('click', () => { @@ -457,7 +497,7 @@ bannerUpdate.addEventListener('click', () => { } // The bridge restarted, so the source list is fresh from disk. const snapshot = await api.getSnapshot(); - renderSources(snapshot.sources, snapshot.selectedSourceId); + renderSources(snapshot.sources, snapshot.selectedSourceId, snapshot.defaultSourceId); if (currentView === 'extensions') await extensions.refresh(); } catch (error) { setStatus(describe(error), 'error'); @@ -500,7 +540,7 @@ void (async () => { renderBridgeState(state); const snapshot = await api.getSnapshot(); - renderSources(snapshot.sources, snapshot.selectedSourceId); + renderSources(snapshot.sources, snapshot.selectedSourceId, snapshot.defaultSourceId); if (state.stage === 'ready' && snapshot.sources.length > 0) { searchInput.focus(); diff --git a/src/animeui/index.html b/src/animeui/index.html index 1b564cd4..488db2b8 100644 --- a/src/animeui/index.html +++ b/src/animeui/index.html @@ -33,10 +33,19 @@ -