mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-10 03:13:32 -07:00
feat(aniskip): route skip prompts and results through playback feedback
- Add --playback-feedback CLI flag; overlay/both modes show AniSkip hint and skip result on overlay instead of raw OSD - Wire notify_playback_feedback through process.lua and the full deps chain to showPlaybackFeedback - Fallback to OSD when binary unavailable or osd_messages opt enabled
This commit is contained in:
@@ -9,7 +9,7 @@ breaking: true
|
|||||||
- Routed startup tokenization, subtitle annotation, and character dictionary status through queued overlay notifications for `overlay`/`both` instead of falling back to mpv OSD while the overlay loads; queued loading cards are shown before their ready update when both happen before the overlay is ready, and the bundled mpv plugin now only emits startup OSD messages for `osd` and `osd-system`.
|
- Routed startup tokenization, subtitle annotation, and character dictionary status through queued overlay notifications for `overlay`/`both` instead of falling back to mpv OSD while the overlay loads; queued loading cards are shown before their ready update when both happen before the overlay is ready, and the bundled mpv plugin now only emits startup OSD messages for `osd` and `osd-system`.
|
||||||
- Preserved character dictionary checking/building/importing/ready phases in overlay notification history and sent those phases to system notifications when `notificationType` is `both`.
|
- Preserved character dictionary checking/building/importing/ready phases in overlay notification history and sent those phases to system notifications when `notificationType` is `both`.
|
||||||
- Initialized the tray and visible overlay shell before deferred tokenization warmups finish on visible-overlay startup, while keeping playback paused until SubMiner reports autoplay readiness.
|
- Initialized the tray and visible overlay shell before deferred tokenization warmups finish on visible-overlay startup, while keeping playback paused until SubMiner reports autoplay readiness.
|
||||||
- Kept playback feedback such as subtitle visibility, subtitle track, and subtitle delay text on overlay/OSD surfaces only; desktop/system notifications are reserved for real notifications like mined cards, errors, and updates.
|
- Kept playback feedback such as subtitle visibility, subtitle track, subtitle delay, and AniSkip prompt/skip text on overlay/OSD surfaces only; desktop/system notifications are reserved for real notifications like mined cards, errors, and updates.
|
||||||
- Reused the active primary/secondary subtitle mode overlay notification while cycling modes so rapid toggles update one card instead of stacking duplicate feedback.
|
- Reused the active primary/secondary subtitle mode overlay notification while cycling modes so rapid toggles update one card instead of stacking duplicate feedback.
|
||||||
- Updated repeated progress notifications such as subsync syncing in place so their spinner stays live instead of flickering on every tick.
|
- Updated repeated progress notifications such as subsync syncing in place so their spinner stays live instead of flickering on every tick.
|
||||||
- Stabilized overlay startup notifications so queued progress updates do not replay the card entrance animation or trigger macOS pass-through hover flicker after the loading OSD hands off to overlay notifications.
|
- Stabilized overlay startup notifications so queued progress updates do not replay the card entrance animation or trigger macOS pass-through hover flicker after the loading OSD hands off to overlay notifications.
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ Configure where overlay notification cards appear:
|
|||||||
|
|
||||||
Every overlay notification shown during a session is also recorded in a notification history panel. Press `Ctrl/Cmd+N` (configurable via [`shortcuts.toggleNotificationHistory`](#shortcuts-configuration)) to toggle the panel; the binding works whether the overlay or mpv has focus. The panel slides in from the same edge the notifications use — left when `overlayPosition` is `"top-left"`, and right for `"top-right"` or `"top"` (centered). Character dictionary sync uses one live card but records each distinct phase in history. Each entry can be removed individually, or use **Clear** to empty the history. History is session-only and is not persisted across restarts.
|
Every overlay notification shown during a session is also recorded in a notification history panel. Press `Ctrl/Cmd+N` (configurable via [`shortcuts.toggleNotificationHistory`](#shortcuts-configuration)) to toggle the panel; the binding works whether the overlay or mpv has focus. The panel slides in from the same edge the notifications use — left when `overlayPosition` is `"top-left"`, and right for `"top-right"` or `"top"` (centered). Character dictionary sync uses one live card but records each distinct phase in history. Each entry can be removed individually, or use **Clear** to empty the history. History is session-only and is not persisted across restarts.
|
||||||
|
|
||||||
Startup tokenization, subtitle annotation, and character dictionary status follow the configured notification surface. When the surface is `"overlay"` or `"both"`, SubMiner queues those startup notifications until the overlay renderer is ready instead of falling back to mpv OSD. If loading and ready states both finish before the overlay can paint, the loading card is delivered first and then updates to ready shortly after. With `"both"`, character dictionary checking/building/importing/ready status also goes to system notifications; building and importing are only emitted when that work is actually needed. The bundled mpv plugin only shows its startup OSD messages when `ankiConnect.behavior.notificationType` is set to `"osd"` or `"osd-system"` in `config.jsonc`.
|
Startup tokenization, subtitle annotation, and character dictionary status follow the configured notification surface. When the surface is `"overlay"` or `"both"`, SubMiner queues those startup notifications until the overlay renderer is ready instead of falling back to mpv OSD. If loading and ready states both finish before the overlay can paint, the loading card is delivered first and then updates to ready shortly after. With `"both"`, character dictionary checking/building/importing/ready status also goes to system notifications; building and importing are only emitted when that work is actually needed. The bundled mpv plugin only shows its startup OSD messages when `ankiConnect.behavior.notificationType` is set to `"osd"` or `"osd-system"` in `config.jsonc`; AniSkip prompts and skip result messages are playback feedback and still route to overlay notifications when configured.
|
||||||
|
|
||||||
### Auto-Start Overlay
|
### Auto-Start Overlay
|
||||||
|
|
||||||
|
|||||||
@@ -428,6 +428,9 @@ function M.create(ctx)
|
|||||||
table.insert(args, "--texthooker")
|
table.insert(args, "--texthooker")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if action == "playback-feedback" and type(overrides.message) == "string" and overrides.message ~= "" then
|
||||||
|
table.insert(args, overrides.message)
|
||||||
|
end
|
||||||
|
|
||||||
return args
|
return args
|
||||||
end
|
end
|
||||||
@@ -515,6 +518,27 @@ function M.create(ctx)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function notify_playback_feedback(message, fallback)
|
||||||
|
if type(message) ~= "string" or message == "" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if resolve_osd_messages_enabled() then
|
||||||
|
show_osd(message)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not binary.ensure_binary_available() then
|
||||||
|
if fallback then
|
||||||
|
fallback()
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
run_control_command_async("playback-feedback", { message = message }, function(ok)
|
||||||
|
if not ok and fallback then
|
||||||
|
fallback()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local function wait_for_app_ping_state(expected_running, label, on_ready, on_timeout, attempt)
|
local function wait_for_app_ping_state(expected_running, label, on_ready, on_timeout, attempt)
|
||||||
attempt = attempt or 1
|
attempt = attempt or 1
|
||||||
run_control_command_async("app-ping", nil, function(_ok, result)
|
run_control_command_async("app-ping", nil, function(_ok, result)
|
||||||
@@ -934,6 +958,7 @@ function M.create(ctx)
|
|||||||
describe_mpv_ipc_socket_match = describe_mpv_ipc_socket_match,
|
describe_mpv_ipc_socket_match = describe_mpv_ipc_socket_match,
|
||||||
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,
|
||||||
|
notify_playback_feedback = notify_playback_feedback,
|
||||||
record_visible_overlay_visibility = record_visible_overlay_visibility,
|
record_visible_overlay_visibility = record_visible_overlay_visibility,
|
||||||
run_binary_command_async = run_binary_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,
|
||||||
|
|||||||
@@ -131,6 +131,15 @@ test('parseArgs captures session action forwarding flags', () => {
|
|||||||
assert.equal(shouldStartApp(args), true);
|
assert.equal(shouldStartApp(args), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parseArgs captures internal playback feedback command', () => {
|
||||||
|
const args = parseArgs(['--playback-feedback', 'You can skip by pressing TAB']);
|
||||||
|
|
||||||
|
assert.equal(args.playbackFeedback, 'You can skip by pressing TAB');
|
||||||
|
assert.equal(hasExplicitCommand(args), true);
|
||||||
|
assert.equal(shouldStartApp(args), true);
|
||||||
|
assert.equal(commandNeedsOverlayRuntime(args), true);
|
||||||
|
});
|
||||||
|
|
||||||
test('parseArgs ignores non-positive numeric session action counts', () => {
|
test('parseArgs ignores non-positive numeric session action counts', () => {
|
||||||
const args = parseArgs(['--copy-subtitle-count=0', '--mine-sentence-count', '-1']);
|
const args = parseArgs(['--copy-subtitle-count=0', '--mine-sentence-count', '-1']);
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -43,6 +43,7 @@ export interface CliArgs {
|
|||||||
playNextSubtitle: boolean;
|
playNextSubtitle: boolean;
|
||||||
shiftSubDelayPrevLine: boolean;
|
shiftSubDelayPrevLine: boolean;
|
||||||
shiftSubDelayNextLine: boolean;
|
shiftSubDelayNextLine: boolean;
|
||||||
|
playbackFeedback?: string;
|
||||||
cycleRuntimeOptionId?: string;
|
cycleRuntimeOptionId?: string;
|
||||||
cycleRuntimeOptionDirection?: 1 | -1;
|
cycleRuntimeOptionDirection?: 1 | -1;
|
||||||
sessionAction?: SessionActionDispatchRequest;
|
sessionAction?: SessionActionDispatchRequest;
|
||||||
@@ -150,6 +151,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
playNextSubtitle: false,
|
playNextSubtitle: false,
|
||||||
shiftSubDelayPrevLine: false,
|
shiftSubDelayPrevLine: false,
|
||||||
shiftSubDelayNextLine: false,
|
shiftSubDelayNextLine: false,
|
||||||
|
playbackFeedback: undefined,
|
||||||
anilistStatus: false,
|
anilistStatus: false,
|
||||||
anilistLogout: false,
|
anilistLogout: false,
|
||||||
anilistSetup: false,
|
anilistSetup: false,
|
||||||
@@ -296,7 +298,13 @@ 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=')) {
|
else if (arg.startsWith('--playback-feedback=')) {
|
||||||
|
const value = arg.slice('--playback-feedback='.length).trim();
|
||||||
|
if (value) args.playbackFeedback = value;
|
||||||
|
} else if (arg === '--playback-feedback') {
|
||||||
|
const value = readValue(argv[i + 1])?.trim();
|
||||||
|
if (value) args.playbackFeedback = value;
|
||||||
|
} else if (arg.startsWith('--cycle-runtime-option=')) {
|
||||||
const parsed = parseCycleRuntimeOption(arg.split('=', 2)[1]);
|
const parsed = parseCycleRuntimeOption(arg.split('=', 2)[1]);
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
args.cycleRuntimeOptionId = parsed.id;
|
args.cycleRuntimeOptionId = parsed.id;
|
||||||
@@ -556,6 +564,7 @@ export function hasExplicitCommand(args: CliArgs): boolean {
|
|||||||
args.playNextSubtitle ||
|
args.playNextSubtitle ||
|
||||||
args.shiftSubDelayPrevLine ||
|
args.shiftSubDelayPrevLine ||
|
||||||
args.shiftSubDelayNextLine ||
|
args.shiftSubDelayNextLine ||
|
||||||
|
args.playbackFeedback !== undefined ||
|
||||||
args.cycleRuntimeOptionId !== undefined ||
|
args.cycleRuntimeOptionId !== undefined ||
|
||||||
args.sessionAction !== undefined ||
|
args.sessionAction !== undefined ||
|
||||||
args.copySubtitleCount !== undefined ||
|
args.copySubtitleCount !== undefined ||
|
||||||
@@ -631,6 +640,7 @@ export function isStandaloneTexthookerCommand(args: CliArgs): boolean {
|
|||||||
!args.playNextSubtitle &&
|
!args.playNextSubtitle &&
|
||||||
!args.shiftSubDelayPrevLine &&
|
!args.shiftSubDelayPrevLine &&
|
||||||
!args.shiftSubDelayNextLine &&
|
!args.shiftSubDelayNextLine &&
|
||||||
|
args.playbackFeedback === undefined &&
|
||||||
args.cycleRuntimeOptionId === undefined &&
|
args.cycleRuntimeOptionId === undefined &&
|
||||||
args.sessionAction === undefined &&
|
args.sessionAction === undefined &&
|
||||||
args.copySubtitleCount === undefined &&
|
args.copySubtitleCount === undefined &&
|
||||||
@@ -697,6 +707,7 @@ export function shouldStartApp(args: CliArgs): boolean {
|
|||||||
args.playNextSubtitle ||
|
args.playNextSubtitle ||
|
||||||
args.shiftSubDelayPrevLine ||
|
args.shiftSubDelayPrevLine ||
|
||||||
args.shiftSubDelayNextLine ||
|
args.shiftSubDelayNextLine ||
|
||||||
|
args.playbackFeedback !== undefined ||
|
||||||
args.cycleRuntimeOptionId !== undefined ||
|
args.cycleRuntimeOptionId !== undefined ||
|
||||||
args.sessionAction !== undefined ||
|
args.sessionAction !== undefined ||
|
||||||
args.copySubtitleCount !== undefined ||
|
args.copySubtitleCount !== undefined ||
|
||||||
@@ -757,6 +768,7 @@ export function shouldRunYomitanOnlyStartup(args: CliArgs): boolean {
|
|||||||
!args.playNextSubtitle &&
|
!args.playNextSubtitle &&
|
||||||
!args.shiftSubDelayPrevLine &&
|
!args.shiftSubDelayPrevLine &&
|
||||||
!args.shiftSubDelayNextLine &&
|
!args.shiftSubDelayNextLine &&
|
||||||
|
args.playbackFeedback === undefined &&
|
||||||
args.cycleRuntimeOptionId === undefined &&
|
args.cycleRuntimeOptionId === undefined &&
|
||||||
args.sessionAction === undefined &&
|
args.sessionAction === undefined &&
|
||||||
args.copySubtitleCount === undefined &&
|
args.copySubtitleCount === undefined &&
|
||||||
@@ -822,6 +834,7 @@ export function commandNeedsOverlayRuntime(args: CliArgs): boolean {
|
|||||||
args.playNextSubtitle ||
|
args.playNextSubtitle ||
|
||||||
args.shiftSubDelayPrevLine ||
|
args.shiftSubDelayPrevLine ||
|
||||||
args.shiftSubDelayNextLine ||
|
args.shiftSubDelayNextLine ||
|
||||||
|
args.playbackFeedback !== undefined ||
|
||||||
args.cycleRuntimeOptionId !== undefined ||
|
args.cycleRuntimeOptionId !== undefined ||
|
||||||
args.sessionAction !== undefined ||
|
args.sessionAction !== undefined ||
|
||||||
args.copySubtitleCount !== undefined ||
|
args.copySubtitleCount !== undefined ||
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
playNextSubtitle: false,
|
playNextSubtitle: false,
|
||||||
shiftSubDelayPrevLine: false,
|
shiftSubDelayPrevLine: false,
|
||||||
shiftSubDelayNextLine: false,
|
shiftSubDelayNextLine: false,
|
||||||
|
playbackFeedback: undefined,
|
||||||
cycleRuntimeOptionId: undefined,
|
cycleRuntimeOptionId: undefined,
|
||||||
cycleRuntimeOptionDirection: undefined,
|
cycleRuntimeOptionDirection: undefined,
|
||||||
anilistStatus: false,
|
anilistStatus: false,
|
||||||
@@ -252,6 +253,9 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
|||||||
showMpvOsd: (text) => {
|
showMpvOsd: (text) => {
|
||||||
osd.push(text);
|
osd.push(text);
|
||||||
},
|
},
|
||||||
|
showPlaybackFeedback: (text) => {
|
||||||
|
calls.push(`feedback:${text}`);
|
||||||
|
},
|
||||||
log: (message) => {
|
log: (message) => {
|
||||||
calls.push(`log:${message}`);
|
calls.push(`log:${message}`);
|
||||||
},
|
},
|
||||||
@@ -493,6 +497,15 @@ test('handleCliCommand reports async mine errors to OSD', async () => {
|
|||||||
assert.ok(osd.some((value) => value.includes('Mine sentence failed: boom')));
|
assert.ok(osd.some((value) => value.includes('Mine sentence failed: boom')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('handleCliCommand routes playback feedback through configured feedback surface', () => {
|
||||||
|
const { deps, calls, osd } = createDeps();
|
||||||
|
|
||||||
|
handleCliCommand(makeArgs({ playbackFeedback: 'You can skip by pressing TAB' }), 'initial', deps);
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['initializeOverlayRuntime', 'feedback:You can skip by pressing TAB']);
|
||||||
|
assert.deepEqual(osd, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('handleCliCommand applies socket path and connects on start', () => {
|
test('handleCliCommand applies socket path and connects on start', () => {
|
||||||
const { deps, calls } = createDeps();
|
const { deps, calls } = createDeps();
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export interface CliCommandServiceDeps {
|
|||||||
hasMainWindow: () => boolean;
|
hasMainWindow: () => boolean;
|
||||||
getMultiCopyTimeoutMs: () => number;
|
getMultiCopyTimeoutMs: () => number;
|
||||||
showMpvOsd: (text: string) => void;
|
showMpvOsd: (text: string) => void;
|
||||||
|
showPlaybackFeedback?: (text: string) => void;
|
||||||
log: (message: string) => void;
|
log: (message: string) => void;
|
||||||
logDebug: (message: string) => void;
|
logDebug: (message: string) => void;
|
||||||
warn: (message: string) => void;
|
warn: (message: string) => void;
|
||||||
@@ -128,6 +129,7 @@ interface MpvCliRuntime {
|
|||||||
setSocketPath: (socketPath: string) => void;
|
setSocketPath: (socketPath: string) => void;
|
||||||
getClient: () => MpvClientLike | null;
|
getClient: () => MpvClientLike | null;
|
||||||
showOsd: (text: string) => void;
|
showOsd: (text: string) => void;
|
||||||
|
showPlaybackFeedback?: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TexthookerCliRuntime {
|
interface TexthookerCliRuntime {
|
||||||
@@ -295,6 +297,7 @@ export function createCliCommandDepsRuntime(
|
|||||||
hasMainWindow: options.app.hasMainWindow,
|
hasMainWindow: options.app.hasMainWindow,
|
||||||
getMultiCopyTimeoutMs: options.getMultiCopyTimeoutMs,
|
getMultiCopyTimeoutMs: options.getMultiCopyTimeoutMs,
|
||||||
showMpvOsd: options.mpv.showOsd,
|
showMpvOsd: options.mpv.showOsd,
|
||||||
|
showPlaybackFeedback: options.mpv.showPlaybackFeedback,
|
||||||
log: options.log,
|
log: options.log,
|
||||||
logDebug: options.logDebug,
|
logDebug: options.logDebug,
|
||||||
warn: options.warn,
|
warn: options.warn,
|
||||||
@@ -546,6 +549,9 @@ export function handleCliCommand(
|
|||||||
'shiftSubDelayNextLine',
|
'shiftSubDelayNextLine',
|
||||||
'Shift subtitle delay failed',
|
'Shift subtitle delay failed',
|
||||||
);
|
);
|
||||||
|
} else if (args.playbackFeedback) {
|
||||||
|
const showFeedback = deps.showPlaybackFeedback ?? deps.showMpvOsd;
|
||||||
|
showFeedback(args.playbackFeedback);
|
||||||
} else if (args.cycleRuntimeOptionId !== undefined) {
|
} else if (args.cycleRuntimeOptionId !== undefined) {
|
||||||
dispatchCliSessionAction(
|
dispatchCliSessionAction(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5748,6 +5748,7 @@ const aniSkipRuntime = createAniSkipRuntime({
|
|||||||
showMpvOsd: (text, durationMs) => {
|
showMpvOsd: (text, durationMs) => {
|
||||||
appState.mpvClient?.send({ command: ['show-text', text, durationMs] });
|
appState.mpvClient?.send({ command: ['show-text', text, durationMs] });
|
||||||
},
|
},
|
||||||
|
showPlaybackFeedback: (text) => showConfiguredPlaybackFeedback(text),
|
||||||
logInfo: (message) => logger.info(message),
|
logInfo: (message) => logger.info(message),
|
||||||
logWarn: (message, error) => logger.warn(message, error),
|
logWarn: (message, error) => logger.warn(message, error),
|
||||||
logDebug: (message) => logger.debug(message),
|
logDebug: (message) => logger.debug(message),
|
||||||
@@ -7366,6 +7367,7 @@ const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
|
|||||||
logBrowserOpenError: (url: string, error: unknown) =>
|
logBrowserOpenError: (url: string, error: unknown) =>
|
||||||
logger.error(`Failed to open browser for texthooker URL: ${url}`, error),
|
logger.error(`Failed to open browser for texthooker URL: ${url}`, error),
|
||||||
showMpvOsd: (text: string) => showConfiguredStatusNotification(text),
|
showMpvOsd: (text: string) => showConfiguredStatusNotification(text),
|
||||||
|
showPlaybackFeedback: (text: string) => showConfiguredPlaybackFeedback(text),
|
||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
togglePrimarySubtitleBar: () => togglePrimarySubtitleBar(),
|
togglePrimarySubtitleBar: () => togglePrimarySubtitleBar(),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface CliCommandRuntimeServiceContext {
|
|||||||
setSocketPath: (socketPath: string) => void;
|
setSocketPath: (socketPath: string) => void;
|
||||||
getClient: CliCommandRuntimeServiceDepsParams['mpv']['getClient'];
|
getClient: CliCommandRuntimeServiceDepsParams['mpv']['getClient'];
|
||||||
showOsd: CliCommandRuntimeServiceDepsParams['mpv']['showOsd'];
|
showOsd: CliCommandRuntimeServiceDepsParams['mpv']['showOsd'];
|
||||||
|
showPlaybackFeedback?: CliCommandRuntimeServiceDepsParams['mpv']['showPlaybackFeedback'];
|
||||||
getTexthookerPort: () => number;
|
getTexthookerPort: () => number;
|
||||||
setTexthookerPort: (port: number) => void;
|
setTexthookerPort: (port: number) => void;
|
||||||
getTexthookerWebsocketUrl: () => string | undefined;
|
getTexthookerWebsocketUrl: () => string | undefined;
|
||||||
@@ -74,6 +75,7 @@ function createCliCommandDepsFromContext(
|
|||||||
setSocketPath: context.setSocketPath,
|
setSocketPath: context.setSocketPath,
|
||||||
getClient: context.getClient,
|
getClient: context.getClient,
|
||||||
showOsd: context.showOsd,
|
showOsd: context.showOsd,
|
||||||
|
showPlaybackFeedback: context.showPlaybackFeedback,
|
||||||
},
|
},
|
||||||
texthooker: {
|
texthooker: {
|
||||||
service: context.texthookerService,
|
service: context.texthookerService,
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export interface CliCommandRuntimeServiceDepsParams {
|
|||||||
setSocketPath: CliCommandDepsRuntimeOptions['mpv']['setSocketPath'];
|
setSocketPath: CliCommandDepsRuntimeOptions['mpv']['setSocketPath'];
|
||||||
getClient: CliCommandDepsRuntimeOptions['mpv']['getClient'];
|
getClient: CliCommandDepsRuntimeOptions['mpv']['getClient'];
|
||||||
showOsd: CliCommandDepsRuntimeOptions['mpv']['showOsd'];
|
showOsd: CliCommandDepsRuntimeOptions['mpv']['showOsd'];
|
||||||
|
showPlaybackFeedback?: CliCommandDepsRuntimeOptions['mpv']['showPlaybackFeedback'];
|
||||||
};
|
};
|
||||||
texthooker: {
|
texthooker: {
|
||||||
service: CliCommandDepsRuntimeOptions['texthooker']['service'];
|
service: CliCommandDepsRuntimeOptions['texthooker']['service'];
|
||||||
@@ -342,6 +343,7 @@ export function createCliCommandRuntimeServiceDeps(
|
|||||||
setSocketPath: params.mpv.setSocketPath,
|
setSocketPath: params.mpv.setSocketPath,
|
||||||
getClient: params.mpv.getClient,
|
getClient: params.mpv.getClient,
|
||||||
showOsd: params.mpv.showOsd,
|
showOsd: params.mpv.showOsd,
|
||||||
|
showPlaybackFeedback: params.mpv.showPlaybackFeedback,
|
||||||
},
|
},
|
||||||
texthooker: {
|
texthooker: {
|
||||||
service: params.texthooker.service,
|
service: params.texthooker.service,
|
||||||
|
|||||||
@@ -22,19 +22,21 @@ function createHarness(options?: {
|
|||||||
buttonKey?: string;
|
buttonKey?: string;
|
||||||
metadata?: AniSkipMetadata | (() => Promise<AniSkipMetadata>);
|
metadata?: AniSkipMetadata | (() => Promise<AniSkipMetadata>);
|
||||||
chapterList?: unknown;
|
chapterList?: unknown;
|
||||||
|
playbackFeedback?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const state = {
|
const state = {
|
||||||
enabled: options?.enabled ?? true,
|
enabled: options?.enabled ?? true,
|
||||||
buttonKey: options?.buttonKey ?? 'TAB',
|
buttonKey: options?.buttonKey ?? 'TAB',
|
||||||
commands: [] as unknown[][],
|
commands: [] as unknown[][],
|
||||||
osd: [] as string[],
|
osd: [] as string[],
|
||||||
|
feedback: [] as string[],
|
||||||
resolveCalls: [] as string[],
|
resolveCalls: [] as string[],
|
||||||
connected: true,
|
connected: true,
|
||||||
timePos: 0,
|
timePos: 0,
|
||||||
chapterList: options?.chapterList ?? [],
|
chapterList: options?.chapterList ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const deps: AniSkipRuntimeDeps = {
|
const deps = {
|
||||||
getAniSkipConfig: () => ({
|
getAniSkipConfig: () => ({
|
||||||
aniskipEnabled: state.enabled,
|
aniskipEnabled: state.enabled,
|
||||||
aniskipButtonKey: state.buttonKey,
|
aniskipButtonKey: state.buttonKey,
|
||||||
@@ -57,10 +59,17 @@ function createHarness(options?: {
|
|||||||
showMpvOsd: (text) => {
|
showMpvOsd: (text) => {
|
||||||
state.osd.push(text);
|
state.osd.push(text);
|
||||||
},
|
},
|
||||||
|
...(options?.playbackFeedback
|
||||||
|
? {
|
||||||
|
showPlaybackFeedback: (text: string) => {
|
||||||
|
state.feedback.push(text);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
logInfo: () => {},
|
logInfo: () => {},
|
||||||
logWarn: () => {},
|
logWarn: () => {},
|
||||||
logDebug: () => {},
|
logDebug: () => {},
|
||||||
};
|
} satisfies AniSkipRuntimeDeps & { showPlaybackFeedback?: (text: string) => void };
|
||||||
|
|
||||||
return { runtime: createAniSkipRuntime(deps), state };
|
return { runtime: createAniSkipRuntime(deps), state };
|
||||||
}
|
}
|
||||||
@@ -152,6 +161,19 @@ test('time-pos prompt shows once near intro start', async () => {
|
|||||||
assert.deepEqual(state.osd, ['You can skip by pressing TAB']);
|
assert.deepEqual(state.osd, ['You can skip by pressing TAB']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('prompt and skip messages use playback feedback when configured', async () => {
|
||||||
|
const { runtime, state } = createHarness({ buttonKey: 'TAB', playbackFeedback: true });
|
||||||
|
runtime.handleMediaPathChange({ path: '/media/show.mkv' });
|
||||||
|
await flushAsync();
|
||||||
|
|
||||||
|
runtime.handleTimePosChange({ time: 10.5 });
|
||||||
|
state.timePos = 30;
|
||||||
|
runtime.handleClientMessage({ args: ['subminer-skip-intro'] });
|
||||||
|
|
||||||
|
assert.deepEqual(state.feedback, ['You can skip by pressing TAB', 'Skipped intro']);
|
||||||
|
assert.deepEqual(state.osd, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('connection change binds skip key and legacy fallback for custom keys', () => {
|
test('connection change binds skip key and legacy fallback for custom keys', () => {
|
||||||
const { runtime, state } = createHarness({ buttonKey: 'F6' });
|
const { runtime, state } = createHarness({ buttonKey: 'F6' });
|
||||||
runtime.handleConnectionChange({ connected: true });
|
runtime.handleConnectionChange({ connected: true });
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface AniSkipRuntimeDeps {
|
|||||||
isMpvConnected: () => boolean;
|
isMpvConnected: () => boolean;
|
||||||
getCurrentTimePos: () => number;
|
getCurrentTimePos: () => number;
|
||||||
showMpvOsd: (text: string, durationMs: number) => void;
|
showMpvOsd: (text: string, durationMs: number) => void;
|
||||||
|
showPlaybackFeedback?: (text: string) => void;
|
||||||
logInfo: (message: string) => void;
|
logInfo: (message: string) => void;
|
||||||
logWarn: (message: string, error?: unknown) => void;
|
logWarn: (message: string, error?: unknown) => void;
|
||||||
logDebug: (message: string) => void;
|
logDebug: (message: string) => void;
|
||||||
@@ -53,6 +54,14 @@ export function createAniSkipRuntime(deps: AniSkipRuntimeDeps) {
|
|||||||
return key || DEFAULT_ANISKIP_BUTTON_KEY;
|
return key || DEFAULT_ANISKIP_BUTTON_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showPlaybackFeedback(text: string, durationMs = PROMPT_OSD_DURATION_MS): void {
|
||||||
|
if (deps.showPlaybackFeedback) {
|
||||||
|
deps.showPlaybackFeedback(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deps.showMpvOsd(text, durationMs);
|
||||||
|
}
|
||||||
|
|
||||||
function bindSkipKeys(): void {
|
function bindSkipKeys(): void {
|
||||||
if (!deps.isMpvConnected()) return;
|
if (!deps.isMpvConnected()) return;
|
||||||
const enabled = deps.getAniSkipConfig().aniskipEnabled;
|
const enabled = deps.getAniSkipConfig().aniskipEnabled;
|
||||||
@@ -204,23 +213,23 @@ export function createAniSkipRuntime(deps: AniSkipRuntimeDeps) {
|
|||||||
function skipIntroNow(): void {
|
function skipIntroNow(): void {
|
||||||
if (!deps.getAniSkipConfig().aniskipEnabled) return;
|
if (!deps.getAniSkipConfig().aniskipEnabled) return;
|
||||||
if (!introWindow) {
|
if (!introWindow) {
|
||||||
deps.showMpvOsd('Intro skip unavailable', PROMPT_OSD_DURATION_MS);
|
showPlaybackFeedback('Intro skip unavailable');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const now = deps.getCurrentTimePos();
|
const now = deps.getCurrentTimePos();
|
||||||
if (!Number.isFinite(now)) {
|
if (!Number.isFinite(now)) {
|
||||||
deps.showMpvOsd('Skip unavailable', PROMPT_OSD_DURATION_MS);
|
showPlaybackFeedback('Skip unavailable');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
now < introWindow.start - SKIP_WINDOW_EPSILON_SECONDS ||
|
now < introWindow.start - SKIP_WINDOW_EPSILON_SECONDS ||
|
||||||
now > introWindow.end + SKIP_WINDOW_EPSILON_SECONDS
|
now > introWindow.end + SKIP_WINDOW_EPSILON_SECONDS
|
||||||
) {
|
) {
|
||||||
deps.showMpvOsd('Skip intro only during intro', PROMPT_OSD_DURATION_MS);
|
showPlaybackFeedback('Skip intro only during intro');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deps.sendMpvCommand(['set_property', 'time-pos', introWindow.end]);
|
deps.sendMpvCommand(['set_property', 'time-pos', introWindow.end]);
|
||||||
deps.showMpvOsd('Skipped intro', PROMPT_OSD_DURATION_MS);
|
showPlaybackFeedback('Skipped intro');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTimePosChange({ time }: { time: number }): void {
|
function handleTimePosChange({ time }: { time: number }): void {
|
||||||
@@ -229,7 +238,7 @@ export function createAniSkipRuntime(deps: AniSkipRuntimeDeps) {
|
|||||||
const promptWindowEnd = Math.min(introWindow.start + PROMPT_WINDOW_SECONDS, introWindow.end);
|
const promptWindowEnd = Math.min(introWindow.start + PROMPT_WINDOW_SECONDS, introWindow.end);
|
||||||
if (time >= introWindow.start && time < promptWindowEnd) {
|
if (time >= introWindow.start && time < promptWindowEnd) {
|
||||||
promptShown = true;
|
promptShown = true;
|
||||||
deps.showMpvOsd(`You can skip by pressing ${resolveButtonKey()}`, PROMPT_OSD_DURATION_MS);
|
showPlaybackFeedback(`You can skip by pressing ${resolveButtonKey()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
|
|||||||
setSocketPath: (socketPath: string) => void;
|
setSocketPath: (socketPath: string) => void;
|
||||||
getMpvClient: CliCommandContextFactoryDeps['getMpvClient'];
|
getMpvClient: CliCommandContextFactoryDeps['getMpvClient'];
|
||||||
showOsd: (text: string) => void;
|
showOsd: (text: string) => void;
|
||||||
|
showPlaybackFeedback?: (text: string) => void;
|
||||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||||
getTexthookerPort: () => number;
|
getTexthookerPort: () => number;
|
||||||
setTexthookerPort: (port: number) => void;
|
setTexthookerPort: (port: number) => void;
|
||||||
@@ -63,6 +64,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
|
|||||||
setSocketPath: deps.setSocketPath,
|
setSocketPath: deps.setSocketPath,
|
||||||
getMpvClient: deps.getMpvClient,
|
getMpvClient: deps.getMpvClient,
|
||||||
showOsd: deps.showOsd,
|
showOsd: deps.showOsd,
|
||||||
|
showPlaybackFeedback: deps.showPlaybackFeedback,
|
||||||
texthookerService: deps.texthookerService,
|
texthookerService: deps.texthookerService,
|
||||||
getTexthookerPort: deps.getTexthookerPort,
|
getTexthookerPort: deps.getTexthookerPort,
|
||||||
setTexthookerPort: deps.setTexthookerPort,
|
setTexthookerPort: deps.setTexthookerPort,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
|||||||
openExternal: (url: string) => Promise<unknown>;
|
openExternal: (url: string) => Promise<unknown>;
|
||||||
logBrowserOpenError: (url: string, error: unknown) => void;
|
logBrowserOpenError: (url: string, error: unknown) => void;
|
||||||
showMpvOsd: (text: string) => void;
|
showMpvOsd: (text: string) => void;
|
||||||
|
showPlaybackFeedback?: (text: string) => void;
|
||||||
|
|
||||||
initializeOverlayRuntime: () => void;
|
initializeOverlayRuntime: () => void;
|
||||||
toggleVisibleOverlay: () => void;
|
toggleVisibleOverlay: () => void;
|
||||||
@@ -83,6 +84,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
|||||||
},
|
},
|
||||||
getMpvClient: () => deps.appState.mpvClient,
|
getMpvClient: () => deps.appState.mpvClient,
|
||||||
showOsd: (text: string) => deps.showMpvOsd(text),
|
showOsd: (text: string) => deps.showMpvOsd(text),
|
||||||
|
showPlaybackFeedback: deps.showPlaybackFeedback,
|
||||||
texthookerService: deps.texthookerService,
|
texthookerService: deps.texthookerService,
|
||||||
getTexthookerPort: () => deps.appState.texthookerPort,
|
getTexthookerPort: () => deps.appState.texthookerPort,
|
||||||
setTexthookerPort: (port: number) => {
|
setTexthookerPort: (port: number) => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type CliCommandContextFactoryDeps = {
|
|||||||
setSocketPath: (socketPath: string) => void;
|
setSocketPath: (socketPath: string) => void;
|
||||||
getMpvClient: () => MpvClientLike;
|
getMpvClient: () => MpvClientLike;
|
||||||
showOsd: (text: string) => void;
|
showOsd: (text: string) => void;
|
||||||
|
showPlaybackFeedback?: (text: string) => void;
|
||||||
texthookerService: CliCommandRuntimeServiceContextHandlers['texthookerService'];
|
texthookerService: CliCommandRuntimeServiceContextHandlers['texthookerService'];
|
||||||
getTexthookerPort: () => number;
|
getTexthookerPort: () => number;
|
||||||
setTexthookerPort: (port: number) => void;
|
setTexthookerPort: (port: number) => void;
|
||||||
@@ -72,6 +73,7 @@ export function createCliCommandContext(
|
|||||||
setSocketPath: deps.setSocketPath,
|
setSocketPath: deps.setSocketPath,
|
||||||
getClient: deps.getMpvClient,
|
getClient: deps.getMpvClient,
|
||||||
showOsd: deps.showOsd,
|
showOsd: deps.showOsd,
|
||||||
|
showPlaybackFeedback: deps.showPlaybackFeedback,
|
||||||
texthookerService: deps.texthookerService,
|
texthookerService: deps.texthookerService,
|
||||||
getTexthookerPort: deps.getTexthookerPort,
|
getTexthookerPort: deps.getTexthookerPort,
|
||||||
setTexthookerPort: deps.setTexthookerPort,
|
setTexthookerPort: deps.setTexthookerPort,
|
||||||
|
|||||||
Reference in New Issue
Block a user