mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-03 19:21:32 -07:00
feat(anime): add anime browser powered by Aniyomi extensions
- Add `subminer anime` / `--anime` and a tray entry to open a browser that searches installed Aniyomi extension sources, shows cover art and episodes, and plays into mpv with overlay/mining attached - Add an Extensions tab to add repos and install/update/remove sources, and per-source settings for sources needing config - Support searching all sources at once with streaming, per-source results and status - Prefer Japanese audio/subtitle tracks from the source and keep the primary subtitle slot reserved for Japanese - Fix window/tray/Dock handling so the browser and mpv can be switched between without quitting the app or losing the Dock icon - Add anime.repos, anime.extensionsDir, anime.preferredQuality config keys (no bundled repos or discovery)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import type { BridgePreference } from './types';
|
||||
|
||||
/**
|
||||
* Extension preferences arrive as Android preference objects, one wrapper key
|
||||
* per widget type. They are stored and sent back verbatim so the extension sees
|
||||
* exactly the shape it produced; only the value field is edited.
|
||||
*/
|
||||
|
||||
export type PreferenceKind = 'text' | 'list' | 'multi' | 'switch';
|
||||
|
||||
/** A preference flattened for rendering. */
|
||||
export interface SourcePreferenceView {
|
||||
key: string;
|
||||
kind: PreferenceKind;
|
||||
title: string;
|
||||
summary: string | null;
|
||||
/** Current value: string for text/list, string[] for multi, boolean for switch. */
|
||||
value: string | string[] | boolean;
|
||||
/** Display labels, parallel to entryValues. Empty for text/switch. */
|
||||
entries: string[];
|
||||
entryValues: string[];
|
||||
}
|
||||
|
||||
const WIDGETS = {
|
||||
editTextPreference: 'text',
|
||||
listPreference: 'list',
|
||||
multiSelectListPreference: 'multi',
|
||||
switchPreferenceCompat: 'switch',
|
||||
checkBoxPreference: 'switch',
|
||||
} as const satisfies Record<string, PreferenceKind>;
|
||||
|
||||
type WidgetName = keyof typeof WIDGETS;
|
||||
|
||||
function widgetOf(
|
||||
entry: BridgePreference,
|
||||
): { name: WidgetName; body: Record<string, unknown> } | null {
|
||||
for (const name of Object.keys(WIDGETS) as WidgetName[]) {
|
||||
const body = entry[name];
|
||||
if (body !== null && typeof body === 'object') {
|
||||
return { name, body: body as Record<string, unknown> };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function stringList(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === 'string')
|
||||
: [];
|
||||
}
|
||||
|
||||
/** Flatten bridge preference entries for display. Unknown widgets are skipped. */
|
||||
export function parsePreferences(raw: BridgePreference[]): SourcePreferenceView[] {
|
||||
const views: SourcePreferenceView[] = [];
|
||||
|
||||
for (const entry of raw) {
|
||||
if (typeof entry.key !== 'string' || entry.key.startsWith('__')) continue;
|
||||
const widget = widgetOf(entry);
|
||||
if (!widget) continue;
|
||||
|
||||
const { body } = widget;
|
||||
const kind = WIDGETS[widget.name];
|
||||
const entries = stringList(body.entries);
|
||||
const entryValues = stringList(body.entryValues);
|
||||
|
||||
let value: string | string[] | boolean;
|
||||
if (kind === 'multi') {
|
||||
value = stringList(body.values);
|
||||
} else if (kind === 'switch') {
|
||||
value = body.value === true;
|
||||
} else if (kind === 'list') {
|
||||
const index = typeof body.valueIndex === 'number' ? body.valueIndex : -1;
|
||||
// valueIndex is -1 when the extension has no selection yet.
|
||||
value = index >= 0 && index < entryValues.length ? entryValues[index]! : '';
|
||||
} else {
|
||||
value = typeof body.value === 'string' ? body.value : '';
|
||||
}
|
||||
|
||||
views.push({
|
||||
key: entry.key,
|
||||
kind,
|
||||
title: typeof body.title === 'string' ? body.title : entry.key,
|
||||
summary: typeof body.summary === 'string' ? body.summary : null,
|
||||
value,
|
||||
entries,
|
||||
entryValues,
|
||||
});
|
||||
}
|
||||
|
||||
return views;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a copy of `raw` with one preference's value replaced, in whichever
|
||||
* fields that widget type reads. Unknown keys are returned unchanged.
|
||||
*/
|
||||
export function applyPreferenceValue(
|
||||
raw: BridgePreference[],
|
||||
key: string,
|
||||
value: string | string[] | boolean,
|
||||
): BridgePreference[] {
|
||||
return raw.map((entry) => {
|
||||
if (entry.key !== key) return entry;
|
||||
const widget = widgetOf(entry);
|
||||
if (!widget) return entry;
|
||||
|
||||
const body = { ...widget.body };
|
||||
const kind = WIDGETS[widget.name];
|
||||
|
||||
if (kind === 'multi') {
|
||||
body.values = Array.isArray(value) ? value : [];
|
||||
} else if (kind === 'switch') {
|
||||
body.value = value === true;
|
||||
} else if (kind === 'list') {
|
||||
const entryValues = stringList(body.entryValues);
|
||||
const index = entryValues.indexOf(String(value));
|
||||
body.valueIndex = index;
|
||||
if (index >= 0) body.value = entryValues[index];
|
||||
} else {
|
||||
// editTextPreference carries the same string in both value and text.
|
||||
body.value = String(value);
|
||||
body.text = String(value);
|
||||
}
|
||||
|
||||
return { ...entry, [widget.name]: body };
|
||||
});
|
||||
}
|
||||
|
||||
/** True when the preference should be masked in the UI and in logs. */
|
||||
export function isSecretPreference(view: SourcePreferenceView): boolean {
|
||||
return /password|token|api[-_ ]?key|secret/i.test(`${view.key} ${view.title}`);
|
||||
}
|
||||
Reference in New Issue
Block a user