mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
Fix CodeRabbit review feedback
This commit is contained in:
+2
-2
@@ -50,8 +50,8 @@
|
|||||||
"test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua",
|
"test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua",
|
||||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
||||||
"test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/main/runtime/autoplay-tokenization-warm-release.test.ts src/main/runtime/autoplay-subtitle-primer.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/subtitle-render-word-class.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts",
|
"test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/stats-window-lifecycle.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/main/runtime/autoplay-tokenization-warm-release.test.ts src/main/runtime/autoplay-subtitle-primer.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/subtitle-render-word-class.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts",
|
||||||
"test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/settings/settings-field-layout.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/main/runtime/autoplay-tokenization-warm-release.test.js dist/main/runtime/autoplay-subtitle-primer.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/subtitle-render-word-class.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
|
"test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/settings/settings-field-layout.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/stats-window-lifecycle.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/main/runtime/autoplay-tokenization-warm-release.test.js dist/main/runtime/autoplay-subtitle-primer.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/subtitle-render-word-class.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
|
||||||
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||||
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export type StatsWindowLayerSuspensionState = {
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createStatsWindowLayerSuspensionState(): StatsWindowLayerSuspensionState {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStatsWindowLayerSuspended(state: StatsWindowLayerSuspensionState): boolean {
|
||||||
|
return state.count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function suspendStatsWindowLayer(state: StatsWindowLayerSuspensionState): boolean {
|
||||||
|
state.count += 1;
|
||||||
|
return state.count === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreStatsWindowLayer(state: StatsWindowLayerSuspensionState): boolean {
|
||||||
|
if (state.count <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.count -= 1;
|
||||||
|
return state.count === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetStatsWindowLayerSuspension(state: StatsWindowLayerSuspensionState): void {
|
||||||
|
state.count = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
createStatsWindowLayerSuspensionState,
|
||||||
|
isStatsWindowLayerSuspended,
|
||||||
|
resetStatsWindowLayerSuspension,
|
||||||
|
restoreStatsWindowLayer,
|
||||||
|
suspendStatsWindowLayer,
|
||||||
|
} from './stats-window-layer';
|
||||||
|
|
||||||
|
test('stats window layer suspension reset clears missed native dialog closes', () => {
|
||||||
|
const state = createStatsWindowLayerSuspensionState();
|
||||||
|
|
||||||
|
assert.equal(suspendStatsWindowLayer(state), true);
|
||||||
|
assert.equal(suspendStatsWindowLayer(state), false);
|
||||||
|
assert.equal(isStatsWindowLayerSuspended(state), true);
|
||||||
|
|
||||||
|
resetStatsWindowLayerSuspension(state);
|
||||||
|
|
||||||
|
assert.equal(isStatsWindowLayerSuspended(state), false);
|
||||||
|
assert.equal(restoreStatsWindowLayer(state), false);
|
||||||
|
assert.equal(suspendStatsWindowLayer(state), true);
|
||||||
|
});
|
||||||
@@ -15,11 +15,18 @@ import {
|
|||||||
STATS_WINDOW_TITLE,
|
STATS_WINDOW_TITLE,
|
||||||
} from './stats-window-runtime.js';
|
} from './stats-window-runtime.js';
|
||||||
import { ensureHyprlandWindowFloatingByTitle } from './hyprland-window-placement.js';
|
import { ensureHyprlandWindowFloatingByTitle } from './hyprland-window-placement.js';
|
||||||
|
import {
|
||||||
|
createStatsWindowLayerSuspensionState,
|
||||||
|
isStatsWindowLayerSuspended,
|
||||||
|
resetStatsWindowLayerSuspension,
|
||||||
|
restoreStatsWindowLayer,
|
||||||
|
suspendStatsWindowLayer,
|
||||||
|
} from './stats-window-layer.js';
|
||||||
|
|
||||||
let statsWindow: BrowserWindow | null = null;
|
let statsWindow: BrowserWindow | null = null;
|
||||||
let toggleRegistered = false;
|
let toggleRegistered = false;
|
||||||
let nativeDialogLayerRegistered = false;
|
let nativeDialogLayerRegistered = false;
|
||||||
let nativeDialogLayerSuspensionCount = 0;
|
const nativeDialogLayerSuspension = createStatsWindowLayerSuspensionState();
|
||||||
|
|
||||||
export interface StatsWindowOptions {
|
export interface StatsWindowOptions {
|
||||||
/** Absolute path to stats/dist/ directory */
|
/** Absolute path to stats/dist/ directory */
|
||||||
@@ -67,7 +74,7 @@ function showStatsWindow(window: BrowserWindow, options: StatsWindowOptions): vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function promoteStatsOverlayAbovePlayback(): boolean {
|
export function promoteStatsOverlayAbovePlayback(): boolean {
|
||||||
if (nativeDialogLayerSuspensionCount > 0) {
|
if (isStatsWindowLayerSuspended(nativeDialogLayerSuspension)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +98,7 @@ export function demoteStatsOverlayBelowDialogs(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function suspendStatsWindowLayerForNativeDialog(): void {
|
export function suspendStatsWindowLayerForNativeDialog(): void {
|
||||||
nativeDialogLayerSuspensionCount += 1;
|
if (!suspendStatsWindowLayer(nativeDialogLayerSuspension)) {
|
||||||
if (nativeDialogLayerSuspensionCount !== 1) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,16 +106,15 @@ export function suspendStatsWindowLayerForNativeDialog(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function restoreStatsWindowLayerAfterNativeDialog(): void {
|
export function restoreStatsWindowLayerAfterNativeDialog(): void {
|
||||||
if (nativeDialogLayerSuspensionCount <= 0) {
|
if (restoreStatsWindowLayer(nativeDialogLayerSuspension)) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nativeDialogLayerSuspensionCount -= 1;
|
|
||||||
if (nativeDialogLayerSuspensionCount === 0) {
|
|
||||||
promoteStatsOverlayAbovePlayback();
|
promoteStatsOverlayAbovePlayback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetStatsWindowLayerAfterLifecycleEnd(): void {
|
||||||
|
resetStatsWindowLayerSuspension(nativeDialogLayerSuspension);
|
||||||
|
}
|
||||||
|
|
||||||
export async function withStatsWindowLayerSuspendedForNativeDialog<T>(
|
export async function withStatsWindowLayerSuspendedForNativeDialog<T>(
|
||||||
showDialog: () => Promise<T>,
|
showDialog: () => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
@@ -172,6 +177,7 @@ export function toggleStatsOverlay(options: StatsWindowOptions): void {
|
|||||||
statsWindow.on('closed', () => {
|
statsWindow.on('closed', () => {
|
||||||
options.onVisibilityChanged?.(false);
|
options.onVisibilityChanged?.(false);
|
||||||
statsWindow = null;
|
statsWindow = null;
|
||||||
|
resetStatsWindowLayerAfterLifecycleEnd();
|
||||||
});
|
});
|
||||||
|
|
||||||
statsWindow.webContents.on('before-input-event', (event, input) => {
|
statsWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
@@ -222,4 +228,5 @@ export function destroyStatsWindow(): void {
|
|||||||
statsWindow.destroy();
|
statsWindow.destroy();
|
||||||
statsWindow = null;
|
statsWindow = null;
|
||||||
}
|
}
|
||||||
|
resetStatsWindowLayerAfterLifecycleEnd();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,16 @@ test('resolveLinuxPasswordStoreValue defaults Linux safeStorage to gnome-libsecr
|
|||||||
assert.equal(resolveLinuxPasswordStoreValue(['SubMiner.exe'], 'win32'), null);
|
assert.equal(resolveLinuxPasswordStoreValue(['SubMiner.exe'], 'win32'), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolveLinuxPasswordStoreValue keeps scanning after a bare password-store flag', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveLinuxPasswordStoreValue(
|
||||||
|
['SubMiner.AppImage', '--password-store', '--start', '--password-store=kwallet6'],
|
||||||
|
'linux',
|
||||||
|
),
|
||||||
|
'kwallet6',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('applyEarlyLinuxCommandLineSwitches appends password store before main startup', () => {
|
test('applyEarlyLinuxCommandLineSwitches appends password store before main startup', () => {
|
||||||
const switches: Array<[string, string | undefined]> = [];
|
const switches: Array<[string, string | undefined]> = [];
|
||||||
applyEarlyLinuxCommandLineSwitches(
|
applyEarlyLinuxCommandLineSwitches(
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ function removePassiveStartupArgs(argv: string[]): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPasswordStoreArg(argv: string[]): string | null {
|
function getPasswordStoreArg(argv: string[]): string | null {
|
||||||
|
let resolved: string | null = null;
|
||||||
for (let i = 0; i < argv.length; i += 1) {
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
const arg = argv[i];
|
const arg = argv[i];
|
||||||
if (!arg?.startsWith(PASSWORD_STORE_ARG)) {
|
if (!arg?.startsWith(PASSWORD_STORE_ARG)) {
|
||||||
@@ -88,17 +89,18 @@ function getPasswordStoreArg(argv: string[]): string | null {
|
|||||||
if (arg === PASSWORD_STORE_ARG) {
|
if (arg === PASSWORD_STORE_ARG) {
|
||||||
const value = argv[i + 1];
|
const value = argv[i + 1];
|
||||||
if (value && !value.startsWith('--')) {
|
if (value && !value.startsWith('--')) {
|
||||||
return value;
|
resolved = value.trim();
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
return null;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [prefix, value] = arg.split('=', 2);
|
const [prefix, value] = arg.split('=', 2);
|
||||||
if (prefix === PASSWORD_STORE_ARG && value && value.trim().length > 0) {
|
if (prefix === PASSWORD_STORE_ARG && value && value.trim().length > 0) {
|
||||||
return value.trim();
|
resolved = value.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePasswordStoreArg(value: string): string {
|
function normalizePasswordStoreArg(value: string): string {
|
||||||
|
|||||||
+8
-7
@@ -35,6 +35,7 @@ import { applyControllerConfigUpdate } from './main/controller-config-update.js'
|
|||||||
import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open';
|
import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open';
|
||||||
import { createDiscordRpcClient } from './main/runtime/discord-rpc-client.js';
|
import { createDiscordRpcClient } from './main/runtime/discord-rpc-client.js';
|
||||||
import { startAppControlServer } from './main/runtime/app-control-server';
|
import { startAppControlServer } from './main/runtime/app-control-server';
|
||||||
|
import { markJellyfinRemotePlaybackLoaded as markJellyfinRemotePlaybackLoadedState } from './main/runtime/jellyfin-remote-playback';
|
||||||
import { getAppControlSocketPath } from './shared/app-control';
|
import { getAppControlSocketPath } from './shared/app-control';
|
||||||
import {
|
import {
|
||||||
type CancelLinuxMpvFullscreenOverlayRefreshBurst,
|
type CancelLinuxMpvFullscreenOverlayRefreshBurst,
|
||||||
@@ -44,6 +45,7 @@ import {
|
|||||||
import { mergeAiConfig } from './ai/config';
|
import { mergeAiConfig } from './ai/config';
|
||||||
|
|
||||||
function getPasswordStoreArg(argv: string[]): string | null {
|
function getPasswordStoreArg(argv: string[]): string | null {
|
||||||
|
let resolved: string | null = null;
|
||||||
for (let i = 0; i < argv.length; i += 1) {
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
const arg = argv[i];
|
const arg = argv[i];
|
||||||
if (!arg?.startsWith('--password-store')) {
|
if (!arg?.startsWith('--password-store')) {
|
||||||
@@ -53,17 +55,18 @@ function getPasswordStoreArg(argv: string[]): string | null {
|
|||||||
if (arg === '--password-store') {
|
if (arg === '--password-store') {
|
||||||
const value = argv[i + 1];
|
const value = argv[i + 1];
|
||||||
if (value && !value.startsWith('--')) {
|
if (value && !value.startsWith('--')) {
|
||||||
return value;
|
resolved = value.trim();
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
return null;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [prefix, value] = arg.split('=', 2);
|
const [prefix, value] = arg.split('=', 2);
|
||||||
if (prefix === '--password-store' && value && value.trim().length > 0) {
|
if (prefix === '--password-store' && value && value.trim().length > 0) {
|
||||||
return value.trim();
|
resolved = value.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePasswordStoreArg(value: string): string {
|
function normalizePasswordStoreArg(value: string): string {
|
||||||
@@ -4465,9 +4468,7 @@ const {
|
|||||||
},
|
},
|
||||||
signalAutoplayReadyIfWarm: (path) => signalAutoplayReadyFromWarmTokenization?.(path),
|
signalAutoplayReadyIfWarm: (path) => signalAutoplayReadyFromWarmTokenization?.(path),
|
||||||
markJellyfinRemotePlaybackLoaded: (path) => {
|
markJellyfinRemotePlaybackLoaded: (path) => {
|
||||||
if (activeJellyfinRemotePlayback) {
|
markJellyfinRemotePlaybackLoadedState(activeJellyfinRemotePlayback, path);
|
||||||
activeJellyfinRemotePlayback.loadedMediaPath = path;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
scheduleCharacterDictionarySync: () => {
|
scheduleCharacterDictionarySync: () => {
|
||||||
if (!yomitanProfilePolicy.isCharacterDictionaryEnabled() || isYoutubePlaybackActiveNow()) {
|
if (!yomitanProfilePolicy.isCharacterDictionaryEnabled() || isYoutubePlaybackActiveNow()) {
|
||||||
|
|||||||
@@ -82,8 +82,9 @@ test('playback handler drives mpv commands and playback state', async () => {
|
|||||||
setLastProgressAtMs: (value) => calls.push(`progress:${value}`),
|
setLastProgressAtMs: (value) => calls.push(`progress:${value}`),
|
||||||
reportPlaying: (payload) => reportPayloads.push(payload as Record<string, unknown>),
|
reportPlaying: (payload) => reportPayloads.push(payload as Record<string, unknown>),
|
||||||
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
||||||
recordJellyfinPlaybackMetadata: (metadata) =>
|
recordJellyfinPlaybackMetadata: (metadata) => {
|
||||||
statsMetadata.push(metadata as Record<string, unknown>),
|
statsMetadata.push(metadata as Record<string, unknown>);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler({
|
await handler({
|
||||||
@@ -164,7 +165,9 @@ test('playback handler publishes Jellyfin title before loading tokenized stream
|
|||||||
setLastProgressAtMs: () => {},
|
setLastProgressAtMs: () => {},
|
||||||
reportPlaying: () => {},
|
reportPlaying: () => {},
|
||||||
showMpvOsd: () => {},
|
showMpvOsd: () => {},
|
||||||
updateCurrentMediaTitle: (title) => timeline.push(`title:${title}`),
|
updateCurrentMediaTitle: (title) => {
|
||||||
|
timeline.push(`title:${title}`);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler({
|
await handler({
|
||||||
@@ -353,3 +356,51 @@ test('playback handler does not let media title failures block playback startup'
|
|||||||
|
|
||||||
assert.deepEqual(commands[1], ['loadfile', 'https://stream.example/video.m3u8', 'replace']);
|
assert.deepEqual(commands[1], ['loadfile', 'https://stream.example/video.m3u8', 'replace']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('playback handler handles rejected best-effort hook promises', async () => {
|
||||||
|
const commands: Array<Array<string | number>> = [];
|
||||||
|
const handler = createPlayJellyfinItemInMpvHandler({
|
||||||
|
ensureMpvConnectedForPlayback: async () => true,
|
||||||
|
getMpvClient: () => ({ connected: true, send: () => {} }),
|
||||||
|
resolvePlaybackPlan: async () => ({
|
||||||
|
url: 'https://stream.example/video.m3u8',
|
||||||
|
mode: 'direct',
|
||||||
|
title: 'Episode 5',
|
||||||
|
itemTitle: 'Episode 5',
|
||||||
|
seriesTitle: null,
|
||||||
|
seasonNumber: null,
|
||||||
|
episodeNumber: null,
|
||||||
|
startTimeTicks: 0,
|
||||||
|
audioStreamIndex: null,
|
||||||
|
subtitleStreamIndex: null,
|
||||||
|
}),
|
||||||
|
applyJellyfinMpvDefaults: () => {},
|
||||||
|
showVisibleOverlay: () => {},
|
||||||
|
sendMpvCommand: (command) => commands.push(command),
|
||||||
|
armQuitOnDisconnect: () => {},
|
||||||
|
schedule: () => {},
|
||||||
|
convertTicksToSeconds: (ticks) => ticks / 10_000_000,
|
||||||
|
preloadExternalSubtitles: () => {},
|
||||||
|
setActivePlayback: () => {},
|
||||||
|
setLastProgressAtMs: () => {},
|
||||||
|
reportPlaying: () => {},
|
||||||
|
showMpvOsd: () => {},
|
||||||
|
updateCurrentMediaTitle: async () => {
|
||||||
|
throw new Error('title async unavailable');
|
||||||
|
},
|
||||||
|
recordJellyfinPlaybackMetadata: async () => {
|
||||||
|
throw new Error('stats async unavailable');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
session: baseSession,
|
||||||
|
clientInfo: baseClientInfo,
|
||||||
|
jellyfinConfig: {},
|
||||||
|
itemId: 'item-5',
|
||||||
|
});
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
assert.deepEqual(commands[1], ['loadfile', 'https://stream.example/video.m3u8', 'replace']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ export type JellyfinPlaybackStatsMetadata = {
|
|||||||
itemId: string;
|
itemId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function runBestEffortPlaybackHook(callback: () => void | Promise<void>): void {
|
||||||
|
try {
|
||||||
|
void Promise.resolve(callback()).catch(() => {});
|
||||||
|
} catch {
|
||||||
|
// Best-effort metadata/title hooks must not block playback startup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyStartTimeTicksToPlaybackUrl(url: string, startTimeTicksOverride?: number): string {
|
function applyStartTimeTicksToPlaybackUrl(url: string, startTimeTicksOverride?: number): string {
|
||||||
if (typeof startTimeTicksOverride !== 'number') return url;
|
if (typeof startTimeTicksOverride !== 'number') return url;
|
||||||
try {
|
try {
|
||||||
@@ -78,8 +86,10 @@ export function createPlayJellyfinItemInMpvHandler(deps: {
|
|||||||
eventName: 'start';
|
eventName: 'start';
|
||||||
}) => void;
|
}) => void;
|
||||||
showMpvOsd: (text: string) => void;
|
showMpvOsd: (text: string) => void;
|
||||||
recordJellyfinPlaybackMetadata?: (metadata: JellyfinPlaybackStatsMetadata) => void;
|
recordJellyfinPlaybackMetadata?: (
|
||||||
updateCurrentMediaTitle?: (title: string) => void;
|
metadata: JellyfinPlaybackStatsMetadata,
|
||||||
|
) => void | Promise<void>;
|
||||||
|
updateCurrentMediaTitle?: (title: string) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
return async (params: {
|
return async (params: {
|
||||||
session: JellyfinAuthSession;
|
session: JellyfinAuthSession;
|
||||||
@@ -112,8 +122,8 @@ export function createPlayJellyfinItemInMpvHandler(deps: {
|
|||||||
deps.sendMpvCommand(['set_property', 'sub-auto', 'no']);
|
deps.sendMpvCommand(['set_property', 'sub-auto', 'no']);
|
||||||
const playbackUrl = applyStartTimeTicksToPlaybackUrl(plan.url, params.startTimeTicksOverride);
|
const playbackUrl = applyStartTimeTicksToPlaybackUrl(plan.url, params.startTimeTicksOverride);
|
||||||
const playMethod = plan.mode === 'direct' ? 'DirectPlay' : 'Transcode';
|
const playMethod = plan.mode === 'direct' ? 'DirectPlay' : 'Transcode';
|
||||||
try {
|
runBestEffortPlaybackHook(() => deps.updateCurrentMediaTitle?.(plan.title));
|
||||||
deps.updateCurrentMediaTitle?.(plan.title);
|
runBestEffortPlaybackHook(() =>
|
||||||
deps.recordJellyfinPlaybackMetadata?.({
|
deps.recordJellyfinPlaybackMetadata?.({
|
||||||
mediaPath: playbackUrl,
|
mediaPath: playbackUrl,
|
||||||
displayTitle: plan.title,
|
displayTitle: plan.title,
|
||||||
@@ -122,10 +132,8 @@ export function createPlayJellyfinItemInMpvHandler(deps: {
|
|||||||
seasonNumber: plan.seasonNumber,
|
seasonNumber: plan.seasonNumber,
|
||||||
episodeNumber: plan.episodeNumber,
|
episodeNumber: plan.episodeNumber,
|
||||||
itemId: params.itemId,
|
itemId: params.itemId,
|
||||||
});
|
}),
|
||||||
} catch {
|
);
|
||||||
// Best-effort metadata/title hooks must not block playback startup.
|
|
||||||
}
|
|
||||||
deps.setActivePlayback({
|
deps.setActivePlayback({
|
||||||
itemId: params.itemId,
|
itemId: params.itemId,
|
||||||
mediaSourceId: undefined,
|
mediaSourceId: undefined,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
|
markJellyfinRemotePlaybackLoaded,
|
||||||
createReportJellyfinRemoteProgressHandler,
|
createReportJellyfinRemoteProgressHandler,
|
||||||
createReportJellyfinRemoteStoppedHandler,
|
createReportJellyfinRemoteStoppedHandler,
|
||||||
secondsToJellyfinTicks,
|
secondsToJellyfinTicks,
|
||||||
@@ -335,6 +336,21 @@ test('createReportJellyfinRemoteStoppedHandler ignores unloaded active playback'
|
|||||||
assert.equal(cleared, false);
|
assert.equal(cleared, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('markJellyfinRemotePlaybackLoaded preserves the loaded marker on unload paths', () => {
|
||||||
|
const playback = {
|
||||||
|
itemId: 'item-2',
|
||||||
|
playMethod: 'Transcode' as const,
|
||||||
|
loadedMediaPath: 'https://stream.example/video.m3u8',
|
||||||
|
};
|
||||||
|
|
||||||
|
markJellyfinRemotePlaybackLoaded(playback, '');
|
||||||
|
markJellyfinRemotePlaybackLoaded(playback, ' ');
|
||||||
|
assert.equal(playback.loadedMediaPath, 'https://stream.example/video.m3u8');
|
||||||
|
|
||||||
|
markJellyfinRemotePlaybackLoaded(playback, ' https://stream.example/next.m3u8 ');
|
||||||
|
assert.equal(playback.loadedMediaPath, 'https://stream.example/next.m3u8');
|
||||||
|
});
|
||||||
|
|
||||||
test('createReportJellyfinRemoteStoppedHandler ignores startup stop churn before grace expires', async () => {
|
test('createReportJellyfinRemoteStoppedHandler ignores startup stop churn before grace expires', async () => {
|
||||||
let cleared = false;
|
let cleared = false;
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ export function secondsToJellyfinTicks(seconds: number, ticksPerSecond: number):
|
|||||||
return Math.max(0, Math.floor(seconds * ticksPerSecond));
|
return Math.max(0, Math.floor(seconds * ticksPerSecond));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function markJellyfinRemotePlaybackLoaded(
|
||||||
|
playback: ActiveJellyfinRemotePlaybackState | null,
|
||||||
|
path: string,
|
||||||
|
): void {
|
||||||
|
const normalizedPath = path.trim();
|
||||||
|
if (playback && normalizedPath) {
|
||||||
|
playback.loadedMediaPath = normalizedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isMpvPauseEnabled(value: unknown): boolean {
|
function isMpvPauseEnabled(value: unknown): boolean {
|
||||||
if (typeof value === 'boolean') return value;
|
if (typeof value === 'boolean') return value;
|
||||||
if (typeof value === 'number') return value !== 0;
|
if (typeof value === 'number') return value !== 0;
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ test('confirmDayGroupDelete uses singular for one session', async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
assert.equal(await confirmDayGroupDelete('Yesterday', 1), true);
|
assert.equal(await confirmDayGroupDelete('Yesterday', 1), true);
|
||||||
assert.deepEqual(calls, ['Delete all 1 session from Yesterday and all associated data?']);
|
assert.deepEqual(calls, ['Delete this session from Yesterday and all associated data?']);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.confirm = originalConfirm;
|
globalThis.confirm = originalConfirm;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,13 @@ export function confirmSessionDelete(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function confirmDayGroupDelete(dayLabel: string, count: number): Promise<boolean> {
|
export function confirmDayGroupDelete(dayLabel: string, count: number): Promise<boolean> {
|
||||||
|
if (count === 1) {
|
||||||
|
return confirmWithStatsNativeDialogLayer(
|
||||||
|
`Delete this session from ${dayLabel} and all associated data?`,
|
||||||
|
);
|
||||||
|
}
|
||||||
return confirmWithStatsNativeDialogLayer(
|
return confirmWithStatsNativeDialogLayer(
|
||||||
`Delete all ${count} session${count === 1 ? '' : 's'} from ${dayLabel} and all associated data?`,
|
`Delete all ${count} sessions from ${dayLabel} and all associated data?`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user