Sync Stats & History window, headless --sync-cli, and Windows remote support (#160)

This commit is contained in:
2026-07-13 18:56:51 -07:00
committed by GitHub
parent 66f8ca4f80
commit 49b926e08c
111 changed files with 6983 additions and 1130 deletions
+82
View File
@@ -530,8 +530,14 @@ import {
createCreateConfigSettingsWindowHandler,
createCreateFirstRunSetupWindowHandler,
createCreateJellyfinSetupWindowHandler,
createCreateSyncUiWindowHandler,
} from './main/runtime/setup-window-factory';
import { createConfigSettingsRuntime } from './main/runtime/config-settings-runtime';
import { createOpenConfigSettingsWindowHandler } from './main/runtime/config-settings-window';
import { createSyncUiRuntime } from './main/runtime/sync-ui-runtime';
import { createSyncAutoScheduler } from './main/runtime/sync-auto-scheduler';
import { resolveSyncLauncherCommand, runSyncLauncher } from './main/runtime/sync-launcher-client';
import { getSyncHostsPath } from './shared/sync/sync-hosts-store';
import { shouldSuppressVisibleOverlayRaiseForSeparateWindow } from './main/runtime/settings-window-z-order';
import {
isSameYoutubeMediaPath,
@@ -903,6 +909,7 @@ const configSettingsFields = buildConfigSettingsRegistry(DEFAULT_CONFIG);
function getOverlayForegroundSeparateWindows(): BrowserWindow[] {
return [
appState.configSettingsWindow,
appState.syncUiWindow,
appState.yomitanSettingsWindow,
appState.anilistSetupWindow,
appState.jellyfinSetupWindow,
@@ -2233,6 +2240,75 @@ const configSettingsRuntime = createConfigSettingsRuntime({
configSettingsRuntime.registerHandlers();
const openConfigSettingsWindow = () => configSettingsRuntime.openWindow();
const openSyncUiWindowHandler = createOpenConfigSettingsWindowHandler({
getSettingsWindow: () => appState.syncUiWindow,
setSettingsWindow: (window) => {
appState.syncUiWindow = window as BrowserWindow | null;
},
createSettingsWindow: createCreateSyncUiWindowHandler({
createBrowserWindow: (options) => new BrowserWindow(options),
preloadPath: path.join(__dirname, 'preload-syncui.js'),
}),
settingsHtmlPath: path.join(__dirname, 'syncui', 'index.html'),
promoteSettingsWindowAboveOverlay: (window) =>
promoteSettingsWindowAboveOverlay(window as BrowserWindow),
onClosed: () => {
// requestAppQuit (not app.quit) so the forced-exit fallback covers a
// stalled quit; a standalone sync window is the app's only reason to live.
if (appState.initialArgs?.syncWindow) requestAppQuit();
},
log: (message) => logger.error(message),
});
const openSyncUiWindow = () => openSyncUiWindowHandler();
const syncUiRuntime = createSyncUiRuntime({
ipcMain,
hostsFilePath: getSyncHostsPath(USER_DATA_PATH),
snapshotsDir: path.join(os.tmpdir(), 'subminer-db-snapshots'),
getDbPath: () => {
const configured = getResolvedConfig().immersionTracking?.dbPath?.trim();
return configured || DEFAULT_IMMERSION_DB_PATH;
},
resolveLauncherCommand: () =>
resolveSyncLauncherCommand({
execPath: process.execPath,
appPath: app.isPackaged ? null : app.getAppPath(),
}),
runLauncher: runSyncLauncher,
getWindow: () => appState.syncUiWindow,
pickSnapshotFile: async () => {
const result = await dialog.showOpenDialog({
title: 'Choose a SubMiner stats snapshot',
filters: [{ name: 'SQLite snapshots', extensions: ['sqlite'] }],
properties: ['openFile'],
});
return result.canceled ? null : (result.filePaths[0] ?? null);
},
revealPath: (targetPath) => shell.showItemInFolder(targetPath),
nowMs: () => Date.now(),
log: (message) => logger.info(message),
notify: (payload) =>
showOverlayNotification({
title: payload.title,
body: payload.body,
variant: payload.variant,
}),
});
syncUiRuntime.registerHandlers();
// Live sync uses a consistent WAL snapshot plus a transactional merge, and it
// skips unfinished sessions, so resident stats writers do not block scheduling.
const syncAutoScheduler = createSyncAutoScheduler({
readState: () => syncUiRuntime.readState(),
isRunning: () => syncUiRuntime.isRunning(),
triggerHostSync: (host, direction) => {
void syncUiRuntime.runHostSync({ host, direction }, { notify: true });
},
nowMs: () => Date.now(),
log: (message) => logger.info(message),
});
syncAutoScheduler.start();
const buildDictionaryRootsHandler = createBuildDictionaryRootsMainHandler({
platform: process.platform,
dirname: __dirname,
@@ -3893,6 +3969,10 @@ const {
annotationSubtitleWsService.stop();
},
stopTexthookerService: () => texthookerService.stop(),
stopSyncAutoScheduler: async () => {
syncAutoScheduler.stop();
await syncUiRuntime.shutdown();
},
clearWindowsVisibleOverlayForegroundPollLoop: () =>
clearWindowsVisibleOverlayForegroundPollLoop(),
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => {
@@ -6006,6 +6086,7 @@ const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
ensureBackgroundStatsServer: () => ensureBackgroundStatsServer(),
openYomitanSettings: () => openYomitanSettings(),
openConfigSettingsWindow: () => openConfigSettingsWindow(),
openSyncUiWindow: () => openSyncUiWindow(),
cycleSecondarySubMode: () => handleCycleSecondarySubMode(),
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
@@ -6254,6 +6335,7 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
showWindowsMpvLauncherSetup: () => process.platform === 'win32',
openYomitanSettings: () => openYomitanSettings(),
openConfigSettingsWindow: () => openConfigSettingsWindow(),
openSyncUiWindow: () => openSyncUiWindow(),
exportLogs: () => {
void exportLogsFromTray();
},