mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-11 16:19:27 -07:00
fix: address CodeRabbit follow-ups
This commit is contained in:
@@ -229,6 +229,22 @@ function M.create(ctx)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function run_binary_command_async(args, callback)
|
||||||
|
subminer_log("debug", "process", "Binary command: " .. table.concat(args, " "))
|
||||||
|
mp.command_native_async({
|
||||||
|
name = "subprocess",
|
||||||
|
args = args,
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
}, function(success, result, error)
|
||||||
|
local ok = success and (result == nil or result.status == 0)
|
||||||
|
if callback then
|
||||||
|
callback(ok, result, error)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local function parse_start_script_message_overrides(...)
|
local function parse_start_script_message_overrides(...)
|
||||||
local overrides = {}
|
local overrides = {}
|
||||||
for i = 1, select("#", ...) do
|
for i = 1, select("#", ...) do
|
||||||
@@ -528,6 +544,7 @@ function M.create(ctx)
|
|||||||
build_command_args = build_command_args,
|
build_command_args = build_command_args,
|
||||||
has_matching_mpv_ipc_socket = has_matching_mpv_ipc_socket,
|
has_matching_mpv_ipc_socket = has_matching_mpv_ipc_socket,
|
||||||
run_control_command_async = run_control_command_async,
|
run_control_command_async = run_control_command_async,
|
||||||
|
run_binary_command_async = run_binary_command_async,
|
||||||
parse_start_script_message_overrides = parse_start_script_message_overrides,
|
parse_start_script_message_overrides = parse_start_script_message_overrides,
|
||||||
ensure_texthooker_running = ensure_texthooker_running,
|
ensure_texthooker_running = ensure_texthooker_running,
|
||||||
start_overlay = start_overlay,
|
start_overlay = start_overlay,
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ function M.create(ctx)
|
|||||||
local function build_cli_args(action_id, payload)
|
local function build_cli_args(action_id, payload)
|
||||||
if action_id == "toggleVisibleOverlay" then
|
if action_id == "toggleVisibleOverlay" then
|
||||||
return { "--toggle-visible-overlay" }
|
return { "--toggle-visible-overlay" }
|
||||||
|
elseif action_id == "toggleStatsOverlay" then
|
||||||
|
return { "--toggle-stats-overlay" }
|
||||||
elseif action_id == "copySubtitle" then
|
elseif action_id == "copySubtitle" then
|
||||||
return { "--copy-subtitle" }
|
return { "--copy-subtitle" }
|
||||||
elseif action_id == "copySubtitleMultiple" then
|
elseif action_id == "copySubtitleMultiple" then
|
||||||
@@ -142,6 +144,13 @@ function M.create(ctx)
|
|||||||
return { "--shift-sub-delay-prev-line" }
|
return { "--shift-sub-delay-prev-line" }
|
||||||
elseif action_id == "shiftSubDelayNextLine" then
|
elseif action_id == "shiftSubDelayNextLine" then
|
||||||
return { "--shift-sub-delay-next-line" }
|
return { "--shift-sub-delay-next-line" }
|
||||||
|
elseif action_id == "cycleRuntimeOption" then
|
||||||
|
local runtime_option_id = payload and payload.runtimeOptionId or nil
|
||||||
|
if type(runtime_option_id) ~= "string" or runtime_option_id == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local direction = payload and payload.direction == -1 and "prev" or "next"
|
||||||
|
return { "--cycle-runtime-option", runtime_option_id .. ":" .. direction }
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -163,7 +172,24 @@ function M.create(ctx)
|
|||||||
for _, arg in ipairs(cli_args) do
|
for _, arg in ipairs(cli_args) do
|
||||||
args[#args + 1] = arg
|
args[#args + 1] = arg
|
||||||
end
|
end
|
||||||
process.run_binary_command_async(args, function(ok, result, error)
|
local runner = process.run_binary_command_async
|
||||||
|
if type(runner) ~= "function" then
|
||||||
|
runner = function(binary_args, callback)
|
||||||
|
mp.command_native_async({
|
||||||
|
name = "subprocess",
|
||||||
|
args = binary_args,
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
|
}, function(success, result, error)
|
||||||
|
local ok = success and (result == nil or result.status == 0)
|
||||||
|
if callback then
|
||||||
|
callback(ok, result, error)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
runner(args, function(ok, result, error)
|
||||||
if ok then
|
if ok then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ test('parseArgs captures youtube startup forwarding flags', () => {
|
|||||||
|
|
||||||
test('parseArgs captures session action forwarding flags', () => {
|
test('parseArgs captures session action forwarding flags', () => {
|
||||||
const args = parseArgs([
|
const args = parseArgs([
|
||||||
|
'--toggle-stats-overlay',
|
||||||
'--open-jimaku',
|
'--open-jimaku',
|
||||||
'--open-youtube-picker',
|
'--open-youtube-picker',
|
||||||
'--open-playlist-browser',
|
'--open-playlist-browser',
|
||||||
@@ -82,11 +83,14 @@ test('parseArgs captures session action forwarding flags', () => {
|
|||||||
'--play-next-subtitle',
|
'--play-next-subtitle',
|
||||||
'--shift-sub-delay-prev-line',
|
'--shift-sub-delay-prev-line',
|
||||||
'--shift-sub-delay-next-line',
|
'--shift-sub-delay-next-line',
|
||||||
|
'--cycle-runtime-option',
|
||||||
|
'anki.autoUpdateNewCards:prev',
|
||||||
'--copy-subtitle-count',
|
'--copy-subtitle-count',
|
||||||
'3',
|
'3',
|
||||||
'--mine-sentence-count=2',
|
'--mine-sentence-count=2',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
assert.equal(args.toggleStatsOverlay, true);
|
||||||
assert.equal(args.openJimaku, true);
|
assert.equal(args.openJimaku, true);
|
||||||
assert.equal(args.openYoutubePicker, true);
|
assert.equal(args.openYoutubePicker, true);
|
||||||
assert.equal(args.openPlaylistBrowser, true);
|
assert.equal(args.openPlaylistBrowser, true);
|
||||||
@@ -94,6 +98,8 @@ test('parseArgs captures session action forwarding flags', () => {
|
|||||||
assert.equal(args.playNextSubtitle, true);
|
assert.equal(args.playNextSubtitle, true);
|
||||||
assert.equal(args.shiftSubDelayPrevLine, true);
|
assert.equal(args.shiftSubDelayPrevLine, true);
|
||||||
assert.equal(args.shiftSubDelayNextLine, true);
|
assert.equal(args.shiftSubDelayNextLine, true);
|
||||||
|
assert.equal(args.cycleRuntimeOptionId, 'anki.autoUpdateNewCards');
|
||||||
|
assert.equal(args.cycleRuntimeOptionDirection, -1);
|
||||||
assert.equal(args.copySubtitleCount, 3);
|
assert.equal(args.copySubtitleCount, 3);
|
||||||
assert.equal(args.mineSentenceCount, 2);
|
assert.equal(args.mineSentenceCount, 2);
|
||||||
assert.equal(hasExplicitCommand(args), true);
|
assert.equal(hasExplicitCommand(args), true);
|
||||||
@@ -199,6 +205,21 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => {
|
|||||||
assert.equal(hasExplicitCommand(anilistRetryQueue), true);
|
assert.equal(hasExplicitCommand(anilistRetryQueue), true);
|
||||||
assert.equal(shouldStartApp(anilistRetryQueue), false);
|
assert.equal(shouldStartApp(anilistRetryQueue), false);
|
||||||
|
|
||||||
|
const toggleStatsOverlay = parseArgs(['--toggle-stats-overlay']);
|
||||||
|
assert.equal(toggleStatsOverlay.toggleStatsOverlay, true);
|
||||||
|
assert.equal(hasExplicitCommand(toggleStatsOverlay), true);
|
||||||
|
assert.equal(shouldStartApp(toggleStatsOverlay), true);
|
||||||
|
|
||||||
|
const cycleRuntimeOption = parseArgs([
|
||||||
|
'--cycle-runtime-option',
|
||||||
|
'anki.autoUpdateNewCards:next',
|
||||||
|
]);
|
||||||
|
assert.equal(cycleRuntimeOption.cycleRuntimeOptionId, 'anki.autoUpdateNewCards');
|
||||||
|
assert.equal(cycleRuntimeOption.cycleRuntimeOptionDirection, 1);
|
||||||
|
assert.equal(hasExplicitCommand(cycleRuntimeOption), true);
|
||||||
|
assert.equal(shouldStartApp(cycleRuntimeOption), true);
|
||||||
|
assert.equal(commandNeedsOverlayRuntime(cycleRuntimeOption), true);
|
||||||
|
|
||||||
const dictionary = parseArgs(['--dictionary']);
|
const dictionary = parseArgs(['--dictionary']);
|
||||||
assert.equal(dictionary.dictionary, true);
|
assert.equal(dictionary.dictionary, true);
|
||||||
assert.equal(hasExplicitCommand(dictionary), true);
|
assert.equal(hasExplicitCommand(dictionary), true);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface CliArgs {
|
|||||||
triggerFieldGrouping: boolean;
|
triggerFieldGrouping: boolean;
|
||||||
triggerSubsync: boolean;
|
triggerSubsync: boolean;
|
||||||
markAudioCard: boolean;
|
markAudioCard: boolean;
|
||||||
|
toggleStatsOverlay: boolean;
|
||||||
openRuntimeOptions: boolean;
|
openRuntimeOptions: boolean;
|
||||||
openJimaku: boolean;
|
openJimaku: boolean;
|
||||||
openYoutubePicker: boolean;
|
openYoutubePicker: boolean;
|
||||||
@@ -32,6 +33,8 @@ export interface CliArgs {
|
|||||||
playNextSubtitle: boolean;
|
playNextSubtitle: boolean;
|
||||||
shiftSubDelayPrevLine: boolean;
|
shiftSubDelayPrevLine: boolean;
|
||||||
shiftSubDelayNextLine: boolean;
|
shiftSubDelayNextLine: boolean;
|
||||||
|
cycleRuntimeOptionId?: string;
|
||||||
|
cycleRuntimeOptionDirection?: 1 | -1;
|
||||||
copySubtitleCount?: number;
|
copySubtitleCount?: number;
|
||||||
mineSentenceCount?: number;
|
mineSentenceCount?: number;
|
||||||
anilistStatus: boolean;
|
anilistStatus: boolean;
|
||||||
@@ -111,6 +114,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
triggerFieldGrouping: false,
|
triggerFieldGrouping: false,
|
||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
|
toggleStatsOverlay: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
openYoutubePicker: false,
|
openYoutubePicker: false,
|
||||||
@@ -154,6 +158,24 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseCycleRuntimeOption = (
|
||||||
|
value: string | undefined,
|
||||||
|
): { id: string; direction: 1 | -1 } | null => {
|
||||||
|
if (!value) return null;
|
||||||
|
const separatorIndex = value.lastIndexOf(':');
|
||||||
|
if (separatorIndex <= 0 || separatorIndex === value.length - 1) return null;
|
||||||
|
const id = value.slice(0, separatorIndex).trim();
|
||||||
|
const rawDirection = value.slice(separatorIndex + 1).trim().toLowerCase();
|
||||||
|
if (!id) return null;
|
||||||
|
if (rawDirection === 'next' || rawDirection === '1') {
|
||||||
|
return { id, direction: 1 };
|
||||||
|
}
|
||||||
|
if (rawDirection === 'prev' || rawDirection === '-1') {
|
||||||
|
return { id, direction: -1 };
|
||||||
|
}
|
||||||
|
return 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 || !arg.startsWith('--')) continue;
|
if (!arg || !arg.startsWith('--')) continue;
|
||||||
@@ -195,6 +217,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
else if (arg === '--trigger-field-grouping') args.triggerFieldGrouping = true;
|
else if (arg === '--trigger-field-grouping') args.triggerFieldGrouping = true;
|
||||||
else if (arg === '--trigger-subsync') args.triggerSubsync = true;
|
else if (arg === '--trigger-subsync') args.triggerSubsync = true;
|
||||||
else if (arg === '--mark-audio-card') args.markAudioCard = true;
|
else if (arg === '--mark-audio-card') args.markAudioCard = true;
|
||||||
|
else if (arg === '--toggle-stats-overlay') args.toggleStatsOverlay = true;
|
||||||
else if (arg === '--open-runtime-options') args.openRuntimeOptions = true;
|
else if (arg === '--open-runtime-options') args.openRuntimeOptions = true;
|
||||||
else if (arg === '--open-jimaku') args.openJimaku = true;
|
else if (arg === '--open-jimaku') args.openJimaku = true;
|
||||||
else if (arg === '--open-youtube-picker') args.openYoutubePicker = true;
|
else if (arg === '--open-youtube-picker') args.openYoutubePicker = true;
|
||||||
@@ -203,6 +226,19 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
else if (arg === '--play-next-subtitle') args.playNextSubtitle = true;
|
else if (arg === '--play-next-subtitle') args.playNextSubtitle = true;
|
||||||
else if (arg === '--shift-sub-delay-prev-line') args.shiftSubDelayPrevLine = true;
|
else if (arg === '--shift-sub-delay-prev-line') args.shiftSubDelayPrevLine = true;
|
||||||
else if (arg === '--shift-sub-delay-next-line') args.shiftSubDelayNextLine = true;
|
else if (arg === '--shift-sub-delay-next-line') args.shiftSubDelayNextLine = true;
|
||||||
|
else if (arg.startsWith('--cycle-runtime-option=')) {
|
||||||
|
const parsed = parseCycleRuntimeOption(arg.split('=', 2)[1]);
|
||||||
|
if (parsed) {
|
||||||
|
args.cycleRuntimeOptionId = parsed.id;
|
||||||
|
args.cycleRuntimeOptionDirection = parsed.direction;
|
||||||
|
}
|
||||||
|
} else if (arg === '--cycle-runtime-option') {
|
||||||
|
const parsed = parseCycleRuntimeOption(readValue(argv[i + 1]));
|
||||||
|
if (parsed) {
|
||||||
|
args.cycleRuntimeOptionId = parsed.id;
|
||||||
|
args.cycleRuntimeOptionDirection = parsed.direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (arg.startsWith('--copy-subtitle-count=')) {
|
else if (arg.startsWith('--copy-subtitle-count=')) {
|
||||||
const value = Number(arg.split('=', 2)[1]);
|
const value = Number(arg.split('=', 2)[1]);
|
||||||
if (Number.isInteger(value)) args.copySubtitleCount = value;
|
if (Number.isInteger(value)) args.copySubtitleCount = value;
|
||||||
@@ -407,6 +443,7 @@ export function hasExplicitCommand(args: CliArgs): boolean {
|
|||||||
args.triggerFieldGrouping ||
|
args.triggerFieldGrouping ||
|
||||||
args.triggerSubsync ||
|
args.triggerSubsync ||
|
||||||
args.markAudioCard ||
|
args.markAudioCard ||
|
||||||
|
args.toggleStatsOverlay ||
|
||||||
args.openRuntimeOptions ||
|
args.openRuntimeOptions ||
|
||||||
args.openJimaku ||
|
args.openJimaku ||
|
||||||
args.openYoutubePicker ||
|
args.openYoutubePicker ||
|
||||||
@@ -415,6 +452,7 @@ export function hasExplicitCommand(args: CliArgs): boolean {
|
|||||||
args.playNextSubtitle ||
|
args.playNextSubtitle ||
|
||||||
args.shiftSubDelayPrevLine ||
|
args.shiftSubDelayPrevLine ||
|
||||||
args.shiftSubDelayNextLine ||
|
args.shiftSubDelayNextLine ||
|
||||||
|
args.cycleRuntimeOptionId !== undefined ||
|
||||||
args.copySubtitleCount !== undefined ||
|
args.copySubtitleCount !== undefined ||
|
||||||
args.mineSentenceCount !== undefined ||
|
args.mineSentenceCount !== undefined ||
|
||||||
args.anilistStatus ||
|
args.anilistStatus ||
|
||||||
@@ -468,6 +506,7 @@ export function isStandaloneTexthookerCommand(args: CliArgs): boolean {
|
|||||||
!args.triggerFieldGrouping &&
|
!args.triggerFieldGrouping &&
|
||||||
!args.triggerSubsync &&
|
!args.triggerSubsync &&
|
||||||
!args.markAudioCard &&
|
!args.markAudioCard &&
|
||||||
|
!args.toggleStatsOverlay &&
|
||||||
!args.openRuntimeOptions &&
|
!args.openRuntimeOptions &&
|
||||||
!args.openJimaku &&
|
!args.openJimaku &&
|
||||||
!args.openYoutubePicker &&
|
!args.openYoutubePicker &&
|
||||||
@@ -476,6 +515,7 @@ export function isStandaloneTexthookerCommand(args: CliArgs): boolean {
|
|||||||
!args.playNextSubtitle &&
|
!args.playNextSubtitle &&
|
||||||
!args.shiftSubDelayPrevLine &&
|
!args.shiftSubDelayPrevLine &&
|
||||||
!args.shiftSubDelayNextLine &&
|
!args.shiftSubDelayNextLine &&
|
||||||
|
args.cycleRuntimeOptionId === undefined &&
|
||||||
args.copySubtitleCount === undefined &&
|
args.copySubtitleCount === undefined &&
|
||||||
args.mineSentenceCount === undefined &&
|
args.mineSentenceCount === undefined &&
|
||||||
!args.anilistStatus &&
|
!args.anilistStatus &&
|
||||||
@@ -520,6 +560,7 @@ export function shouldStartApp(args: CliArgs): boolean {
|
|||||||
args.triggerFieldGrouping ||
|
args.triggerFieldGrouping ||
|
||||||
args.triggerSubsync ||
|
args.triggerSubsync ||
|
||||||
args.markAudioCard ||
|
args.markAudioCard ||
|
||||||
|
args.toggleStatsOverlay ||
|
||||||
args.openRuntimeOptions ||
|
args.openRuntimeOptions ||
|
||||||
args.openJimaku ||
|
args.openJimaku ||
|
||||||
args.openYoutubePicker ||
|
args.openYoutubePicker ||
|
||||||
@@ -528,6 +569,7 @@ export function shouldStartApp(args: CliArgs): boolean {
|
|||||||
args.playNextSubtitle ||
|
args.playNextSubtitle ||
|
||||||
args.shiftSubDelayPrevLine ||
|
args.shiftSubDelayPrevLine ||
|
||||||
args.shiftSubDelayNextLine ||
|
args.shiftSubDelayNextLine ||
|
||||||
|
args.cycleRuntimeOptionId !== undefined ||
|
||||||
args.copySubtitleCount !== undefined ||
|
args.copySubtitleCount !== undefined ||
|
||||||
args.mineSentenceCount !== undefined ||
|
args.mineSentenceCount !== undefined ||
|
||||||
args.dictionary ||
|
args.dictionary ||
|
||||||
@@ -567,6 +609,7 @@ export function shouldRunSettingsOnlyStartup(args: CliArgs): boolean {
|
|||||||
!args.triggerFieldGrouping &&
|
!args.triggerFieldGrouping &&
|
||||||
!args.triggerSubsync &&
|
!args.triggerSubsync &&
|
||||||
!args.markAudioCard &&
|
!args.markAudioCard &&
|
||||||
|
!args.toggleStatsOverlay &&
|
||||||
!args.openRuntimeOptions &&
|
!args.openRuntimeOptions &&
|
||||||
!args.openJimaku &&
|
!args.openJimaku &&
|
||||||
!args.openYoutubePicker &&
|
!args.openYoutubePicker &&
|
||||||
@@ -575,6 +618,7 @@ export function shouldRunSettingsOnlyStartup(args: CliArgs): boolean {
|
|||||||
!args.playNextSubtitle &&
|
!args.playNextSubtitle &&
|
||||||
!args.shiftSubDelayPrevLine &&
|
!args.shiftSubDelayPrevLine &&
|
||||||
!args.shiftSubDelayNextLine &&
|
!args.shiftSubDelayNextLine &&
|
||||||
|
args.cycleRuntimeOptionId === undefined &&
|
||||||
args.copySubtitleCount === undefined &&
|
args.copySubtitleCount === undefined &&
|
||||||
args.mineSentenceCount === undefined &&
|
args.mineSentenceCount === undefined &&
|
||||||
!args.anilistStatus &&
|
!args.anilistStatus &&
|
||||||
@@ -627,6 +671,7 @@ export function commandNeedsOverlayRuntime(args: CliArgs): boolean {
|
|||||||
args.playNextSubtitle ||
|
args.playNextSubtitle ||
|
||||||
args.shiftSubDelayPrevLine ||
|
args.shiftSubDelayPrevLine ||
|
||||||
args.shiftSubDelayNextLine ||
|
args.shiftSubDelayNextLine ||
|
||||||
|
args.cycleRuntimeOptionId !== undefined ||
|
||||||
args.copySubtitleCount !== undefined ||
|
args.copySubtitleCount !== undefined ||
|
||||||
args.mineSentenceCount !== undefined
|
args.mineSentenceCount !== undefined
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
triggerFieldGrouping: false,
|
triggerFieldGrouping: false,
|
||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
|
toggleStatsOverlay: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
openYoutubePicker: false,
|
openYoutubePicker: false,
|
||||||
@@ -36,6 +37,8 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
playNextSubtitle: false,
|
playNextSubtitle: false,
|
||||||
shiftSubDelayPrevLine: false,
|
shiftSubDelayPrevLine: false,
|
||||||
shiftSubDelayNextLine: false,
|
shiftSubDelayNextLine: false,
|
||||||
|
cycleRuntimeOptionId: undefined,
|
||||||
|
cycleRuntimeOptionDirection: undefined,
|
||||||
anilistStatus: false,
|
anilistStatus: false,
|
||||||
anilistLogout: false,
|
anilistLogout: false,
|
||||||
anilistSetup: false,
|
anilistSetup: false,
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ test('runAppReadyRuntime starts websocket in auto mode when plugin missing', asy
|
|||||||
);
|
);
|
||||||
assert.ok(calls.includes('startBackgroundWarmups'));
|
assert.ok(calls.includes('startBackgroundWarmups'));
|
||||||
assert.ok(
|
assert.ok(
|
||||||
calls.includes(
|
calls.includes('log:Runtime ready: immersion tracker startup requested.'),
|
||||||
'log:Runtime ready: immersion tracker startup deferred until first media activity.',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,6 +101,17 @@ test('runAppReadyRuntime starts texthooker on startup when enabled in config', a
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('runAppReadyRuntime creates immersion tracker during heavy startup', async () => {
|
||||||
|
const { deps, calls } = makeDeps({
|
||||||
|
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runAppReadyRuntime(deps);
|
||||||
|
|
||||||
|
assert.ok(calls.includes('createImmersionTracker'));
|
||||||
|
assert.ok(calls.indexOf('createImmersionTracker') < calls.indexOf('handleInitialArgs'));
|
||||||
|
});
|
||||||
|
|
||||||
test('runAppReadyRuntime keeps annotation websocket enabled when regular websocket auto-skips', async () => {
|
test('runAppReadyRuntime keeps annotation websocket enabled when regular websocket auto-skips', async () => {
|
||||||
const { deps, calls } = makeDeps({
|
const { deps, calls } = makeDeps({
|
||||||
getResolvedConfig: () => ({
|
getResolvedConfig: () => ({
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
triggerFieldGrouping: false,
|
triggerFieldGrouping: false,
|
||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
|
toggleStatsOverlay: false,
|
||||||
refreshKnownWords: false,
|
refreshKnownWords: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
@@ -38,6 +39,8 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
playNextSubtitle: false,
|
playNextSubtitle: false,
|
||||||
shiftSubDelayPrevLine: false,
|
shiftSubDelayPrevLine: false,
|
||||||
shiftSubDelayNextLine: false,
|
shiftSubDelayNextLine: false,
|
||||||
|
cycleRuntimeOptionId: undefined,
|
||||||
|
cycleRuntimeOptionDirection: undefined,
|
||||||
anilistStatus: false,
|
anilistStatus: false,
|
||||||
anilistLogout: false,
|
anilistLogout: false,
|
||||||
anilistSetup: false,
|
anilistSetup: false,
|
||||||
@@ -509,6 +512,7 @@ test('handleCliCommand handles visibility and utility command dispatches', () =>
|
|||||||
expected: 'startPendingMineSentenceMultiple:2500',
|
expected: 'startPendingMineSentenceMultiple:2500',
|
||||||
},
|
},
|
||||||
{ args: { toggleSecondarySub: true }, expected: 'cycleSecondarySubMode' },
|
{ args: { toggleSecondarySub: true }, expected: 'cycleSecondarySubMode' },
|
||||||
|
{ args: { toggleStatsOverlay: true }, expected: 'dispatchSessionAction' },
|
||||||
{
|
{
|
||||||
args: { openRuntimeOptions: true },
|
args: { openRuntimeOptions: true },
|
||||||
expected: 'openRuntimeOptionsPalette',
|
expected: 'openRuntimeOptionsPalette',
|
||||||
@@ -528,6 +532,33 @@ test('handleCliCommand handles visibility and utility command dispatches', () =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('handleCliCommand dispatches cycle-runtime-option session action', async () => {
|
||||||
|
let request: unknown = null;
|
||||||
|
const { deps } = createDeps({
|
||||||
|
dispatchSessionAction: async (nextRequest) => {
|
||||||
|
request = nextRequest;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
handleCliCommand(
|
||||||
|
makeArgs({
|
||||||
|
cycleRuntimeOptionId: 'anki.autoUpdateNewCards',
|
||||||
|
cycleRuntimeOptionDirection: -1,
|
||||||
|
}),
|
||||||
|
'initial',
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
assert.deepEqual(request, {
|
||||||
|
actionId: 'cycleRuntimeOption',
|
||||||
|
payload: {
|
||||||
|
runtimeOptionId: 'anki.autoUpdateNewCards',
|
||||||
|
direction: -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('handleCliCommand logs AniList status details', () => {
|
test('handleCliCommand logs AniList status details', () => {
|
||||||
const { deps, calls } = createDeps();
|
const { deps, calls } = createDeps();
|
||||||
handleCliCommand(makeArgs({ anilistStatus: true }), 'initial', deps);
|
handleCliCommand(makeArgs({ anilistStatus: true }), 'initial', deps);
|
||||||
|
|||||||
@@ -396,6 +396,12 @@ export function handleCliCommand(
|
|||||||
'markLastCardAsAudioCard',
|
'markLastCardAsAudioCard',
|
||||||
'Audio card failed',
|
'Audio card failed',
|
||||||
);
|
);
|
||||||
|
} else if (args.toggleStatsOverlay) {
|
||||||
|
dispatchCliSessionAction(
|
||||||
|
{ actionId: 'toggleStatsOverlay' },
|
||||||
|
'toggleStatsOverlay',
|
||||||
|
'Stats toggle failed',
|
||||||
|
);
|
||||||
} else if (args.openRuntimeOptions) {
|
} else if (args.openRuntimeOptions) {
|
||||||
deps.openRuntimeOptionsPalette();
|
deps.openRuntimeOptionsPalette();
|
||||||
} else if (args.openJimaku) {
|
} else if (args.openJimaku) {
|
||||||
@@ -436,6 +442,18 @@ export function handleCliCommand(
|
|||||||
'shiftSubDelayNextLine',
|
'shiftSubDelayNextLine',
|
||||||
'Shift subtitle delay failed',
|
'Shift subtitle delay failed',
|
||||||
);
|
);
|
||||||
|
} else if (args.cycleRuntimeOptionId !== undefined) {
|
||||||
|
dispatchCliSessionAction(
|
||||||
|
{
|
||||||
|
actionId: 'cycleRuntimeOption',
|
||||||
|
payload: {
|
||||||
|
runtimeOptionId: args.cycleRuntimeOptionId,
|
||||||
|
direction: args.cycleRuntimeOptionDirection ?? 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'cycleRuntimeOption',
|
||||||
|
'Runtime option change failed',
|
||||||
|
);
|
||||||
} else if (args.copySubtitleCount !== undefined) {
|
} else if (args.copySubtitleCount !== undefined) {
|
||||||
dispatchCliSessionAction(
|
dispatchCliSessionAction(
|
||||||
{ actionId: 'copySubtitleMultiple', payload: { count: args.copySubtitleCount } },
|
{ actionId: 'copySubtitleMultiple', payload: { count: args.copySubtitleCount } },
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ import {
|
|||||||
const { ipcMain } = electron;
|
const { ipcMain } = electron;
|
||||||
|
|
||||||
export interface IpcServiceDeps {
|
export interface IpcServiceDeps {
|
||||||
onOverlayModalClosed: (modal: OverlayHostedModal) => void;
|
onOverlayModalClosed: (
|
||||||
|
modal: OverlayHostedModal,
|
||||||
|
senderWindow: ElectronBrowserWindow | null,
|
||||||
|
) => void;
|
||||||
onOverlayModalOpened?: (
|
onOverlayModalOpened?: (
|
||||||
modal: OverlayHostedModal,
|
modal: OverlayHostedModal,
|
||||||
senderWindow: ElectronBrowserWindow | null,
|
senderWindow: ElectronBrowserWindow | null,
|
||||||
@@ -160,7 +163,10 @@ interface IpcMainRegistrar {
|
|||||||
export interface IpcDepsRuntimeOptions {
|
export interface IpcDepsRuntimeOptions {
|
||||||
getMainWindow: () => WindowLike | null;
|
getMainWindow: () => WindowLike | null;
|
||||||
getVisibleOverlayVisibility: () => boolean;
|
getVisibleOverlayVisibility: () => boolean;
|
||||||
onOverlayModalClosed: (modal: OverlayHostedModal) => void;
|
onOverlayModalClosed: (
|
||||||
|
modal: OverlayHostedModal,
|
||||||
|
senderWindow: ElectronBrowserWindow | null,
|
||||||
|
) => void;
|
||||||
onOverlayModalOpened?: (
|
onOverlayModalOpened?: (
|
||||||
modal: OverlayHostedModal,
|
modal: OverlayHostedModal,
|
||||||
senderWindow: ElectronBrowserWindow | null,
|
senderWindow: ElectronBrowserWindow | null,
|
||||||
@@ -321,10 +327,12 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ipc.on(IPC_CHANNELS.command.overlayModalClosed, (_event: unknown, modal: unknown) => {
|
ipc.on(IPC_CHANNELS.command.overlayModalClosed, (event: unknown, modal: unknown) => {
|
||||||
const parsedModal = parseOverlayHostedModal(modal);
|
const parsedModal = parseOverlayHostedModal(modal);
|
||||||
if (!parsedModal) return;
|
if (!parsedModal) return;
|
||||||
deps.onOverlayModalClosed(parsedModal);
|
const senderWindow =
|
||||||
|
electron.BrowserWindow?.fromWebContents((event as IpcMainEvent).sender) ?? null;
|
||||||
|
deps.onOverlayModalClosed(parsedModal, senderWindow);
|
||||||
});
|
});
|
||||||
ipc.on(IPC_CHANNELS.command.overlayModalOpened, (event: unknown, modal: unknown) => {
|
ipc.on(IPC_CHANNELS.command.overlayModalOpened, (event: unknown, modal: unknown) => {
|
||||||
const parsedModal = parseOverlayHostedModal(modal);
|
const parsedModal = parseOverlayHostedModal(modal);
|
||||||
|
|||||||
@@ -320,22 +320,7 @@ test('shouldActivateOverlayShortcuts preserves non-macOS behavior', () => {
|
|||||||
|
|
||||||
test('registerOverlayShortcutsRuntime reports active shortcuts when configured', () => {
|
test('registerOverlayShortcutsRuntime reports active shortcuts when configured', () => {
|
||||||
const result = registerOverlayShortcutsRuntime({
|
const result = registerOverlayShortcutsRuntime({
|
||||||
getConfiguredShortcuts: () =>
|
getConfiguredShortcuts: () => makeShortcuts({ openJimaku: 'Ctrl+J' }),
|
||||||
({
|
|
||||||
toggleVisibleOverlayGlobal: null,
|
|
||||||
copySubtitle: null,
|
|
||||||
copySubtitleMultiple: null,
|
|
||||||
updateLastCardFromClipboard: null,
|
|
||||||
triggerFieldGrouping: null,
|
|
||||||
triggerSubsync: null,
|
|
||||||
mineSentence: null,
|
|
||||||
mineSentenceMultiple: null,
|
|
||||||
multiCopyTimeoutMs: 2500,
|
|
||||||
toggleSecondarySub: null,
|
|
||||||
markAudioCard: null,
|
|
||||||
openRuntimeOptions: null,
|
|
||||||
openJimaku: 'Ctrl+J',
|
|
||||||
}) as never,
|
|
||||||
getOverlayHandlers: () => ({
|
getOverlayHandlers: () => ({
|
||||||
copySubtitle: () => {},
|
copySubtitle: () => {},
|
||||||
copySubtitleMultiple: () => {},
|
copySubtitleMultiple: () => {},
|
||||||
@@ -359,22 +344,7 @@ test('registerOverlayShortcutsRuntime reports active shortcuts when configured',
|
|||||||
test('unregisterOverlayShortcutsRuntime clears pending shortcut work when active', () => {
|
test('unregisterOverlayShortcutsRuntime clears pending shortcut work when active', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const result = unregisterOverlayShortcutsRuntime(true, {
|
const result = unregisterOverlayShortcutsRuntime(true, {
|
||||||
getConfiguredShortcuts: () =>
|
getConfiguredShortcuts: () => makeShortcuts(),
|
||||||
({
|
|
||||||
toggleVisibleOverlayGlobal: null,
|
|
||||||
copySubtitle: null,
|
|
||||||
copySubtitleMultiple: null,
|
|
||||||
updateLastCardFromClipboard: null,
|
|
||||||
triggerFieldGrouping: null,
|
|
||||||
triggerSubsync: null,
|
|
||||||
mineSentence: null,
|
|
||||||
mineSentenceMultiple: null,
|
|
||||||
multiCopyTimeoutMs: 2500,
|
|
||||||
toggleSecondarySub: null,
|
|
||||||
markAudioCard: null,
|
|
||||||
openRuntimeOptions: null,
|
|
||||||
openJimaku: null,
|
|
||||||
}) as never,
|
|
||||||
getOverlayHandlers: () => ({
|
getOverlayHandlers: () => ({
|
||||||
copySubtitle: () => {},
|
copySubtitle: () => {},
|
||||||
copySubtitleMultiple: () => {},
|
copySubtitleMultiple: () => {},
|
||||||
|
|||||||
@@ -664,6 +664,80 @@ test('tracked Windows overlay stays interactive while the overlay window itself
|
|||||||
assert.ok(!calls.includes('enforce-order'));
|
assert.ok(!calls.includes('enforce-order'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tracked Windows overlay reshows click-through even if focus state is stale after a modal closes', () => {
|
||||||
|
const { window, calls, setFocused } = createMainWindowRecorder();
|
||||||
|
const tracker: WindowTrackerStub = {
|
||||||
|
isTracking: () => true,
|
||||||
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
||||||
|
isTargetWindowFocused: () => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateVisibleOverlayVisibility({
|
||||||
|
visibleOverlayVisible: true,
|
||||||
|
mainWindow: window as never,
|
||||||
|
windowTracker: tracker as never,
|
||||||
|
trackerNotReadyWarningShown: false,
|
||||||
|
setTrackerNotReadyWarningShown: () => {},
|
||||||
|
updateVisibleOverlayBounds: () => {
|
||||||
|
calls.push('update-bounds');
|
||||||
|
},
|
||||||
|
ensureOverlayWindowLevel: () => {
|
||||||
|
calls.push('ensure-level');
|
||||||
|
},
|
||||||
|
syncWindowsOverlayToMpvZOrder: () => {
|
||||||
|
calls.push('sync-windows-z-order');
|
||||||
|
},
|
||||||
|
syncPrimaryOverlayWindowLayer: () => {
|
||||||
|
calls.push('sync-layer');
|
||||||
|
},
|
||||||
|
enforceOverlayLayerOrder: () => {
|
||||||
|
calls.push('enforce-order');
|
||||||
|
},
|
||||||
|
syncOverlayShortcuts: () => {
|
||||||
|
calls.push('sync-shortcuts');
|
||||||
|
},
|
||||||
|
isMacOSPlatform: false,
|
||||||
|
isWindowsPlatform: true,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
calls.length = 0;
|
||||||
|
window.hide();
|
||||||
|
calls.length = 0;
|
||||||
|
setFocused(true);
|
||||||
|
|
||||||
|
updateVisibleOverlayVisibility({
|
||||||
|
visibleOverlayVisible: true,
|
||||||
|
mainWindow: window as never,
|
||||||
|
windowTracker: tracker as never,
|
||||||
|
trackerNotReadyWarningShown: false,
|
||||||
|
setTrackerNotReadyWarningShown: () => {},
|
||||||
|
updateVisibleOverlayBounds: () => {
|
||||||
|
calls.push('update-bounds');
|
||||||
|
},
|
||||||
|
ensureOverlayWindowLevel: () => {
|
||||||
|
calls.push('ensure-level');
|
||||||
|
},
|
||||||
|
syncWindowsOverlayToMpvZOrder: () => {
|
||||||
|
calls.push('sync-windows-z-order');
|
||||||
|
},
|
||||||
|
syncPrimaryOverlayWindowLayer: () => {
|
||||||
|
calls.push('sync-layer');
|
||||||
|
},
|
||||||
|
enforceOverlayLayerOrder: () => {
|
||||||
|
calls.push('enforce-order');
|
||||||
|
},
|
||||||
|
syncOverlayShortcuts: () => {
|
||||||
|
calls.push('sync-shortcuts');
|
||||||
|
},
|
||||||
|
isMacOSPlatform: false,
|
||||||
|
isWindowsPlatform: true,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
||||||
|
assert.ok(calls.includes('show-inactive'));
|
||||||
|
assert.ok(!calls.includes('show'));
|
||||||
|
});
|
||||||
|
|
||||||
test('tracked Windows overlay binds above mpv even when tracker focus lags', () => {
|
test('tracked Windows overlay binds above mpv even when tracker focus lags', () => {
|
||||||
const { window, calls } = createMainWindowRecorder();
|
const { window, calls } = createMainWindowRecorder();
|
||||||
const tracker: WindowTrackerStub = {
|
const tracker: WindowTrackerStub = {
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export function updateVisibleOverlayVisibility(args: {
|
|||||||
|
|
||||||
const showPassiveVisibleOverlay = (): void => {
|
const showPassiveVisibleOverlay = (): void => {
|
||||||
const forceMousePassthrough = args.forceMousePassthrough === true;
|
const forceMousePassthrough = args.forceMousePassthrough === true;
|
||||||
|
const wasVisible = mainWindow.isVisible();
|
||||||
const shouldDefaultToPassthrough =
|
const shouldDefaultToPassthrough =
|
||||||
args.isMacOSPlatform || args.isWindowsPlatform || forceMousePassthrough;
|
args.isMacOSPlatform || args.isWindowsPlatform || forceMousePassthrough;
|
||||||
const isVisibleOverlayFocused =
|
const isVisibleOverlayFocused =
|
||||||
@@ -116,8 +117,10 @@ export function updateVisibleOverlayVisibility(args: {
|
|||||||
windowsForegroundProcessName === windowsOverlayProcessName)) &&
|
windowsForegroundProcessName === windowsOverlayProcessName)) &&
|
||||||
!isTrackedWindowsTargetMinimized &&
|
!isTrackedWindowsTargetMinimized &&
|
||||||
(args.windowTracker.isTracking() || args.windowTracker.getGeometry() !== null);
|
(args.windowTracker.isTracking() || args.windowTracker.getGeometry() !== null);
|
||||||
|
const shouldForcePassiveReshow = args.isWindowsPlatform && !wasVisible;
|
||||||
const shouldIgnoreMouseEvents =
|
const shouldIgnoreMouseEvents =
|
||||||
forceMousePassthrough || (shouldDefaultToPassthrough && !isVisibleOverlayFocused);
|
forceMousePassthrough ||
|
||||||
|
(shouldDefaultToPassthrough && (!isVisibleOverlayFocused || shouldForcePassiveReshow));
|
||||||
const shouldBindTrackedWindowsOverlay = args.isWindowsPlatform && !!args.windowTracker;
|
const shouldBindTrackedWindowsOverlay = args.isWindowsPlatform && !!args.windowTracker;
|
||||||
const shouldKeepTrackedWindowsOverlayTopmost =
|
const shouldKeepTrackedWindowsOverlayTopmost =
|
||||||
!args.isWindowsPlatform ||
|
!args.isWindowsPlatform ||
|
||||||
@@ -126,8 +129,6 @@ export function updateVisibleOverlayVisibility(args: {
|
|||||||
isTrackedWindowsTargetFocused ||
|
isTrackedWindowsTargetFocused ||
|
||||||
shouldPreserveWindowsOverlayDuringFocusHandoff ||
|
shouldPreserveWindowsOverlayDuringFocusHandoff ||
|
||||||
(hasWindowsForegroundProcessSignal && windowsForegroundProcessName === 'mpv');
|
(hasWindowsForegroundProcessSignal && windowsForegroundProcessName === 'mpv');
|
||||||
const wasVisible = mainWindow.isVisible();
|
|
||||||
|
|
||||||
if (shouldIgnoreMouseEvents) {
|
if (shouldIgnoreMouseEvents) {
|
||||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ export function createOverlayWindow(
|
|||||||
},
|
},
|
||||||
): BrowserWindow {
|
): BrowserWindow {
|
||||||
const window = new BrowserWindow(buildOverlayWindowOptions(kind, options));
|
const window = new BrowserWindow(buildOverlayWindowOptions(kind, options));
|
||||||
|
(window as BrowserWindow & { [OVERLAY_WINDOW_CONTENT_READY_FLAG]?: boolean })[
|
||||||
|
OVERLAY_WINDOW_CONTENT_READY_FLAG
|
||||||
|
] = false;
|
||||||
|
|
||||||
if (!(process.platform === 'win32' && kind === 'visible')) {
|
if (!(process.platform === 'win32' && kind === 'visible')) {
|
||||||
options.ensureOverlayWindowLevel(window);
|
options.ensureOverlayWindowLevel(window);
|
||||||
|
|||||||
@@ -97,7 +97,26 @@ function normalizeCodeToken(token: string): string | null {
|
|||||||
/^arrow(?:up|down|left|right)$/i.test(normalized) ||
|
/^arrow(?:up|down|left|right)$/i.test(normalized) ||
|
||||||
/^f\d{1,2}$/i.test(normalized)
|
/^f\d{1,2}$/i.test(normalized)
|
||||||
) {
|
) {
|
||||||
return normalized[0]!.toUpperCase() + normalized.slice(1);
|
const keyMatch = normalized.match(/^key([a-z])$/i);
|
||||||
|
if (keyMatch) {
|
||||||
|
return `Key${keyMatch[1]!.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const digitMatch = normalized.match(/^digit([0-9])$/i);
|
||||||
|
if (digitMatch) {
|
||||||
|
return `Digit${digitMatch[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrowMatch = normalized.match(/^arrow(up|down|left|right)$/i);
|
||||||
|
if (arrowMatch) {
|
||||||
|
const direction = arrowMatch[1]!;
|
||||||
|
return `Arrow${direction[0]!.toUpperCase()}${direction.slice(1).toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const functionKeyMatch = normalized.match(/^f(\d{1,2})$/i);
|
||||||
|
if (functionKeyMatch) {
|
||||||
|
return `F${functionKeyMatch[1]}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
triggerFieldGrouping: false,
|
triggerFieldGrouping: false,
|
||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
|
toggleStatsOverlay: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
openYoutubePicker: false,
|
openYoutubePicker: false,
|
||||||
@@ -36,6 +37,8 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
playNextSubtitle: false,
|
playNextSubtitle: false,
|
||||||
shiftSubDelayPrevLine: false,
|
shiftSubDelayPrevLine: false,
|
||||||
shiftSubDelayNextLine: false,
|
shiftSubDelayNextLine: false,
|
||||||
|
cycleRuntimeOptionId: undefined,
|
||||||
|
cycleRuntimeOptionDirection: undefined,
|
||||||
anilistStatus: false,
|
anilistStatus: false,
|
||||||
anilistLogout: false,
|
anilistLogout: false,
|
||||||
anilistSetup: false,
|
anilistSetup: false,
|
||||||
|
|||||||
@@ -311,7 +311,8 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
|
|||||||
|
|
||||||
deps.createSubtitleTimingTracker();
|
deps.createSubtitleTimingTracker();
|
||||||
if (deps.createImmersionTracker) {
|
if (deps.createImmersionTracker) {
|
||||||
deps.log('Runtime ready: immersion tracker startup deferred until first media activity.');
|
deps.createImmersionTracker();
|
||||||
|
deps.log('Runtime ready: immersion tracker startup requested.');
|
||||||
} else {
|
} else {
|
||||||
deps.log('Runtime ready: immersion tracker dependency is missing.');
|
deps.log('Runtime ready: immersion tracker dependency is missing.');
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/main.ts
58
src/main.ts
@@ -454,6 +454,7 @@ import { createOverlayModalInputState } from './main/runtime/overlay-modal-input
|
|||||||
import { openYoutubeTrackPicker } from './main/runtime/youtube-picker-open';
|
import { openYoutubeTrackPicker } from './main/runtime/youtube-picker-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 { writeSessionBindingsArtifact } from './main/runtime/session-bindings-artifact';
|
||||||
|
import { openOverlayHostedModal } from './main/runtime/overlay-hosted-modal-open';
|
||||||
import { createOverlayShortcutsRuntimeService } from './main/overlay-shortcuts-runtime';
|
import { createOverlayShortcutsRuntimeService } from './main/overlay-shortcuts-runtime';
|
||||||
import {
|
import {
|
||||||
createFrequencyDictionaryRuntimeService,
|
createFrequencyDictionaryRuntimeService,
|
||||||
@@ -1484,9 +1485,7 @@ const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService(
|
|||||||
openRuntimeOptionsPalette();
|
openRuntimeOptionsPalette();
|
||||||
},
|
},
|
||||||
openJimaku: () => {
|
openJimaku: () => {
|
||||||
sendToActiveOverlayWindow('jimaku:open', undefined, {
|
openJimakuOverlay();
|
||||||
restoreOnModalClose: 'jimaku',
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
markAudioCard: () => markLastCardAsAudioCard(),
|
markAudioCard: () => markLastCardAsAudioCard(),
|
||||||
copySubtitleMultiple: (timeoutMs: number) => {
|
copySubtitleMultiple: (timeoutMs: number) => {
|
||||||
@@ -2206,7 +2205,42 @@ function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openRuntimeOptionsPalette(): void {
|
function openRuntimeOptionsPalette(): void {
|
||||||
overlayVisibilityComposer.openRuntimeOptionsPalette();
|
const opened = openOverlayHostedModal(
|
||||||
|
{
|
||||||
|
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () =>
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions(),
|
||||||
|
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
|
||||||
|
sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: IPC_CHANNELS.event.runtimeOptionsOpen,
|
||||||
|
modal: 'runtime-options',
|
||||||
|
preferModalWindow: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!opened) {
|
||||||
|
showMpvOsd('Runtime options overlay unavailable.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openJimakuOverlay(): void {
|
||||||
|
const opened = openOverlayHostedModal(
|
||||||
|
{
|
||||||
|
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () =>
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions(),
|
||||||
|
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
|
||||||
|
sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: IPC_CHANNELS.event.jimakuOpen,
|
||||||
|
modal: 'jimaku',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!opened) {
|
||||||
|
showMpvOsd('Jimaku overlay unavailable.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPlaylistBrowser(): void {
|
function openPlaylistBrowser(): void {
|
||||||
@@ -4522,7 +4556,7 @@ async function dispatchSessionAction(request: SessionActionDispatchRequest): Pro
|
|||||||
toggleSecondarySub: () => handleCycleSecondarySubMode(),
|
toggleSecondarySub: () => handleCycleSecondarySubMode(),
|
||||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
openJimaku: () => overlayModalRuntime.openJimaku(),
|
openJimaku: () => openJimakuOverlay(),
|
||||||
openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
|
openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
|
||||||
openPlaylistBrowser: () => openPlaylistBrowser(),
|
openPlaylistBrowser: () => openPlaylistBrowser(),
|
||||||
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
||||||
@@ -4551,7 +4585,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
mpvCommandMainDeps: {
|
mpvCommandMainDeps: {
|
||||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
openJimaku: () => overlayModalRuntime.openJimaku(),
|
openJimaku: () => openJimakuOverlay(),
|
||||||
openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
|
openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
|
||||||
openPlaylistBrowser: () => openPlaylistBrowser(),
|
openPlaylistBrowser: () => openPlaylistBrowser(),
|
||||||
cycleRuntimeOption: (id, direction) => {
|
cycleRuntimeOption: (id, direction) => {
|
||||||
@@ -4591,7 +4625,17 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOverlayModalClosed: (modal) => {
|
onOverlayModalClosed: (modal, senderWindow) => {
|
||||||
|
const modalWindow = overlayManager.getModalWindow();
|
||||||
|
if (
|
||||||
|
senderWindow &&
|
||||||
|
modalWindow &&
|
||||||
|
senderWindow === modalWindow &&
|
||||||
|
!senderWindow.isDestroyed()
|
||||||
|
) {
|
||||||
|
senderWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||||
|
senderWindow.hide();
|
||||||
|
}
|
||||||
handleOverlayModalClosed(modal);
|
handleOverlayModalClosed(modal);
|
||||||
},
|
},
|
||||||
onOverlayModalOpened: (modal) => {
|
onOverlayModalOpened: (modal) => {
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ type MockWindow = {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
ignoreMouseEvents: boolean;
|
ignoreMouseEvents: boolean;
|
||||||
|
forwardedIgnoreMouseEvents: boolean;
|
||||||
webContentsFocused: boolean;
|
webContentsFocused: boolean;
|
||||||
showCount: number;
|
showCount: number;
|
||||||
hideCount: number;
|
hideCount: number;
|
||||||
sent: unknown[][];
|
sent: unknown[][];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
|
contentReady: boolean;
|
||||||
loadCallbacks: Array<() => void>;
|
loadCallbacks: Array<() => void>;
|
||||||
|
readyToShowCallbacks: Array<() => void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createMockWindow(): MockWindow & {
|
function createMockWindow(): MockWindow & {
|
||||||
@@ -29,6 +32,7 @@ function createMockWindow(): MockWindow & {
|
|||||||
show: () => void;
|
show: () => void;
|
||||||
hide: () => void;
|
hide: () => void;
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
|
once: (event: 'ready-to-show', cb: () => void) => void;
|
||||||
webContents: {
|
webContents: {
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
isLoading: () => boolean;
|
isLoading: () => boolean;
|
||||||
@@ -44,13 +48,16 @@ function createMockWindow(): MockWindow & {
|
|||||||
visible: false,
|
visible: false,
|
||||||
focused: false,
|
focused: false,
|
||||||
ignoreMouseEvents: false,
|
ignoreMouseEvents: false,
|
||||||
|
forwardedIgnoreMouseEvents: false,
|
||||||
webContentsFocused: false,
|
webContentsFocused: false,
|
||||||
showCount: 0,
|
showCount: 0,
|
||||||
hideCount: 0,
|
hideCount: 0,
|
||||||
sent: [],
|
sent: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
url: 'file:///overlay/index.html?layer=modal',
|
url: 'file:///overlay/index.html?layer=modal',
|
||||||
|
contentReady: true,
|
||||||
loadCallbacks: [],
|
loadCallbacks: [],
|
||||||
|
readyToShowCallbacks: [],
|
||||||
};
|
};
|
||||||
const window = {
|
const window = {
|
||||||
...state,
|
...state,
|
||||||
@@ -58,8 +65,9 @@ function createMockWindow(): MockWindow & {
|
|||||||
isVisible: () => state.visible,
|
isVisible: () => state.visible,
|
||||||
isFocused: () => state.focused,
|
isFocused: () => state.focused,
|
||||||
getURL: () => state.url,
|
getURL: () => state.url,
|
||||||
setIgnoreMouseEvents: (ignore: boolean, _options?: { forward?: boolean }) => {
|
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
||||||
state.ignoreMouseEvents = ignore;
|
state.ignoreMouseEvents = ignore;
|
||||||
|
state.forwardedIgnoreMouseEvents = options?.forward === true;
|
||||||
},
|
},
|
||||||
setAlwaysOnTop: (_flag: boolean, _level?: string, _relativeLevel?: number) => {},
|
setAlwaysOnTop: (_flag: boolean, _level?: string, _relativeLevel?: number) => {},
|
||||||
moveTop: () => {},
|
moveTop: () => {},
|
||||||
@@ -76,6 +84,9 @@ function createMockWindow(): MockWindow & {
|
|||||||
focus: () => {
|
focus: () => {
|
||||||
state.focused = true;
|
state.focused = true;
|
||||||
},
|
},
|
||||||
|
once: (_event: 'ready-to-show', cb: () => void) => {
|
||||||
|
state.readyToShowCallbacks.push(cb);
|
||||||
|
},
|
||||||
webContents: {
|
webContents: {
|
||||||
isLoading: () => state.loading,
|
isLoading: () => state.loading,
|
||||||
getURL: () => state.url,
|
getURL: () => state.url,
|
||||||
@@ -139,6 +150,25 @@ function createMockWindow(): MockWindow & {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'forwardedIgnoreMouseEvents', {
|
||||||
|
get: () => state.forwardedIgnoreMouseEvents,
|
||||||
|
set: (value: boolean) => {
|
||||||
|
state.forwardedIgnoreMouseEvents = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'contentReady', {
|
||||||
|
get: () => state.contentReady,
|
||||||
|
set: (value: boolean) => {
|
||||||
|
state.contentReady = value;
|
||||||
|
(window as typeof window & { __subminerOverlayContentReady?: boolean }).__subminerOverlayContentReady =
|
||||||
|
value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(window as typeof window & { __subminerOverlayContentReady?: boolean }).__subminerOverlayContentReady =
|
||||||
|
state.contentReady;
|
||||||
|
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +229,7 @@ test('sendToActiveOverlayWindow waits for blank modal URL before sending open co
|
|||||||
const window = createMockWindow();
|
const window = createMockWindow();
|
||||||
window.url = '';
|
window.url = '';
|
||||||
window.loading = true;
|
window.loading = true;
|
||||||
|
window.contentReady = false;
|
||||||
const runtime = createOverlayModalRuntimeService({
|
const runtime = createOverlayModalRuntimeService({
|
||||||
getMainWindow: () => null,
|
getMainWindow: () => null,
|
||||||
getModalWindow: () => window as never,
|
getModalWindow: () => window as never,
|
||||||
@@ -217,9 +248,14 @@ test('sendToActiveOverlayWindow waits for blank modal URL before sending open co
|
|||||||
assert.deepEqual(window.sent, []);
|
assert.deepEqual(window.sent, []);
|
||||||
|
|
||||||
assert.equal(window.loadCallbacks.length, 1);
|
assert.equal(window.loadCallbacks.length, 1);
|
||||||
|
assert.equal(window.readyToShowCallbacks.length, 1);
|
||||||
window.loading = false;
|
window.loading = false;
|
||||||
window.url = 'file:///overlay/index.html?layer=modal';
|
window.url = 'file:///overlay/index.html?layer=modal';
|
||||||
window.loadCallbacks[0]!();
|
window.loadCallbacks[0]!();
|
||||||
|
assert.deepEqual(window.sent, []);
|
||||||
|
|
||||||
|
window.contentReady = true;
|
||||||
|
window.readyToShowCallbacks[0]!();
|
||||||
|
|
||||||
runtime.notifyOverlayModalOpened('runtime-options');
|
runtime.notifyOverlayModalOpened('runtime-options');
|
||||||
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||||
@@ -325,11 +361,12 @@ test('modal window path makes visible main overlay click-through until modal clo
|
|||||||
|
|
||||||
assert.equal(sent, true);
|
assert.equal(sent, true);
|
||||||
assert.equal(mainWindow.ignoreMouseEvents, true);
|
assert.equal(mainWindow.ignoreMouseEvents, true);
|
||||||
|
assert.equal(mainWindow.forwardedIgnoreMouseEvents, true);
|
||||||
assert.equal(modalWindow.ignoreMouseEvents, false);
|
assert.equal(modalWindow.ignoreMouseEvents, false);
|
||||||
|
|
||||||
runtime.handleOverlayModalClosed('youtube-track-picker');
|
runtime.handleOverlayModalClosed('youtube-track-picker');
|
||||||
|
|
||||||
assert.equal(mainWindow.ignoreMouseEvents, false);
|
assert.equal(mainWindow.ignoreMouseEvents, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('modal window path hides visible main overlay until modal closes', () => {
|
test('modal window path hides visible main overlay until modal closes', () => {
|
||||||
@@ -359,8 +396,8 @@ test('modal window path hides visible main overlay until modal closes', () => {
|
|||||||
|
|
||||||
runtime.handleOverlayModalClosed('youtube-track-picker');
|
runtime.handleOverlayModalClosed('youtube-track-picker');
|
||||||
|
|
||||||
assert.equal(mainWindow.getShowCount(), 1);
|
assert.equal(mainWindow.getShowCount(), 0);
|
||||||
assert.equal(mainWindow.isVisible(), true);
|
assert.equal(mainWindow.isVisible(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('modal runtime notifies callers when modal input state becomes active/inactive', () => {
|
test('modal runtime notifies callers when modal input state becomes active/inactive', () => {
|
||||||
@@ -500,6 +537,7 @@ test('modal fallback reveal keeps mouse events ignored until modal confirms open
|
|||||||
|
|
||||||
window.loading = true;
|
window.loading = true;
|
||||||
window.url = '';
|
window.url = '';
|
||||||
|
window.contentReady = false;
|
||||||
|
|
||||||
const sent = runtime.sendToActiveOverlayWindow('jimaku:open', undefined, {
|
const sent = runtime.sendToActiveOverlayWindow('jimaku:open', undefined, {
|
||||||
restoreOnModalClose: 'jimaku',
|
restoreOnModalClose: 'jimaku',
|
||||||
@@ -519,6 +557,36 @@ test('modal fallback reveal keeps mouse events ignored until modal confirms open
|
|||||||
assert.equal(window.ignoreMouseEvents, false);
|
assert.equal(window.ignoreMouseEvents, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sendToActiveOverlayWindow waits for modal ready-to-show before delivering open event', () => {
|
||||||
|
const window = createMockWindow();
|
||||||
|
window.contentReady = false;
|
||||||
|
const runtime = createOverlayModalRuntimeService({
|
||||||
|
getMainWindow: () => null,
|
||||||
|
getModalWindow: () => window as never,
|
||||||
|
createModalWindow: () => {
|
||||||
|
throw new Error('modal window should not be created when already present');
|
||||||
|
},
|
||||||
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
|
setModalWindowBounds: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(sent, true);
|
||||||
|
assert.deepEqual(window.sent, []);
|
||||||
|
assert.equal(window.loadCallbacks.length, 1);
|
||||||
|
assert.equal(window.readyToShowCallbacks.length, 1);
|
||||||
|
|
||||||
|
window.loadCallbacks[0]!();
|
||||||
|
assert.deepEqual(window.sent, []);
|
||||||
|
|
||||||
|
window.contentReady = true;
|
||||||
|
window.readyToShowCallbacks[0]!();
|
||||||
|
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||||
|
});
|
||||||
|
|
||||||
test('waitForModalOpen resolves true after modal acknowledgement', async () => {
|
test('waitForModalOpen resolves true after modal acknowledgement', async () => {
|
||||||
const runtime = createOverlayModalRuntimeService({
|
const runtime = createOverlayModalRuntimeService({
|
||||||
getMainWindow: () => null,
|
getMainWindow: () => null,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { OverlayHostedModal } from '../shared/ipc/contracts';
|
|||||||
import type { WindowGeometry } from '../types';
|
import type { WindowGeometry } from '../types';
|
||||||
|
|
||||||
const MODAL_REVEAL_FALLBACK_DELAY_MS = 250;
|
const MODAL_REVEAL_FALLBACK_DELAY_MS = 250;
|
||||||
|
const OVERLAY_WINDOW_CONTENT_READY_FLAG = '__subminerOverlayContentReady';
|
||||||
|
|
||||||
export interface OverlayWindowResolver {
|
export interface OverlayWindowResolver {
|
||||||
getMainWindow: () => BrowserWindow | null;
|
getMainWindow: () => BrowserWindow | null;
|
||||||
@@ -90,6 +91,15 @@ export function createOverlayModalRuntimeService(
|
|||||||
if (window.webContents.isLoading()) {
|
if (window.webContents.isLoading()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const overlayWindow = window as BrowserWindow & {
|
||||||
|
[OVERLAY_WINDOW_CONTENT_READY_FLAG]?: boolean;
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
typeof overlayWindow[OVERLAY_WINDOW_CONTENT_READY_FLAG] === 'boolean' &&
|
||||||
|
overlayWindow[OVERLAY_WINDOW_CONTENT_READY_FLAG] !== true
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const currentURL = window.webContents.getURL();
|
const currentURL = window.webContents.getURL();
|
||||||
return currentURL !== '' && currentURL !== 'about:blank';
|
return currentURL !== '' && currentURL !== 'about:blank';
|
||||||
};
|
};
|
||||||
@@ -109,11 +119,17 @@ export function createOverlayModalRuntimeService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.webContents.once('did-finish-load', () => {
|
let delivered = false;
|
||||||
if (!window.isDestroyed() && !window.webContents.isLoading()) {
|
const deliverWhenReady = (): void => {
|
||||||
sendNow(window);
|
if (delivered || window.isDestroyed() || !isWindowReadyForIpc(window)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
delivered = true;
|
||||||
|
sendNow(window);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.webContents.once('did-finish-load', deliverWhenReady);
|
||||||
|
window.once('ready-to-show', deliverWhenReady);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showModalWindow = (
|
const showModalWindow = (
|
||||||
@@ -320,12 +336,12 @@ export function createOverlayModalRuntimeService(
|
|||||||
const modalWindow = deps.getModalWindow();
|
const modalWindow = deps.getModalWindow();
|
||||||
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||||
clearPendingModalWindowReveal();
|
clearPendingModalWindowReveal();
|
||||||
notifyModalStateChange(false);
|
|
||||||
setMainWindowMousePassthroughForModal(false);
|
|
||||||
setMainWindowVisibilityForModal(false);
|
|
||||||
if (modalWindow && !modalWindow.isDestroyed()) {
|
if (modalWindow && !modalWindow.isDestroyed()) {
|
||||||
modalWindow.hide();
|
modalWindow.hide();
|
||||||
}
|
}
|
||||||
|
mainWindowMousePassthroughForcedByModal = false;
|
||||||
|
mainWindowHiddenByModal = false;
|
||||||
|
notifyModalStateChange(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
triggerFieldGrouping: false,
|
triggerFieldGrouping: false,
|
||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
|
toggleStatsOverlay: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
openYoutubePicker: false,
|
openYoutubePicker: false,
|
||||||
@@ -50,6 +51,8 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
playNextSubtitle: false,
|
playNextSubtitle: false,
|
||||||
shiftSubDelayPrevLine: false,
|
shiftSubDelayPrevLine: false,
|
||||||
shiftSubDelayNextLine: false,
|
shiftSubDelayNextLine: false,
|
||||||
|
cycleRuntimeOptionId: undefined,
|
||||||
|
cycleRuntimeOptionDirection: undefined,
|
||||||
anilistStatus: false,
|
anilistStatus: false,
|
||||||
anilistLogout: false,
|
anilistLogout: false,
|
||||||
anilistSetup: false,
|
anilistSetup: false,
|
||||||
|
|||||||
@@ -76,7 +76,16 @@ function hasAnyStartupCommandBeyondSetup(args: CliArgs): boolean {
|
|||||||
args.triggerFieldGrouping ||
|
args.triggerFieldGrouping ||
|
||||||
args.triggerSubsync ||
|
args.triggerSubsync ||
|
||||||
args.markAudioCard ||
|
args.markAudioCard ||
|
||||||
|
args.toggleStatsOverlay ||
|
||||||
args.openRuntimeOptions ||
|
args.openRuntimeOptions ||
|
||||||
|
args.openJimaku ||
|
||||||
|
args.openYoutubePicker ||
|
||||||
|
args.openPlaylistBrowser ||
|
||||||
|
args.replayCurrentSubtitle ||
|
||||||
|
args.playNextSubtitle ||
|
||||||
|
args.shiftSubDelayPrevLine ||
|
||||||
|
args.shiftSubDelayNextLine ||
|
||||||
|
args.cycleRuntimeOptionId !== undefined ||
|
||||||
args.anilistStatus ||
|
args.anilistStatus ||
|
||||||
args.anilistLogout ||
|
args.anilistLogout ||
|
||||||
args.anilistSetup ||
|
args.anilistSetup ||
|
||||||
|
|||||||
@@ -161,6 +161,41 @@ test('createImmersionTrackerStartupHandler creates tracker and auto-connects mpv
|
|||||||
assert.ok(calls.includes('info:Auto-connecting MPV client for immersion tracking'));
|
assert.ok(calls.includes('info:Auto-connecting MPV client for immersion tracking'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createImmersionTrackerStartupHandler keeps tracker startup alive when mpv auto-connect throws', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const trackerInstance = { kind: 'tracker' };
|
||||||
|
let assignedTracker: unknown = null;
|
||||||
|
const handler = createImmersionTrackerStartupHandler({
|
||||||
|
getResolvedConfig: () => makeConfig(),
|
||||||
|
getConfiguredDbPath: () => '/tmp/subminer.db',
|
||||||
|
createTrackerService: () => trackerInstance,
|
||||||
|
setTracker: (nextTracker) => {
|
||||||
|
assignedTracker = nextTracker;
|
||||||
|
},
|
||||||
|
getMpvClient: () => ({
|
||||||
|
connected: false,
|
||||||
|
connect: () => {
|
||||||
|
throw new Error('socket not ready');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
seedTrackerFromCurrentMedia: () => calls.push('seedTracker'),
|
||||||
|
logInfo: (message) => calls.push(`info:${message}`),
|
||||||
|
logDebug: (message) => calls.push(`debug:${message}`),
|
||||||
|
logWarn: (message, details) => calls.push(`warn:${message}:${(details as Error).message}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
handler();
|
||||||
|
|
||||||
|
assert.equal(assignedTracker, trackerInstance);
|
||||||
|
assert.ok(calls.includes('seedTracker'));
|
||||||
|
assert.ok(
|
||||||
|
calls.includes(
|
||||||
|
'warn:MPV auto-connect failed during immersion tracker startup; continuing.:socket not ready',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(calls.includes('warn:Immersion tracker startup failed; disabling tracking.'), false);
|
||||||
|
});
|
||||||
|
|
||||||
test('createImmersionTrackerStartupHandler disables tracker on failure', () => {
|
test('createImmersionTrackerStartupHandler disables tracker on failure', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
let assignedTracker: unknown = 'initial';
|
let assignedTracker: unknown = 'initial';
|
||||||
|
|||||||
@@ -102,7 +102,11 @@ export function createImmersionTrackerStartupHandler(
|
|||||||
const mpvClient = deps.getMpvClient();
|
const mpvClient = deps.getMpvClient();
|
||||||
if ((deps.shouldAutoConnectMpv?.() ?? true) && mpvClient && !mpvClient.connected) {
|
if ((deps.shouldAutoConnectMpv?.() ?? true) && mpvClient && !mpvClient.connected) {
|
||||||
deps.logInfo('Auto-connecting MPV client for immersion tracking');
|
deps.logInfo('Auto-connecting MPV client for immersion tracking');
|
||||||
mpvClient.connect();
|
try {
|
||||||
|
mpvClient.connect();
|
||||||
|
} catch (error) {
|
||||||
|
deps.logWarn('MPV auto-connect failed during immersion tracker startup; continuing.', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
deps.seedTrackerFromCurrentMedia();
|
deps.seedTrackerFromCurrentMedia();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
66
src/main/runtime/overlay-hosted-modal-open.test.ts
Normal file
66
src/main/runtime/overlay-hosted-modal-open.test.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { openOverlayHostedModal } from './overlay-hosted-modal-open';
|
||||||
|
|
||||||
|
test('openOverlayHostedModal ensures overlay readiness before sending the open event', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
const opened = openOverlayHostedModal(
|
||||||
|
{
|
||||||
|
ensureOverlayStartupPrereqs: () => {
|
||||||
|
calls.push('ensureOverlayStartupPrereqs');
|
||||||
|
},
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => {
|
||||||
|
calls.push('ensureOverlayWindowsReadyForVisibilityActions');
|
||||||
|
},
|
||||||
|
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) => {
|
||||||
|
calls.push(`send:${channel}`);
|
||||||
|
assert.equal(payload, undefined);
|
||||||
|
assert.deepEqual(runtimeOptions, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
preferModalWindow: undefined,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: 'runtime-options:open',
|
||||||
|
modal: 'runtime-options',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(opened, true);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'ensureOverlayStartupPrereqs',
|
||||||
|
'ensureOverlayWindowsReadyForVisibilityActions',
|
||||||
|
'send:runtime-options:open',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openOverlayHostedModal forwards payload and modal-window preference', () => {
|
||||||
|
const payload = { sessionId: 'yt-1' };
|
||||||
|
|
||||||
|
const opened = openOverlayHostedModal(
|
||||||
|
{
|
||||||
|
ensureOverlayStartupPrereqs: () => {},
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => {},
|
||||||
|
sendToActiveOverlayWindow: (channel, forwardedPayload, runtimeOptions) => {
|
||||||
|
assert.equal(channel, 'youtube:picker-open');
|
||||||
|
assert.deepEqual(forwardedPayload, payload);
|
||||||
|
assert.deepEqual(runtimeOptions, {
|
||||||
|
restoreOnModalClose: 'youtube-track-picker',
|
||||||
|
preferModalWindow: true,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: 'youtube:picker-open',
|
||||||
|
modal: 'youtube-track-picker',
|
||||||
|
payload,
|
||||||
|
preferModalWindow: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(opened, false);
|
||||||
|
});
|
||||||
29
src/main/runtime/overlay-hosted-modal-open.ts
Normal file
29
src/main/runtime/overlay-hosted-modal-open.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
||||||
|
|
||||||
|
export function openOverlayHostedModal(
|
||||||
|
deps: {
|
||||||
|
ensureOverlayStartupPrereqs: () => void;
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
||||||
|
sendToActiveOverlayWindow: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: {
|
||||||
|
restoreOnModalClose?: OverlayHostedModal;
|
||||||
|
preferModalWindow?: boolean;
|
||||||
|
},
|
||||||
|
) => boolean;
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
channel: string;
|
||||||
|
modal: OverlayHostedModal;
|
||||||
|
payload?: unknown;
|
||||||
|
preferModalWindow?: boolean;
|
||||||
|
},
|
||||||
|
): boolean {
|
||||||
|
deps.ensureOverlayStartupPrereqs();
|
||||||
|
deps.ensureOverlayWindowsReadyForVisibilityActions();
|
||||||
|
return deps.sendToActiveOverlayWindow(input.channel, input.payload, {
|
||||||
|
restoreOnModalClose: input.modal,
|
||||||
|
preferModalWindow: input.preferModalWindow,
|
||||||
|
});
|
||||||
|
}
|
||||||
211
src/renderer/modals/runtime-options.test.ts
Normal file
211
src/renderer/modals/runtime-options.test.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import type { ElectronAPI, RuntimeOptionState } from '../../types';
|
||||||
|
import { createRendererState } from '../state.js';
|
||||||
|
import { createRuntimeOptionsModal } from './runtime-options.js';
|
||||||
|
|
||||||
|
function createClassList(initialTokens: string[] = []) {
|
||||||
|
const tokens = new Set(initialTokens);
|
||||||
|
return {
|
||||||
|
add: (...entries: string[]) => {
|
||||||
|
for (const entry of entries) tokens.add(entry);
|
||||||
|
},
|
||||||
|
remove: (...entries: string[]) => {
|
||||||
|
for (const entry of entries) tokens.delete(entry);
|
||||||
|
},
|
||||||
|
toggle: (entry: string, force?: boolean) => {
|
||||||
|
if (force === undefined) {
|
||||||
|
if (tokens.has(entry)) {
|
||||||
|
tokens.delete(entry);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tokens.add(entry);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (force) tokens.add(entry);
|
||||||
|
else tokens.delete(entry);
|
||||||
|
return force;
|
||||||
|
},
|
||||||
|
contains: (entry: string) => tokens.has(entry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElementStub() {
|
||||||
|
return {
|
||||||
|
className: '',
|
||||||
|
textContent: '',
|
||||||
|
title: '',
|
||||||
|
classList: createClassList(),
|
||||||
|
appendChild: () => {},
|
||||||
|
addEventListener: () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRuntimeOptionsListStub() {
|
||||||
|
return {
|
||||||
|
innerHTML: '',
|
||||||
|
appendChild: () => {},
|
||||||
|
querySelector: () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeferred<T>() {
|
||||||
|
let resolve!: (value: T) => void;
|
||||||
|
let reject!: (reason?: unknown) => void;
|
||||||
|
const promise = new Promise<T>((nextResolve, nextReject) => {
|
||||||
|
resolve = nextResolve;
|
||||||
|
reject = nextReject;
|
||||||
|
});
|
||||||
|
return { promise, resolve, reject };
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushAsyncWork(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function withRuntimeOptionsModal(
|
||||||
|
getRuntimeOptions: () => Promise<RuntimeOptionState[]>,
|
||||||
|
run: (input: {
|
||||||
|
modal: ReturnType<typeof createRuntimeOptionsModal>;
|
||||||
|
state: ReturnType<typeof createRendererState>;
|
||||||
|
overlayClassList: ReturnType<typeof createClassList>;
|
||||||
|
modalClassList: ReturnType<typeof createClassList>;
|
||||||
|
statusNode: {
|
||||||
|
textContent: string;
|
||||||
|
classList: ReturnType<typeof createClassList>;
|
||||||
|
};
|
||||||
|
syncCalls: string[];
|
||||||
|
}) => Promise<void> | void,
|
||||||
|
): Promise<void> {
|
||||||
|
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||||
|
const previousWindow = globals.window;
|
||||||
|
const previousDocument = globals.document;
|
||||||
|
|
||||||
|
const statusNode = {
|
||||||
|
textContent: '',
|
||||||
|
classList: createClassList(),
|
||||||
|
};
|
||||||
|
const overlayClassList = createClassList();
|
||||||
|
const modalClassList = createClassList(['hidden']);
|
||||||
|
const syncCalls: string[] = [];
|
||||||
|
const state = createRendererState();
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'window', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
electronAPI: {
|
||||||
|
getRuntimeOptions,
|
||||||
|
setRuntimeOptionValue: async () => ({ ok: true }),
|
||||||
|
notifyOverlayModalClosed: () => {},
|
||||||
|
} satisfies Pick<
|
||||||
|
ElectronAPI,
|
||||||
|
'getRuntimeOptions' | 'setRuntimeOptionValue' | 'notifyOverlayModalClosed'
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'document', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
createElement: () => createElementStub(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modal = createRuntimeOptionsModal(
|
||||||
|
{
|
||||||
|
dom: {
|
||||||
|
overlay: { classList: overlayClassList },
|
||||||
|
runtimeOptionsModal: {
|
||||||
|
classList: modalClassList,
|
||||||
|
setAttribute: () => {},
|
||||||
|
},
|
||||||
|
runtimeOptionsClose: {
|
||||||
|
addEventListener: () => {},
|
||||||
|
},
|
||||||
|
runtimeOptionsList: createRuntimeOptionsListStub(),
|
||||||
|
runtimeOptionsStatus: statusNode,
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
} as never,
|
||||||
|
{
|
||||||
|
modalStateReader: { isAnyModalOpen: () => false },
|
||||||
|
syncSettingsModalSubtitleSuppression: () => {
|
||||||
|
syncCalls.push('sync');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
run({
|
||||||
|
modal,
|
||||||
|
state,
|
||||||
|
overlayClassList,
|
||||||
|
modalClassList,
|
||||||
|
statusNode,
|
||||||
|
syncCalls,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.finally(() => {
|
||||||
|
Object.defineProperty(globalThis, 'window', {
|
||||||
|
configurable: true,
|
||||||
|
value: previousWindow,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'document', {
|
||||||
|
configurable: true,
|
||||||
|
value: previousDocument,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('openRuntimeOptionsModal shows loading shell before runtime options resolve', async () => {
|
||||||
|
const deferred = createDeferred<RuntimeOptionState[]>();
|
||||||
|
|
||||||
|
await withRuntimeOptionsModal(() => deferred.promise, async (input) => {
|
||||||
|
input.modal.openRuntimeOptionsModal();
|
||||||
|
|
||||||
|
assert.equal(input.state.runtimeOptionsModalOpen, true);
|
||||||
|
assert.equal(input.overlayClassList.contains('interactive'), true);
|
||||||
|
assert.equal(input.modalClassList.contains('hidden'), false);
|
||||||
|
assert.equal(input.statusNode.textContent, 'Loading runtime options...');
|
||||||
|
assert.deepEqual(input.syncCalls, ['sync']);
|
||||||
|
|
||||||
|
deferred.resolve([
|
||||||
|
{
|
||||||
|
id: 'anki.autoUpdateNewCards',
|
||||||
|
label: 'Auto-update new cards',
|
||||||
|
scope: 'ankiConnect',
|
||||||
|
valueType: 'boolean',
|
||||||
|
value: true,
|
||||||
|
allowedValues: [true, false],
|
||||||
|
requiresRestart: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
input.statusNode.textContent,
|
||||||
|
'Use arrow keys. Click value to cycle. Enter or double-click to apply.',
|
||||||
|
);
|
||||||
|
assert.equal(input.statusNode.classList.contains('error'), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openRuntimeOptionsModal keeps modal visible when loading fails', async () => {
|
||||||
|
const deferred = createDeferred<RuntimeOptionState[]>();
|
||||||
|
|
||||||
|
await withRuntimeOptionsModal(() => deferred.promise, async (input) => {
|
||||||
|
input.modal.openRuntimeOptionsModal();
|
||||||
|
deferred.reject(new Error('boom'));
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
assert.equal(input.state.runtimeOptionsModalOpen, true);
|
||||||
|
assert.equal(input.overlayClassList.contains('interactive'), true);
|
||||||
|
assert.equal(input.modalClassList.contains('hidden'), false);
|
||||||
|
assert.equal(input.statusNode.textContent, 'Failed to load runtime options');
|
||||||
|
assert.equal(input.statusNode.classList.contains('error'), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,6 +22,9 @@ export function createRuntimeOptionsModal(
|
|||||||
syncSettingsModalSubtitleSuppression: () => void;
|
syncSettingsModalSubtitleSuppression: () => void;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
const DEFAULT_STATUS_MESSAGE =
|
||||||
|
'Use arrow keys. Click value to cycle. Enter or double-click to apply.';
|
||||||
|
|
||||||
function formatRuntimeOptionValue(value: RuntimeOptionValue): string {
|
function formatRuntimeOptionValue(value: RuntimeOptionValue): string {
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
return value ? 'On' : 'Off';
|
return value ? 'On' : 'Off';
|
||||||
@@ -177,10 +180,13 @@ export function createRuntimeOptionsModal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openRuntimeOptionsModal(): Promise<void> {
|
async function refreshRuntimeOptions(): Promise<void> {
|
||||||
const optionsList = await window.electronAPI.getRuntimeOptions();
|
const optionsList = await window.electronAPI.getRuntimeOptions();
|
||||||
updateRuntimeOptions(optionsList);
|
updateRuntimeOptions(optionsList);
|
||||||
|
setRuntimeOptionsStatus(DEFAULT_STATUS_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRuntimeOptionsModalShell(): void {
|
||||||
ctx.state.runtimeOptionsModalOpen = true;
|
ctx.state.runtimeOptionsModalOpen = true;
|
||||||
options.syncSettingsModalSubtitleSuppression();
|
options.syncSettingsModalSubtitleSuppression();
|
||||||
|
|
||||||
@@ -188,9 +194,19 @@ export function createRuntimeOptionsModal(
|
|||||||
ctx.dom.runtimeOptionsModal.classList.remove('hidden');
|
ctx.dom.runtimeOptionsModal.classList.remove('hidden');
|
||||||
ctx.dom.runtimeOptionsModal.setAttribute('aria-hidden', 'false');
|
ctx.dom.runtimeOptionsModal.setAttribute('aria-hidden', 'false');
|
||||||
|
|
||||||
setRuntimeOptionsStatus(
|
setRuntimeOptionsStatus('Loading runtime options...');
|
||||||
'Use arrow keys. Click value to cycle. Enter or double-click to apply.',
|
}
|
||||||
);
|
|
||||||
|
function openRuntimeOptionsModal(): void {
|
||||||
|
if (!ctx.state.runtimeOptionsModalOpen) {
|
||||||
|
showRuntimeOptionsModalShell();
|
||||||
|
} else {
|
||||||
|
setRuntimeOptionsStatus('Refreshing runtime options...');
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshRuntimeOptions().catch(() => {
|
||||||
|
setRuntimeOptionsStatus('Failed to load runtime options', true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRuntimeOptionsKeydown(e: KeyboardEvent): boolean {
|
function handleRuntimeOptionsKeydown(e: KeyboardEvent): boolean {
|
||||||
|
|||||||
@@ -432,15 +432,9 @@ registerRendererGlobalErrorHandlers(window, recovery);
|
|||||||
|
|
||||||
function registerModalOpenHandlers(): void {
|
function registerModalOpenHandlers(): void {
|
||||||
window.electronAPI.onOpenRuntimeOptions(() => {
|
window.electronAPI.onOpenRuntimeOptions(() => {
|
||||||
runGuardedAsync('runtime-options:open', async () => {
|
runGuarded('runtime-options:open', () => {
|
||||||
try {
|
runtimeOptionsModal.openRuntimeOptionsModal();
|
||||||
await runtimeOptionsModal.openRuntimeOptionsModal();
|
window.electronAPI.notifyOverlayModalOpened('runtime-options');
|
||||||
window.electronAPI.notifyOverlayModalOpened('runtime-options');
|
|
||||||
} catch {
|
|
||||||
runtimeOptionsModal.setRuntimeOptionsStatus('Failed to load runtime options', true);
|
|
||||||
window.electronAPI.notifyOverlayModalClosed('runtime-options');
|
|
||||||
syncSettingsModalSubtitleSuppression();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
window.electronAPI.onOpenJimaku(() => {
|
window.electronAPI.onOpenJimaku(() => {
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export class WindowsWindowTracker extends BaseWindowTracker {
|
|||||||
const focusedMatch = result.matches.find((m) => m.isForeground);
|
const focusedMatch = result.matches.find((m) => m.isForeground);
|
||||||
const best =
|
const best =
|
||||||
focusedMatch ??
|
focusedMatch ??
|
||||||
result.matches.sort((a, b) => b.area - a.area || b.bounds.width - a.bounds.width)[0]!;
|
[...result.matches].sort((a, b) => b.area - a.area || b.bounds.width - a.bounds.width)[0]!;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
geometry: best.bounds,
|
geometry: best.bounds,
|
||||||
|
|||||||
Reference in New Issue
Block a user