mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-10 15:13:32 -07:00
fix(startup): signal autoplay gate from subtitle resolve; dedupe dict sy
- Add onResolvedSubtitle callback to resolveCurrentSubtitleForRenderer so the startup overlay-ready gate fires after the initial subtitle resolves - Guard scheduleCharacterDictionarySync behind a last-path check so duplicate MPV media-path events don't re-trigger sync for the same video
This commit is contained in:
@@ -8,5 +8,6 @@ breaking: true
|
||||
- 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.
|
||||
- Reused the active primary/secondary subtitle mode overlay notification while cycling modes so rapid toggles update one card instead of stacking duplicate feedback.
|
||||
- Fixed mined-card overlay notifications so `overlay` and `both` modes show the same generated card thumbnail as system notifications.
|
||||
- Fixed character dictionary sync so duplicate MPV media-path events do not repeat check/ready notifications for the same opened video.
|
||||
- Changed `both` notification routing to mean overlay + system; users who used `both` for mpv OSD + system notifications should set `notificationType` to `osd-system` in `config.jsonc`.
|
||||
- Kept `osd` and `osd-system` as config-file-only legacy notification values; Settings normally offers only overlay, system, both, and none, while still showing an already configured legacy value as selected.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Fixed pause-until-overlay-ready startup on macOS so the initial renderer subtitle snapshot can release the mpv startup gate after the overlay paints annotations.
|
||||
@@ -6863,6 +6863,10 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
||||
tokenizeSubtitle: tokenizeSubtitleForCurrent
|
||||
? (text) => tokenizeSubtitleForCurrent(text)
|
||||
: undefined,
|
||||
onResolvedSubtitle: (payload) => {
|
||||
appState.currentSubtitleData = payload;
|
||||
autoplayReadyGate.maybeSignalPluginAutoplayReady(payload, { forceWhilePaused: true });
|
||||
},
|
||||
});
|
||||
},
|
||||
getCurrentSubtitleRaw: () => appState.currentSubText,
|
||||
|
||||
@@ -62,6 +62,21 @@ test('renderer current subtitle snapshot tokenizes uncached subtitles when token
|
||||
assert.deepEqual(payload.tokens, [{ text: '新' }]);
|
||||
});
|
||||
|
||||
test('renderer current subtitle snapshot reports resolved payload for startup readiness', async () => {
|
||||
const resolvedPayloads: SubtitleData[] = [];
|
||||
const payload = await resolveCurrentSubtitleForRenderer({
|
||||
currentSubText: '起動字幕',
|
||||
currentSubtitleData: null,
|
||||
withCurrentSubtitleTiming: withTiming,
|
||||
tokenizeSubtitle: async (text) => ({ text, tokens: [{ text: '起' } as never] }),
|
||||
onResolvedSubtitle: (resolved) => {
|
||||
resolvedPayloads.push(resolved);
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(resolvedPayloads, [payload]);
|
||||
});
|
||||
|
||||
test('visible overlay subtitle prime refreshes current text from mpv before showing overlay', async () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
|
||||
@@ -10,13 +10,20 @@ export async function resolveCurrentSubtitleForRenderer(deps: {
|
||||
currentSubtitleData: SubtitleData | null;
|
||||
withCurrentSubtitleTiming: (payload: SubtitleData) => SubtitleData;
|
||||
tokenizeSubtitle?: (text: string) => Promise<SubtitleData | null>;
|
||||
onResolvedSubtitle?: (payload: SubtitleData) => void;
|
||||
}): Promise<SubtitleData> {
|
||||
const resolve = (payload: SubtitleData): SubtitleData => {
|
||||
const timedPayload = deps.withCurrentSubtitleTiming(payload);
|
||||
deps.onResolvedSubtitle?.(timedPayload);
|
||||
return timedPayload;
|
||||
};
|
||||
|
||||
if (deps.currentSubtitleData?.text === deps.currentSubText) {
|
||||
return deps.withCurrentSubtitleTiming(deps.currentSubtitleData);
|
||||
return resolve(deps.currentSubtitleData);
|
||||
}
|
||||
|
||||
if (!deps.currentSubText.trim()) {
|
||||
return deps.withCurrentSubtitleTiming({
|
||||
return resolve({
|
||||
text: deps.currentSubText,
|
||||
tokens: null,
|
||||
});
|
||||
@@ -24,10 +31,10 @@ export async function resolveCurrentSubtitleForRenderer(deps: {
|
||||
|
||||
const tokenized = await deps.tokenizeSubtitle?.(deps.currentSubText);
|
||||
if (tokenized) {
|
||||
return deps.withCurrentSubtitleTiming(tokenized);
|
||||
return resolve(tokenized);
|
||||
}
|
||||
|
||||
return deps.withCurrentSubtitleTiming({
|
||||
return resolve({
|
||||
text: deps.currentSubText,
|
||||
tokens: null,
|
||||
});
|
||||
|
||||
@@ -183,6 +183,34 @@ test('media path change handler signals autoplay readiness from warm media path'
|
||||
]);
|
||||
});
|
||||
|
||||
test('media path change handler schedules character dictionary once per media path', () => {
|
||||
const calls: string[] = [];
|
||||
const handler = createHandleMpvMediaPathChangeHandler({
|
||||
updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
|
||||
reportJellyfinRemoteStopped: () => calls.push('stopped'),
|
||||
restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
|
||||
resetSubtitleSidebarEmbeddedLayout: () => calls.push('reset-sidebar-layout'),
|
||||
getCurrentAnilistMediaKey: () => null,
|
||||
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${String(mediaKey)}`),
|
||||
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||
ensureAnilistMediaGuess: (mediaKey) => calls.push(`guess:${mediaKey}`),
|
||||
syncImmersionMediaState: () => calls.push('sync'),
|
||||
scheduleCharacterDictionarySync: () => calls.push('dict-sync'),
|
||||
refreshDiscordPresence: () => calls.push('presence'),
|
||||
});
|
||||
|
||||
handler({ path: '/tmp/video.mkv' });
|
||||
handler({ path: '/tmp/video.mkv' });
|
||||
handler({ path: '/tmp/next-video.mkv' });
|
||||
handler({ path: '' });
|
||||
handler({ path: '/tmp/video.mkv' });
|
||||
|
||||
assert.deepEqual(
|
||||
calls.filter((call) => call === 'dict-sync'),
|
||||
['dict-sync', 'dict-sync', 'dict-sync'],
|
||||
);
|
||||
});
|
||||
|
||||
test('media path change handler marks Jellyfin remote playback loaded from media path', () => {
|
||||
const calls: string[] = [];
|
||||
const handler = createHandleMpvMediaPathChangeHandler({
|
||||
|
||||
@@ -74,9 +74,13 @@ export function createHandleMpvMediaPathChangeHandler(deps: {
|
||||
flushPlaybackPositionOnMediaPathClear?: (mediaPath: string) => void;
|
||||
refreshDiscordPresence: () => void;
|
||||
}) {
|
||||
let lastCharacterDictionarySyncMediaPath: string | null = null;
|
||||
|
||||
return ({ path }: { path: string | null }): void => {
|
||||
const normalizedPath = typeof path === 'string' ? path : '';
|
||||
if (!normalizedPath) {
|
||||
const trimmedPath = normalizedPath.trim();
|
||||
if (!trimmedPath) {
|
||||
lastCharacterDictionarySyncMediaPath = null;
|
||||
deps.flushPlaybackPositionOnMediaPathClear?.(normalizedPath);
|
||||
}
|
||||
deps.updateCurrentMediaPath(normalizedPath);
|
||||
@@ -92,9 +96,12 @@ export function createHandleMpvMediaPathChangeHandler(deps: {
|
||||
deps.ensureAnilistMediaGuess(mediaKey);
|
||||
}
|
||||
deps.syncImmersionMediaState();
|
||||
if (normalizedPath.trim().length > 0) {
|
||||
if (trimmedPath.length > 0) {
|
||||
deps.markJellyfinRemotePlaybackLoaded?.(normalizedPath);
|
||||
deps.scheduleCharacterDictionarySync?.();
|
||||
if (trimmedPath !== lastCharacterDictionarySyncMediaPath) {
|
||||
lastCharacterDictionarySyncMediaPath = trimmedPath;
|
||||
deps.scheduleCharacterDictionarySync?.();
|
||||
}
|
||||
deps.signalAutoplayReadyIfWarm?.(normalizedPath);
|
||||
}
|
||||
deps.refreshDiscordPresence();
|
||||
|
||||
Reference in New Issue
Block a user