mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-12 15:13:32 -07:00
refactor(main): extract password-store args, mpv plugin detection, yomitan anki sync, session bindings, log export from main.ts
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
export function getPasswordStoreArg(argv: string[]): string | null {
|
||||
let resolved: string | null = null;
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (!arg?.startsWith('--password-store')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--password-store') {
|
||||
const value = argv[i + 1];
|
||||
if (value && !value.startsWith('--')) {
|
||||
resolved = value.trim();
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const [prefix, value] = arg.split('=', 2);
|
||||
if (prefix === '--password-store' && value && value.trim().length > 0) {
|
||||
resolved = value.trim();
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function normalizePasswordStoreArg(value: string): string {
|
||||
const normalized = value.trim();
|
||||
if (normalized.toLowerCase() === 'gnome') {
|
||||
return 'gnome-libsecret';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function getDefaultPasswordStore(): string {
|
||||
return 'gnome-libsecret';
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { app, dialog, shell } from 'electron';
|
||||
import * as os from 'os';
|
||||
import { exportLogsArchive } from './log-export';
|
||||
|
||||
export interface LogExportTrayRuntimeDeps {
|
||||
flushMpvLog: () => Promise<void>;
|
||||
logInfo: (message: string) => void;
|
||||
logWarn: (message: string, details?: unknown) => void;
|
||||
}
|
||||
|
||||
export function createLogExportTrayRuntime(deps: LogExportTrayRuntimeDeps): {
|
||||
exportLogsFromTray: () => Promise<void>;
|
||||
} {
|
||||
function describeUnknownError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
async function exportLogsFromTray(): Promise<void> {
|
||||
try {
|
||||
await deps.flushMpvLog();
|
||||
} catch (error) {
|
||||
deps.logWarn('Failed to flush mpv log before exporting logs from tray.', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = exportLogsArchive({
|
||||
platform: process.platform,
|
||||
homeDir: os.homedir(),
|
||||
appDataDir: app.getPath('appData'),
|
||||
});
|
||||
deps.logInfo(
|
||||
`Exported ${result.exportedFiles.length} sanitized log file(s) to ${result.zipPath}`,
|
||||
);
|
||||
void dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'SubMiner logs exported',
|
||||
message: 'SubMiner log export created.',
|
||||
detail: result.zipPath,
|
||||
buttons: ['OK', 'Show in Folder'],
|
||||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.response === 1) {
|
||||
shell.showItemInFolder(result.zipPath);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const message = describeUnknownError(error);
|
||||
deps.logWarn('Failed to export logs from tray.', error);
|
||||
void dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'SubMiner log export failed',
|
||||
message: 'Could not export SubMiner logs.',
|
||||
detail: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { exportLogsFromTray };
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { sendMpvCommandRuntime, type MpvRuntimeClientLike } from '../../core/services';
|
||||
import {
|
||||
buildPluginSessionBindingsArtifact,
|
||||
compileSessionBindings,
|
||||
} from '../../core/services/session-bindings';
|
||||
import type { ConfiguredShortcuts } from '../../core/utils/shortcut-config';
|
||||
import type { CompiledSessionBinding, Keybinding, ResolvedConfig } from '../../types';
|
||||
import { writeSessionBindingsArtifact } from './session-bindings-artifact';
|
||||
|
||||
export interface SessionBindingsRuntimeDeps {
|
||||
configDir: string;
|
||||
getKeybindings: () => Keybinding[];
|
||||
getConfiguredShortcuts: () => ConfiguredShortcuts;
|
||||
getResolvedConfig: () => ResolvedConfig;
|
||||
getMpvClient: () => MpvRuntimeClientLike | null;
|
||||
setSessionBindings: (bindings: CompiledSessionBinding[]) => void;
|
||||
setSessionBindingsInitialized: (initialized: boolean) => void;
|
||||
logWarn: (message: string) => void;
|
||||
}
|
||||
|
||||
export function createSessionBindingsRuntime(deps: SessionBindingsRuntimeDeps): {
|
||||
persistSessionBindings: (
|
||||
bindings: CompiledSessionBinding[],
|
||||
warnings?: ReturnType<typeof compileSessionBindings>['warnings'],
|
||||
) => void;
|
||||
refreshCurrentSessionBindings: () => void;
|
||||
} {
|
||||
function resolveSessionBindingPlatform(): 'darwin' | 'win32' | 'linux' {
|
||||
if (process.platform === 'darwin') return 'darwin';
|
||||
if (process.platform === 'win32') return 'win32';
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
function compileCurrentSessionBindings(): {
|
||||
bindings: CompiledSessionBinding[];
|
||||
warnings: ReturnType<typeof compileSessionBindings>['warnings'];
|
||||
} {
|
||||
return compileSessionBindings({
|
||||
keybindings: deps.getKeybindings(),
|
||||
shortcuts: deps.getConfiguredShortcuts(),
|
||||
statsToggleKey: deps.getResolvedConfig().stats.toggleKey,
|
||||
statsMarkWatchedKey: deps.getResolvedConfig().stats.markWatchedKey,
|
||||
platform: resolveSessionBindingPlatform(),
|
||||
rawConfig: deps.getResolvedConfig(),
|
||||
});
|
||||
}
|
||||
|
||||
function persistSessionBindings(
|
||||
bindings: CompiledSessionBinding[],
|
||||
warnings: ReturnType<typeof compileSessionBindings>['warnings'] = [],
|
||||
): void {
|
||||
const artifact = buildPluginSessionBindingsArtifact({
|
||||
bindings,
|
||||
warnings,
|
||||
numericSelectionTimeoutMs: deps.getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
});
|
||||
writeSessionBindingsArtifact(deps.configDir, artifact);
|
||||
deps.setSessionBindings(bindings);
|
||||
deps.setSessionBindingsInitialized(true);
|
||||
const mpvClient = deps.getMpvClient();
|
||||
if (mpvClient?.connected) {
|
||||
sendMpvCommandRuntime(mpvClient, ['script-message', 'subminer-reload-session-bindings']);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshCurrentSessionBindings(): void {
|
||||
const compiled = compileCurrentSessionBindings();
|
||||
for (const warning of compiled.warnings) {
|
||||
deps.logWarn(`[session-bindings] ${warning.message}`);
|
||||
}
|
||||
persistSessionBindings(compiled.bindings, compiled.warnings);
|
||||
}
|
||||
|
||||
return { persistSessionBindings, refreshCurrentSessionBindings };
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { app, dialog, shell } from 'electron';
|
||||
import * as os from 'os';
|
||||
import {
|
||||
detectInstalledFirstRunPluginCandidates,
|
||||
detectInstalledMpvPlugin,
|
||||
removeLegacyMpvPluginCandidates,
|
||||
resolvePackagedRuntimePluginPath,
|
||||
} from './first-run-setup-plugin';
|
||||
|
||||
export interface WindowsMpvPluginDetectionRuntimeDeps {
|
||||
mainDirname: string;
|
||||
logWarn: (message: string) => void;
|
||||
}
|
||||
|
||||
export function createWindowsMpvPluginDetectionRuntime(
|
||||
deps: WindowsMpvPluginDetectionRuntimeDeps,
|
||||
): {
|
||||
resolveBundledMpvRuntimePluginEntrypoint: () => string | undefined;
|
||||
detectWindowsInstalledMpvPlugin: (
|
||||
mpvExecutablePath: string,
|
||||
) => ReturnType<typeof detectInstalledMpvPlugin>;
|
||||
logInstalledMpvPluginDetected: (detection: {
|
||||
path: string | null;
|
||||
version: string | null;
|
||||
}) => void;
|
||||
promptForLegacyMpvPluginRemovalBeforeWindowsLaunch: (
|
||||
mpvPath: string,
|
||||
detection: { path: string | null; version: string | null },
|
||||
) => Promise<'removed' | 'continue' | 'cancel'>;
|
||||
} {
|
||||
function resolveBundledMpvRuntimePluginEntrypoint(): string | undefined {
|
||||
return (
|
||||
resolvePackagedRuntimePluginPath({
|
||||
dirname: deps.mainDirname,
|
||||
appPath: app.getAppPath(),
|
||||
resourcesPath: process.resourcesPath,
|
||||
}) ?? undefined
|
||||
);
|
||||
}
|
||||
|
||||
function detectWindowsInstalledMpvPlugin(mpvExecutablePath: string) {
|
||||
return detectInstalledMpvPlugin({
|
||||
platform: 'win32',
|
||||
homeDir: os.homedir(),
|
||||
appDataDir: app.getPath('appData'),
|
||||
mpvExecutablePath,
|
||||
});
|
||||
}
|
||||
|
||||
function logInstalledMpvPluginDetected(detection: {
|
||||
path: string | null;
|
||||
version: string | null;
|
||||
}) {
|
||||
if (!detection.path) return;
|
||||
deps.logWarn(
|
||||
`SubMiner detected an installed mpv plugin at ${detection.path}. This mpv session will use the installed plugin. Remove it to use the bundled runtime plugin automatically. Detected plugin version: ${detection.version ?? 'unknown or legacy'}.`,
|
||||
);
|
||||
}
|
||||
|
||||
async function promptForLegacyMpvPluginRemovalBeforeWindowsLaunch(
|
||||
mpvPath: string,
|
||||
detection: { path: string | null; version: string | null },
|
||||
): Promise<'removed' | 'continue' | 'cancel'> {
|
||||
const response = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: 'SubMiner mpv plugin detected',
|
||||
message: [
|
||||
'SubMiner detected an installed mpv plugin at:',
|
||||
detection.path ?? 'unknown path',
|
||||
'',
|
||||
"This mpv session will use the installed plugin unless it is removed. Remove it now to use SubMiner's bundled runtime plugin automatically.",
|
||||
`Detected plugin version: ${detection.version ?? 'unknown or legacy'}`,
|
||||
].join('\n'),
|
||||
detail:
|
||||
'Remove the legacy SubMiner mpv plugin files from mpv before launching this video? This moves the files to the OS trash.',
|
||||
buttons: ['Remove legacy plugin', 'Continue with installed plugin', 'Cancel'],
|
||||
defaultId: 0,
|
||||
cancelId: 2,
|
||||
});
|
||||
|
||||
if (response.response === 2) {
|
||||
return 'cancel';
|
||||
}
|
||||
if (response.response === 1) {
|
||||
return 'continue';
|
||||
}
|
||||
|
||||
const result = await removeLegacyMpvPluginCandidates({
|
||||
candidates: detectInstalledFirstRunPluginCandidates({
|
||||
platform: 'win32',
|
||||
homeDir: os.homedir(),
|
||||
appDataDir: app.getPath('appData'),
|
||||
mpvExecutablePath: mpvPath,
|
||||
}),
|
||||
trashItem: (candidatePath) => shell.trashItem(candidatePath),
|
||||
});
|
||||
if (result.ok) {
|
||||
await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Legacy mpv plugin removed',
|
||||
message:
|
||||
'Legacy mpv plugin removed. SubMiner-managed playback will use the bundled runtime plugin.',
|
||||
});
|
||||
return 'removed';
|
||||
}
|
||||
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'Could not remove legacy mpv plugin',
|
||||
message: 'Some legacy SubMiner mpv plugin files could not be moved to the trash.',
|
||||
detail: result.failedPaths.map((failure) => `${failure.path}: ${failure.message}`).join('\n'),
|
||||
});
|
||||
return 'cancel';
|
||||
}
|
||||
|
||||
return {
|
||||
resolveBundledMpvRuntimePluginEntrypoint,
|
||||
detectWindowsInstalledMpvPlugin,
|
||||
logInstalledMpvPluginDetected,
|
||||
promptForLegacyMpvPluginRemovalBeforeWindowsLaunch,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { syncYomitanDefaultAnkiServer as syncYomitanDefaultAnkiServerCore } from '../../core/services';
|
||||
import type { ResolvedConfig } from '../../types';
|
||||
import {
|
||||
getPreferredYomitanAnkiServerUrl as getPreferredYomitanAnkiServerUrlRuntime,
|
||||
shouldForceOverrideYomitanAnkiServer,
|
||||
} from './yomitan-anki-server';
|
||||
|
||||
export interface YomitanAnkiServerSyncRuntimeDeps {
|
||||
isExternalReadOnlyMode: () => boolean;
|
||||
getResolvedConfig: () => ResolvedConfig;
|
||||
getYomitanParserRuntimeDeps: () => Parameters<typeof syncYomitanDefaultAnkiServerCore>[1];
|
||||
logError: (message: string, ...args: unknown[]) => void;
|
||||
logInfo: (message: string, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
export function createYomitanAnkiServerSyncRuntime(deps: YomitanAnkiServerSyncRuntimeDeps): {
|
||||
syncYomitanDefaultProfileAnkiServer: () => Promise<void>;
|
||||
} {
|
||||
let lastSyncedYomitanAnkiSettingsKey: string | null = null;
|
||||
|
||||
function getPreferredYomitanAnkiServerUrl(): string {
|
||||
return getPreferredYomitanAnkiServerUrlRuntime(deps.getResolvedConfig().ankiConnect);
|
||||
}
|
||||
|
||||
async function syncYomitanDefaultProfileAnkiServer(): Promise<void> {
|
||||
if (deps.isExternalReadOnlyMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUrl = getPreferredYomitanAnkiServerUrl().trim();
|
||||
const ankiConnectConfig = deps.getResolvedConfig().ankiConnect;
|
||||
const targetDeck = ankiConnectConfig?.deck?.trim() ?? '';
|
||||
const targetSettingsKey = `${targetUrl}\n${targetDeck}`;
|
||||
if (!targetUrl || targetSettingsKey === lastSyncedYomitanAnkiSettingsKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const synced = await syncYomitanDefaultAnkiServerCore(
|
||||
targetUrl,
|
||||
deps.getYomitanParserRuntimeDeps(),
|
||||
{
|
||||
error: (message, ...args) => {
|
||||
deps.logError(message, ...args);
|
||||
},
|
||||
info: (message, ...args) => {
|
||||
deps.logInfo(message, ...args);
|
||||
},
|
||||
},
|
||||
{
|
||||
forceOverride: ankiConnectConfig
|
||||
? shouldForceOverrideYomitanAnkiServer(ankiConnectConfig)
|
||||
: false,
|
||||
deck: targetDeck,
|
||||
},
|
||||
);
|
||||
|
||||
if (synced) {
|
||||
lastSyncedYomitanAnkiSettingsKey = targetSettingsKey;
|
||||
}
|
||||
}
|
||||
|
||||
return { syncYomitanDefaultProfileAnkiServer };
|
||||
}
|
||||
Reference in New Issue
Block a user