mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-11 17:16:18 -07:00
feat(anime): support shared bridge installs and updates
- Prefer configured or package-managed bridge bundles before downloading - Select compatible upstream releases and offer managed-bundle updates - Document bridge configuration and package dependencies
This commit is contained in:
+46
-13
@@ -57,6 +57,7 @@ const banner = el<HTMLDivElement>('bridge-banner');
|
||||
const bannerMessage = el<HTMLSpanElement>('bridge-message');
|
||||
const bannerMeter = el<HTMLSpanElement>('bridge-meter');
|
||||
const bannerMeterFill = el<HTMLElement>('bridge-meter-fill');
|
||||
const bannerUpdate = el<HTMLButtonElement>('bridge-update');
|
||||
const statusMessage = el<HTMLSpanElement>('status-message');
|
||||
const browseTab = el<HTMLButtonElement>('tab-browse');
|
||||
const extensionsTab = el<HTMLButtonElement>('tab-extensions');
|
||||
@@ -121,31 +122,33 @@ const BRIDGE_LABELS: Record<AnimeBrowserBridgeState['stage'], string> = {
|
||||
idle: 'Starting the extension bridge',
|
||||
locating: 'Looking up the extension bridge release',
|
||||
downloading: 'Downloading the extension bridge',
|
||||
verifying: 'Verifying the download',
|
||||
extracting: 'Unpacking the extension bridge',
|
||||
starting: 'Starting the extension bridge',
|
||||
ready: 'Bridge ready',
|
||||
failed: 'Bridge failed to start',
|
||||
};
|
||||
|
||||
const BUSY_STAGES = new Set([
|
||||
'idle',
|
||||
'locating',
|
||||
'downloading',
|
||||
'verifying',
|
||||
'extracting',
|
||||
'starting',
|
||||
]);
|
||||
const BUSY_STAGES = new Set(['idle', 'locating', 'downloading', 'extracting', 'starting']);
|
||||
|
||||
function renderBridgeState(state: AnimeBrowserBridgeState): void {
|
||||
const busy = BUSY_STAGES.has(state.stage);
|
||||
banner.dataset.stage = state.stage;
|
||||
banner.dataset.busy = String(busy);
|
||||
|
||||
// An update is only offered from a running bridge; mid-start it would race
|
||||
// the start it interrupts.
|
||||
const update = state.stage === 'ready' ? (state.install?.updateAvailable ?? null) : null;
|
||||
// Once ready with nothing to report, the banner has nothing to say.
|
||||
const hide = state.stage === 'ready' && state.message === null;
|
||||
const hide = state.stage === 'ready' && state.message === null && update === null;
|
||||
banner.classList.toggle('hidden', hide);
|
||||
bannerMessage.textContent = state.message ?? BRIDGE_LABELS[state.stage];
|
||||
bannerMessage.textContent =
|
||||
state.message ??
|
||||
(update === null
|
||||
? BRIDGE_LABELS[state.stage]
|
||||
: `Extension bridge ${state.install?.version ?? 'of unknown version'} is installed; ${update} is available.`);
|
||||
bannerUpdate.classList.toggle('hidden', update === null);
|
||||
bannerUpdate.textContent = update === null ? '' : `Update to ${update}`;
|
||||
bannerUpdate.disabled = busy;
|
||||
|
||||
const showMeter = state.progress !== null;
|
||||
bannerMeter.classList.toggle('hidden', !showMeter);
|
||||
@@ -438,6 +441,31 @@ sourceSelect.addEventListener('change', () => {
|
||||
|
||||
loadMoreButton.addEventListener('click', () => void loadNextPage());
|
||||
|
||||
bannerUpdate.addEventListener('click', () => {
|
||||
void (async () => {
|
||||
bannerUpdate.disabled = true;
|
||||
try {
|
||||
const state = await api.updateBridge();
|
||||
renderBridgeState(state);
|
||||
if (state.stage === 'ready' && state.install?.updateAvailable === null) {
|
||||
setStatus(
|
||||
`Extension bridge updated to ${state.install.version ?? 'the pinned release'}.`,
|
||||
'ok',
|
||||
);
|
||||
} else if (state.message !== null) {
|
||||
setStatus(state.message, 'error');
|
||||
}
|
||||
// The bridge restarted, so the source list is fresh from disk.
|
||||
const snapshot = await api.getSnapshot();
|
||||
renderSources(snapshot.sources, snapshot.selectedSourceId);
|
||||
if (currentView === 'extensions') await extensions.refresh();
|
||||
} catch (error) {
|
||||
setStatus(describe(error), 'error');
|
||||
bannerUpdate.disabled = false;
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
api.onBridgeState(renderBridgeState);
|
||||
|
||||
// The queue changes without this window asking: it advances by itself when an
|
||||
@@ -450,7 +478,7 @@ api.onPlaybackState((state) => {
|
||||
});
|
||||
|
||||
void (async () => {
|
||||
renderBridgeState({ stage: 'idle', progress: null, message: null });
|
||||
renderBridgeState({ stage: 'idle', progress: null, message: null, install: null });
|
||||
// A queue survives the window being closed and reopened, so start from what
|
||||
// the main process already holds rather than from empty.
|
||||
void api.getQueue().then(
|
||||
@@ -485,7 +513,12 @@ void (async () => {
|
||||
} catch (error) {
|
||||
// Without this the window keeps the "starting" banner up forever, with the
|
||||
// search box disabled and nothing saying why.
|
||||
renderBridgeState({ stage: 'failed', progress: null, message: describe(error) });
|
||||
renderBridgeState({
|
||||
stage: 'failed',
|
||||
progress: null,
|
||||
message: describe(error),
|
||||
install: null,
|
||||
});
|
||||
setStatus(describe(error), 'error');
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, el } from './dom';
|
||||
import { buildIconIndex, iconMonogram, isSafeIconUrl, repoFaviconUrl } from './extension-icons';
|
||||
import { describeInstalled } from './format';
|
||||
import { describeBridgeInstall, describeInstalled } from './format';
|
||||
import {
|
||||
collectLanguages,
|
||||
filterByLanguage,
|
||||
@@ -121,6 +121,7 @@ export function createExtensionsPanel(options: ExtensionsPanelOptions) {
|
||||
const { api, setStatus, onSourcesChanged } = options;
|
||||
|
||||
const extensionsDirLabel = el<HTMLSpanElement>('extensions-dir');
|
||||
const bridgeInfo = el<HTMLParagraphElement>('bridge-info');
|
||||
const installedList = el<HTMLDivElement>('installed-list');
|
||||
const installedCount = el<HTMLSpanElement>('installed-count');
|
||||
const availableList = el<HTMLDivElement>('extensions-list');
|
||||
@@ -328,6 +329,7 @@ export function createExtensionsPanel(options: ExtensionsPanelOptions) {
|
||||
async function refresh(): Promise<void> {
|
||||
const snapshot = await api.getSnapshot();
|
||||
extensionsDirLabel.textContent = snapshot.extensionsDir;
|
||||
bridgeInfo.textContent = describeBridgeInstall(snapshot.bridge.install);
|
||||
renderRepos(snapshot.repos);
|
||||
|
||||
repoFailures = [];
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { describeInstalled, sourceOptionLabel, summarizeSearch } from './format';
|
||||
import {
|
||||
describeInstalled,
|
||||
sourceOptionLabel,
|
||||
summarizeSearch,
|
||||
describeBridgeInstall,
|
||||
} from './format';
|
||||
import type { AnimeBrowserSearchResult } from '../types/anime-browser';
|
||||
|
||||
const result = (
|
||||
@@ -58,3 +63,29 @@ test('describeInstalled falls back to the package alone when nothing loaded', ()
|
||||
'broken',
|
||||
);
|
||||
});
|
||||
|
||||
test('describeBridgeInstall says who updates the bridge', () => {
|
||||
assert.match(describeBridgeInstall(null), /not started/);
|
||||
assert.match(
|
||||
describeBridgeInstall({
|
||||
origin: 'system',
|
||||
version: 'v1.0.6.2',
|
||||
dir: '/usr/share/mangatan/extension_server',
|
||||
updateAvailable: null,
|
||||
}),
|
||||
/v1\.0\.6\.2 from \/usr\/share\/mangatan\/extension_server.*package manager/,
|
||||
);
|
||||
assert.match(
|
||||
describeBridgeInstall({
|
||||
origin: 'managed',
|
||||
version: 'v1.0.5.0',
|
||||
dir: '/home/u/.config/SubMiner/anime-bridge',
|
||||
updateAvailable: 'v1.0.6.0',
|
||||
}),
|
||||
/v1\.0\.6\.0 is available/,
|
||||
);
|
||||
assert.match(
|
||||
describeBridgeInstall({ origin: 'managed', version: null, dir: '/d', updateAvailable: null }),
|
||||
/unknown version.*up to date/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AnimeBrowserBridgeInstall,
|
||||
AnimeBrowserSearchResult,
|
||||
AnimeBrowserSource,
|
||||
InstalledExtensionView,
|
||||
@@ -33,3 +34,16 @@ export function describeInstalled(view: InstalledExtensionView): string {
|
||||
if (view.langs.length > 0) parts.push(view.langs.join(', '));
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
/** One line for the Extensions tab: which bridge is running and who updates it. */
|
||||
export function describeBridgeInstall(install: AnimeBrowserBridgeInstall | null): string {
|
||||
if (install === null) return 'The extension bridge has not started yet.';
|
||||
const version = install.version ?? 'unknown version';
|
||||
if (install.origin === 'system') {
|
||||
return `M-Extension-Server ${version} from ${install.dir}, installed outside SubMiner (for example by your package manager), which is where updates come from.`;
|
||||
}
|
||||
if (install.updateAvailable !== null) {
|
||||
return `M-Extension-Server ${version} in ${install.dir}, downloaded by SubMiner. ${install.updateAvailable} is available from the banner above.`;
|
||||
}
|
||||
return `M-Extension-Server ${version} in ${install.dir}, downloaded by SubMiner and up to date.`;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
<span class="bridge-dot" id="bridge-dot"></span>
|
||||
<span class="bridge-message" id="bridge-message"></span>
|
||||
<span class="bridge-meter hidden" id="bridge-meter"><i id="bridge-meter-fill"></i></span>
|
||||
<button
|
||||
class="ghost-button bridge-update hidden"
|
||||
id="bridge-update"
|
||||
type="button"
|
||||
title="Downloads the verified release and restarts the bridge. A playing episode's stream stops."
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<section class="settings hidden" id="settings" role="tabpanel" aria-labelledby="tab-settings">
|
||||
@@ -122,6 +128,9 @@
|
||||
</h3>
|
||||
<div class="lang-filter" id="lang-filter" role="group" aria-label="Filter by language"></div>
|
||||
<div class="ext-list" id="extensions-list" aria-label="Available extensions"></div>
|
||||
|
||||
<h3 class="ext-group-title">Bridge</h3>
|
||||
<p class="repo-hint" id="bridge-info"></p>
|
||||
</section>
|
||||
|
||||
<main class="layout" id="layout" role="tabpanel" aria-labelledby="tab-browse">
|
||||
|
||||
@@ -304,6 +304,13 @@ body {
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.bridge-update {
|
||||
margin-left: auto;
|
||||
padding: 4px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---------- layout ---------- */
|
||||
|
||||
.layout {
|
||||
|
||||
Reference in New Issue
Block a user