mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 03:16:46 -07:00
393 lines
13 KiB
TypeScript
393 lines
13 KiB
TypeScript
import fs from 'node:fs';
|
|
import {
|
|
createDefaultSetupState,
|
|
getDefaultConfigFilePaths,
|
|
getSetupStatePath,
|
|
isSetupCompleted,
|
|
readSetupState,
|
|
writeSetupState,
|
|
type SetupPluginInstallStatus,
|
|
type SetupWindowsMpvShortcutInstallStatus,
|
|
type SetupState,
|
|
} from '../../shared/setup-state';
|
|
import type { CliArgs } from '../../cli/args';
|
|
|
|
export interface SetupWindowsMpvShortcutSnapshot {
|
|
supported: boolean;
|
|
startMenuEnabled: boolean;
|
|
desktopEnabled: boolean;
|
|
startMenuInstalled: boolean;
|
|
desktopInstalled: boolean;
|
|
status: 'installed' | 'optional' | 'skipped' | 'failed';
|
|
message: string | null;
|
|
}
|
|
|
|
export interface SetupStatusSnapshot {
|
|
configReady: boolean;
|
|
dictionaryCount: number;
|
|
canFinish: boolean;
|
|
externalYomitanConfigured: boolean;
|
|
pluginStatus: 'installed' | 'optional' | 'skipped' | 'failed';
|
|
pluginInstallPathSummary: string | null;
|
|
windowsMpvShortcuts: SetupWindowsMpvShortcutSnapshot;
|
|
message: string | null;
|
|
state: SetupState;
|
|
}
|
|
|
|
export interface PluginInstallResult {
|
|
ok: boolean;
|
|
pluginInstallStatus: SetupPluginInstallStatus;
|
|
pluginInstallPathSummary: string | null;
|
|
message: string;
|
|
}
|
|
|
|
export interface FirstRunSetupService {
|
|
ensureSetupStateInitialized: () => Promise<SetupStatusSnapshot>;
|
|
getSetupStatus: () => Promise<SetupStatusSnapshot>;
|
|
refreshStatus: (message?: string | null) => Promise<SetupStatusSnapshot>;
|
|
markSetupInProgress: () => Promise<SetupStatusSnapshot>;
|
|
markSetupCancelled: () => Promise<SetupStatusSnapshot>;
|
|
markSetupCompleted: () => Promise<SetupStatusSnapshot>;
|
|
skipPluginInstall: () => Promise<SetupStatusSnapshot>;
|
|
installMpvPlugin: () => Promise<SetupStatusSnapshot>;
|
|
configureWindowsMpvShortcuts: (preferences: {
|
|
startMenuEnabled: boolean;
|
|
desktopEnabled: boolean;
|
|
}) => Promise<SetupStatusSnapshot>;
|
|
isSetupCompleted: () => boolean;
|
|
}
|
|
|
|
function hasAnyStartupCommandBeyondSetup(args: CliArgs): boolean {
|
|
return Boolean(
|
|
args.toggle ||
|
|
args.toggleVisibleOverlay ||
|
|
args.launchMpv ||
|
|
args.settings ||
|
|
args.show ||
|
|
args.hide ||
|
|
args.showVisibleOverlay ||
|
|
args.hideVisibleOverlay ||
|
|
args.copySubtitle ||
|
|
args.copySubtitleMultiple ||
|
|
args.mineSentence ||
|
|
args.mineSentenceMultiple ||
|
|
args.updateLastCardFromClipboard ||
|
|
args.refreshKnownWords ||
|
|
args.toggleSecondarySub ||
|
|
args.triggerFieldGrouping ||
|
|
args.triggerSubsync ||
|
|
args.markAudioCard ||
|
|
args.openRuntimeOptions ||
|
|
args.anilistStatus ||
|
|
args.anilistLogout ||
|
|
args.anilistSetup ||
|
|
args.anilistRetryQueue ||
|
|
args.dictionary ||
|
|
args.jellyfin ||
|
|
args.jellyfinLogin ||
|
|
args.jellyfinLogout ||
|
|
args.jellyfinLibraries ||
|
|
args.jellyfinItems ||
|
|
args.jellyfinSubtitles ||
|
|
args.jellyfinPlay ||
|
|
args.jellyfinRemoteAnnounce ||
|
|
args.jellyfinPreviewAuth ||
|
|
args.texthooker ||
|
|
args.help,
|
|
);
|
|
}
|
|
|
|
export function shouldAutoOpenFirstRunSetup(args: CliArgs): boolean {
|
|
if (args.setup) return true;
|
|
if (!args.start && !args.background) return false;
|
|
return !hasAnyStartupCommandBeyondSetup(args);
|
|
}
|
|
|
|
function getPluginStatus(
|
|
state: SetupState,
|
|
pluginInstalled: boolean,
|
|
): SetupStatusSnapshot['pluginStatus'] {
|
|
if (pluginInstalled) return 'installed';
|
|
if (state.pluginInstallStatus === 'skipped') return 'skipped';
|
|
if (state.pluginInstallStatus === 'failed') return 'failed';
|
|
return 'optional';
|
|
}
|
|
|
|
function getWindowsMpvShortcutStatus(
|
|
state: SetupState,
|
|
installed: { startMenuInstalled: boolean; desktopInstalled: boolean },
|
|
): SetupWindowsMpvShortcutSnapshot['status'] {
|
|
if (installed.startMenuInstalled || installed.desktopInstalled) return 'installed';
|
|
if (state.windowsMpvShortcutLastStatus === 'skipped') return 'skipped';
|
|
if (state.windowsMpvShortcutLastStatus === 'failed') return 'failed';
|
|
return 'optional';
|
|
}
|
|
|
|
function getEffectiveWindowsMpvShortcutPreferences(
|
|
state: SetupState,
|
|
installed: { startMenuInstalled: boolean; desktopInstalled: boolean },
|
|
): { startMenuEnabled: boolean; desktopEnabled: boolean } {
|
|
if (state.windowsMpvShortcutLastStatus === 'unknown') {
|
|
return {
|
|
startMenuEnabled: installed.startMenuInstalled,
|
|
desktopEnabled: installed.desktopInstalled,
|
|
};
|
|
}
|
|
|
|
return {
|
|
startMenuEnabled: state.windowsMpvShortcutPreferences.startMenuEnabled,
|
|
desktopEnabled: state.windowsMpvShortcutPreferences.desktopEnabled,
|
|
};
|
|
}
|
|
|
|
function isYomitanSetupSatisfied(options: {
|
|
configReady: boolean;
|
|
dictionaryCount: number;
|
|
externalYomitanConfigured: boolean;
|
|
}): boolean {
|
|
if (!options.configReady) {
|
|
return false;
|
|
}
|
|
return options.externalYomitanConfigured || options.dictionaryCount >= 1;
|
|
}
|
|
|
|
async function resolveYomitanSetupStatus(deps: {
|
|
configFilePaths: { jsoncPath: string; jsonPath: string };
|
|
getYomitanDictionaryCount: () => Promise<number>;
|
|
isExternalYomitanConfigured?: () => boolean;
|
|
}): Promise<{
|
|
configReady: boolean;
|
|
dictionaryCount: number;
|
|
externalYomitanConfigured: boolean;
|
|
}> {
|
|
const configReady =
|
|
fs.existsSync(deps.configFilePaths.jsoncPath) || fs.existsSync(deps.configFilePaths.jsonPath);
|
|
const externalYomitanConfigured = deps.isExternalYomitanConfigured?.() ?? false;
|
|
|
|
if (configReady && externalYomitanConfigured) {
|
|
return {
|
|
configReady,
|
|
dictionaryCount: 0,
|
|
externalYomitanConfigured,
|
|
};
|
|
}
|
|
|
|
return {
|
|
configReady,
|
|
dictionaryCount: await deps.getYomitanDictionaryCount(),
|
|
externalYomitanConfigured,
|
|
};
|
|
}
|
|
|
|
export function createFirstRunSetupService(deps: {
|
|
platform?: NodeJS.Platform;
|
|
configDir: string;
|
|
getYomitanDictionaryCount: () => Promise<number>;
|
|
isExternalYomitanConfigured?: () => boolean;
|
|
detectPluginInstalled: () => boolean | Promise<boolean>;
|
|
installPlugin: () => Promise<PluginInstallResult>;
|
|
detectWindowsMpvShortcuts?: () =>
|
|
| { startMenuInstalled: boolean; desktopInstalled: boolean }
|
|
| Promise<{ startMenuInstalled: boolean; desktopInstalled: boolean }>;
|
|
applyWindowsMpvShortcuts?: (preferences: {
|
|
startMenuEnabled: boolean;
|
|
desktopEnabled: boolean;
|
|
}) => Promise<{ ok: boolean; status: SetupWindowsMpvShortcutInstallStatus; message: string }>;
|
|
onStateChanged?: (state: SetupState) => void;
|
|
}): FirstRunSetupService {
|
|
const setupStatePath = getSetupStatePath(deps.configDir);
|
|
const configFilePaths = getDefaultConfigFilePaths(deps.configDir);
|
|
const isWindows = (deps.platform ?? process.platform) === 'win32';
|
|
let completed = false;
|
|
|
|
const readState = (): SetupState => readSetupState(setupStatePath) ?? createDefaultSetupState();
|
|
const writeState = (state: SetupState): SetupState => {
|
|
writeSetupState(setupStatePath, state);
|
|
completed = state.status === 'completed';
|
|
deps.onStateChanged?.(state);
|
|
return state;
|
|
};
|
|
|
|
const buildSnapshot = async (state: SetupState, message: string | null = null) => {
|
|
const { configReady, dictionaryCount, externalYomitanConfigured } =
|
|
await resolveYomitanSetupStatus({
|
|
configFilePaths,
|
|
getYomitanDictionaryCount: deps.getYomitanDictionaryCount,
|
|
isExternalYomitanConfigured: deps.isExternalYomitanConfigured,
|
|
});
|
|
const pluginInstalled = await deps.detectPluginInstalled();
|
|
const detectedWindowsMpvShortcuts = isWindows
|
|
? await deps.detectWindowsMpvShortcuts?.()
|
|
: undefined;
|
|
const installedWindowsMpvShortcuts = {
|
|
startMenuInstalled: detectedWindowsMpvShortcuts?.startMenuInstalled ?? false,
|
|
desktopInstalled: detectedWindowsMpvShortcuts?.desktopInstalled ?? false,
|
|
};
|
|
const effectiveWindowsMpvShortcutPreferences = getEffectiveWindowsMpvShortcutPreferences(
|
|
state,
|
|
installedWindowsMpvShortcuts,
|
|
);
|
|
return {
|
|
configReady,
|
|
dictionaryCount,
|
|
canFinish: isYomitanSetupSatisfied({
|
|
configReady,
|
|
dictionaryCount,
|
|
externalYomitanConfigured,
|
|
}),
|
|
externalYomitanConfigured,
|
|
pluginStatus: getPluginStatus(state, pluginInstalled),
|
|
pluginInstallPathSummary: state.pluginInstallPathSummary,
|
|
windowsMpvShortcuts: {
|
|
supported: isWindows,
|
|
startMenuEnabled: effectiveWindowsMpvShortcutPreferences.startMenuEnabled,
|
|
desktopEnabled: effectiveWindowsMpvShortcutPreferences.desktopEnabled,
|
|
startMenuInstalled: installedWindowsMpvShortcuts.startMenuInstalled,
|
|
desktopInstalled: installedWindowsMpvShortcuts.desktopInstalled,
|
|
status: getWindowsMpvShortcutStatus(state, installedWindowsMpvShortcuts),
|
|
message: null,
|
|
},
|
|
message,
|
|
state,
|
|
} satisfies SetupStatusSnapshot;
|
|
};
|
|
|
|
const refreshWithState = async (state: SetupState, message: string | null = null) => {
|
|
const snapshot = await buildSnapshot(state, message);
|
|
if (snapshot.state.lastSeenYomitanDictionaryCount !== snapshot.dictionaryCount) {
|
|
snapshot.state = writeState({
|
|
...snapshot.state,
|
|
lastSeenYomitanDictionaryCount: snapshot.dictionaryCount,
|
|
});
|
|
}
|
|
return snapshot;
|
|
};
|
|
|
|
return {
|
|
ensureSetupStateInitialized: async () => {
|
|
const state = readState();
|
|
const { configReady, dictionaryCount, externalYomitanConfigured } =
|
|
await resolveYomitanSetupStatus({
|
|
configFilePaths,
|
|
getYomitanDictionaryCount: deps.getYomitanDictionaryCount,
|
|
isExternalYomitanConfigured: deps.isExternalYomitanConfigured,
|
|
});
|
|
const yomitanSetupSatisfied = isYomitanSetupSatisfied({
|
|
configReady,
|
|
dictionaryCount,
|
|
externalYomitanConfigured,
|
|
});
|
|
if (
|
|
isSetupCompleted(state) &&
|
|
!(
|
|
state.yomitanSetupMode === 'external' &&
|
|
!externalYomitanConfigured &&
|
|
!yomitanSetupSatisfied
|
|
)
|
|
) {
|
|
completed = true;
|
|
return refreshWithState(state);
|
|
}
|
|
|
|
if (yomitanSetupSatisfied) {
|
|
const completedState = writeState({
|
|
...state,
|
|
status: 'completed',
|
|
completedAt: new Date().toISOString(),
|
|
completionSource: 'legacy_auto_detected',
|
|
yomitanSetupMode: externalYomitanConfigured ? 'external' : 'internal',
|
|
lastSeenYomitanDictionaryCount: dictionaryCount,
|
|
});
|
|
return buildSnapshot(completedState);
|
|
}
|
|
|
|
return refreshWithState(
|
|
writeState({
|
|
...state,
|
|
status: state.status === 'cancelled' ? 'cancelled' : 'incomplete',
|
|
completedAt: null,
|
|
completionSource: null,
|
|
yomitanSetupMode: null,
|
|
lastSeenYomitanDictionaryCount: dictionaryCount,
|
|
}),
|
|
);
|
|
},
|
|
getSetupStatus: async () => refreshWithState(readState()),
|
|
refreshStatus: async (message = null) => refreshWithState(readState(), message),
|
|
markSetupInProgress: async () => {
|
|
const state = readState();
|
|
if (state.status === 'completed') {
|
|
completed = true;
|
|
return refreshWithState(state);
|
|
}
|
|
return refreshWithState(writeState({ ...state, status: 'in_progress' }));
|
|
},
|
|
markSetupCancelled: async () => {
|
|
const state = readState();
|
|
if (state.status === 'completed') {
|
|
completed = true;
|
|
return refreshWithState(state);
|
|
}
|
|
return refreshWithState(writeState({ ...state, status: 'cancelled' }));
|
|
},
|
|
markSetupCompleted: async () => {
|
|
const state = readState();
|
|
const snapshot = await buildSnapshot(state);
|
|
if (!snapshot.canFinish) {
|
|
return snapshot;
|
|
}
|
|
return refreshWithState(
|
|
writeState({
|
|
...state,
|
|
status: 'completed',
|
|
completedAt: new Date().toISOString(),
|
|
completionSource: 'user',
|
|
yomitanSetupMode: snapshot.externalYomitanConfigured ? 'external' : 'internal',
|
|
lastSeenYomitanDictionaryCount: snapshot.dictionaryCount,
|
|
}),
|
|
);
|
|
},
|
|
skipPluginInstall: async () =>
|
|
refreshWithState(writeState({ ...readState(), pluginInstallStatus: 'skipped' })),
|
|
installMpvPlugin: async () => {
|
|
const result = await deps.installPlugin();
|
|
return refreshWithState(
|
|
writeState({
|
|
...readState(),
|
|
pluginInstallStatus: result.pluginInstallStatus,
|
|
pluginInstallPathSummary: result.pluginInstallPathSummary,
|
|
}),
|
|
result.message,
|
|
);
|
|
},
|
|
configureWindowsMpvShortcuts: async (preferences) => {
|
|
if (!isWindows || !deps.applyWindowsMpvShortcuts) {
|
|
return refreshWithState(
|
|
writeState({
|
|
...readState(),
|
|
windowsMpvShortcutPreferences: {
|
|
startMenuEnabled: preferences.startMenuEnabled,
|
|
desktopEnabled: preferences.desktopEnabled,
|
|
},
|
|
}),
|
|
null,
|
|
);
|
|
}
|
|
const result = await deps.applyWindowsMpvShortcuts(preferences);
|
|
const latestState = readState();
|
|
return refreshWithState(
|
|
writeState({
|
|
...latestState,
|
|
windowsMpvShortcutPreferences: {
|
|
startMenuEnabled: preferences.startMenuEnabled,
|
|
desktopEnabled: preferences.desktopEnabled,
|
|
},
|
|
windowsMpvShortcutLastStatus: result.status,
|
|
}),
|
|
result.message,
|
|
);
|
|
},
|
|
isSetupCompleted: () => completed || isSetupCompleted(readState()),
|
|
};
|
|
}
|