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
This commit is contained in:
2026-09-02 23:17:20 -07:00
parent 516fadd530
commit 68dd789fbf
22 changed files with 260 additions and 19 deletions
+44 -4
View File
@@ -50,6 +50,7 @@ const searchForm = el<HTMLFormElement>('search-form');
const searchInput = el<HTMLInputElement>('search-input');
const searchButton = el<HTMLButtonElement>('search-button');
const sourceSelect = el<HTMLSelectElement>('source-select');
const sourceDefaultButton = el<HTMLButtonElement>('source-default');
const grid = el<HTMLDivElement>('grid');
const gridEmpty = el<HTMLParagraphElement>('grid-empty');
const loadMoreButton = el<HTMLButtonElement>('load-more');
@@ -70,6 +71,8 @@ const settingsTitle = el<HTMLHeadingElement>('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<void> {
async function refreshSources(): Promise<void> {
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();
+12 -3
View File
@@ -33,10 +33,19 @@
<button class="primary-button" id="search-button" type="submit">Search</button>
</form>
<label class="source-picker">
<span class="source-label">Source</span>
<div class="source-picker">
<label class="source-label" for="source-select">Source</label>
<select class="text-input" id="source-select" aria-label="Extension source"></select>
</label>
<button
class="ghost-button default-source-button"
id="source-default"
type="button"
title="Open the browser on this source"
aria-label="Set as default source"
>
&#9734;
</button>
</div>
<nav class="tabs" role="tablist" aria-label="View">
<button
+11
View File
@@ -276,6 +276,17 @@ body {
width: auto;
}
.default-source-button {
padding: 6px 9px;
font-size: 15px;
line-height: 1;
}
.default-source-button[aria-pressed='true'] {
color: var(--accent);
border-color: var(--accent);
}
/* ---------- bridge banner ---------- */
.bridge-banner {