From ec64eebb8007a24ef8cb5810a0199af07caea811 Mon Sep 17 00:00:00 2001 From: sudacode Date: Wed, 1 Apr 2026 00:16:39 -0700 Subject: [PATCH] fix: wire playlist-browser IPC deps through bootstrap surfaces - Thread `openPlaylistBrowser` action into `IpcRuntimeBootstrapInput` - Pass `playlistBrowserMainDeps` through bootstrap into `createIpcRuntime` - Add playlist-browser mock deps to ipc-runtime tests - Bump subminer-yomitan submodule --- ...s-composition-root-into-domain-runtimes.md | 9 ++++++++- src/main.ts | 15 ++++++++++++++ src/main/ipc-runtime-bootstrap.ts | 5 +++++ src/main/ipc-runtime.test.ts | 20 +++++++++++++++++++ src/main/ipc-runtime.ts | 9 +++++++++ vendor/subminer-yomitan | 2 +- 6 files changed, 58 insertions(+), 2 deletions(-) diff --git a/backlog/tasks/task-238.8 - Refactor-src-main.ts-composition-root-into-domain-runtimes.md b/backlog/tasks/task-238.8 - Refactor-src-main.ts-composition-root-into-domain-runtimes.md index 12e5c6d6..b9bcb33d 100644 --- a/backlog/tasks/task-238.8 - Refactor-src-main.ts-composition-root-into-domain-runtimes.md +++ b/backlog/tasks/task-238.8 - Refactor-src-main.ts-composition-root-into-domain-runtimes.md @@ -1,9 +1,10 @@ --- id: TASK-238.8 title: Refactor src/main.ts composition root into domain runtimes -status: To Do +status: In Progress assignee: [] created_date: '2026-03-31 06:28' +updated_date: '2026-04-01 07:07' labels: - tech-debt - runtime @@ -34,3 +35,9 @@ Refactor `src/main.ts` so it becomes a thin composition root and the domain-spec - [ ] #5 Cross-domain coordination stays in `main.ts`; wrapper modules stay acyclic and communicate via injected callbacks. - [ ] #6 No user-facing behavior, config fields, or IPC channel names change. + +## Implementation Notes + + +CI follow-up: typecheck failed after the runtime split because playlist-browser IPC deps were not threaded through the new bootstrap/composer surfaces. Wiring the missing open action and registration deps now. + diff --git a/src/main.ts b/src/main.ts index 8b15999a..c09d7563 100644 --- a/src/main.ts +++ b/src/main.ts @@ -131,6 +131,8 @@ import { shouldAutoOpenFirstRunSetup } from './main/first-run-runtime'; import { createWindowsMpvLaunchDeps, launchWindowsMpv } from './main/runtime/windows-mpv-launch'; import { shouldEnsureTrayOnStartupForInitialArgs } from './main/runtime/startup-tray-policy'; import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup'; +import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc'; +import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open'; import { createRunStatsCliCommandHandler, writeStatsCliCommandResponse, @@ -931,6 +933,7 @@ const { mpvRuntime, mining } = createMainPlaybackRuntime({ }, }, }); +const playlistBrowserIpcRuntime = createPlaylistBrowserIpcRuntime(() => appState.mpvClient); function createMpvClientRuntimeService(): MpvIpcClient { return mpvRuntime.createMpvClientRuntimeService(); @@ -1101,6 +1104,15 @@ const { registerIpcRuntimeHandlers } = createIpcRuntimeBootstrap({ actions: { requestAppQuit, openYomitanSettings: () => yomitan.openYomitanSettings(), + openPlaylistBrowser: () => { + void openPlaylistBrowserRuntime({ + ensureOverlayStartupPrereqs: () => startupRuntime.appReady.ensureOverlayStartupPrereqs(), + ensureOverlayWindowsReadyForVisibilityActions: () => + overlayUi?.ensureOverlayWindowsReadyForVisibilityActions(), + sendToActiveOverlayWindow: (channel, payload, runtimeOptions) => + overlayUi?.sendToActiveOverlayWindow(channel, payload, runtimeOptions) ?? false, + }); + }, showDesktopNotification, setAnkiIntegration: (integration: AnkiIntegration | null) => { appState.ankiIntegration = integration; @@ -1112,6 +1124,9 @@ const { registerIpcRuntimeHandlers } = createIpcRuntimeBootstrap({ anilist, mining, dictionarySupport, + playlistBrowser: { + playlistBrowserMainDeps: playlistBrowserIpcRuntime.playlistBrowserMainDeps, + }, configDerived: configDerivedRuntime, subsync: subsyncRuntime, }, diff --git a/src/main/ipc-runtime-bootstrap.ts b/src/main/ipc-runtime-bootstrap.ts index 29d8a07f..dd0dcf70 100644 --- a/src/main/ipc-runtime-bootstrap.ts +++ b/src/main/ipc-runtime-bootstrap.ts @@ -29,6 +29,7 @@ import type { OverlayModalRuntime } from './overlay-runtime'; import type { OverlayUiRuntime } from './overlay-ui-runtime'; import type { AppState } from './state'; import type { SubtitleRuntime } from './subtitle-runtime'; +import type { PlaylistBrowserIpcRuntime } from './runtime/playlist-browser-ipc'; import type { YoutubeRuntime } from './youtube-runtime'; import { resolveSubtitleStyleForRenderer } from './runtime/domains/overlay'; import type { ShortcutsRuntime } from './shortcuts-runtime'; @@ -82,6 +83,7 @@ export interface IpcRuntimeBootstrapInput { actions: { requestAppQuit: () => void; openYomitanSettings: () => boolean; + openPlaylistBrowser: () => void | Promise; showDesktopNotification: (title: string, options: { body?: string }) => void; setAnkiIntegration: (integration: AnkiIntegration | null) => void; }; @@ -103,6 +105,7 @@ export interface IpcRuntimeBootstrapInput { | 'setFieldGroupingResolver' | 'resolveMediaPathForJimaku' >; + playlistBrowser: Pick; configDerived: ConfigDerivedRuntimeLike; subsync: SubsyncRuntimeLike; }; @@ -115,6 +118,7 @@ export function createIpcRuntimeBootstrap(input: IpcRuntimeBootstrapInput): IpcR triggerSubsyncFromConfig: () => input.runtimes.subsync.triggerFromConfig(), openRuntimeOptionsPalette: () => input.overlay.getOverlayUi()?.openRuntimeOptionsPalette(), openYoutubeTrackPicker: () => input.runtimes.youtube.openYoutubeTrackPickerFromPlayback(), + openPlaylistBrowser: () => input.actions.openPlaylistBrowser(), cycleRuntimeOption: (id, direction) => { if (!input.appState.runtimeOptionsManager) { return { ok: false, error: 'Runtime options manager unavailable' }; @@ -204,6 +208,7 @@ export function createIpcRuntimeBootstrap(input: IpcRuntimeBootstrapInput): IpcR }, getImmersionTracker: () => input.appState.immersionTracker, }, + playlistBrowser: input.runtimes.playlistBrowser.playlistBrowserMainDeps, anilist: { getStatus: () => input.runtimes.anilist.getStatusSnapshot(), clearToken: () => input.runtimes.anilist.clearTokenState(), diff --git a/src/main/ipc-runtime.test.ts b/src/main/ipc-runtime.test.ts index fc6069c8..f4eb72cf 100644 --- a/src/main/ipc-runtime.test.ts +++ b/src/main/ipc-runtime.test.ts @@ -17,6 +17,21 @@ function createBaseRuntimeInput(capturedRegistration: { value: unknown | null }) quitApp: () => {}, toggleVisibleOverlay: () => {}, }, + playlistBrowser: { + getPlaylistBrowserSnapshot: async () => ({ + directoryPath: null, + directoryAvailable: false, + directoryStatus: '', + directoryItems: [], + playlistItems: [], + playingIndex: null, + currentFilePath: null, + }), + appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok' }), + playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }), + removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }), + movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }), + }, subtitle: { tokenizeCurrentSubtitle: async () => null, getCurrentSubtitleRaw: () => '', @@ -86,6 +101,7 @@ function createBaseRuntimeInput(capturedRegistration: { value: unknown | null }) triggerSubsyncFromConfig: () => {}, openRuntimeOptionsPalette: () => {}, openYoutubeTrackPicker: () => {}, + openPlaylistBrowser: () => {}, cycleRuntimeOption: () => ({ ok: true }), showMpvOsd: () => {}, replayCurrentSubtitle: () => {}, @@ -134,11 +150,13 @@ test('ipc runtime registers composed IPC handlers from explicit registration inp const registration = capturedRegistration.value as { runtimeOptions: { showMpvOsd: unknown }; mainDeps: { + getPlaylistBrowserSnapshot: unknown; handleMpvCommand: unknown; runSubsyncManual: (payload: unknown) => Promise; }; }; assert.equal(registration.runtimeOptions.showMpvOsd !== undefined, true); + assert.equal(registration.mainDeps.getPlaylistBrowserSnapshot instanceof Function, true); assert.equal(registration.mainDeps.handleMpvCommand instanceof Function, true); assert.deepEqual( await registration.mainDeps.runSubsyncManual({ payload: null } as never), @@ -163,11 +181,13 @@ test('ipc runtime builds grouped registration input from main state', async () = const registration = capturedRegistration.value as { runtimeOptions: { showMpvOsd: unknown }; mainDeps: { + getPlaylistBrowserSnapshot: unknown; handleMpvCommand: unknown; runSubsyncManual: (payload: unknown) => Promise; }; }; assert.equal(registration.runtimeOptions.showMpvOsd !== undefined, true); + assert.equal(registration.mainDeps.getPlaylistBrowserSnapshot instanceof Function, true); assert.equal(registration.mainDeps.handleMpvCommand instanceof Function, true); assert.deepEqual( await registration.mainDeps.runSubsyncManual({ payload: null } as never), diff --git a/src/main/ipc-runtime.ts b/src/main/ipc-runtime.ts index 4a0bddb5..f8e5f323 100644 --- a/src/main/ipc-runtime.ts +++ b/src/main/ipc-runtime.ts @@ -30,6 +30,14 @@ export interface IpcRuntimeMainInput { | 'quitApp' | 'toggleVisibleOverlay' >; + playlistBrowser: Pick< + RegisterIpcRuntimeServicesParams['mainDeps'], + | 'getPlaylistBrowserSnapshot' + | 'appendPlaylistBrowserFile' + | 'playPlaylistBrowserIndex' + | 'removePlaylistBrowserIndex' + | 'movePlaylistBrowserIndex' + >; subtitle: Pick< RegisterIpcRuntimeServicesParams['mainDeps'], | 'tokenizeCurrentSubtitle' @@ -110,6 +118,7 @@ export function createIpcRuntime(input: IpcRuntimeInput): IpcRuntime { runtimeOptions: input.registration.runtimeOptions, mainDeps: { ...input.registration.main.window, + ...input.registration.main.playlistBrowser, ...input.registration.main.subtitle, ...input.registration.main.controller, ...input.registration.main.runtime, diff --git a/vendor/subminer-yomitan b/vendor/subminer-yomitan index 3c9ee577..69620abc 160000 --- a/vendor/subminer-yomitan +++ b/vendor/subminer-yomitan @@ -1 +1 @@ -Subproject commit 3c9ee577ac11266ad402344ddad5137f89ae6113 +Subproject commit 69620abcbc126edd2dcbe637f0fac582e9b555c5