mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-10 03: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.
|
- 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.
|
- 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 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`.
|
- 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.
|
- 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
|
tokenizeSubtitle: tokenizeSubtitleForCurrent
|
||||||
? (text) => tokenizeSubtitleForCurrent(text)
|
? (text) => tokenizeSubtitleForCurrent(text)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
onResolvedSubtitle: (payload) => {
|
||||||
|
appState.currentSubtitleData = payload;
|
||||||
|
autoplayReadyGate.maybeSignalPluginAutoplayReady(payload, { forceWhilePaused: true });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getCurrentSubtitleRaw: () => appState.currentSubText,
|
getCurrentSubtitleRaw: () => appState.currentSubText,
|
||||||
|
|||||||
@@ -62,6 +62,21 @@ test('renderer current subtitle snapshot tokenizes uncached subtitles when token
|
|||||||
assert.deepEqual(payload.tokens, [{ text: '新' }]);
|
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 () => {
|
test('visible overlay subtitle prime refreshes current text from mpv before showing overlay', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,20 @@ export async function resolveCurrentSubtitleForRenderer(deps: {
|
|||||||
currentSubtitleData: SubtitleData | null;
|
currentSubtitleData: SubtitleData | null;
|
||||||
withCurrentSubtitleTiming: (payload: SubtitleData) => SubtitleData;
|
withCurrentSubtitleTiming: (payload: SubtitleData) => SubtitleData;
|
||||||
tokenizeSubtitle?: (text: string) => Promise<SubtitleData | null>;
|
tokenizeSubtitle?: (text: string) => Promise<SubtitleData | null>;
|
||||||
|
onResolvedSubtitle?: (payload: SubtitleData) => void;
|
||||||
}): Promise<SubtitleData> {
|
}): Promise<SubtitleData> {
|
||||||
|
const resolve = (payload: SubtitleData): SubtitleData => {
|
||||||
|
const timedPayload = deps.withCurrentSubtitleTiming(payload);
|
||||||
|
deps.onResolvedSubtitle?.(timedPayload);
|
||||||
|
return timedPayload;
|
||||||
|
};
|
||||||
|
|
||||||
if (deps.currentSubtitleData?.text === deps.currentSubText) {
|
if (deps.currentSubtitleData?.text === deps.currentSubText) {
|
||||||
return deps.withCurrentSubtitleTiming(deps.currentSubtitleData);
|
return resolve(deps.currentSubtitleData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deps.currentSubText.trim()) {
|
if (!deps.currentSubText.trim()) {
|
||||||
return deps.withCurrentSubtitleTiming({
|
return resolve({
|
||||||
text: deps.currentSubText,
|
text: deps.currentSubText,
|
||||||
tokens: null,
|
tokens: null,
|
||||||
});
|
});
|
||||||
@@ -24,10 +31,10 @@ export async function resolveCurrentSubtitleForRenderer(deps: {
|
|||||||
|
|
||||||
const tokenized = await deps.tokenizeSubtitle?.(deps.currentSubText);
|
const tokenized = await deps.tokenizeSubtitle?.(deps.currentSubText);
|
||||||
if (tokenized) {
|
if (tokenized) {
|
||||||
return deps.withCurrentSubtitleTiming(tokenized);
|
return resolve(tokenized);
|
||||||
}
|
}
|
||||||
|
|
||||||
return deps.withCurrentSubtitleTiming({
|
return resolve({
|
||||||
text: deps.currentSubText,
|
text: deps.currentSubText,
|
||||||
tokens: null,
|
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', () => {
|
test('media path change handler marks Jellyfin remote playback loaded from media path', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const handler = createHandleMpvMediaPathChangeHandler({
|
const handler = createHandleMpvMediaPathChangeHandler({
|
||||||
|
|||||||
@@ -74,9 +74,13 @@ export function createHandleMpvMediaPathChangeHandler(deps: {
|
|||||||
flushPlaybackPositionOnMediaPathClear?: (mediaPath: string) => void;
|
flushPlaybackPositionOnMediaPathClear?: (mediaPath: string) => void;
|
||||||
refreshDiscordPresence: () => void;
|
refreshDiscordPresence: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
let lastCharacterDictionarySyncMediaPath: string | null = null;
|
||||||
|
|
||||||
return ({ path }: { path: string | null }): void => {
|
return ({ path }: { path: string | null }): void => {
|
||||||
const normalizedPath = typeof path === 'string' ? path : '';
|
const normalizedPath = typeof path === 'string' ? path : '';
|
||||||
if (!normalizedPath) {
|
const trimmedPath = normalizedPath.trim();
|
||||||
|
if (!trimmedPath) {
|
||||||
|
lastCharacterDictionarySyncMediaPath = null;
|
||||||
deps.flushPlaybackPositionOnMediaPathClear?.(normalizedPath);
|
deps.flushPlaybackPositionOnMediaPathClear?.(normalizedPath);
|
||||||
}
|
}
|
||||||
deps.updateCurrentMediaPath(normalizedPath);
|
deps.updateCurrentMediaPath(normalizedPath);
|
||||||
@@ -92,9 +96,12 @@ export function createHandleMpvMediaPathChangeHandler(deps: {
|
|||||||
deps.ensureAnilistMediaGuess(mediaKey);
|
deps.ensureAnilistMediaGuess(mediaKey);
|
||||||
}
|
}
|
||||||
deps.syncImmersionMediaState();
|
deps.syncImmersionMediaState();
|
||||||
if (normalizedPath.trim().length > 0) {
|
if (trimmedPath.length > 0) {
|
||||||
deps.markJellyfinRemotePlaybackLoaded?.(normalizedPath);
|
deps.markJellyfinRemotePlaybackLoaded?.(normalizedPath);
|
||||||
deps.scheduleCharacterDictionarySync?.();
|
if (trimmedPath !== lastCharacterDictionarySyncMediaPath) {
|
||||||
|
lastCharacterDictionarySyncMediaPath = trimmedPath;
|
||||||
|
deps.scheduleCharacterDictionarySync?.();
|
||||||
|
}
|
||||||
deps.signalAutoplayReadyIfWarm?.(normalizedPath);
|
deps.signalAutoplayReadyIfWarm?.(normalizedPath);
|
||||||
}
|
}
|
||||||
deps.refreshDiscordPresence();
|
deps.refreshDiscordPresence();
|
||||||
|
|||||||
Reference in New Issue
Block a user