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

@@ -88,7 +88,7 @@ test('setup service auto-completes legacy installs with config and dictionaries'
const service = createFirstRunSetupService({
configDir,
getYomitanDictionaryCount: async () => 2,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
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) => {
const configDir = path.join(root, 'SubMiner');
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(path.join(configDir, 'config.jsonc'), '{}');
let dictionaryCount = 0;
let pluginInstalled = false;
const service = createFirstRunSetupService({
configDir,
getYomitanDictionaryCount: async () => dictionaryCount,
detectPluginInstalled: () => false,
detectPluginInstalled: () => pluginInstalled,
installPlugin: async () => ({
ok: true,
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.canFinish, false);
const skipped = await service.skipPluginInstall();
assert.equal(skipped.state.pluginInstallStatus, 'skipped');
const installed = await service.installMpvPlugin();
assert.equal(installed.state.pluginInstallStatus, 'installed');
assert.equal(installed.pluginInstallPathSummary, '/tmp/mpv');
pluginInstalled = true;
dictionaryCount = 1;
const refreshed = await service.refreshStatus();
assert.equal(refreshed.canFinish, true);
@@ -158,7 +157,7 @@ test('setup service allows completion without internal dictionaries when externa
configDir,
getYomitanDictionaryCount: async () => 0,
isExternalYomitanConfigured: () => true,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
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');
},
isExternalYomitanConfigured: () => true,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
pluginInstallStatus: 'installed',
@@ -218,7 +217,7 @@ test('setup service reopens when external-yomitan completion later has no extern
configDir,
getYomitanDictionaryCount: async () => 0,
isExternalYomitanConfigured: () => true,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
pluginInstallStatus: 'installed',
@@ -235,7 +234,7 @@ test('setup service reopens when external-yomitan completion later has no extern
configDir,
getYomitanDictionaryCount: async () => 0,
isExternalYomitanConfigured: () => false,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
pluginInstallStatus: 'installed',
@@ -262,7 +261,7 @@ test('setup service keeps completed when external-yomitan completion later has i
configDir,
getYomitanDictionaryCount: async () => 0,
isExternalYomitanConfigured: () => true,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
pluginInstallStatus: 'installed',
@@ -279,7 +278,7 @@ test('setup service keeps completed when external-yomitan completion later has i
configDir,
getYomitanDictionaryCount: async () => 2,
isExternalYomitanConfigured: () => false,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
pluginInstallStatus: 'installed',
@@ -304,7 +303,7 @@ test('setup service marks cancelled when popup closes before completion', async
const service = createFirstRunSetupService({
configDir,
getYomitanDictionaryCount: async () => 0,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
pluginInstallStatus: 'installed',
@@ -331,7 +330,7 @@ test('setup service reflects detected Windows mpv shortcuts before preferences a
platform: 'win32',
configDir,
getYomitanDictionaryCount: async () => 0,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
pluginInstallStatus: 'installed',
@@ -364,7 +363,7 @@ test('setup service persists Windows mpv shortcut preferences and status with on
platform: 'win32',
configDir,
getYomitanDictionaryCount: async () => 0,
detectPluginInstalled: () => false,
detectPluginInstalled: () => true,
installPlugin: async () => ({
ok: true,
pluginInstallStatus: 'installed',

View File

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

View File

@@ -14,7 +14,7 @@ test('buildFirstRunSetupHtml renders macchiato setup actions and disabled finish
dictionaryCount: 0,
canFinish: false,
externalYomitanConfigured: false,
pluginStatus: 'optional',
pluginStatus: 'required',
pluginInstallPathSummary: null,
windowsMpvShortcuts: {
supported: false,
@@ -29,6 +29,7 @@ test('buildFirstRunSetupHtml renders macchiato setup actions and disabled finish
assert.match(html, /SubMiner setup/);
assert.match(html, /Install mpv plugin/);
assert.match(html, /Required before SubMiner setup can finish/);
assert.match(html, /Open Yomitan Settings/);
assert.match(html, /Finish setup/);
assert.match(html, /disabled/);
@@ -62,7 +63,7 @@ test('buildFirstRunSetupHtml explains external yomitan mode and keeps finish ena
dictionaryCount: 0,
canFinish: true,
externalYomitanConfigured: true,
pluginStatus: 'optional',
pluginStatus: 'installed',
pluginInstallPathSummary: null,
windowsMpvShortcuts: {
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,
/Finish stays unlocked while SubMiner is reusing an external Yomitan profile\./,
);
assert.match(html, /Finish stays unlocked while SubMiner is reusing an external Yomitan profile\./);
});
test('parseFirstRunSetupSubmissionUrl parses supported custom actions', () => {
assert.deepEqual(parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=refresh'), {
action: 'refresh',
});
assert.equal(parseFirstRunSetupSubmissionUrl('subminer://first-run-setup?action=skip-plugin'), 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,
canFinish: false,
externalYomitanConfigured: false,
pluginStatus: 'optional',
pluginStatus: 'required',
pluginInstallPathSummary: null,
windowsMpvShortcuts: {
supported: false,

View File

@@ -19,7 +19,6 @@ export type FirstRunSetupAction =
| 'configure-windows-mpv-shortcuts'
| 'open-yomitan-settings'
| 'refresh'
| 'skip-plugin'
| 'finish';
export interface FirstRunSetupSubmission {
@@ -33,7 +32,7 @@ export interface FirstRunSetupHtmlModel {
dictionaryCount: number;
canFinish: boolean;
externalYomitanConfigured: boolean;
pluginStatus: 'installed' | 'optional' | 'skipped' | 'failed';
pluginStatus: 'installed' | 'required' | 'failed';
pluginInstallPathSummary: string | null;
windowsMpvShortcuts: {
supported: boolean;
@@ -64,19 +63,15 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
const pluginLabel =
model.pluginStatus === 'installed'
? 'Installed'
: model.pluginStatus === 'skipped'
? 'Skipped'
: model.pluginStatus === 'failed'
? 'Failed'
: 'Optional';
: model.pluginStatus === 'failed'
? 'Failed'
: 'Required';
const pluginTone =
model.pluginStatus === 'installed'
? 'ready'
: model.pluginStatus === 'failed'
? 'danger'
: model.pluginStatus === 'skipped'
? 'muted'
: 'warn';
: 'warn';
const windowsShortcutLabel =
model.windowsMpvShortcuts.status === 'installed'
? 'Installed'
@@ -129,8 +124,10 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
? 'ready'
: 'warn';
const footerMessage = model.externalYomitanConfigured
? '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.';
? 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 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>
<html>
@@ -269,6 +266,7 @@ export function buildFirstRunSetupHtml(model: FirstRunSetupHtmlModel): string {
<div>
<strong>mpv plugin</strong>
<div class="meta">${escapeHtml(model.pluginInstallPathSummary ?? 'Default mpv scripts location')}</div>
<div class="meta">Required before SubMiner setup can finish.</div>
</div>
${renderStatusBadge(pluginLabel, pluginTone)}
</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=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=skip-plugin'">Skip plugin</button>
<button class="primary" ${model.canFinish ? '' : 'disabled'} onclick="window.location.href='subminer://first-run-setup?action=finish'">Finish setup</button>
</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 !== 'open-yomitan-settings' &&
action !== 'refresh' &&
action !== 'skip-plugin' &&
action !== 'finish'
) {
return null;