mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-28 04:19:27 -07:00
fix: address CodeRabbit review comments
This commit is contained in:
@@ -3086,6 +3086,27 @@ test('tokenizeSubtitle uses Yomitan word classes to classify standalone particle
|
|||||||
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
|
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tokenizeSubtitle uses Yomitan word classes to classify auxiliary subclasses', async () => {
|
||||||
|
const result = await tokenizeSubtitle(
|
||||||
|
'です',
|
||||||
|
makeDepsFromYomitanTokens(
|
||||||
|
[{ surface: 'です', reading: 'です', headword: 'です', wordClasses: ['aux-v'] }],
|
||||||
|
{
|
||||||
|
getFrequencyDictionaryEnabled: () => true,
|
||||||
|
getFrequencyRank: () => 10,
|
||||||
|
getJlptLevel: () => 'N5',
|
||||||
|
tokenizeWithMecab: async () => null,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.tokens?.length, 1);
|
||||||
|
assert.equal(result.tokens?.[0]?.partOfSpeech, PartOfSpeech.bound_auxiliary);
|
||||||
|
assert.equal(result.tokens?.[0]?.pos1, '助動詞');
|
||||||
|
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
|
||||||
|
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
test('tokenizeSubtitle fills detailed MeCab POS when Yomitan word class supplies coarse POS', async () => {
|
test('tokenizeSubtitle fills detailed MeCab POS when Yomitan word class supplies coarse POS', async () => {
|
||||||
const result = await tokenizeSubtitle(
|
const result = await tokenizeSubtitle(
|
||||||
'は',
|
'は',
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ function resolvePartOfSpeechFromYomitanWordClasses(wordClasses: string[]): {
|
|||||||
if (wordClasses.includes('prt')) {
|
if (wordClasses.includes('prt')) {
|
||||||
return { partOfSpeech: PartOfSpeech.particle, pos1: '助詞' };
|
return { partOfSpeech: PartOfSpeech.particle, pos1: '助詞' };
|
||||||
}
|
}
|
||||||
if (wordClasses.includes('aux')) {
|
if (wordClasses.some((wordClass) => wordClass === 'aux' || wordClass.startsWith('aux-'))) {
|
||||||
return { partOfSpeech: PartOfSpeech.bound_auxiliary, pos1: '助動詞' };
|
return { partOfSpeech: PartOfSpeech.bound_auxiliary, pos1: '助動詞' };
|
||||||
}
|
}
|
||||||
if (wordClasses.some((wordClass) => wordClass.startsWith('v'))) {
|
if (wordClasses.some((wordClass) => wordClass.startsWith('v'))) {
|
||||||
|
|||||||
@@ -713,6 +713,57 @@ test('annotateTokens N+1 sentence word count respects source punctuation gaps om
|
|||||||
assert.equal(result[3]?.isNPlusOneTarget, false);
|
assert.equal(result[3]?.isNPlusOneTarget, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('annotateTokens N+1 sentence word count normalizes line breaks before gap detection', () => {
|
||||||
|
const tokens = [
|
||||||
|
makeToken({
|
||||||
|
surface: '私',
|
||||||
|
headword: '私',
|
||||||
|
pos1: '名詞',
|
||||||
|
startPos: 0,
|
||||||
|
endPos: 1,
|
||||||
|
}),
|
||||||
|
makeToken({
|
||||||
|
surface: '猫',
|
||||||
|
headword: '猫',
|
||||||
|
pos1: '名詞',
|
||||||
|
startPos: 2,
|
||||||
|
endPos: 3,
|
||||||
|
}),
|
||||||
|
makeToken({
|
||||||
|
surface: '犬',
|
||||||
|
headword: '犬',
|
||||||
|
pos1: '名詞',
|
||||||
|
startPos: 3,
|
||||||
|
endPos: 4,
|
||||||
|
}),
|
||||||
|
makeToken({
|
||||||
|
surface: 'ふざけん',
|
||||||
|
headword: 'ふざける',
|
||||||
|
partOfSpeech: PartOfSpeech.verb,
|
||||||
|
pos1: '動詞',
|
||||||
|
pos2: '自立',
|
||||||
|
startPos: 5,
|
||||||
|
endPos: 9,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = annotateTokens(
|
||||||
|
tokens,
|
||||||
|
makeDeps({
|
||||||
|
isKnownWord: (text) => text === '私' || text === '猫' || text === '犬',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
minSentenceWordsForNPlusOne: 3,
|
||||||
|
sourceText: '私\r\n猫犬!ふざけんなよ!',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result[0]?.isNPlusOneTarget, false);
|
||||||
|
assert.equal(result[1]?.isNPlusOneTarget, false);
|
||||||
|
assert.equal(result[2]?.isNPlusOneTarget, false);
|
||||||
|
assert.equal(result[3]?.isNPlusOneTarget, false);
|
||||||
|
});
|
||||||
|
|
||||||
test('annotateTokens applies configured pos1 exclusions to both frequency and N+1', () => {
|
test('annotateTokens applies configured pos1 exclusions to both frequency and N+1', () => {
|
||||||
const tokens = [
|
const tokens = [
|
||||||
makeToken({
|
makeToken({
|
||||||
|
|||||||
59
src/main.ts
59
src/main.ts
@@ -33,6 +33,10 @@ import {
|
|||||||
import { applyControllerConfigUpdate } from './main/controller-config-update.js';
|
import { applyControllerConfigUpdate } from './main/controller-config-update.js';
|
||||||
import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open';
|
import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open';
|
||||||
import { createDiscordRpcClient } from './main/runtime/discord-rpc-client.js';
|
import { createDiscordRpcClient } from './main/runtime/discord-rpc-client.js';
|
||||||
|
import {
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts,
|
||||||
|
scheduleLinuxVisibleOverlayFullscreenRefreshBurst,
|
||||||
|
} from './main/runtime/linux-mpv-fullscreen-overlay-refresh';
|
||||||
import { mergeAiConfig } from './ai/config';
|
import { mergeAiConfig } from './ai/config';
|
||||||
|
|
||||||
function getPasswordStoreArg(argv: string[]): string | null {
|
function getPasswordStoreArg(argv: string[]): string | null {
|
||||||
@@ -1911,7 +1915,6 @@ const WINDOWS_VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS = [0, 25, 100, 250] as cons
|
|||||||
const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const;
|
const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const;
|
||||||
const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75;
|
const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75;
|
||||||
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
|
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
|
||||||
const LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS = [0, 50, 150, 300, 600] as const;
|
|
||||||
let windowsVisibleOverlayBlurRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
let windowsVisibleOverlayBlurRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||||
let windowsVisibleOverlayZOrderRetryTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
let windowsVisibleOverlayZOrderRetryTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||||
let windowsVisibleOverlayZOrderSyncInFlight = false;
|
let windowsVisibleOverlayZOrderSyncInFlight = false;
|
||||||
@@ -1919,7 +1922,6 @@ let windowsVisibleOverlayZOrderSyncQueued = false;
|
|||||||
let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
|
let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
||||||
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
||||||
let linuxMpvFullscreenOverlayRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
|
||||||
|
|
||||||
function clearWindowsVisibleOverlayBlurRefreshTimeouts(): void {
|
function clearWindowsVisibleOverlayBlurRefreshTimeouts(): void {
|
||||||
for (const timeout of windowsVisibleOverlayBlurRefreshTimeouts) {
|
for (const timeout of windowsVisibleOverlayBlurRefreshTimeouts) {
|
||||||
@@ -1935,48 +1937,6 @@ function clearWindowsVisibleOverlayZOrderRetryTimeouts(): void {
|
|||||||
windowsVisibleOverlayZOrderRetryTimeouts = [];
|
windowsVisibleOverlayZOrderRetryTimeouts = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLinuxMpvFullscreenOverlayRefreshTimeouts(): void {
|
|
||||||
for (const timeout of linuxMpvFullscreenOverlayRefreshTimeouts) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
linuxMpvFullscreenOverlayRefreshTimeouts = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshLinuxVisibleOverlayAfterMpvFullscreenChange(): void {
|
|
||||||
if (process.platform !== 'linux' || !overlayManager.getVisibleOverlayVisible()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
|
||||||
|
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
|
||||||
if (!mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow.hide();
|
|
||||||
mainWindow.showInactive();
|
|
||||||
ensureOverlayWindowLevel(mainWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleLinuxVisibleOverlayFullscreenRefreshBurst(): void {
|
|
||||||
if (process.platform !== 'linux') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
|
|
||||||
for (const delayMs of LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS) {
|
|
||||||
const refreshTimeout = setTimeout(() => {
|
|
||||||
linuxMpvFullscreenOverlayRefreshTimeouts = linuxMpvFullscreenOverlayRefreshTimeouts.filter(
|
|
||||||
(timeout) => timeout !== refreshTimeout,
|
|
||||||
);
|
|
||||||
refreshLinuxVisibleOverlayAfterMpvFullscreenChange();
|
|
||||||
}, delayMs);
|
|
||||||
refreshTimeout.unref?.();
|
|
||||||
linuxMpvFullscreenOverlayRefreshTimeouts.push(refreshTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWindowsNativeWindowHandle(window: BrowserWindow): string {
|
function getWindowsNativeWindowHandle(window: BrowserWindow): string {
|
||||||
const handle = window.getNativeWindowHandle();
|
const handle = window.getNativeWindowHandle();
|
||||||
return handle.length >= 8
|
return handle.length >= 8
|
||||||
@@ -3146,6 +3106,8 @@ const {
|
|||||||
stopTexthookerService: () => texthookerService.stop(),
|
stopTexthookerService: () => texthookerService.stop(),
|
||||||
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
||||||
clearWindowsVisibleOverlayForegroundPollLoop(),
|
clearWindowsVisibleOverlayForegroundPollLoop(),
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () =>
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts(),
|
||||||
getMainOverlayWindow: () => overlayManager.getMainWindow(),
|
getMainOverlayWindow: () => overlayManager.getMainWindow(),
|
||||||
clearMainOverlayWindow: () => overlayManager.setMainWindow(null),
|
clearMainOverlayWindow: () => overlayManager.setMainWindow(null),
|
||||||
getModalOverlayWindow: () => overlayManager.getModalWindow(),
|
getModalOverlayWindow: () => overlayManager.getModalWindow(),
|
||||||
@@ -3851,7 +3813,14 @@ const {
|
|||||||
lastObservedTimePos = time;
|
lastObservedTimePos = time;
|
||||||
},
|
},
|
||||||
onFullscreenChange: () => {
|
onFullscreenChange: () => {
|
||||||
scheduleLinuxVisibleOverlayFullscreenRefreshBurst();
|
scheduleLinuxVisibleOverlayFullscreenRefreshBurst({
|
||||||
|
overlayManager: {
|
||||||
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
},
|
||||||
|
overlayVisibilityRuntime,
|
||||||
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onSubtitleTrackChange: (sid) => {
|
onSubtitleTrackChange: (sid) => {
|
||||||
scheduleSubtitlePrefetchRefresh();
|
scheduleSubtitlePrefetchRefresh();
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
|
|||||||
stopTexthookerService: () => calls.push('stop-texthooker'),
|
stopTexthookerService: () => calls.push('stop-texthooker'),
|
||||||
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
||||||
calls.push('clear-windows-visible-overlay-poll'),
|
calls.push('clear-windows-visible-overlay-poll'),
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () =>
|
||||||
|
calls.push('clear-linux-mpv-fullscreen-overlay-refresh-timeouts'),
|
||||||
destroyMainOverlayWindow: () => calls.push('destroy-main-overlay-window'),
|
destroyMainOverlayWindow: () => calls.push('destroy-main-overlay-window'),
|
||||||
destroyModalOverlayWindow: () => calls.push('destroy-modal-overlay-window'),
|
destroyModalOverlayWindow: () => calls.push('destroy-modal-overlay-window'),
|
||||||
destroyYomitanParserWindow: () => calls.push('destroy-yomitan-window'),
|
destroyYomitanParserWindow: () => calls.push('destroy-yomitan-window'),
|
||||||
@@ -42,10 +44,11 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
assert.equal(calls.length, 29);
|
assert.equal(calls.length, 30);
|
||||||
assert.equal(calls[0], 'destroy-tray');
|
assert.equal(calls[0], 'destroy-tray');
|
||||||
assert.equal(calls[calls.length - 1], 'stop-discord-presence');
|
assert.equal(calls[calls.length - 1], 'stop-discord-presence');
|
||||||
assert.ok(calls.includes('clear-windows-visible-overlay-poll'));
|
assert.ok(calls.includes('clear-windows-visible-overlay-poll'));
|
||||||
|
assert.ok(calls.includes('clear-linux-mpv-fullscreen-overlay-refresh-timeouts'));
|
||||||
assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket'));
|
assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export function createOnWillQuitCleanupHandler(deps: {
|
|||||||
stopSubtitleWebsocket: () => void;
|
stopSubtitleWebsocket: () => void;
|
||||||
stopTexthookerService: () => void;
|
stopTexthookerService: () => void;
|
||||||
clearWindowsVisibleOverlayForegroundPollLoop: () => void;
|
clearWindowsVisibleOverlayForegroundPollLoop: () => void;
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => void;
|
||||||
destroyMainOverlayWindow: () => void;
|
destroyMainOverlayWindow: () => void;
|
||||||
destroyModalOverlayWindow: () => void;
|
destroyModalOverlayWindow: () => void;
|
||||||
destroyYomitanParserWindow: () => void;
|
destroyYomitanParserWindow: () => void;
|
||||||
@@ -38,6 +39,7 @@ export function createOnWillQuitCleanupHandler(deps: {
|
|||||||
deps.stopSubtitleWebsocket();
|
deps.stopSubtitleWebsocket();
|
||||||
deps.stopTexthookerService();
|
deps.stopTexthookerService();
|
||||||
deps.clearWindowsVisibleOverlayForegroundPollLoop();
|
deps.clearWindowsVisibleOverlayForegroundPollLoop();
|
||||||
|
deps.clearLinuxMpvFullscreenOverlayRefreshTimeouts();
|
||||||
deps.destroyMainOverlayWindow();
|
deps.destroyMainOverlayWindow();
|
||||||
deps.destroyModalOverlayWindow();
|
deps.destroyModalOverlayWindow();
|
||||||
deps.destroyYomitanParserWindow();
|
deps.destroyYomitanParserWindow();
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ test('cleanup deps builder returns handlers that guard optional runtime objects'
|
|||||||
stopTexthookerService: () => calls.push('stop-texthooker'),
|
stopTexthookerService: () => calls.push('stop-texthooker'),
|
||||||
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
||||||
calls.push('clear-windows-visible-overlay-foreground-poll-loop'),
|
calls.push('clear-windows-visible-overlay-foreground-poll-loop'),
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () =>
|
||||||
|
calls.push('clear-linux-mpv-fullscreen-overlay-refresh-timeouts'),
|
||||||
getMainOverlayWindow: () => ({
|
getMainOverlayWindow: () => ({
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
destroy: () => calls.push('destroy-main-overlay-window'),
|
destroy: () => calls.push('destroy-main-overlay-window'),
|
||||||
@@ -88,6 +90,7 @@ test('cleanup deps builder returns handlers that guard optional runtime objects'
|
|||||||
assert.ok(calls.includes('stop-jellyfin-remote'));
|
assert.ok(calls.includes('stop-jellyfin-remote'));
|
||||||
assert.ok(calls.includes('stop-discord-presence'));
|
assert.ok(calls.includes('stop-discord-presence'));
|
||||||
assert.ok(calls.includes('clear-windows-visible-overlay-foreground-poll-loop'));
|
assert.ok(calls.includes('clear-windows-visible-overlay-foreground-poll-loop'));
|
||||||
|
assert.ok(calls.includes('clear-linux-mpv-fullscreen-overlay-refresh-timeouts'));
|
||||||
assert.equal(reconnectTimer, null);
|
assert.equal(reconnectTimer, null);
|
||||||
assert.equal(immersionTracker, null);
|
assert.equal(immersionTracker, null);
|
||||||
});
|
});
|
||||||
@@ -103,6 +106,7 @@ test('cleanup deps builder skips destroyed yomitan window', () => {
|
|||||||
stopSubtitleWebsocket: () => {},
|
stopSubtitleWebsocket: () => {},
|
||||||
stopTexthookerService: () => {},
|
stopTexthookerService: () => {},
|
||||||
clearWindowsVisibleOverlayForegroundPollLoop: () => {},
|
clearWindowsVisibleOverlayForegroundPollLoop: () => {},
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => {},
|
||||||
getMainOverlayWindow: () => ({
|
getMainOverlayWindow: () => ({
|
||||||
isDestroyed: () => true,
|
isDestroyed: () => true,
|
||||||
destroy: () => calls.push('destroy-main-overlay-window'),
|
destroy: () => calls.push('destroy-main-overlay-window'),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
|||||||
stopSubtitleWebsocket: () => void;
|
stopSubtitleWebsocket: () => void;
|
||||||
stopTexthookerService: () => void;
|
stopTexthookerService: () => void;
|
||||||
clearWindowsVisibleOverlayForegroundPollLoop: () => void;
|
clearWindowsVisibleOverlayForegroundPollLoop: () => void;
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => void;
|
||||||
getMainOverlayWindow: () => DestroyableWindow | null;
|
getMainOverlayWindow: () => DestroyableWindow | null;
|
||||||
clearMainOverlayWindow: () => void;
|
clearMainOverlayWindow: () => void;
|
||||||
getModalOverlayWindow: () => DestroyableWindow | null;
|
getModalOverlayWindow: () => DestroyableWindow | null;
|
||||||
@@ -67,6 +68,8 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
|||||||
stopTexthookerService: () => deps.stopTexthookerService(),
|
stopTexthookerService: () => deps.stopTexthookerService(),
|
||||||
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
||||||
deps.clearWindowsVisibleOverlayForegroundPollLoop(),
|
deps.clearWindowsVisibleOverlayForegroundPollLoop(),
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () =>
|
||||||
|
deps.clearLinuxMpvFullscreenOverlayRefreshTimeouts(),
|
||||||
destroyMainOverlayWindow: () => {
|
destroyMainOverlayWindow: () => {
|
||||||
const window = deps.getMainOverlayWindow();
|
const window = deps.getMainOverlayWindow();
|
||||||
if (!window) return;
|
if (!window) return;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
|
|||||||
stopSubtitleWebsocket: () => {},
|
stopSubtitleWebsocket: () => {},
|
||||||
stopTexthookerService: () => {},
|
stopTexthookerService: () => {},
|
||||||
clearWindowsVisibleOverlayForegroundPollLoop: () => {},
|
clearWindowsVisibleOverlayForegroundPollLoop: () => {},
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => {},
|
||||||
getMainOverlayWindow: () => null,
|
getMainOverlayWindow: () => null,
|
||||||
clearMainOverlayWindow: () => {},
|
clearMainOverlayWindow: () => {},
|
||||||
getModalOverlayWindow: () => null,
|
getModalOverlayWindow: () => null,
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts,
|
||||||
|
scheduleLinuxVisibleOverlayFullscreenRefreshBurst,
|
||||||
|
} from './linux-mpv-fullscreen-overlay-refresh';
|
||||||
|
|
||||||
|
test('linux mpv fullscreen overlay refresh burst schedules overlay refresh work on linux', async () => {
|
||||||
|
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
configurable: true,
|
||||||
|
value: 'linux',
|
||||||
|
});
|
||||||
|
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
scheduleLinuxVisibleOverlayFullscreenRefreshBurst({
|
||||||
|
overlayManager: {
|
||||||
|
getMainWindow: () =>
|
||||||
|
({
|
||||||
|
hide: () => calls.push('hide'),
|
||||||
|
isDestroyed: () => false,
|
||||||
|
isVisible: () => true,
|
||||||
|
showInactive: () => calls.push('showInactive'),
|
||||||
|
}) as never,
|
||||||
|
getVisibleOverlayVisible: () => true,
|
||||||
|
},
|
||||||
|
overlayVisibilityRuntime: {
|
||||||
|
updateVisibleOverlayVisibility: () => calls.push('updateVisibleOverlayVisibility'),
|
||||||
|
},
|
||||||
|
ensureOverlayWindowLevel: () => calls.push('ensureOverlayWindowLevel'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
|
assert.ok(calls.includes('updateVisibleOverlayVisibility'));
|
||||||
|
assert.ok(calls.includes('hide'));
|
||||||
|
assert.ok(calls.includes('showInactive'));
|
||||||
|
assert.ok(calls.includes('ensureOverlayWindowLevel'));
|
||||||
|
} finally {
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
|
||||||
|
if (originalPlatformDescriptor) {
|
||||||
|
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
68
src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
Normal file
68
src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
type LinuxMpvFullscreenOverlayWindow = {
|
||||||
|
hide: () => void;
|
||||||
|
isDestroyed: () => boolean;
|
||||||
|
isVisible: () => boolean;
|
||||||
|
showInactive: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LinuxMpvFullscreenOverlayRefreshDeps = {
|
||||||
|
overlayManager: {
|
||||||
|
getMainWindow: () => LinuxMpvFullscreenOverlayWindow | null;
|
||||||
|
getVisibleOverlayVisible: () => boolean;
|
||||||
|
};
|
||||||
|
overlayVisibilityRuntime: {
|
||||||
|
updateVisibleOverlayVisibility: () => void;
|
||||||
|
};
|
||||||
|
ensureOverlayWindowLevel: (window: LinuxMpvFullscreenOverlayWindow) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS = [0, 50, 150, 300, 600] as const;
|
||||||
|
let linuxMpvFullscreenOverlayRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||||
|
|
||||||
|
function clearLinuxMpvFullscreenOverlayRefreshTimeouts(): void {
|
||||||
|
for (const timeout of linuxMpvFullscreenOverlayRefreshTimeouts) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
linuxMpvFullscreenOverlayRefreshTimeouts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshLinuxVisibleOverlayAfterMpvFullscreenChange(
|
||||||
|
deps: LinuxMpvFullscreenOverlayRefreshDeps,
|
||||||
|
): void {
|
||||||
|
if (process.platform !== 'linux' || !deps.overlayManager.getVisibleOverlayVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||||
|
|
||||||
|
const mainWindow = deps.overlayManager.getMainWindow();
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.hide();
|
||||||
|
mainWindow.showInactive();
|
||||||
|
deps.ensureOverlayWindowLevel(mainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scheduleLinuxVisibleOverlayFullscreenRefreshBurst(
|
||||||
|
deps: LinuxMpvFullscreenOverlayRefreshDeps,
|
||||||
|
): void {
|
||||||
|
if (process.platform !== 'linux') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
|
||||||
|
for (const delayMs of LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS) {
|
||||||
|
const refreshTimeout = setTimeout(() => {
|
||||||
|
linuxMpvFullscreenOverlayRefreshTimeouts = linuxMpvFullscreenOverlayRefreshTimeouts.filter(
|
||||||
|
(timeout) => timeout !== refreshTimeout,
|
||||||
|
);
|
||||||
|
refreshLinuxVisibleOverlayAfterMpvFullscreenChange(deps);
|
||||||
|
}, delayMs);
|
||||||
|
refreshTimeout.unref?.();
|
||||||
|
linuxMpvFullscreenOverlayRefreshTimeouts.push(refreshTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { clearLinuxMpvFullscreenOverlayRefreshTimeouts };
|
||||||
@@ -177,8 +177,7 @@ export function mergeTokens(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result: MergedToken[] = [];
|
const result: MergedToken[] = [];
|
||||||
const normalizedSourceText =
|
const normalizedSourceText = normalizeSourceTextForTokenOffsets(sourceText);
|
||||||
typeof sourceText === 'string' ? sourceText.replace(/\r?\n/g, ' ').trim() : null;
|
|
||||||
let charOffset = 0;
|
let charOffset = 0;
|
||||||
let sourceCursor = 0;
|
let sourceCursor = 0;
|
||||||
let lastStandaloneToken: Token | null = null;
|
let lastStandaloneToken: Token | null = null;
|
||||||
@@ -191,7 +190,9 @@ export function mergeTokens(
|
|||||||
|
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
const matchedStart =
|
const matchedStart =
|
||||||
normalizedSourceText !== null ? normalizedSourceText.indexOf(token.word, sourceCursor) : -1;
|
typeof normalizedSourceText === 'string'
|
||||||
|
? normalizedSourceText.indexOf(token.word, sourceCursor)
|
||||||
|
: -1;
|
||||||
const start = matchedStart >= sourceCursor ? matchedStart : charOffset;
|
const start = matchedStart >= sourceCursor ? matchedStart : charOffset;
|
||||||
const end = start + token.word.length;
|
const end = start + token.word.length;
|
||||||
charOffset = end;
|
charOffset = end;
|
||||||
@@ -302,6 +303,10 @@ function isKanaOnlyText(text: string): boolean {
|
|||||||
return normalized.length > 0 && Array.from(normalized).every((char) => isKanaChar(char));
|
return normalized.length > 0 && Array.from(normalized).every((char) => isKanaChar(char));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSourceTextForTokenOffsets(sourceText: string | undefined): string | undefined {
|
||||||
|
return typeof sourceText === 'string' ? sourceText.replace(/\r?\n/g, ' ').trim() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function isNPlusOneCandidateToken(
|
export function isNPlusOneCandidateToken(
|
||||||
token: MergedToken,
|
token: MergedToken,
|
||||||
pos1Exclusions: ReadonlySet<string> = N_PLUS_ONE_IGNORED_POS1,
|
pos1Exclusions: ReadonlySet<string> = N_PLUS_ONE_IGNORED_POS1,
|
||||||
@@ -394,8 +399,7 @@ export function markNPlusOneTargets(
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedSourceText =
|
const normalizedSourceText = normalizeSourceTextForTokenOffsets(sourceText);
|
||||||
typeof sourceText === 'string' ? sourceText.replace(/\r?\n/g, ' ').trim() : undefined;
|
|
||||||
|
|
||||||
const markedTokens = tokens.map((token) => ({
|
const markedTokens = tokens.map((token) => ({
|
||||||
...token,
|
...token,
|
||||||
|
|||||||
@@ -302,11 +302,10 @@ export class HyprlandWindowTracker extends BaseWindowTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private scheduleGeometryPollBurst(): void {
|
private scheduleGeometryPollBurst(): void {
|
||||||
this.pollGeometry();
|
|
||||||
for (const timeout of this.pollTimeouts) {
|
for (const timeout of this.pollTimeouts) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
this.pollTimeouts = [50, 150, 300].map((delayMs) => {
|
this.pollTimeouts = [0, 50, 150, 300].map((delayMs) => {
|
||||||
const pollTimeout = setTimeout(() => {
|
const pollTimeout = setTimeout(() => {
|
||||||
this.pollTimeouts = this.pollTimeouts.filter((timeout) => timeout !== pollTimeout);
|
this.pollTimeouts = this.pollTimeouts.filter((timeout) => timeout !== pollTimeout);
|
||||||
this.pollGeometry();
|
this.pollGeometry();
|
||||||
|
|||||||
Reference in New Issue
Block a user