mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
build(ts): enable noUncheckedIndexedAccess and isolatedModules
This commit is contained in:
@@ -411,7 +411,7 @@ export class AnkiIntegration {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteInfo = notesInfo[0];
|
||||
const noteInfo = notesInfo[0]!;
|
||||
this.appendKnownWordsFromNoteInfo(noteInfo);
|
||||
const fields = this.extractFields(noteInfo.fields);
|
||||
|
||||
@@ -1000,13 +1000,13 @@ export class AnkiIntegration {
|
||||
private extractLastSoundTag(value: string): string {
|
||||
const matches = value.match(/\[sound:[^\]]+\]/g);
|
||||
if (!matches || matches.length === 0) return '';
|
||||
return matches[matches.length - 1];
|
||||
return matches[matches.length - 1]!;
|
||||
}
|
||||
|
||||
private extractLastImageTag(value: string): string {
|
||||
const matches = value.match(/<img\b[^>]*>/gi);
|
||||
if (!matches || matches.length === 0) return '';
|
||||
return matches[matches.length - 1];
|
||||
return matches[matches.length - 1]!;
|
||||
}
|
||||
|
||||
private extractImageTags(value: string): string[] {
|
||||
@@ -1472,7 +1472,7 @@ export class AnkiIntegration {
|
||||
log.warn('Keep note not found:', keepNoteId);
|
||||
return;
|
||||
}
|
||||
const keepNoteInfo = keepNotesInfo[0];
|
||||
const keepNoteInfo = keepNotesInfo[0]!;
|
||||
const mergedFields = await this.computeFieldGroupingMergedFields(
|
||||
keepNoteId,
|
||||
deleteNoteId,
|
||||
@@ -1539,7 +1539,7 @@ export class AnkiIntegration {
|
||||
if (!originalNotesInfo || originalNotesInfo.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const originalNoteInfo = originalNotesInfo[0];
|
||||
const originalNoteInfo = originalNotesInfo[0]!;
|
||||
const sentenceCardConfig = this.getEffectiveSentenceCardConfig();
|
||||
|
||||
const originalFields = this.extractFields(originalNoteInfo.fields);
|
||||
|
||||
@@ -199,7 +199,7 @@ export class CardCreationService {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteInfo = notesInfoResult[0];
|
||||
const noteInfo = notesInfoResult[0]!;
|
||||
const fields = this.deps.extractFields(noteInfo.fields);
|
||||
const expressionText = fields.expression || fields.word || '';
|
||||
const sentenceAudioField = this.getResolvedSentenceAudioFieldName(noteInfo);
|
||||
@@ -366,7 +366,7 @@ export class CardCreationService {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteInfo = notesInfoResult[0];
|
||||
const noteInfo = notesInfoResult[0]!;
|
||||
const fields = this.deps.extractFields(noteInfo.fields);
|
||||
const expressionText = fields.expression || fields.word || '';
|
||||
|
||||
@@ -536,7 +536,7 @@ export class CardCreationService {
|
||||
const noteInfoResult = await this.deps.client.notesInfo([noteId]);
|
||||
const noteInfos = noteInfoResult as CardCreationNoteInfo[];
|
||||
if (noteInfos.length > 0) {
|
||||
const createdNoteInfo = noteInfos[0];
|
||||
const createdNoteInfo = noteInfos[0]!;
|
||||
this.deps.appendKnownWordsFromNoteInfo(createdNoteInfo);
|
||||
resolvedSentenceAudioField =
|
||||
this.deps.resolveNoteFieldName(createdNoteInfo, audioFieldName) || audioFieldName;
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function findDuplicateNote(
|
||||
): Promise<number | null> {
|
||||
let fieldName = '';
|
||||
for (const name of Object.keys(noteInfo.fields)) {
|
||||
if (['word', 'expression'].includes(name.toLowerCase()) && noteInfo.fields[name].value) {
|
||||
if (['word', 'expression'].includes(name.toLowerCase()) && noteInfo.fields[name]?.value) {
|
||||
fieldName = name;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export class FieldGroupingService {
|
||||
this.deps.showOsdNotification('Card not found');
|
||||
return;
|
||||
}
|
||||
const noteInfoBeforeUpdate = notesInfo[0];
|
||||
const noteInfoBeforeUpdate = notesInfo[0]!;
|
||||
const fields = this.deps.extractFields(noteInfoBeforeUpdate.fields);
|
||||
const expressionText = fields.expression || fields.word || '';
|
||||
if (!expressionText) {
|
||||
@@ -135,7 +135,7 @@ export class FieldGroupingService {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteInfo = refreshedInfo[0];
|
||||
const noteInfo = refreshedInfo[0]!;
|
||||
|
||||
if (sentenceCardConfig.kikuFieldGrouping === 'auto') {
|
||||
await this.deps.handleFieldGroupingAuto(
|
||||
|
||||
@@ -236,7 +236,7 @@ export class KnownWordCacheManager {
|
||||
}
|
||||
|
||||
if (decks.length === 1) {
|
||||
return `deck:"${escapeAnkiSearchValue(decks[0])}"`;
|
||||
return `deck:"${escapeAnkiSearchValue(decks[0]!)}"`;
|
||||
}
|
||||
|
||||
const deckQueries = decks.map((deck) => `deck:"${escapeAnkiSearchValue(deck)}"`);
|
||||
|
||||
@@ -115,7 +115,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (!arg.startsWith('--')) continue;
|
||||
if (!arg || !arg.startsWith('--')) continue;
|
||||
|
||||
if (arg === '--background') args.background = true;
|
||||
else if (arg === '--start') args.start = true;
|
||||
|
||||
@@ -54,7 +54,7 @@ export function resolveConfigDir(options: ConfigPathOptions): string {
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(baseDirs[0], getDefaultAppName(options));
|
||||
return path.join(baseDirs[0]!, getDefaultAppName(options));
|
||||
}
|
||||
|
||||
export function resolveConfigFilePath(options: ConfigPathOptions): string {
|
||||
@@ -72,5 +72,5 @@ export function resolveConfigFilePath(options: ConfigPathOptions): string {
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(baseDirs[0], getDefaultAppName(options), DEFAULT_FILE_NAMES[0]);
|
||||
return path.join(baseDirs[0]!, getDefaultAppName(options), DEFAULT_FILE_NAMES[0]!);
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ function pickBestSearchResult(
|
||||
return titles.includes(normalizedTarget);
|
||||
});
|
||||
|
||||
const selected = exact ?? candidates[0];
|
||||
const selected = exact ?? candidates[0]!;
|
||||
const selectedTitle =
|
||||
selected.title?.english || selected.title?.romaji || selected.title?.native || title;
|
||||
return { id: selected.id, title: selectedTitle };
|
||||
|
||||
@@ -129,7 +129,7 @@ test('refreshKnownWords throws when integration is unavailable', async () => {
|
||||
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await registered.refreshKnownWords();
|
||||
await registered.refreshKnownWords!();
|
||||
},
|
||||
{ message: 'AnkiConnect integration not enabled' },
|
||||
);
|
||||
@@ -144,7 +144,7 @@ test('refreshKnownWords delegates to integration', async () => {
|
||||
},
|
||||
};
|
||||
|
||||
await registered.refreshKnownWords();
|
||||
await registered.refreshKnownWords!();
|
||||
|
||||
assert.equal(refreshed, 1);
|
||||
});
|
||||
@@ -158,7 +158,7 @@ test('setAnkiConnectEnabled disables active integration and broadcasts changes',
|
||||
},
|
||||
};
|
||||
|
||||
registered.setAnkiConnectEnabled(false);
|
||||
registered.setAnkiConnectEnabled!(false);
|
||||
|
||||
assert.deepEqual(state.patches, [false]);
|
||||
assert.equal(destroyed, 1);
|
||||
@@ -188,8 +188,8 @@ test('clearAnkiHistory and respondFieldGrouping execute runtime callbacks', () =
|
||||
deleteDuplicate: true,
|
||||
cancelled: false,
|
||||
};
|
||||
registered.clearAnkiHistory();
|
||||
registered.respondFieldGrouping(choice);
|
||||
registered.clearAnkiHistory!();
|
||||
registered.respondFieldGrouping!(choice);
|
||||
|
||||
options.getSubtitleTimingTracker = originalGetTracker;
|
||||
|
||||
@@ -201,7 +201,7 @@ test('clearAnkiHistory and respondFieldGrouping execute runtime callbacks', () =
|
||||
test('buildKikuMergePreview returns guard error when integration is missing', async () => {
|
||||
const { registered } = createHarness();
|
||||
|
||||
const result = await registered.buildKikuMergePreview({
|
||||
const result = await registered.buildKikuMergePreview!({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
deleteDuplicate: false,
|
||||
@@ -227,7 +227,7 @@ test('buildKikuMergePreview delegates to integration when available', async () =
|
||||
},
|
||||
};
|
||||
|
||||
const result = await registered.buildKikuMergePreview({
|
||||
const result = await registered.buildKikuMergePreview!({
|
||||
keepNoteId: 3,
|
||||
deleteNoteId: 4,
|
||||
deleteDuplicate: true,
|
||||
@@ -240,7 +240,7 @@ test('buildKikuMergePreview delegates to integration when available', async () =
|
||||
test('searchJimakuEntries caps results and onDownloadedSubtitle sends sub-add to mpv', async () => {
|
||||
const { registered, state } = createHarness();
|
||||
|
||||
const searchResult = await registered.searchJimakuEntries({ query: 'test' });
|
||||
const searchResult = await registered.searchJimakuEntries!({ query: 'test' });
|
||||
assert.deepEqual(state.fetchCalls, [
|
||||
{
|
||||
endpoint: '/api/entries/search',
|
||||
@@ -250,6 +250,6 @@ test('searchJimakuEntries caps results and onDownloadedSubtitle sends sub-add to
|
||||
assert.equal((searchResult as { ok: boolean }).ok, true);
|
||||
assert.equal((searchResult as { data: unknown[] }).data.length, 2);
|
||||
|
||||
registered.onDownloadedSubtitle('/tmp/subtitle.ass');
|
||||
registered.onDownloadedSubtitle!('/tmp/subtitle.ass');
|
||||
assert.deepEqual(state.sentCommands, [{ command: ['sub-add', '/tmp/subtitle.ass', 'select'] }]);
|
||||
});
|
||||
|
||||
@@ -209,30 +209,31 @@ test('runAppReadyRuntime aggregates multiple critical anki mapping errors', asyn
|
||||
|
||||
await runAppReadyRuntime(deps);
|
||||
|
||||
const firstErrorSet = capturedErrors[0]!;
|
||||
assert.equal(capturedErrors.length, 1);
|
||||
assert.equal(capturedErrors[0].length, 5);
|
||||
assert.equal(firstErrorSet.length, 5);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.audio must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.image must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.sentence must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.miscInfo must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
capturedErrors[0].includes(
|
||||
firstErrorSet.includes(
|
||||
'ankiConnect.fields.translation must be a non-empty string when ankiConnect is enabled.',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -124,8 +124,8 @@ testIfSqlite('persists and retrieves minimum immersion tracking fields', async (
|
||||
|
||||
const summaries = await tracker.getSessionSummaries(10);
|
||||
assert.ok(summaries.length >= 1);
|
||||
assert.ok(summaries[0].linesSeen >= 1);
|
||||
assert.ok(summaries[0].cardsMined >= 2);
|
||||
assert.ok(summaries[0]!.linesSeen >= 1);
|
||||
assert.ok(summaries[0]!.cardsMined >= 2);
|
||||
|
||||
tracker.destroy();
|
||||
|
||||
@@ -376,8 +376,8 @@ testIfSqlite('monthly rollups are grouped by calendar month', async () => {
|
||||
const videoRows = rows.filter((row) => row.videoId === 1);
|
||||
|
||||
assert.equal(videoRows.length, 2);
|
||||
assert.equal(videoRows[0].rollupDayOrMonth, 202602);
|
||||
assert.equal(videoRows[1].rollupDayOrMonth, 202601);
|
||||
assert.equal(videoRows[0]!.rollupDayOrMonth, 202602);
|
||||
assert.equal(videoRows[1]!.rollupDayOrMonth, 202601);
|
||||
} finally {
|
||||
tracker?.destroy();
|
||||
cleanupDbPath(dbPath);
|
||||
|
||||
@@ -1443,7 +1443,7 @@ export class ImmersionTrackerService {
|
||||
const parsed = new URL(mediaPath);
|
||||
const parts = parsed.pathname.split('/').filter(Boolean);
|
||||
if (parts.length > 0) {
|
||||
const leaf = decodeURIComponent(parts[parts.length - 1]);
|
||||
const leaf = decodeURIComponent(parts[parts.length - 1]!);
|
||||
return this.normalizeText(leaf.replace(/\.[^/.]+$/, ''));
|
||||
}
|
||||
return this.normalizeText(parsed.hostname) || 'unknown';
|
||||
|
||||
@@ -35,7 +35,6 @@ export { createFrequencyDictionaryLookup } from './frequency-dictionary';
|
||||
export { createJlptVocabularyLookup } from './jlpt-vocab';
|
||||
export {
|
||||
getIgnoredPos1Entries,
|
||||
JlptIgnoredPos1Entry,
|
||||
JLPT_EXCLUDED_TERMS,
|
||||
JLPT_IGNORED_MECAB_POS1,
|
||||
JLPT_IGNORED_MECAB_POS1_ENTRIES,
|
||||
@@ -43,6 +42,7 @@ export {
|
||||
shouldIgnoreJlptByTerm,
|
||||
shouldIgnoreJlptForMecabPos1,
|
||||
} from './jlpt-token-filter';
|
||||
export type { JlptIgnoredPos1Entry } from './jlpt-token-filter';
|
||||
export { loadYomitanExtension } from './yomitan-extension-loader';
|
||||
export {
|
||||
getJimakuLanguagePreference,
|
||||
@@ -72,8 +72,6 @@ export {
|
||||
export {
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
MpvIpcClient,
|
||||
MpvRuntimeClientLike,
|
||||
MpvTrackProperty,
|
||||
playNextSubtitleRuntime,
|
||||
replayCurrentSubtitleRuntime,
|
||||
resolveCurrentAudioStreamIndex,
|
||||
@@ -81,6 +79,7 @@ export {
|
||||
setMpvSubVisibilityRuntime,
|
||||
showMpvOsdRuntime,
|
||||
} from './mpv';
|
||||
export type { MpvRuntimeClientLike, MpvTrackProperty } from './mpv';
|
||||
export {
|
||||
applyMpvSubtitleRenderMetricsPatch,
|
||||
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||
|
||||
@@ -70,11 +70,11 @@ test('start posts capabilities on socket connect', async () => {
|
||||
});
|
||||
|
||||
service.start();
|
||||
sockets[0].emit('open');
|
||||
sockets[0]!.emit('open');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0].input, 'http://jellyfin.local:8096/Sessions/Capabilities/Full');
|
||||
assert.equal(fetchCalls[0]!.input, 'http://jellyfin.local:8096/Sessions/Capabilities/Full');
|
||||
assert.equal(service.isConnected(), true);
|
||||
});
|
||||
|
||||
@@ -97,9 +97,9 @@ test('socket headers include jellyfin authorization metadata', () => {
|
||||
|
||||
service.start();
|
||||
assert.equal(seenHeaders.length, 1);
|
||||
assert.ok(seenHeaders[0].Authorization.includes('Client="SubMiner"'));
|
||||
assert.ok(seenHeaders[0].Authorization.includes('DeviceId="device-auth"'));
|
||||
assert.ok(seenHeaders[0]['X-Emby-Authorization']);
|
||||
assert.ok(seenHeaders[0]!['Authorization']!.includes('Client="SubMiner"'));
|
||||
assert.ok(seenHeaders[0]!['Authorization']!.includes('DeviceId="device-auth"'));
|
||||
assert.ok(seenHeaders[0]!['X-Emby-Authorization']);
|
||||
});
|
||||
|
||||
test('dispatches inbound Play, Playstate, and GeneralCommand messages', () => {
|
||||
@@ -124,7 +124,7 @@ test('dispatches inbound Play, Playstate, and GeneralCommand messages', () => {
|
||||
});
|
||||
|
||||
service.start();
|
||||
const socket = sockets[0];
|
||||
const socket = sockets[0]!;
|
||||
socket.emit('message', JSON.stringify({ MessageType: 'Play', Data: { ItemId: 'movie-1' } }));
|
||||
socket.emit(
|
||||
'message',
|
||||
@@ -174,13 +174,13 @@ test('schedules reconnect with bounded exponential backoff', () => {
|
||||
});
|
||||
|
||||
service.start();
|
||||
sockets[0].emit('close');
|
||||
sockets[0]!.emit('close');
|
||||
pendingTimers.shift()?.();
|
||||
sockets[1].emit('close');
|
||||
sockets[1]!.emit('close');
|
||||
pendingTimers.shift()?.();
|
||||
sockets[2].emit('close');
|
||||
sockets[2]!.emit('close');
|
||||
pendingTimers.shift()?.();
|
||||
sockets[3].emit('close');
|
||||
sockets[3]!.emit('close');
|
||||
|
||||
assert.deepEqual(delays, [100, 200, 400, 400]);
|
||||
assert.equal(sockets.length, 4);
|
||||
@@ -216,7 +216,7 @@ test('Jellyfin remote stop prevents further reconnect/network activity', () => {
|
||||
|
||||
service.start();
|
||||
assert.equal(sockets.length, 1);
|
||||
sockets[0].emit('close');
|
||||
sockets[0]!.emit('close');
|
||||
assert.equal(pendingTimers.length, 1);
|
||||
|
||||
service.stop();
|
||||
@@ -252,7 +252,7 @@ test('reportProgress posts timeline payload and treats failure as non-fatal', as
|
||||
});
|
||||
|
||||
service.start();
|
||||
sockets[0].emit('open');
|
||||
sockets[0]!.emit('open');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const expectedPayload = buildJellyfinTimelinePayload({
|
||||
@@ -310,7 +310,7 @@ test('advertiseNow validates server registration using Sessions endpoint', async
|
||||
});
|
||||
|
||||
service.start();
|
||||
sockets[0].emit('open');
|
||||
sockets[0]!.emit('open');
|
||||
const ok = await service.advertiseNow();
|
||||
assert.equal(ok, true);
|
||||
assert.ok(calls.some((url) => url.endsWith('/Sessions')));
|
||||
|
||||
@@ -119,7 +119,7 @@ test('listItems supports search and formats title', async () => {
|
||||
limit: 25,
|
||||
},
|
||||
);
|
||||
assert.equal(items[0].title, 'Space Show S01E02 Pilot');
|
||||
assert.equal(items[0]!.title, 'Space Show S01E02 Pilot');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -338,14 +338,14 @@ test('listSubtitleTracks returns all subtitle streams with delivery urls', async
|
||||
[2, 3, 4],
|
||||
);
|
||||
assert.equal(
|
||||
tracks[0].deliveryUrl,
|
||||
tracks[0]!.deliveryUrl,
|
||||
'http://jellyfin.local/Videos/movie-1/ms-1/Subtitles/2/Stream.srt?api_key=token',
|
||||
);
|
||||
assert.equal(
|
||||
tracks[1].deliveryUrl,
|
||||
tracks[1]!.deliveryUrl,
|
||||
'http://jellyfin.local/Videos/movie-1/ms-1/Subtitles/3/Stream.srt?api_key=token',
|
||||
);
|
||||
assert.equal(tracks[2].deliveryUrl, 'https://cdn.example.com/subs.srt');
|
||||
assert.equal(tracks[2]!.deliveryUrl, 'https://cdn.example.com/subs.srt');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -556,9 +556,9 @@ test('listSubtitleTracks falls back from PlaybackInfo to item media sources', as
|
||||
);
|
||||
assert.equal(requestCount, 2);
|
||||
assert.equal(tracks.length, 1);
|
||||
assert.equal(tracks[0].index, 11);
|
||||
assert.equal(tracks[0]!.index, 11);
|
||||
assert.equal(
|
||||
tracks[0].deliveryUrl,
|
||||
tracks[0]!.deliveryUrl,
|
||||
'http://jellyfin.local/Videos/movie-fallback/ms-fallback/Subtitles/11/Stream.srt?api_key=token',
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -177,8 +177,8 @@ test('splitMpvMessagesFromBuffer parses complete lines and preserves partial buf
|
||||
|
||||
assert.equal(parsed.messages.length, 2);
|
||||
assert.equal(parsed.nextBuffer, '{"partial"');
|
||||
assert.equal(parsed.messages[0].event, 'shutdown');
|
||||
assert.equal(parsed.messages[1].name, 'media-title');
|
||||
assert.equal(parsed.messages[0]!.event, 'shutdown');
|
||||
assert.equal(parsed.messages[1]!.name, 'media-title');
|
||||
});
|
||||
|
||||
test('splitMpvMessagesFromBuffer reports invalid JSON lines', () => {
|
||||
@@ -189,7 +189,7 @@ test('splitMpvMessagesFromBuffer reports invalid JSON lines', () => {
|
||||
});
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].line, '{invalid}');
|
||||
assert.equal(errors[0]!.line, '{invalid}');
|
||||
});
|
||||
|
||||
test('visibility and boolean parsers handle text values', () => {
|
||||
|
||||
@@ -84,11 +84,11 @@ test('scheduleMpvReconnect clears existing timer and increments attempt', () =>
|
||||
|
||||
assert.equal(nextAttempt, 4);
|
||||
assert.equal(cleared.length, 1);
|
||||
assert.equal(cleared[0], existing);
|
||||
assert.equal(cleared[0]!, existing);
|
||||
assert.equal(setTimers.length, 1);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].attempt, 4);
|
||||
assert.equal(calls[0].delay, getMpvReconnectDelay(3, true));
|
||||
assert.equal(calls[0]!.attempt, 4);
|
||||
assert.equal(calls[0]!.delay, getMpvReconnectDelay(3, true));
|
||||
assert.equal(connected, 1);
|
||||
});
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ test('MpvIpcClient handles sub-text property change and broadcasts tokenized sub
|
||||
});
|
||||
|
||||
assert.equal(events.length, 1);
|
||||
assert.equal(events[0].text, '字幕');
|
||||
assert.equal(events[0].isOverlayVisible, false);
|
||||
assert.equal(events[0]!.text, '字幕');
|
||||
assert.equal(events[0]!.isOverlayVisible, false);
|
||||
});
|
||||
|
||||
test('MpvIpcClient parses JSON line protocol in processBuffer', () => {
|
||||
@@ -70,8 +70,8 @@ test('MpvIpcClient parses JSON line protocol in processBuffer', () => {
|
||||
(client as any).processBuffer();
|
||||
|
||||
assert.equal(seen.length, 2);
|
||||
assert.equal(seen[0].name, 'path');
|
||||
assert.equal(seen[1].request_id, 1);
|
||||
assert.equal(seen[0]!.name, 'path');
|
||||
assert.equal(seen[1]!.request_id, 1);
|
||||
assert.equal((client as any).buffer, '{"partial":');
|
||||
});
|
||||
|
||||
|
||||
@@ -83,5 +83,5 @@ test('overlay measurement store rate-limits invalid payload warnings', () => {
|
||||
now = 11_000;
|
||||
store.report({ layer: 'visible' });
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.match(warnings[0], /Dropped 3 invalid measurement payload/);
|
||||
assert.match(warnings[0]!, /Dropped 3 invalid measurement payload/);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ const VIDEO_EXTENSIONS = new Set([
|
||||
]);
|
||||
|
||||
function getPathExtension(pathValue: string): string {
|
||||
const normalized = pathValue.split(/[?#]/, 1)[0];
|
||||
const normalized = pathValue.split(/[?#]/, 1)[0] ?? '';
|
||||
const dot = normalized.lastIndexOf('.');
|
||||
return dot >= 0 ? normalized.slice(dot).toLowerCase() : '';
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function shortcutMatchesInputForLocalFallback(
|
||||
const parts = normalized.split('+').filter(Boolean);
|
||||
if (parts.length === 0) return false;
|
||||
|
||||
const keyToken = parts[parts.length - 1];
|
||||
const keyToken = parts[parts.length - 1]!;
|
||||
const modifierTokens = new Set(parts.slice(0, -1));
|
||||
const allowedModifiers = new Set(['shift', 'alt', 'meta', 'control', 'commandorcontrol']);
|
||||
for (const token of modifierTokens) {
|
||||
|
||||
@@ -339,6 +339,6 @@ test('runSubsyncManual resolves string sid values from mpv stream properties', a
|
||||
assert.equal(syncOutputIndex >= 0, true);
|
||||
const outputPath = ffArgs[syncOutputIndex + 1];
|
||||
assert.equal(typeof outputPath, 'string');
|
||||
assert.ok(outputPath.length > 0);
|
||||
assert.equal(fs.readFileSync(outputPath, 'utf8'), '');
|
||||
assert.ok(outputPath!.length > 0);
|
||||
assert.equal(fs.readFileSync(outputPath!, 'utf8'), '');
|
||||
});
|
||||
|
||||
@@ -28,7 +28,8 @@ export function cycleSecondarySubMode(deps: CycleSecondarySubModeDeps): void {
|
||||
|
||||
const currentMode = deps.getSecondarySubMode();
|
||||
const currentIndex = SECONDARY_SUB_CYCLE.indexOf(currentMode);
|
||||
const nextMode = SECONDARY_SUB_CYCLE[(currentIndex + 1) % SECONDARY_SUB_CYCLE.length];
|
||||
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
|
||||
const nextMode = SECONDARY_SUB_CYCLE[(safeIndex + 1) % SECONDARY_SUB_CYCLE.length]!;
|
||||
deps.setSecondarySubMode(nextMode);
|
||||
deps.broadcastSecondarySubMode(nextMode);
|
||||
deps.showMpvOsd(`Secondary subtitle: ${nextMode}`);
|
||||
|
||||
@@ -82,8 +82,9 @@ export function serializeSubtitleMarkup(
|
||||
const klass = computeWordClass(token, options);
|
||||
const parts = token.surface.split('\n');
|
||||
for (let index = 0; index < parts.length; index += 1) {
|
||||
if (parts[index]) {
|
||||
chunks.push(`<span class="${klass}">${escapeHtml(parts[index])}</span>`);
|
||||
const part = parts[index];
|
||||
if (part) {
|
||||
chunks.push(`<span class="${klass}">${escapeHtml(part)}</span>`);
|
||||
}
|
||||
if (index < parts.length - 1) {
|
||||
chunks.push('<br>');
|
||||
|
||||
@@ -20,7 +20,7 @@ export class Texthooker {
|
||||
}
|
||||
|
||||
this.server = http.createServer((req, res) => {
|
||||
const urlPath = (req.url || '/').split('?')[0];
|
||||
const urlPath = (req.url || '/').split('?')[0] ?? '/';
|
||||
const filePath = path.join(texthookerPath, urlPath === '/' ? 'index.html' : urlPath);
|
||||
|
||||
const ext = path.extname(filePath);
|
||||
|
||||
@@ -377,7 +377,7 @@ function isRepeatedKanaSfx(text: string): boolean {
|
||||
let hasAdjacentRepeat = false;
|
||||
|
||||
for (let i = 0; i < chars.length; i += 1) {
|
||||
const char = chars[i];
|
||||
const char = chars[i]!;
|
||||
counts.set(char, (counts.get(char) ?? 0) + 1);
|
||||
if (i > 0 && chars[i] === chars[i - 1]) {
|
||||
hasAdjacentRepeat = true;
|
||||
|
||||
@@ -73,7 +73,8 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
|
||||
|
||||
setTimeout(() => {
|
||||
if (!settingsWindow.isDestroyed()) {
|
||||
settingsWindow.setSize(settingsWindow.getSize()[0], settingsWindow.getSize()[1]);
|
||||
const [width = 0, height = 0] = settingsWindow.getSize();
|
||||
settingsWindow.setSize(width, height);
|
||||
settingsWindow.webContents.invalidate();
|
||||
settingsWindow.show();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ function getRetryAfter(headers: http.IncomingHttpHeaders): number | undefined {
|
||||
const value = headers['x-ratelimit-reset-after'];
|
||||
if (!value) return undefined;
|
||||
const raw = Array.isArray(value) ? value[0] : value;
|
||||
const parsed = Number.parseFloat(raw);
|
||||
const parsed = Number.parseFloat(raw!);
|
||||
if (!Number.isFinite(parsed)) return undefined;
|
||||
return parsed;
|
||||
}
|
||||
@@ -146,8 +146,8 @@ function matchEpisodeFromName(name: string): {
|
||||
const seasonEpisode = name.match(/S(\d{1,2})E(\d{1,3})/i);
|
||||
if (seasonEpisode && seasonEpisode.index !== undefined) {
|
||||
return {
|
||||
season: Number.parseInt(seasonEpisode[1], 10),
|
||||
episode: Number.parseInt(seasonEpisode[2], 10),
|
||||
season: Number.parseInt(seasonEpisode[1]!, 10),
|
||||
episode: Number.parseInt(seasonEpisode[2]!, 10),
|
||||
index: seasonEpisode.index,
|
||||
confidence: 'high',
|
||||
};
|
||||
@@ -156,8 +156,8 @@ function matchEpisodeFromName(name: string): {
|
||||
const alt = name.match(/(\d{1,2})x(\d{1,3})/i);
|
||||
if (alt && alt.index !== undefined) {
|
||||
return {
|
||||
season: Number.parseInt(alt[1], 10),
|
||||
episode: Number.parseInt(alt[2], 10),
|
||||
season: Number.parseInt(alt[1]!, 10),
|
||||
episode: Number.parseInt(alt[2]!, 10),
|
||||
index: alt.index,
|
||||
confidence: 'high',
|
||||
};
|
||||
@@ -167,7 +167,7 @@ function matchEpisodeFromName(name: string): {
|
||||
if (epOnly && epOnly.index !== undefined) {
|
||||
return {
|
||||
season: null,
|
||||
episode: Number.parseInt(epOnly[1], 10),
|
||||
episode: Number.parseInt(epOnly[1]!, 10),
|
||||
index: epOnly.index,
|
||||
confidence: 'medium',
|
||||
};
|
||||
@@ -177,7 +177,7 @@ function matchEpisodeFromName(name: string): {
|
||||
if (numeric && numeric.index !== undefined) {
|
||||
return {
|
||||
season: null,
|
||||
episode: Number.parseInt(numeric[1], 10),
|
||||
episode: Number.parseInt(numeric[1]!, 10),
|
||||
index: numeric.index,
|
||||
confidence: 'medium',
|
||||
};
|
||||
@@ -190,7 +190,7 @@ function detectSeasonFromDir(mediaPath: string): number | null {
|
||||
const parent = path.basename(path.dirname(mediaPath));
|
||||
const match = parent.match(/(?:Season|S)\s*(\d{1,2})/i);
|
||||
if (!match) return null;
|
||||
const parsed = Number.parseInt(match[1], 10);
|
||||
const parsed = Number.parseInt(match[1]!, 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ test('manual anilist setup submission forwards access token to callback consumer
|
||||
const handled = handleSubmission('subminer://anilist-setup?access_token=abc123');
|
||||
assert.equal(handled, true);
|
||||
assert.equal(consumed.length, 1);
|
||||
assert.ok(consumed[0].includes('https://anilist.subminer.moe/#access_token=abc123'));
|
||||
assert.ok(consumed[0]!.includes('https://anilist.subminer.moe/#access_token=abc123'));
|
||||
});
|
||||
|
||||
test('maybe focus anilist setup window focuses existing window', () => {
|
||||
@@ -179,7 +179,7 @@ test('anilist setup did-fail-load handler forwards details', () => {
|
||||
});
|
||||
|
||||
assert.equal(seen.length, 1);
|
||||
assert.equal(seen[0].errorCode, -3);
|
||||
assert.equal(seen[0]!.errorCode, -3);
|
||||
});
|
||||
|
||||
test('anilist setup did-finish-load handler triggers fallback on blank page', () => {
|
||||
|
||||
@@ -49,8 +49,8 @@ test('createLaunchMpvIdleForJellyfinPlaybackHandler builds expected mpv args', (
|
||||
|
||||
launch();
|
||||
assert.equal(spawnedArgs.length, 1);
|
||||
assert.ok(spawnedArgs[0].includes('--idle=yes'));
|
||||
assert.ok(spawnedArgs[0].some((arg) => arg.includes('--input-ipc-server=/tmp/subminer.sock')));
|
||||
assert.ok(spawnedArgs[0]!.includes('--idle=yes'));
|
||||
assert.ok(spawnedArgs[0]!.some((arg) => arg.includes('--input-ipc-server=/tmp/subminer.sock')));
|
||||
assert.ok(logs.some((entry) => entry.includes('Launched mpv for Jellyfin playback')));
|
||||
});
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ test('tray menu template contains expected entries and handlers', () => {
|
||||
});
|
||||
|
||||
assert.equal(template.length, 7);
|
||||
template[0].click?.();
|
||||
template[5].type === 'separator' ? calls.push('separator') : calls.push('bad');
|
||||
template[6].click?.();
|
||||
template[0]!.click?.();
|
||||
template[5]!.type === 'separator' ? calls.push('separator') : calls.push('bad');
|
||||
template[6]!.click?.();
|
||||
assert.deepEqual(calls, ['overlay', 'separator', 'quit']);
|
||||
});
|
||||
|
||||
@@ -180,7 +180,7 @@ export class MediaGenerator {
|
||||
): Promise<Buffer> {
|
||||
const { format, quality = 92, maxWidth, maxHeight } = options;
|
||||
const ext = format === 'webp' ? 'webp' : format === 'png' ? 'png' : 'jpg';
|
||||
const codecMap: Record<string, string> = {
|
||||
const codecMap: Record<'jpg' | 'png' | 'webp', string> = {
|
||||
jpg: 'mjpeg',
|
||||
png: 'png',
|
||||
webp: 'webp',
|
||||
|
||||
@@ -41,7 +41,7 @@ test('handleError logs context and recovers overlay state', () => {
|
||||
assert.equal(dismissed, 1);
|
||||
assert.equal(restored, 1);
|
||||
assert.equal(shown.length, 1);
|
||||
assert.match(shown[0], /recovered/i);
|
||||
assert.match(shown[0]!, /recovered/i);
|
||||
assert.equal(payloads.length, 1);
|
||||
|
||||
const payload = payloads[0] as {
|
||||
|
||||
@@ -126,12 +126,12 @@ export function createMouseHandlers(
|
||||
if (!probeChar || isBoundary(probeChar)) return null;
|
||||
|
||||
let start = probeIndex;
|
||||
while (start > 0 && !isBoundary(text[start - 1])) {
|
||||
while (start > 0 && !isBoundary(text[start - 1] ?? '')) {
|
||||
start -= 1;
|
||||
}
|
||||
|
||||
let end = probeIndex + 1;
|
||||
while (end < text.length && !isBoundary(text[end])) {
|
||||
while (end < text.length && !isBoundary(text[end] ?? '')) {
|
||||
end += 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ export function createJimakuModal(
|
||||
if (index < 0 || index >= ctx.state.jimakuEntries.length) return;
|
||||
|
||||
ctx.state.selectedEntryIndex = index;
|
||||
ctx.state.currentEntryId = ctx.state.jimakuEntries[index].id;
|
||||
ctx.state.currentEntryId = ctx.state.jimakuEntries[index]!.id;
|
||||
renderEntries();
|
||||
|
||||
if (ctx.state.currentEntryId !== null) {
|
||||
@@ -223,7 +223,7 @@ export function createJimakuModal(
|
||||
return;
|
||||
}
|
||||
|
||||
const file = ctx.state.jimakuFiles[index];
|
||||
const file = ctx.state.jimakuFiles[index]!;
|
||||
setJimakuStatus('Downloading subtitle...');
|
||||
|
||||
const result: JimakuDownloadResult = await window.electronAPI.jimakuDownloadFile({
|
||||
|
||||
@@ -30,7 +30,7 @@ export function createRuntimeOptionsModal(
|
||||
if (ctx.state.runtimeOptionSelectedIndex >= ctx.state.runtimeOptions.length) {
|
||||
return null;
|
||||
}
|
||||
return ctx.state.runtimeOptions[ctx.state.runtimeOptionSelectedIndex];
|
||||
return ctx.state.runtimeOptions[ctx.state.runtimeOptionSelectedIndex] ?? null;
|
||||
}
|
||||
|
||||
function renderRuntimeOptionsList(): void {
|
||||
@@ -114,11 +114,11 @@ export function createRuntimeOptionsModal(
|
||||
? (safeIndex + 1) % option.allowedValues.length
|
||||
: (safeIndex - 1 + option.allowedValues.length) % option.allowedValues.length;
|
||||
|
||||
ctx.state.runtimeOptionDraftValues.set(option.id, option.allowedValues[nextIndex]);
|
||||
const nextValue = option.allowedValues[nextIndex];
|
||||
if (nextValue === undefined) return;
|
||||
ctx.state.runtimeOptionDraftValues.set(option.id, nextValue);
|
||||
renderRuntimeOptionsList();
|
||||
setRuntimeOptionsStatus(
|
||||
`Selected ${option.label}: ${formatRuntimeOptionValue(option.allowedValues[nextIndex])}`,
|
||||
);
|
||||
setRuntimeOptionsStatus(`Selected ${option.label}: ${formatRuntimeOptionValue(nextValue)}`);
|
||||
}
|
||||
|
||||
async function applySelectedRuntimeOption(): Promise<void> {
|
||||
|
||||
@@ -357,7 +357,7 @@ function createSectionNode(
|
||||
list.className = 'session-help-item-list';
|
||||
|
||||
section.rows.forEach((row, rowIndex) => {
|
||||
const globalIndex = globalIndexMap[sectionIndex] + rowIndex;
|
||||
const globalIndex = (globalIndexMap[sectionIndex] ?? 0) + rowIndex;
|
||||
const button = createShortcutRow(row, globalIndex);
|
||||
list.appendChild(button);
|
||||
});
|
||||
@@ -406,6 +406,7 @@ export function createSessionHelpModal(
|
||||
item.tabIndex = idx === next ? 0 : -1;
|
||||
});
|
||||
const activeItem = items[next];
|
||||
if (!activeItem) return;
|
||||
activeItem.focus({ preventScroll: true });
|
||||
activeItem.scrollIntoView({
|
||||
block: 'nearest',
|
||||
@@ -562,7 +563,7 @@ export function createSessionHelpModal(
|
||||
|
||||
const shortcutSections = buildOverlayShortcutSections(shortcuts);
|
||||
if (shortcutSections.length > 0) {
|
||||
shortcutSections[0].title = 'Overlay shortcuts (configurable)';
|
||||
shortcutSections[0]!.title = 'Overlay shortcuts (configurable)';
|
||||
}
|
||||
const colorSection = buildColorSection(styleConfig ?? {});
|
||||
helpSections = [...bindingSections, ...shortcutSections, colorSection];
|
||||
|
||||
@@ -145,10 +145,11 @@ function renderWithTokens(
|
||||
if (surface.includes('\n')) {
|
||||
const parts = surface.split('\n');
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
if (parts[i]) {
|
||||
const part = parts[i];
|
||||
if (part) {
|
||||
const span = document.createElement('span');
|
||||
span.className = computeWordClass(token, resolvedFrequencyRenderSettings);
|
||||
span.textContent = parts[i];
|
||||
span.textContent = part;
|
||||
if (token.reading) span.dataset.reading = token.reading;
|
||||
if (token.headword) span.dataset.headword = token.headword;
|
||||
fragment.appendChild(span);
|
||||
@@ -277,7 +278,7 @@ function renderPlainTextPreserveLineBreaks(root: ParentNode, text: string): void
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
fragment.appendChild(document.createTextNode(lines[i]));
|
||||
fragment.appendChild(document.createTextNode(lines[i] ?? ''));
|
||||
if (i < lines.length - 1) {
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
}
|
||||
@@ -361,8 +362,9 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
|
||||
const lines = normalized.split('\n');
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
if (lines[i]) {
|
||||
ctx.dom.secondarySubRoot.appendChild(document.createTextNode(lines[i]));
|
||||
const line = lines[i];
|
||||
if (line) {
|
||||
ctx.dom.secondarySubRoot.appendChild(document.createTextNode(line));
|
||||
}
|
||||
if (i < lines.length - 1) {
|
||||
ctx.dom.secondarySubRoot.appendChild(document.createElement('br'));
|
||||
|
||||
@@ -47,7 +47,7 @@ function setPathValue(target: Record<string, unknown>, path: string, value: unkn
|
||||
const parts = path.split('.');
|
||||
let current = target;
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
const part = parts[i];
|
||||
const part = parts[i]!;
|
||||
const isLeaf = i === parts.length - 1;
|
||||
if (isLeaf) {
|
||||
current[part] = value;
|
||||
@@ -188,7 +188,7 @@ export class RuntimeOptionsManager {
|
||||
direction === 1
|
||||
? (safeIndex + 1) % values.length
|
||||
: (safeIndex - 1 + values.length) % values.length;
|
||||
return this.setOptionValue(id, values[nextIndex]);
|
||||
return this.setOptionValue(id, values[nextIndex]!);
|
||||
}
|
||||
|
||||
getEffectiveAnkiConnectConfig(baseConfig?: AnkiConnectConfig): AnkiConnectConfig {
|
||||
|
||||
@@ -307,6 +307,7 @@ export function markNPlusOneTargets(tokens: MergedToken[], minSentenceWords = 3)
|
||||
let sentenceWordCount = 0;
|
||||
for (let i = start; i < endExclusive; i++) {
|
||||
const token = markedTokens[i];
|
||||
if (!token) continue;
|
||||
if (!isSentenceBoundaryToken(token) && token.surface.trim().length > 0) {
|
||||
sentenceWordCount += 1;
|
||||
}
|
||||
@@ -317,8 +318,8 @@ export function markNPlusOneTargets(tokens: MergedToken[], minSentenceWords = 3)
|
||||
}
|
||||
|
||||
if (sentenceWordCount >= minimumSentenceWords && sentenceCandidates.length === 1) {
|
||||
markedTokens[sentenceCandidates[0]] = {
|
||||
...markedTokens[sentenceCandidates[0]],
|
||||
markedTokens[sentenceCandidates[0]!] = {
|
||||
...markedTokens[sentenceCandidates[0]!]!,
|
||||
isNPlusOneTarget: true,
|
||||
};
|
||||
}
|
||||
@@ -326,6 +327,7 @@ export function markNPlusOneTargets(tokens: MergedToken[], minSentenceWords = 3)
|
||||
|
||||
for (let i = 0; i < markedTokens.length; i++) {
|
||||
const token = markedTokens[i];
|
||||
if (!token) continue;
|
||||
if (isSentenceBoundaryToken(token)) {
|
||||
markSentence(sentenceStart, i);
|
||||
sentenceStart = i + 1;
|
||||
|
||||
@@ -177,10 +177,10 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
||||
if (result && result !== 'not-found') {
|
||||
const parts = result.split(',');
|
||||
if (parts.length === 4) {
|
||||
const x = parseInt(parts[0], 10);
|
||||
const y = parseInt(parts[1], 10);
|
||||
const width = parseInt(parts[2], 10);
|
||||
const height = parseInt(parts[3], 10);
|
||||
const x = parseInt(parts[0]!, 10);
|
||||
const y = parseInt(parts[1]!, 10);
|
||||
const width = parseInt(parts[2]!, 10);
|
||||
const height = parseInt(parts[3]!, 10);
|
||||
|
||||
if (
|
||||
Number.isFinite(x) &&
|
||||
|
||||
@@ -47,10 +47,10 @@ export function parseX11WindowGeometry(winInfo: string): {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
x: parseInt(xMatch[1], 10),
|
||||
y: parseInt(yMatch[1], 10),
|
||||
width: parseInt(widthMatch[1], 10),
|
||||
height: parseInt(heightMatch[1], 10),
|
||||
x: parseInt(xMatch[1]!, 10),
|
||||
y: parseInt(yMatch[1]!, 10),
|
||||
width: parseInt(widthMatch[1]!, 10),
|
||||
height: parseInt(heightMatch[1]!, 10),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export function parseX11WindowPid(raw: string): number | null {
|
||||
if (!pidMatch) {
|
||||
return null;
|
||||
}
|
||||
const pid = Number.parseInt(pidMatch[1], 10);
|
||||
const pid = Number.parseInt(pidMatch[1]!, 10);
|
||||
return Number.isInteger(pid) ? pid : null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user