mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-12 03:13:39 -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:
+46
-262
@@ -72,43 +72,11 @@ import { focusMacOSOverlayWindow } from './main/runtime/macos-overlay-window-foc
|
|||||||
import { restoreMacOSMpvFocusAfterModalClose } from './main/runtime/macos-modal-focus-handoff';
|
import { restoreMacOSMpvFocusAfterModalClose } from './main/runtime/macos-modal-focus-handoff';
|
||||||
import { resolveFreshPlaybackPaused } from './main/runtime/playback-paused-state';
|
import { resolveFreshPlaybackPaused } from './main/runtime/playback-paused-state';
|
||||||
import { mergeAiConfig } from './ai/config';
|
import { mergeAiConfig } from './ai/config';
|
||||||
|
import {
|
||||||
function getPasswordStoreArg(argv: string[]): string | null {
|
getDefaultPasswordStore,
|
||||||
let resolved: string | null = null;
|
getPasswordStoreArg,
|
||||||
for (let i = 0; i < argv.length; i += 1) {
|
normalizePasswordStoreArg,
|
||||||
const arg = argv[i];
|
} from './main/password-store-args';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePasswordStoreArg(value: string): string {
|
|
||||||
const normalized = value.trim();
|
|
||||||
if (normalized.toLowerCase() === 'gnome') {
|
|
||||||
return 'gnome-libsecret';
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultPasswordStore(): string {
|
|
||||||
return 'gnome-libsecret';
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
protocol.registerSchemesAsPrivileged([
|
||||||
{
|
{
|
||||||
@@ -129,7 +97,6 @@ import * as os from 'os';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { MecabTokenizer } from './mecab-tokenizer';
|
import { MecabTokenizer } from './mecab-tokenizer';
|
||||||
import type {
|
import type {
|
||||||
CompiledSessionBinding,
|
|
||||||
JimakuApiResponse,
|
JimakuApiResponse,
|
||||||
KikuFieldGroupingChoice,
|
KikuFieldGroupingChoice,
|
||||||
MpvSubtitleRenderMetrics,
|
MpvSubtitleRenderMetrics,
|
||||||
@@ -445,8 +412,8 @@ import {
|
|||||||
detectInstalledFirstRunPluginCandidates,
|
detectInstalledFirstRunPluginCandidates,
|
||||||
detectInstalledMpvPlugin,
|
detectInstalledMpvPlugin,
|
||||||
removeLegacyMpvPluginCandidates,
|
removeLegacyMpvPluginCandidates,
|
||||||
resolvePackagedRuntimePluginPath,
|
|
||||||
} from './main/runtime/first-run-setup-plugin';
|
} from './main/runtime/first-run-setup-plugin';
|
||||||
|
import { createWindowsMpvPluginDetectionRuntime } from './main/runtime/windows-mpv-plugin-detection';
|
||||||
import {
|
import {
|
||||||
applyWindowsMpvShortcuts,
|
applyWindowsMpvShortcuts,
|
||||||
detectWindowsMpvShortcuts,
|
detectWindowsMpvShortcuts,
|
||||||
@@ -479,7 +446,7 @@ import {
|
|||||||
shouldQuitOnMpvShutdownForTrayState,
|
shouldQuitOnMpvShutdownForTrayState,
|
||||||
shouldQuitOnWindowAllClosedForTrayState,
|
shouldQuitOnWindowAllClosedForTrayState,
|
||||||
} from './main/runtime/startup-tray-policy';
|
} from './main/runtime/startup-tray-policy';
|
||||||
import { exportLogsArchive } from './main/runtime/log-export';
|
import { createLogExportTrayRuntime } from './main/runtime/log-export-tray';
|
||||||
import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup';
|
import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup';
|
||||||
import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/immersion-startup-main-deps';
|
import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/immersion-startup-main-deps';
|
||||||
import {
|
import {
|
||||||
@@ -505,10 +472,6 @@ import { createAnilistRateLimiter } from './core/services/anilist/rate-limiter';
|
|||||||
import { createJellyfinTokenStore } from './core/services/jellyfin-token-store';
|
import { createJellyfinTokenStore } from './core/services/jellyfin-token-store';
|
||||||
import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc';
|
import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc';
|
||||||
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
||||||
import {
|
|
||||||
buildPluginSessionBindingsArtifact,
|
|
||||||
compileSessionBindings,
|
|
||||||
} from './core/services/session-bindings';
|
|
||||||
import { dispatchSessionAction as dispatchSessionActionCore } from './core/services/session-actions';
|
import { dispatchSessionAction as dispatchSessionActionCore } from './core/services/session-actions';
|
||||||
import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts';
|
import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts';
|
||||||
import { createMainRuntimeRegistry } from './main/runtime/registry';
|
import { createMainRuntimeRegistry } from './main/runtime/registry';
|
||||||
@@ -554,7 +517,7 @@ import { openCharacterDictionaryManagerModal as openCharacterDictionaryManagerMo
|
|||||||
import { openControllerSelectModal as openControllerSelectModalRuntime } from './main/runtime/controller-select-open';
|
import { openControllerSelectModal as openControllerSelectModalRuntime } from './main/runtime/controller-select-open';
|
||||||
import { openControllerDebugModal as openControllerDebugModalRuntime } from './main/runtime/controller-debug-open';
|
import { openControllerDebugModal as openControllerDebugModalRuntime } from './main/runtime/controller-debug-open';
|
||||||
import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc';
|
import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc';
|
||||||
import { writeSessionBindingsArtifact } from './main/runtime/session-bindings-artifact';
|
import { createSessionBindingsRuntime } from './main/runtime/session-bindings-runtime';
|
||||||
import { createOverlayShortcutsRuntimeService } from './main/overlay-shortcuts-runtime';
|
import { createOverlayShortcutsRuntimeService } from './main/overlay-shortcuts-runtime';
|
||||||
import {
|
import {
|
||||||
createFrequencyDictionaryRuntimeService,
|
createFrequencyDictionaryRuntimeService,
|
||||||
@@ -631,10 +594,8 @@ import {
|
|||||||
import { createYomitanProfilePolicy } from './main/runtime/yomitan-profile-policy';
|
import { createYomitanProfilePolicy } from './main/runtime/yomitan-profile-policy';
|
||||||
import { reloadOverlayWindowsForYomitanContentScripts } from './main/runtime/yomitan-extension-overlay-reload';
|
import { reloadOverlayWindowsForYomitanContentScripts } from './main/runtime/yomitan-extension-overlay-reload';
|
||||||
import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log';
|
import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log';
|
||||||
import {
|
import { shouldForceOverrideYomitanAnkiServer } from './main/runtime/yomitan-anki-server';
|
||||||
getPreferredYomitanAnkiServerUrl as getPreferredYomitanAnkiServerUrlRuntime,
|
import { createYomitanAnkiServerSyncRuntime } from './main/runtime/yomitan-anki-server-sync';
|
||||||
shouldForceOverrideYomitanAnkiServer,
|
|
||||||
} from './main/runtime/yomitan-anki-server';
|
|
||||||
import {
|
import {
|
||||||
type AnilistMediaGuessRuntimeState,
|
type AnilistMediaGuessRuntimeState,
|
||||||
type StartupState,
|
type StartupState,
|
||||||
@@ -1322,87 +1283,15 @@ const managedLocalSubtitleSelectionRuntime = createManagedLocalSubtitleSelection
|
|||||||
clearScheduled: (timer) => clearTimeout(timer),
|
clearScheduled: (timer) => clearTimeout(timer),
|
||||||
});
|
});
|
||||||
|
|
||||||
function resolveBundledMpvRuntimePluginEntrypoint(): string | undefined {
|
const {
|
||||||
return (
|
resolveBundledMpvRuntimePluginEntrypoint,
|
||||||
resolvePackagedRuntimePluginPath({
|
detectWindowsInstalledMpvPlugin,
|
||||||
dirname: __dirname,
|
logInstalledMpvPluginDetected,
|
||||||
appPath: app.getAppPath(),
|
promptForLegacyMpvPluginRemovalBeforeWindowsLaunch,
|
||||||
resourcesPath: process.resourcesPath,
|
} = createWindowsMpvPluginDetectionRuntime({
|
||||||
}) ?? undefined
|
mainDirname: __dirname,
|
||||||
);
|
logWarn: (message) => logger.warn(message),
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
logger.warn(
|
|
||||||
`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';
|
|
||||||
}
|
|
||||||
|
|
||||||
const youtubePlaybackRuntime = createYoutubePlaybackRuntime({
|
const youtubePlaybackRuntime = createYoutubePlaybackRuntime({
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
@@ -6008,11 +5897,17 @@ async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
|||||||
return extension;
|
return extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastSyncedYomitanAnkiSettingsKey: string | null = null;
|
const { syncYomitanDefaultProfileAnkiServer } = createYomitanAnkiServerSyncRuntime({
|
||||||
|
isExternalReadOnlyMode: () => yomitanProfilePolicy.isExternalReadOnlyMode(),
|
||||||
function getPreferredYomitanAnkiServerUrl(): string {
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
return getPreferredYomitanAnkiServerUrlRuntime(getResolvedConfig().ankiConnect);
|
getYomitanParserRuntimeDeps: () => getYomitanParserRuntimeDeps(),
|
||||||
}
|
logError: (message, ...args) => {
|
||||||
|
logger.error(message, ...args);
|
||||||
|
},
|
||||||
|
logInfo: (message, ...args) => {
|
||||||
|
logger.info(message, ...args);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function getYomitanParserRuntimeDeps() {
|
function getYomitanParserRuntimeDeps() {
|
||||||
return {
|
return {
|
||||||
@@ -6033,43 +5928,6 @@ function getYomitanParserRuntimeDeps() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncYomitanDefaultProfileAnkiServer(): Promise<void> {
|
|
||||||
if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetUrl = getPreferredYomitanAnkiServerUrl().trim();
|
|
||||||
const ankiConnectConfig = getResolvedConfig().ankiConnect;
|
|
||||||
const targetDeck = ankiConnectConfig?.deck?.trim() ?? '';
|
|
||||||
const targetSettingsKey = `${targetUrl}\n${targetDeck}`;
|
|
||||||
if (!targetUrl || targetSettingsKey === lastSyncedYomitanAnkiSettingsKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const synced = await syncYomitanDefaultAnkiServerCore(
|
|
||||||
targetUrl,
|
|
||||||
getYomitanParserRuntimeDeps(),
|
|
||||||
{
|
|
||||||
error: (message, ...args) => {
|
|
||||||
logger.error(message, ...args);
|
|
||||||
},
|
|
||||||
info: (message, ...args) => {
|
|
||||||
logger.info(message, ...args);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
forceOverride: ankiConnectConfig
|
|
||||||
? shouldForceOverrideYomitanAnkiServer(ankiConnectConfig)
|
|
||||||
: false,
|
|
||||||
deck: targetDeck,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (synced) {
|
|
||||||
lastSyncedYomitanAnkiSettingsKey = targetSettingsKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createModalWindow(): BrowserWindow {
|
function createModalWindow(): BrowserWindow {
|
||||||
const existingWindow = overlayManager.getModalWindow();
|
const existingWindow = overlayManager.getModalWindow();
|
||||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||||
@@ -6201,52 +6059,11 @@ function openYomitanSettings(): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeUnknownError(error: unknown): string {
|
const { exportLogsFromTray } = createLogExportTrayRuntime({
|
||||||
return error instanceof Error ? error.message : String(error);
|
flushMpvLog: () => flushMpvLog(),
|
||||||
}
|
logInfo: (message) => logger.info(message),
|
||||||
|
logWarn: (message, details) => logger.warn(message, details),
|
||||||
async function exportLogsFromTray(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await flushMpvLog();
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('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'),
|
|
||||||
});
|
});
|
||||||
logger.info(
|
|
||||||
`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);
|
|
||||||
logger.warn('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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getConfiguredShortcuts,
|
getConfiguredShortcuts,
|
||||||
@@ -6308,53 +6125,20 @@ const {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function resolveSessionBindingPlatform(): 'darwin' | 'win32' | 'linux' {
|
const { persistSessionBindings, refreshCurrentSessionBindings } = createSessionBindingsRuntime({
|
||||||
if (process.platform === 'darwin') return 'darwin';
|
configDir: CONFIG_DIR,
|
||||||
if (process.platform === 'win32') return 'win32';
|
getKeybindings: () => appState.keybindings,
|
||||||
return 'linux';
|
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||||
}
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
|
getMpvClient: () => appState.mpvClient,
|
||||||
function compileCurrentSessionBindings(): {
|
setSessionBindings: (bindings) => {
|
||||||
bindings: CompiledSessionBinding[];
|
|
||||||
warnings: ReturnType<typeof compileSessionBindings>['warnings'];
|
|
||||||
} {
|
|
||||||
return compileSessionBindings({
|
|
||||||
keybindings: appState.keybindings,
|
|
||||||
shortcuts: getConfiguredShortcuts(),
|
|
||||||
statsToggleKey: getResolvedConfig().stats.toggleKey,
|
|
||||||
statsMarkWatchedKey: getResolvedConfig().stats.markWatchedKey,
|
|
||||||
platform: resolveSessionBindingPlatform(),
|
|
||||||
rawConfig: getResolvedConfig(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function persistSessionBindings(
|
|
||||||
bindings: CompiledSessionBinding[],
|
|
||||||
warnings: ReturnType<typeof compileSessionBindings>['warnings'] = [],
|
|
||||||
): void {
|
|
||||||
const artifact = buildPluginSessionBindingsArtifact({
|
|
||||||
bindings,
|
|
||||||
warnings,
|
|
||||||
numericSelectionTimeoutMs: getConfiguredShortcuts().multiCopyTimeoutMs,
|
|
||||||
});
|
|
||||||
writeSessionBindingsArtifact(CONFIG_DIR, artifact);
|
|
||||||
appState.sessionBindings = bindings;
|
appState.sessionBindings = bindings;
|
||||||
appState.sessionBindingsInitialized = true;
|
},
|
||||||
if (appState.mpvClient?.connected) {
|
setSessionBindingsInitialized: (initialized) => {
|
||||||
sendMpvCommandRuntime(appState.mpvClient, [
|
appState.sessionBindingsInitialized = initialized;
|
||||||
'script-message',
|
},
|
||||||
'subminer-reload-session-bindings',
|
logWarn: (message) => logger.warn(message),
|
||||||
]);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshCurrentSessionBindings(): void {
|
|
||||||
const compiled = compileCurrentSessionBindings();
|
|
||||||
for (const warning of compiled.warnings) {
|
|
||||||
logger.warn(`[session-bindings] ${warning.message}`);
|
|
||||||
}
|
|
||||||
persistSessionBindings(compiled.bindings, compiled.warnings);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { flushMpvLog, showMpvOsd } = createMpvOsdRuntimeHandlers({
|
const { flushMpvLog, showMpvOsd } = createMpvOsdRuntimeHandlers({
|
||||||
appendToMpvLogMainDeps: {
|
appendToMpvLogMainDeps: {
|
||||||
|
|||||||
@@ -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