mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
migrate subtitle style config to CSS declaration shape
- Flat style keys (fontFamily, fontSize, hoverTokenColor, etc.) consolidated into subtitleStyle.css, secondary.css, and subtitleSidebar.css objects - Hover token colors migrated to --subtitle-hover-token-color CSS custom properties - Plugin app-ping now checks result.status (0=running, 1=stopped) to avoid treating transient failures as stopped - Note-fields note type picker defaults to configured deck's note type before falling back to Kiku/Lapis - New migration for legacy ankiConnect N+1 config paths
This commit is contained in:
@@ -11,7 +11,8 @@ type WindowTrackerStub = {
|
||||
isTargetWindowMinimized?: () => boolean;
|
||||
};
|
||||
|
||||
function createMainWindowRecorder() {
|
||||
function createMainWindowRecorder(options: { emitShowImmediately?: boolean } = {}) {
|
||||
const emitShowImmediately = options.emitShowImmediately ?? true;
|
||||
const calls: string[] = [];
|
||||
const listeners = new Map<string, Array<() => void>>();
|
||||
let visible = false;
|
||||
@@ -25,6 +26,10 @@ function createMainWindowRecorder() {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
const emitShow = (): void => {
|
||||
visible = true;
|
||||
emit('show');
|
||||
};
|
||||
const window = {
|
||||
webContents: {},
|
||||
isDestroyed: () => false,
|
||||
@@ -39,14 +44,16 @@ function createMainWindowRecorder() {
|
||||
calls.push('hide');
|
||||
},
|
||||
show: () => {
|
||||
visible = true;
|
||||
calls.push('show');
|
||||
emit('show');
|
||||
if (emitShowImmediately) {
|
||||
emitShow();
|
||||
}
|
||||
},
|
||||
showInactive: () => {
|
||||
visible = true;
|
||||
calls.push('show-inactive');
|
||||
emit('show');
|
||||
if (emitShowImmediately) {
|
||||
emitShow();
|
||||
}
|
||||
},
|
||||
focus: () => {
|
||||
focused = true;
|
||||
@@ -81,6 +88,7 @@ function createMainWindowRecorder() {
|
||||
window,
|
||||
calls,
|
||||
getOpacity: () => opacity,
|
||||
emitShow,
|
||||
setContentReady: (nextContentReady: boolean) => {
|
||||
contentReady = nextContentReady;
|
||||
(
|
||||
@@ -267,6 +275,50 @@ test('tracked non-macOS overlay reapplies bounds after first show', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('tracked non-macOS overlay queues only one first-show bounds refresh', () => {
|
||||
const { window, calls, emitShow } = createMainWindowRecorder({ emitShowImmediately: false });
|
||||
let width = 1280;
|
||||
const tracker: WindowTrackerStub = {
|
||||
isTracking: () => true,
|
||||
getGeometry: () => ({ x: 0, y: 0, width, height: 720 }),
|
||||
};
|
||||
const run = () =>
|
||||
updateVisibleOverlayVisibility({
|
||||
visibleOverlayVisible: true,
|
||||
mainWindow: window as never,
|
||||
windowTracker: tracker as never,
|
||||
trackerNotReadyWarningShown: false,
|
||||
setTrackerNotReadyWarningShown: () => {},
|
||||
updateVisibleOverlayBounds: (geometry: { width: number }) => {
|
||||
calls.push(`update-bounds:${geometry.width}`);
|
||||
},
|
||||
ensureOverlayWindowLevel: () => {
|
||||
calls.push('ensure-level');
|
||||
},
|
||||
syncPrimaryOverlayWindowLayer: () => {
|
||||
calls.push('sync-layer');
|
||||
},
|
||||
enforceOverlayLayerOrder: () => {
|
||||
calls.push('enforce-order');
|
||||
},
|
||||
syncOverlayShortcuts: () => {
|
||||
calls.push('sync-shortcuts');
|
||||
},
|
||||
isMacOSPlatform: false,
|
||||
isWindowsPlatform: false,
|
||||
} as never);
|
||||
|
||||
run();
|
||||
width = 1440;
|
||||
run();
|
||||
emitShow();
|
||||
|
||||
assert.deepEqual(
|
||||
calls.filter((call) => call.startsWith('update-bounds:')),
|
||||
['update-bounds:1280', 'update-bounds:1440', 'update-bounds:1440'],
|
||||
);
|
||||
});
|
||||
|
||||
test('Windows visible overlay stays click-through and binds to mpv while tracked', () => {
|
||||
const { window, calls } = createMainWindowRecorder();
|
||||
const tracker: WindowTrackerStub = {
|
||||
|
||||
@@ -8,6 +8,7 @@ const pendingWindowsOverlayRevealTimeoutByWindow = new WeakMap<
|
||||
BrowserWindow,
|
||||
ReturnType<typeof setTimeout>
|
||||
>();
|
||||
const pendingFirstShowBoundsRefreshGeometry = new WeakMap<BrowserWindow, WindowGeometry>();
|
||||
function setOverlayWindowOpacity(window: BrowserWindow, opacity: number): void {
|
||||
const opacityCapableWindow = window as BrowserWindow & {
|
||||
setOpacity?: (opacity: number) => void;
|
||||
@@ -279,11 +280,20 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (pendingFirstShowBoundsRefreshGeometry.has(mainWindow)) {
|
||||
pendingFirstShowBoundsRefreshGeometry.set(mainWindow, geometry);
|
||||
return;
|
||||
}
|
||||
pendingFirstShowBoundsRefreshGeometry.set(mainWindow, geometry);
|
||||
mainWindow.once('show', () => {
|
||||
const pendingGeometry = pendingFirstShowBoundsRefreshGeometry.get(mainWindow);
|
||||
pendingFirstShowBoundsRefreshGeometry.delete(mainWindow);
|
||||
if (mainWindow.isDestroyed() || !mainWindow.isVisible()) {
|
||||
return;
|
||||
}
|
||||
args.updateVisibleOverlayBounds(geometry);
|
||||
if (pendingGeometry) {
|
||||
args.updateVisibleOverlayBounds(pendingGeometry);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface TokenizerServiceDeps {
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||
isKnownWord: (text: string) => boolean;
|
||||
getKnownWordMatchMode: () => NPlusOneMatchMode;
|
||||
getKnownWordsEnabled?: () => boolean;
|
||||
getJlptLevel: (text: string) => JlptLevel | null;
|
||||
getNPlusOneEnabled?: () => boolean;
|
||||
getJlptEnabled?: () => boolean;
|
||||
@@ -74,6 +75,7 @@ export interface TokenizerDepsRuntimeOptions {
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||
isKnownWord: (text: string) => boolean;
|
||||
getKnownWordMatchMode: () => NPlusOneMatchMode;
|
||||
getKnownWordsEnabled?: () => boolean;
|
||||
getJlptLevel: (text: string) => JlptLevel | null;
|
||||
getNPlusOneEnabled?: () => boolean;
|
||||
getJlptEnabled?: () => boolean;
|
||||
@@ -88,6 +90,7 @@ export interface TokenizerDepsRuntimeOptions {
|
||||
}
|
||||
|
||||
interface TokenizerAnnotationOptions {
|
||||
knownWordsEnabled: boolean;
|
||||
nPlusOneEnabled: boolean;
|
||||
jlptEnabled: boolean;
|
||||
nameMatchEnabled: boolean;
|
||||
@@ -119,18 +122,28 @@ function getKnownWordLookup(
|
||||
deps: TokenizerServiceDeps,
|
||||
options: TokenizerAnnotationOptions,
|
||||
): (text: string) => boolean {
|
||||
if (!options.nPlusOneEnabled) {
|
||||
if (!options.knownWordsEnabled && !options.nPlusOneEnabled) {
|
||||
return () => false;
|
||||
}
|
||||
return deps.isKnownWord;
|
||||
}
|
||||
|
||||
function needsMecabPosEnrichment(options: TokenizerAnnotationOptions): boolean {
|
||||
return options.nPlusOneEnabled || options.jlptEnabled || options.frequencyEnabled;
|
||||
return (
|
||||
options.knownWordsEnabled ||
|
||||
options.nPlusOneEnabled ||
|
||||
options.jlptEnabled ||
|
||||
options.frequencyEnabled
|
||||
);
|
||||
}
|
||||
|
||||
function hasAnyAnnotationEnabled(options: TokenizerAnnotationOptions): boolean {
|
||||
return options.nPlusOneEnabled || options.jlptEnabled || options.frequencyEnabled;
|
||||
return (
|
||||
options.knownWordsEnabled ||
|
||||
options.nPlusOneEnabled ||
|
||||
options.jlptEnabled ||
|
||||
options.frequencyEnabled
|
||||
);
|
||||
}
|
||||
|
||||
async function enrichTokensWithMecabAsync(
|
||||
@@ -211,6 +224,7 @@ export function createTokenizerDepsRuntime(
|
||||
setYomitanParserInitPromise: options.setYomitanParserInitPromise,
|
||||
isKnownWord: options.isKnownWord,
|
||||
getKnownWordMatchMode: options.getKnownWordMatchMode,
|
||||
getKnownWordsEnabled: options.getKnownWordsEnabled,
|
||||
getJlptLevel: options.getJlptLevel,
|
||||
getNPlusOneEnabled: options.getNPlusOneEnabled,
|
||||
getJlptEnabled: options.getJlptEnabled,
|
||||
@@ -662,8 +676,12 @@ function applyFrequencyRanks(
|
||||
}
|
||||
|
||||
function getAnnotationOptions(deps: TokenizerServiceDeps): TokenizerAnnotationOptions {
|
||||
const nPlusOneEnabled = deps.getNPlusOneEnabled?.() !== false;
|
||||
return {
|
||||
nPlusOneEnabled: deps.getNPlusOneEnabled?.() !== false,
|
||||
knownWordsEnabled: deps.getKnownWordsEnabled
|
||||
? deps.getKnownWordsEnabled() !== false
|
||||
: nPlusOneEnabled,
|
||||
nPlusOneEnabled,
|
||||
jlptEnabled: deps.getJlptEnabled?.() !== false,
|
||||
nameMatchEnabled: deps.getNameMatchEnabled?.() !== false,
|
||||
frequencyEnabled: deps.getFrequencyDictionaryEnabled?.() !== false,
|
||||
|
||||
@@ -56,6 +56,50 @@ test('annotateTokens known-word match mode uses headword vs surface', () => {
|
||||
assert.equal(surfaceResult[0]?.isKnown, false);
|
||||
});
|
||||
|
||||
test('annotateTokens marks known words when N+1 is disabled', () => {
|
||||
const tokens = [
|
||||
makeToken({ surface: '私', headword: '私', startPos: 0, endPos: 1 }),
|
||||
makeToken({ surface: '猫', headword: '猫', startPos: 1, endPos: 2 }),
|
||||
makeToken({ surface: '犬', headword: '犬', startPos: 2, endPos: 3 }),
|
||||
];
|
||||
|
||||
const result = annotateTokens(
|
||||
tokens,
|
||||
makeDeps({
|
||||
isKnownWord: (text) => text === '私' || text === '猫',
|
||||
}),
|
||||
{ nPlusOneEnabled: false, knownWordsEnabled: true },
|
||||
);
|
||||
|
||||
assert.equal(result[0]?.isKnown, true);
|
||||
assert.equal(result[0]?.isNPlusOneTarget, false);
|
||||
assert.equal(result[1]?.isKnown, true);
|
||||
assert.equal(result[1]?.isNPlusOneTarget, false);
|
||||
assert.equal(result[2]?.isKnown, false);
|
||||
assert.equal(result[2]?.isNPlusOneTarget, false);
|
||||
});
|
||||
|
||||
test('annotateTokens hides known-word marks while still using known words for N+1', () => {
|
||||
const tokens = [
|
||||
makeToken({ surface: '私', headword: '私', startPos: 0, endPos: 1 }),
|
||||
makeToken({ surface: '猫', headword: '猫', startPos: 1, endPos: 2 }),
|
||||
makeToken({ surface: '犬', headword: '犬', startPos: 2, endPos: 3 }),
|
||||
];
|
||||
|
||||
const result = annotateTokens(
|
||||
tokens,
|
||||
makeDeps({
|
||||
isKnownWord: (text) => text === '私' || text === '猫',
|
||||
}),
|
||||
{ nPlusOneEnabled: true, knownWordsEnabled: false, minSentenceWordsForNPlusOne: 3 },
|
||||
);
|
||||
|
||||
assert.equal(result[0]?.isKnown, false);
|
||||
assert.equal(result[1]?.isKnown, false);
|
||||
assert.equal(result[2]?.isKnown, false);
|
||||
assert.equal(result[2]?.isNPlusOneTarget, true);
|
||||
});
|
||||
|
||||
test('annotateTokens falls back to reading for known-word matches when headword lookup misses', () => {
|
||||
const tokens = [
|
||||
makeToken({
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface AnnotationStageDeps {
|
||||
}
|
||||
|
||||
export interface AnnotationStageOptions {
|
||||
knownWordsEnabled?: boolean;
|
||||
nPlusOneEnabled?: boolean;
|
||||
nameMatchEnabled?: boolean;
|
||||
jlptEnabled?: boolean;
|
||||
@@ -669,13 +670,16 @@ export function annotateTokens(
|
||||
): MergedToken[] {
|
||||
const pos1Exclusions = resolvePos1Exclusions(options);
|
||||
const pos2Exclusions = resolvePos2Exclusions(options);
|
||||
const knownWordsEnabled = options.knownWordsEnabled !== false;
|
||||
const nPlusOneEnabled = options.nPlusOneEnabled !== false;
|
||||
const nameMatchEnabled = options.nameMatchEnabled !== false;
|
||||
const frequencyEnabled = options.frequencyEnabled !== false;
|
||||
const jlptEnabled = options.jlptEnabled !== false;
|
||||
const shouldComputeKnownStatus = knownWordsEnabled || nPlusOneEnabled;
|
||||
const nPlusOneKnownStatuses: boolean[] = [];
|
||||
|
||||
// Single pass: compute known word status, frequency filtering, and JLPT level together
|
||||
const annotated = tokens.map((token) => {
|
||||
const annotated = tokens.map((token, index) => {
|
||||
if (
|
||||
sharedShouldExcludeTokenFromSubtitleAnnotations(token, {
|
||||
pos1Exclusions,
|
||||
@@ -686,6 +690,7 @@ export function annotateTokens(
|
||||
pos1Exclusions,
|
||||
pos2Exclusions,
|
||||
});
|
||||
nPlusOneKnownStatuses[index] = false;
|
||||
return {
|
||||
...strippedToken,
|
||||
isKnown: false,
|
||||
@@ -693,9 +698,10 @@ export function annotateTokens(
|
||||
}
|
||||
|
||||
const prioritizedNameMatch = nameMatchEnabled && token.isNameMatch === true;
|
||||
const isKnown = nPlusOneEnabled
|
||||
const isKnownForMatching = shouldComputeKnownStatus
|
||||
? computeTokenKnownStatus(token, deps.isKnownWord, deps.knownWordMatchMode)
|
||||
: false;
|
||||
nPlusOneKnownStatuses[index] = isKnownForMatching;
|
||||
|
||||
const frequencyRank =
|
||||
frequencyEnabled && !prioritizedNameMatch
|
||||
@@ -709,7 +715,7 @@ export function annotateTokens(
|
||||
|
||||
return {
|
||||
...token,
|
||||
isKnown,
|
||||
isKnown: knownWordsEnabled ? isKnownForMatching : false,
|
||||
isNPlusOneTarget: nPlusOneEnabled && !prioritizedNameMatch ? token.isNPlusOneTarget : false,
|
||||
frequencyRank,
|
||||
jlptLevel,
|
||||
@@ -728,13 +734,21 @@ export function annotateTokens(
|
||||
? minSentenceWordsForNPlusOne
|
||||
: 3;
|
||||
|
||||
const nPlusOneMarked = markNPlusOneTargets(
|
||||
annotated,
|
||||
sanitizedMinSentenceWordsForNPlusOne,
|
||||
pos1Exclusions,
|
||||
pos2Exclusions,
|
||||
options.sourceText,
|
||||
);
|
||||
const nPlusOneMarked = nPlusOneEnabled
|
||||
? markNPlusOneTargets(
|
||||
annotated.map((token, index) => ({
|
||||
...token,
|
||||
isKnown: nPlusOneKnownStatuses[index] ?? false,
|
||||
})),
|
||||
sanitizedMinSentenceWordsForNPlusOne,
|
||||
pos1Exclusions,
|
||||
pos2Exclusions,
|
||||
options.sourceText,
|
||||
).map((token, index) => ({
|
||||
...annotated[index]!,
|
||||
isNPlusOneTarget: token.isNPlusOneTarget,
|
||||
}))
|
||||
: annotated;
|
||||
|
||||
if (!nameMatchEnabled) {
|
||||
return nPlusOneMarked;
|
||||
|
||||
Reference in New Issue
Block a user