fix: Kiku field grouping, frequency particles, sidebar media, Yomitan po

- Fix Kiku duplicate-card field grouping: local dupes trigger manual modal or auto-merge; fix modal-open ack race; fix merged field ordering, sentence-audio, furigana, and tag semantics
- Fix frequency annotations for single-token Yomitan compounds with internal particles (e.g. 目の前); keep pure grammar/kana spans unannotated
- Fix subtitle sidebar mining: use audio/image from clicked sidebar line, not current primary line
- Add `subtitleStyle.primaryVisibleOnYomitanPopup` to keep hover-mode primary subtitle visible while Yomitan popup is open
- Normalize trailing commas in config.example.jsonc
This commit is contained in:
2026-05-27 00:12:21 -07:00
parent 5b44981688
commit eb04ea97b1
49 changed files with 1711 additions and 662 deletions
+43 -1
View File
@@ -9,6 +9,7 @@ import type {
ResolvedControllerConfig,
RuntimeOptionId,
RuntimeOptionValue,
SubtitleMiningContext,
SubtitleSidebarSnapshot,
SubtitlePosition,
SubsyncManualRunRequest,
@@ -95,6 +96,7 @@ export interface IpcServiceDeps {
getAnilistQueueStatus: () => unknown;
retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>;
runAnilistPostWatchUpdateOnManualMark?: () => Promise<void>;
recordSubtitleMiningContext?: (context: SubtitleMiningContext | null) => void;
getCharacterDictionarySelection?: (searchTitle?: string) => Promise<unknown>;
setCharacterDictionarySelection?: (
mediaId: number,
@@ -175,6 +177,43 @@ interface IpcMainRegistrar {
handle: (channel: string, listener: (event: unknown, ...args: unknown[]) => unknown) => void;
}
function parseSubtitleMiningContext(payload: unknown): SubtitleMiningContext | null {
if (!payload || typeof payload !== 'object') {
return null;
}
const record = payload as Record<string, unknown>;
const source = record.source;
const text = record.text;
const startTime = record.startTime;
const endTime = record.endTime;
const capturedAtMs = record.capturedAtMs;
if (
source !== 'subtitle-sidebar' ||
typeof text !== 'string' ||
text.trim().length === 0 ||
typeof startTime !== 'number' ||
typeof endTime !== 'number' ||
!Number.isFinite(startTime) ||
!Number.isFinite(endTime) ||
endTime <= startTime
) {
return null;
}
const parsed: SubtitleMiningContext = {
source: 'subtitle-sidebar',
text,
startTime,
endTime,
};
if (typeof capturedAtMs === 'number' && Number.isFinite(capturedAtMs)) {
parsed.capturedAtMs = capturedAtMs;
}
return parsed;
}
export interface IpcDepsRuntimeOptions {
getMainWindow: () => WindowLike | null;
getVisibleOverlayVisibility: () => boolean;
@@ -230,6 +269,7 @@ export interface IpcDepsRuntimeOptions {
getAnilistQueueStatus: () => unknown;
retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>;
runAnilistPostWatchUpdateOnManualMark?: () => Promise<void>;
recordSubtitleMiningContext?: (context: SubtitleMiningContext | null) => void;
getCharacterDictionarySelection?: (searchTitle?: string) => Promise<unknown>;
setCharacterDictionarySelection?: (
mediaId: number,
@@ -257,6 +297,7 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
onOverlayModalOpened: options.onOverlayModalOpened,
onOverlayMouseInteractionChanged: options.onOverlayMouseInteractionChanged,
openYomitanSettings: options.openYomitanSettings,
recordSubtitleMiningContext: options.recordSubtitleMiningContext,
quitApp: options.quitApp,
toggleDevTools: () => {
const mainWindow = options.getMainWindow();
@@ -423,7 +464,8 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
deps.openYomitanSettings();
});
ipc.on(IPC_CHANNELS.command.recordYomitanLookup, () => {
ipc.on(IPC_CHANNELS.command.recordYomitanLookup, (_event: unknown, payload: unknown) => {
deps.recordSubtitleMiningContext?.(parseSubtitleMiningContext(payload));
deps.immersionTracker?.recordYomitanLookup();
});