Require mpv plugin in first-run setup

This commit is contained in:
2026-04-02 23:49:57 -07:00
parent 85e3aa4c6b
commit 8b9ac99f3d
9 changed files with 58 additions and 57 deletions

View File

@@ -21,7 +21,7 @@ Look up words with Yomitan, export to Anki in one key, track your immersion —
SubMiner runs as an invisible Electron overlay on top of mpv. Subtitles render as an interactive layer. Move your cursor over any word and trigger a [Yomitan](https://github.com/yomidevs/yomitan) lookup. Press one key to snapshot the sentence, audio, and screenshot into Anki via AnkiConnect. SubMiner runs as an invisible Electron overlay on top of mpv. Subtitles render as an interactive layer. Move your cursor over any word and trigger a [Yomitan](https://github.com/yomidevs/yomitan) lookup. Press one key to snapshot the sentence, audio, and screenshot into Anki via AnkiConnect.
On Windows, the recommended playback entry point is the optional `SubMiner mpv` shortcut created during setup. It launches `mpv` with SubMiner's defaults directly, so you do not need an `mpv.conf` profile just to use the shortcut. On Windows, the recommended playback entry point is the optional `SubMiner mpv` shortcut created during setup. First-run setup requires the mpv plugin before it can finish. The shortcut launches `mpv` with SubMiner's defaults directly, so you do not need an `mpv.conf` profile just to use it.
## Features ## Features

View File

@@ -0,0 +1,5 @@
type: changed
area: setup
- Made mpv plugin installation mandatory in the first-run setup flow, removed the skip path, and kept Finish disabled until the plugin is installed.
- Updated the Windows setup copy to make the `SubMiner mpv` shortcut the recommended playback entry point after setup completes.

View File

@@ -171,7 +171,7 @@ Install `mpv` separately and ensure `mpv.exe` is on `PATH`. `ffmpeg` is still re
### Windows Usage Notes ### Windows Usage Notes
- Launch `SubMiner.exe` once to let the first-run setup flow seed `%APPDATA%\\SubMiner\\config.jsonc`, offer mpv plugin installation, open bundled Yomitan settings, and optionally create `SubMiner mpv` Start Menu/Desktop shortcuts. On Windows, that shortcut is the recommended way to launch mpv playback with SubMiner defaults. - Launch `SubMiner.exe` once to let the first-run setup flow seed `%APPDATA%\\SubMiner\\config.jsonc`, require mpv plugin installation, open bundled Yomitan settings, and optionally create `SubMiner mpv` Start Menu/Desktop shortcuts. On Windows, that shortcut is the recommended way to launch mpv playback with SubMiner defaults.
- `SubMiner.exe --launch-mpv` and the optional `SubMiner mpv` shortcut pass SubMiner's default mpv socket/subtitle args directly, including the Windows-safe subtitle search paths that skip the extra current-directory scan; they do not require an `mpv.conf` profile named `subminer`. - `SubMiner.exe --launch-mpv` and the optional `SubMiner mpv` shortcut pass SubMiner's default mpv socket/subtitle args directly, including the Windows-safe subtitle search paths that skip the extra current-directory scan; they do not require an `mpv.conf` profile named `subminer`.
- First-run mpv plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is installed in a non-standard location. - First-run mpv plugin installs pin `binary_path` to the current `SubMiner.exe` automatically. Manual plugin configs can leave `binary_path` empty unless SubMiner is installed in a non-standard location.
- Windows plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket`; do not keep `/tmp/subminer-socket` on Windows. - Windows plugin installs rewrite `socket_path` to `\\.\pipe\subminer-socket`; do not keep `/tmp/subminer-socket` on Windows.

View File

@@ -118,6 +118,7 @@ SubMiner.AppImage --help # Show all options
### Windows mpv Shortcut ### Windows mpv Shortcut
If you enabled the optional Windows shortcut during install, SubMiner creates a `SubMiner mpv` shortcut in the Start menu and/or on the desktop. On Windows, that shortcut is the recommended way to launch local files with SubMiner because it starts `mpv.exe` with the right defaults directly. If you enabled the optional Windows shortcut during install, SubMiner creates a `SubMiner mpv` shortcut in the Start menu and/or on the desktop. On Windows, that shortcut is the recommended way to launch local files with SubMiner because it starts `mpv.exe` with the right defaults directly.
First-run setup requires the mpv plugin before it can finish, so the shortcut is the normal Windows playback entry point after setup completes.
You can use it three ways: You can use it three ways:
@@ -157,12 +158,12 @@ SubMiner.AppImage --setup
Setup flow: Setup flow:
- config file: create the default config directory and prefer `config.jsonc` - config file: create the default config directory and prefer `config.jsonc`
- plugin status: install or skip the bundled mpv plugin - plugin status: install the bundled mpv plugin before finishing setup
- Yomitan shortcut: open bundled Yomitan settings directly from the setup window - Yomitan shortcut: open bundled Yomitan settings directly from the setup window
- dictionary check: ensure at least one bundled Yomitan dictionary is available - dictionary check: ensure at least one bundled Yomitan dictionary is available
- Windows: optionally create or remove `SubMiner mpv` Start Menu/Desktop shortcuts (`SubMiner.exe --launch-mpv`) - Windows: optionally create or remove `SubMiner mpv` Start Menu/Desktop shortcuts (`SubMiner.exe --launch-mpv`)
- refresh: re-check plugin + dictionary state without restarting - refresh: re-check plugin + dictionary state without restarting
- `Finish setup` stays disabled until dictionary availability is detected - `Finish setup` stays disabled until the mpv plugin is installed and dictionary availability is detected
- finish action writes setup completion state and suppresses future auto-open prompts - finish action writes setup completion state and suppresses future auto-open prompts
AniList character dictionary auto-sync (optional): AniList character dictionary auto-sync (optional):

View File

@@ -2238,19 +2238,22 @@ const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
firstRunSetupMessage = snapshot.message; firstRunSetupMessage = snapshot.message;
return; return;
} }
if (submission.action === 'skip-plugin') {
await firstRunSetupService.skipPluginInstall();
firstRunSetupMessage = 'mpv plugin installation skipped.';
return;
}
const snapshot = await firstRunSetupService.markSetupCompleted(); const snapshot = await firstRunSetupService.markSetupCompleted();
if (snapshot.state.status === 'completed') { if (snapshot.state.status === 'completed') {
firstRunSetupMessage = null; firstRunSetupMessage = null;
return { closeWindow: true }; return { closeWindow: true };
} }
if (snapshot.pluginStatus !== 'installed') {
firstRunSetupMessage = 'Install the mpv plugin before finishing setup.';
return;
}
if (!snapshot.externalYomitanConfigured && snapshot.dictionaryCount < 1) {
firstRunSetupMessage = 'Install at least one Yomitan dictionary before finishing setup.'; firstRunSetupMessage = 'Install at least one Yomitan dictionary before finishing setup.';
return; return;
}
firstRunSetupMessage = 'Finish setup requires the mpv plugin and Yomitan dictionaries.';
return;
}, },
markSetupInProgress: async () => { markSetupInProgress: async () => {
firstRunSetupMessage = null; firstRunSetupMessage = null;

View File

@@ -88,7 +88,7 @@ test('setup service auto-completes legacy installs with config and dictionaries'
const service = createFirstRunSetupService({ const service = createFirstRunSetupService({
configDir, configDir,
getYomitanDictionaryCount: async () => 2, getYomitanDictionaryCount: async () => 2,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -106,17 +106,18 @@ test('setup service auto-completes legacy installs with config and dictionaries'
}); });
}); });
test('setup service requires explicit finish for incomplete installs and supports plugin skip/install', async () => { test('setup service requires mpv plugin install before finish', async () => {
await withTempDir(async (root) => { await withTempDir(async (root) => {
const configDir = path.join(root, 'SubMiner'); const configDir = path.join(root, 'SubMiner');
fs.mkdirSync(configDir, { recursive: true }); fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(path.join(configDir, 'config.jsonc'), '{}'); fs.writeFileSync(path.join(configDir, 'config.jsonc'), '{}');
let dictionaryCount = 0; let dictionaryCount = 0;
let pluginInstalled = false;
const service = createFirstRunSetupService({ const service = createFirstRunSetupService({
configDir, configDir,
getYomitanDictionaryCount: async () => dictionaryCount, getYomitanDictionaryCount: async () => dictionaryCount,
detectPluginInstalled: () => false, detectPluginInstalled: () => pluginInstalled,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -130,13 +131,11 @@ test('setup service requires explicit finish for incomplete installs and support
assert.equal(initial.state.status, 'incomplete'); assert.equal(initial.state.status, 'incomplete');
assert.equal(initial.canFinish, false); assert.equal(initial.canFinish, false);
const skipped = await service.skipPluginInstall();
assert.equal(skipped.state.pluginInstallStatus, 'skipped');
const installed = await service.installMpvPlugin(); const installed = await service.installMpvPlugin();
assert.equal(installed.state.pluginInstallStatus, 'installed'); assert.equal(installed.state.pluginInstallStatus, 'installed');
assert.equal(installed.pluginInstallPathSummary, '/tmp/mpv'); assert.equal(installed.pluginInstallPathSummary, '/tmp/mpv');
pluginInstalled = true;
dictionaryCount = 1; dictionaryCount = 1;
const refreshed = await service.refreshStatus(); const refreshed = await service.refreshStatus();
assert.equal(refreshed.canFinish, true); assert.equal(refreshed.canFinish, true);
@@ -158,7 +157,7 @@ test('setup service allows completion without internal dictionaries when externa
configDir, configDir,
getYomitanDictionaryCount: async () => 0, getYomitanDictionaryCount: async () => 0,
isExternalYomitanConfigured: () => true, isExternalYomitanConfigured: () => true,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -190,7 +189,7 @@ test('setup service does not probe internal dictionaries when external yomitan i
throw new Error('should not probe internal dictionaries in external mode'); throw new Error('should not probe internal dictionaries in external mode');
}, },
isExternalYomitanConfigured: () => true, isExternalYomitanConfigured: () => true,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -218,7 +217,7 @@ test('setup service reopens when external-yomitan completion later has no extern
configDir, configDir,
getYomitanDictionaryCount: async () => 0, getYomitanDictionaryCount: async () => 0,
isExternalYomitanConfigured: () => true, isExternalYomitanConfigured: () => true,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -235,7 +234,7 @@ test('setup service reopens when external-yomitan completion later has no extern
configDir, configDir,
getYomitanDictionaryCount: async () => 0, getYomitanDictionaryCount: async () => 0,
isExternalYomitanConfigured: () => false, isExternalYomitanConfigured: () => false,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -262,7 +261,7 @@ test('setup service keeps completed when external-yomitan completion later has i
configDir, configDir,
getYomitanDictionaryCount: async () => 0, getYomitanDictionaryCount: async () => 0,
isExternalYomitanConfigured: () => true, isExternalYomitanConfigured: () => true,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -279,7 +278,7 @@ test('setup service keeps completed when external-yomitan completion later has i
configDir, configDir,
getYomitanDictionaryCount: async () => 2, getYomitanDictionaryCount: async () => 2,
isExternalYomitanConfigured: () => false, isExternalYomitanConfigured: () => false,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -304,7 +303,7 @@ test('setup service marks cancelled when popup closes before completion', async
const service = createFirstRunSetupService({ const service = createFirstRunSetupService({
configDir, configDir,
getYomitanDictionaryCount: async () => 0, getYomitanDictionaryCount: async () => 0,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -331,7 +330,7 @@ test('setup service reflects detected Windows mpv shortcuts before preferences a
platform: 'win32', platform: 'win32',
configDir, configDir,
getYomitanDictionaryCount: async () => 0, getYomitanDictionaryCount: async () => 0,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',
@@ -364,7 +363,7 @@ test('setup service persists Windows mpv shortcut preferences and status with on
platform: 'win32', platform: 'win32',
configDir, configDir,
getYomitanDictionaryCount: async () => 0, getYomitanDictionaryCount: async () => 0,
detectPluginInstalled: () => false, detectPluginInstalled: () => true,
installPlugin: async () => ({ installPlugin: async () => ({
ok: true, ok: true,
pluginInstallStatus: 'installed', pluginInstallStatus: 'installed',

View File

@@ -27,7 +27,7 @@ export interface SetupStatusSnapshot {
dictionaryCount: number; dictionaryCount: number;
canFinish: boolean; canFinish: boolean;
externalYomitanConfigured: boolean; externalYomitanConfigured: boolean;
pluginStatus: 'installed' | 'optional' | 'skipped' | 'failed'; pluginStatus: 'installed' | 'required' | 'failed';
pluginInstallPathSummary: string | null; pluginInstallPathSummary: string | null;
windowsMpvShortcuts: SetupWindowsMpvShortcutSnapshot; windowsMpvShortcuts: SetupWindowsMpvShortcutSnapshot;
message: string | null; message: string | null;
@@ -48,7 +48,6 @@ export interface FirstRunSetupService {
markSetupInProgress: () => Promise<SetupStatusSnapshot>; markSetupInProgress: () => Promise<SetupStatusSnapshot>;
markSetupCancelled: () => Promise<SetupStatusSnapshot>; markSetupCancelled: () => Promise<SetupStatusSnapshot>;
markSetupCompleted: () => Promise<SetupStatusSnapshot>; markSetupCompleted: () => Promise<SetupStatusSnapshot>;
skipPluginInstall: () => Promise<SetupStatusSnapshot>;
installMpvPlugin: () => Promise<SetupStatusSnapshot>; installMpvPlugin: () => Promise<SetupStatusSnapshot>;
configureWindowsMpvShortcuts: (preferences: { configureWindowsMpvShortcuts: (preferences: {
startMenuEnabled: boolean; startMenuEnabled: boolean;
@@ -108,9 +107,8 @@ function getPluginStatus(
pluginInstalled: boolean, pluginInstalled: boolean,
): SetupStatusSnapshot['pluginStatus'] { ): SetupStatusSnapshot['pluginStatus'] {
if (pluginInstalled) return 'installed'; if (pluginInstalled) return 'installed';
if (state.pluginInstallStatus === 'skipped') return 'skipped';
if (state.pluginInstallStatus === 'failed') return 'failed'; if (state.pluginInstallStatus === 'failed') return 'failed';
return 'optional'; return 'required';
} }
function getWindowsMpvShortcutStatus( function getWindowsMpvShortcutStatus(
@@ -230,7 +228,9 @@ export function createFirstRunSetupService(deps: {
return { return {
configReady, configReady,
dictionaryCount, dictionaryCount,
canFinish: isYomitanSetupSatisfied({ canFinish:
pluginInstalled &&
isYomitanSetupSatisfied({
configReady, configReady,
dictionaryCount, dictionaryCount,
externalYomitanConfigured, externalYomitanConfigured,
@@ -347,8 +347,6 @@ export function createFirstRunSetupService(deps: {
}), }),
); );
}, },
skipPluginInstall: async () =>
refreshWithState(writeState({ ...readState(), pluginInstallStatus: 'skipped' })),
installMpvPlugin: async () => { installMpvPlugin: async () => {
const result = await deps.installPlugin(); const result = await deps.installPlugin();
return refreshWithState( return refreshWithState(

View File

@@ -14,7 +14,7 @@ test('buildFirstRunSetupHtml renders macchiato setup actions and disabled finish
dictionaryCount: 0, dictionaryCount: 0,
canFinish: false, canFinish: false,
externalYomitanConfigured: false, externalYomitanConfigured: false,
pluginStatus: 'optional', pluginStatus: 'required',
pluginInstallPathSummary: null, pluginInstallPathSummary: null,
windowsMpvShortcuts: { windowsMpvShortcuts: {
supported: false, supported: false,
@@ -29,6 +29,7 @@ test('buildFirstRunSetupHtml renders macchiato setup actions and disabled finish
assert.match(html, /SubMiner setup/); assert.match(html, /SubMiner setup/);
assert.match(html, /Install mpv plugin/); assert.match(html, /Install mpv plugin/);
assert.match(html, /Required before SubMiner setup can finish/);
assert.match(html, /Open Yomitan Settings/); assert.match(html, /Open Yomitan Settings/);
assert.match(html, /Finish setup/); assert.match(html, /Finish setup/);
assert.match(html, /disabled/); assert.match(html, /disabled/);
@@ -62,7 +63,7 @@ test('buildFirstRunSetupHtml explains external yomitan mode and keeps finish ena
dictionaryCount: 0, dictionaryCount: 0,
canFinish: true, canFinish: true,
externalYomitanConfigured: true, externalYomitanConfigured: true,
pluginStatus: 'optional', pluginStatus: 'installed',
pluginInstallPathSummary: null, pluginInstallPathSummary: null,
windowsMpvShortcuts: { windowsMpvShortcuts: {
supported: false, supported: false,
@@ -76,16 +77,14 @@ test('buildFirstRunSetupHtml explains external yomitan mode and keeps finish ena
}); });
assert.match(html, /External profile configured/); assert.match(html, /External profile configured/);
assert.match( assert.match(html, /Finish stays unlocked while SubMiner is reusing an external Yomitan profile\./);
html,
/Finish stays unlocked while SubMiner is reusing an external Yomitan profile\./,
);
}); });
test('parseFirstRunSetupSubmissionUrl parses supported custom actions', () => { test('parseFirstRunSetupSubmissionUrl parses supported custom actions', () => {
assert.deepEqual(parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=refresh'), { assert.deepEqual(parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=refresh'), {
action: 'refresh', action: 'refresh',
}); });
assert.equal(parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=skip-plugin'), null);
assert.equal(parseFirstRunSetupSubmissionUrl('https://example.com'), null); assert.equal(parseFirstRunSetupSubmissionUrl('https://example.com'), null);
}); });
@@ -146,7 +145,7 @@ test('closing incomplete first-run setup quits app outside background mode', asy
dictionaryCount: 0, dictionaryCount: 0,
canFinish: false, canFinish: false,
externalYomitanConfigured: false, externalYomitanConfigured: false,
pluginStatus: 'optional', pluginStatus: 'required',
pluginInstallPathSummary: null, pluginInstallPathSummary: null,
windowsMpvShortcuts: { windowsMpvShortcuts: {
supported: false, supported: false,

View File

@@ -19,7 +19,6 @@ export type FirstRunSetupAction =
| 'configure-windows-mpv-shortcuts' | 'configure-windows-mpv-shortcuts'
| 'open-yomitan-settings' | 'open-yomitan-settings'
| 'refresh' | 'refresh'
| 'skip-plugin'
| 'finish'; | 'finish';
export interface FirstRunSetupSubmission { export interface FirstRunSetupSubmission {
@@ -33,7 +32,7 @@ export interface FirstRunSetupHtmlModel {
dictionaryCount: number; dictionaryCount: number;
canFinish: boolean; canFinish: boolean;
externalYomitanConfigured: boolean; externalYomitanConfigured: boolean;
pluginStatus: 'installed' | 'optional' | 'skipped' | 'failed'; pluginStatus: 'installed' | 'required' | 'failed';
pluginInstallPathSummary: string | null; pluginInstallPathSummary: string | null;
windowsMpvShortcuts: { windowsMpvShortcuts: {
supported: boolean; supported: boolean;
@@ -64,18 +63,14 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
const pluginLabel = const pluginLabel =
model.pluginStatus === 'installed' model.pluginStatus === 'installed'
? 'Installed' ? 'Installed'
: model.pluginStatus === 'skipped'
? 'Skipped'
: model.pluginStatus === 'failed' : model.pluginStatus === 'failed'
? 'Failed' ? 'Failed'
: 'Optional'; : 'Required';
const pluginTone = const pluginTone =
model.pluginStatus === 'installed' model.pluginStatus === 'installed'
? 'ready' ? 'ready'
: model.pluginStatus === 'failed' : model.pluginStatus === 'failed'
? 'danger' ? 'danger'
: model.pluginStatus === 'skipped'
? 'muted'
: 'warn'; : 'warn';
const windowsShortcutLabel = const windowsShortcutLabel =
model.windowsMpvShortcuts.status === 'installed' model.windowsMpvShortcuts.status === 'installed'
@@ -129,8 +124,10 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
? 'ready' ? 'ready'
: 'warn'; : 'warn';
const footerMessage = model.externalYomitanConfigured const footerMessage = model.externalYomitanConfigured
? model.pluginStatus === 'installed'
? 'Finish stays unlocked while SubMiner is reusing an external Yomitan profile. If you later launch without yomitan.externalProfilePath, setup will require at least one internal dictionary.' ? 'Finish stays unlocked while SubMiner is reusing an external Yomitan profile. If you later launch without yomitan.externalProfilePath, setup will require at least one internal dictionary.'
: 'Finish stays locked until Yomitan reports at least one installed dictionary.'; : 'Finish stays locked until the mpv plugin is installed. If you later launch without yomitan.externalProfilePath, setup will also require at least one internal dictionary.'
: 'Finish stays locked until the mpv plugin is installed and Yomitan reports at least one installed dictionary.';
return `<!doctype html> return `<!doctype html>
<html> <html>
@@ -269,6 +266,7 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
<div> <div>
<strong>mpv plugin</strong> <strong>mpv plugin</strong>
<div class="meta">${escapeHtml(model.pluginInstallPathSummary ?? 'Default mpv scripts location')}</div> <div class="meta">${escapeHtml(model.pluginInstallPathSummary ?? 'Default mpv scripts location')}</div>
<div class="meta">Required before SubMiner setup can finish.</div>
</div> </div>
${renderStatusBadge(pluginLabel, pluginTone)} ${renderStatusBadge(pluginLabel, pluginTone)}
</div> </div>
@@ -284,7 +282,6 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
<button onclick="window.location.href='subminer://first-run-setup?action=install-plugin'">${pluginActionLabel}</button> <button onclick="window.location.href='subminer://first-run-setup?action=install-plugin'">${pluginActionLabel}</button>
<button onclick="window.location.href='subminer://first-run-setup?action=open-yomitan-settings'">Open Yomitan Settings</button> <button onclick="window.location.href='subminer://first-run-setup?action=open-yomitan-settings'">Open Yomitan Settings</button>
<button class="ghost" onclick="window.location.href='subminer://first-run-setup?action=refresh'">Refresh status</button> <button class="ghost" onclick="window.location.href='subminer://first-run-setup?action=refresh'">Refresh status</button>
<button class="ghost" onclick="window.location.href='subminer://first-run-setup?action=skip-plugin'">Skip plugin</button>
<button class="primary" ${model.canFinish ? '' : 'disabled'} onclick="window.location.href='subminer://first-run-setup?action=finish'">Finish setup</button> <button class="primary" ${model.canFinish ? '' : 'disabled'} onclick="window.location.href='subminer://first-run-setup?action=finish'">Finish setup</button>
</div> </div>
<div class="message">${model.message ? escapeHtml(model.message) : ''}</div> <div class="message">${model.message ? escapeHtml(model.message) : ''}</div>
@@ -305,7 +302,6 @@ export function parseFirstRunSetupSubmissionUrl(rawUrl: string): FirstRunSetupSu
action !== 'configure-windows-mpv-shortcuts' && action !== 'configure-windows-mpv-shortcuts' &&
action !== 'open-yomitan-settings' && action !== 'open-yomitan-settings' &&
action !== 'refresh' && action !== 'refresh' &&
action !== 'skip-plugin' &&
action !== 'finish' action !== 'finish'
) { ) {
return null; return null;