mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-02 23:54:29 -07:00
fix(tsukihime): filter releases by subtitle language (#235)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
type: changed
|
||||
area: overlay
|
||||
|
||||
- The TsukiHime modal's Japanese and secondary-language tabs now filter the release list by the subtitle languages each release carries, and report when no release has subtitles for the active tab.
|
||||
@@ -14,7 +14,7 @@ Unlike Jimaku, TsukiHime needs no account or API key. The only requirement is th
|
||||
|
||||
## How It Works
|
||||
|
||||
The integration runs through an in-overlay modal opened with `Ctrl+Shift+T` by default. The modal has two tabs that filter the subtitle tracks of the selected release by role: the first follows `secondarySub.secondarySubLanguages` (English when unset), and the second is always **Japanese**, the currently supported primary subtitle language. Tracks with no language tag stay visible on the secondary tab.
|
||||
The integration runs through an in-overlay modal opened with `Ctrl+Shift+T` by default. The modal has two tabs that filter both the release list and the subtitle tracks of the selected release by role: the first follows `secondarySub.secondarySubLanguages` (English when unset), and the second is always **Japanese**, the currently supported primary subtitle language. Each tab lists only the releases whose reported subtitle languages include the tab's language, so the Japanese tab hides the many releases that ship English subtitles only. Releases and tracks with no language tag stay visible on the secondary tab. If nothing on the active tab qualifies, the status line says so and points at the other tab.
|
||||
|
||||
When you open the modal, SubMiner parses the current video filename to extract a title and episode number (same parser as Jimaku - `S01E03`, `1x03`, `E03`, and dash-separated numbers all work). If the filename yields a high-confidence match, SubMiner auto-searches immediately.
|
||||
|
||||
@@ -76,6 +76,7 @@ The previous `--open-animetosho` flag and `__animetosho-open` keybinding command
|
||||
## Troubleshooting
|
||||
|
||||
- **"xz binary not found"** - install `xz`/`xz-utils` with your package manager.
|
||||
- **"No releases with Japanese subtitles"** - none of the search results carry a Japanese track. Most releases only ship English subtitles; try another search, or use the [Jimaku integration](/jimaku-integration) for Japanese subtitles.
|
||||
- **"Batch releases are not supported"** - TsukiHime only exposes extracted attachments for single-file torrents. Pick the single-episode release for your episode instead of a season batch.
|
||||
- **"No text subtitle tracks in this release"** - the release only carries image-based subtitles (PGS/VobSub) or none at all; try a different release (fansub and SubsPlease-style releases almost always carry ASS tracks).
|
||||
- **Timing is off** - the subtitle came from a different release than your video file. Use the subtitle sync modal (`Ctrl+Alt+S`) or pick the release matching your file exactly.
|
||||
|
||||
@@ -40,13 +40,19 @@ function createElementStub() {
|
||||
}
|
||||
|
||||
function createListStub() {
|
||||
return {
|
||||
innerHTML: '',
|
||||
const list = {
|
||||
children: [] as unknown[],
|
||||
appendChild(child: unknown) {
|
||||
this.children.push(child);
|
||||
list.children.push(child);
|
||||
},
|
||||
};
|
||||
// The modal clears lists through innerHTML before re-rendering.
|
||||
return Object.defineProperty(list, 'innerHTML', {
|
||||
get: () => '',
|
||||
set: () => {
|
||||
list.children.length = 0;
|
||||
},
|
||||
}) as typeof list & { innerHTML: string };
|
||||
}
|
||||
|
||||
function createTabStub(active: boolean) {
|
||||
@@ -118,6 +124,7 @@ function createModalHarness(
|
||||
files: TsukihimeSubtitleFile[],
|
||||
options: {
|
||||
secondaryLanguages?: string[];
|
||||
secondaryLanguagesGate?: Promise<void>;
|
||||
downloadFile?: (query: unknown) => Promise<unknown>;
|
||||
listFiles?: (entryId: number) => Promise<unknown>;
|
||||
searchEntries?: (query: unknown) => Promise<unknown>;
|
||||
@@ -138,7 +145,10 @@ function createModalHarness(
|
||||
if (options.downloadFile) return options.downloadFile(query);
|
||||
return { ok: true, path: '/tmp/subtitles/episode01.en.ass' };
|
||||
},
|
||||
tsukihimeGetSecondaryLanguages: async () => options.secondaryLanguages ?? ['en', 'eng'],
|
||||
tsukihimeGetSecondaryLanguages: async () => {
|
||||
await options.secondaryLanguagesGate;
|
||||
return options.secondaryLanguages ?? ['en', 'eng'];
|
||||
},
|
||||
tsukihimeListFiles: async ({ entryId }: { entryId: number }) =>
|
||||
options.listFiles ? options.listFiles(entryId) : { ok: true, data: [] },
|
||||
tsukihimeSearchEntries: async (query: unknown) =>
|
||||
@@ -613,3 +623,202 @@ test('renderFiles omits the size detail when the API does not report one', () =>
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
const ENGLISH_ONLY_ENTRY = {
|
||||
id: 606713,
|
||||
title: 'english only release',
|
||||
timestamp: null,
|
||||
totalSize: null,
|
||||
numFiles: 1,
|
||||
sublangs: ['en'],
|
||||
};
|
||||
|
||||
const MULTI_SUB_ENTRY = {
|
||||
id: 12255,
|
||||
title: 'multi-sub release',
|
||||
timestamp: null,
|
||||
totalSize: null,
|
||||
numFiles: 1,
|
||||
sublangs: ['en-US', 'ja'],
|
||||
};
|
||||
|
||||
const UNLABELED_ENTRY = {
|
||||
id: 12256,
|
||||
title: 'release without langs',
|
||||
timestamp: null,
|
||||
totalSize: null,
|
||||
numFiles: 1,
|
||||
sublangs: [],
|
||||
};
|
||||
|
||||
function visibleEntryTitles(harness: ModalHarness): string[] {
|
||||
return (harness.entriesList.children as Array<{ textContent: string }>).map(
|
||||
(li) => li.textContent,
|
||||
);
|
||||
}
|
||||
|
||||
test('Japanese tab lists only releases that carry Japanese subtitles', async () => {
|
||||
const SECOND_JAPANESE_TRACK: TsukihimeSubtitleFile = {
|
||||
...JAPANESE_TRACK,
|
||||
attachmentId: 1955401,
|
||||
filename: 'episode01.jpn.sdh.ass',
|
||||
};
|
||||
const harness = createModalHarness([], {
|
||||
// Two tracks so the modal does not auto-download a lone match.
|
||||
listFiles: async () => ({ ok: true, data: [JAPANESE_TRACK, SECOND_JAPANESE_TRACK] }),
|
||||
});
|
||||
try {
|
||||
harness.state.currentTsukihimeEntryId = null;
|
||||
harness.state.tsukihimeEntries = [ENGLISH_ONLY_ENTRY, MULTI_SUB_ENTRY, UNLABELED_ENTRY];
|
||||
|
||||
pressKey(harness, 'ArrowRight');
|
||||
assert.deepEqual(visibleEntryTitles(harness), ['multi-sub release']);
|
||||
|
||||
// Enter addresses the visible list, so it must pick the multi-sub release
|
||||
// rather than the hidden first search result.
|
||||
pressKey(harness, 'Enter');
|
||||
await flushAsyncWork();
|
||||
assert.equal(harness.state.currentTsukihimeEntryId, MULTI_SUB_ENTRY.id);
|
||||
assert.equal(harness.status.textContent, 'Select a subtitle track.');
|
||||
|
||||
pressKey(harness, 'ArrowLeft');
|
||||
assert.deepEqual(visibleEntryTitles(harness), [
|
||||
'english only release',
|
||||
'multi-sub release',
|
||||
'release without langs',
|
||||
]);
|
||||
assert.equal(harness.state.currentTsukihimeEntryId, MULTI_SUB_ENTRY.id);
|
||||
assert.equal(harness.state.selectedTsukihimeEntryIndex, 1);
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('Japanese tab reports when no release carries Japanese subtitles', () => {
|
||||
const harness = createModalHarness([]);
|
||||
try {
|
||||
harness.state.currentTsukihimeEntryId = null;
|
||||
harness.state.tsukihimeEntries = [ENGLISH_ONLY_ENTRY, UNLABELED_ENTRY];
|
||||
|
||||
pressKey(harness, 'ArrowRight');
|
||||
assert.deepEqual(visibleEntryTitles(harness), []);
|
||||
assert.equal(
|
||||
harness.status.textContent,
|
||||
'No releases with Japanese subtitles. Switch to the English tab.',
|
||||
);
|
||||
|
||||
pressKey(harness, 'ArrowLeft');
|
||||
assert.deepEqual(visibleEntryTitles(harness), [
|
||||
'english only release',
|
||||
'release without langs',
|
||||
]);
|
||||
assert.equal(harness.status.textContent, 'Select a release.');
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('search reports when no release carries the secondary language', async () => {
|
||||
const harness = createModalHarness([], {
|
||||
searchEntries: async () => ({
|
||||
ok: true,
|
||||
data: [{ ...MULTI_SUB_ENTRY, sublangs: ['ja'] }],
|
||||
}),
|
||||
});
|
||||
try {
|
||||
harness.state.currentTsukihimeEntryId = null;
|
||||
harness.titleInput.value = 'Futsutsuka na Akujo';
|
||||
|
||||
pressKey(harness, 'Enter');
|
||||
await flushAsyncWork();
|
||||
assert.deepEqual(visibleEntryTitles(harness), []);
|
||||
assert.equal(
|
||||
harness.status.textContent,
|
||||
'No releases with English subtitles. Switch to the Japanese tab.',
|
||||
);
|
||||
|
||||
pressKey(harness, 'ArrowRight');
|
||||
assert.deepEqual(visibleEntryTitles(harness), ['multi-sub release']);
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('switching to a tab that hides the selected release clears its tracks', () => {
|
||||
const harness = createModalHarness([ENGLISH_TRACK, JAPANESE_TRACK]);
|
||||
try {
|
||||
harness.state.tsukihimeEntries = [ENGLISH_ONLY_ENTRY, MULTI_SUB_ENTRY];
|
||||
|
||||
pressKey(harness, 'ArrowRight');
|
||||
assert.equal(harness.state.currentTsukihimeEntryId, null);
|
||||
assert.deepEqual(harness.state.tsukihimeFiles, []);
|
||||
assert.deepEqual(visibleEntryTitles(harness), ['multi-sub release']);
|
||||
assert.equal(harness.status.textContent, 'Select a release.');
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('a search waits for the configured secondary languages before filtering', async () => {
|
||||
let openGate!: () => void;
|
||||
const harness = createModalHarness([], {
|
||||
secondaryLanguages: ['de'],
|
||||
secondaryLanguagesGate: new Promise<void>((resolve) => {
|
||||
openGate = resolve;
|
||||
}),
|
||||
searchEntries: async () => ({
|
||||
ok: true,
|
||||
data: [{ ...MULTI_SUB_ENTRY, title: 'german release', sublangs: ['de'] }],
|
||||
}),
|
||||
});
|
||||
try {
|
||||
harness.state.tsukihimeModalOpen = false;
|
||||
harness.modal.openTsukihimeModal();
|
||||
harness.titleInput.value = 'Futsutsuka na Akujo';
|
||||
|
||||
// Searching before the config arrives must not filter against the English
|
||||
// fallback, which would hide this German-only release.
|
||||
pressKey(harness, 'Enter');
|
||||
await flushAsyncWork();
|
||||
assert.deepEqual(visibleEntryTitles(harness), []);
|
||||
|
||||
openGate();
|
||||
await flushAsyncWork();
|
||||
|
||||
assert.deepEqual(visibleEntryTitles(harness), ['german release']);
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('a search from a prior modal session cannot repopulate a reopened modal', async () => {
|
||||
let openGate!: () => void;
|
||||
const harness = createModalHarness([], {
|
||||
secondaryLanguagesGate: new Promise<void>((resolve) => {
|
||||
openGate = resolve;
|
||||
}),
|
||||
searchEntries: async () => ({ ok: true, data: [MULTI_SUB_ENTRY] }),
|
||||
});
|
||||
try {
|
||||
harness.state.tsukihimeModalOpen = false;
|
||||
harness.modal.openTsukihimeModal();
|
||||
harness.titleInput.value = 'Futsutsuka na Akujo';
|
||||
|
||||
// The search parks on the language config, then the user closes and
|
||||
// reopens the modal before it resolves.
|
||||
pressKey(harness, 'Enter');
|
||||
harness.modal.closeTsukihimeModal();
|
||||
harness.modal.openTsukihimeModal();
|
||||
await flushAsyncWork();
|
||||
harness.status.textContent = 'Fresh modal session';
|
||||
|
||||
openGate();
|
||||
await flushAsyncWork();
|
||||
|
||||
assert.deepEqual(harness.state.tsukihimeEntries, []);
|
||||
assert.deepEqual(visibleEntryTitles(harness), []);
|
||||
assert.equal(harness.status.textContent, 'Fresh modal session');
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,7 +41,13 @@ export function createTsukihimeModal(
|
||||
|
||||
// Defaults to English until the configured secondary languages arrive.
|
||||
let secondaryLanguages: string[] = ['en'];
|
||||
// Both tab filters read the configured languages, so a search must wait for
|
||||
// them rather than filtering against the English fallback.
|
||||
let secondaryLanguagesReady: Promise<void> = Promise.resolve();
|
||||
let activeDownloadToken = 0;
|
||||
// Bumped by every new search and by closing the modal, so results that
|
||||
// arrive late cannot repopulate a reopened modal or a newer search.
|
||||
let activeSearchToken = 0;
|
||||
|
||||
function secondaryTabLabel(): string {
|
||||
return describeTsukihimeTabLanguages(secondaryLanguages);
|
||||
@@ -61,6 +67,47 @@ export function createTsukihimeModal(
|
||||
);
|
||||
}
|
||||
|
||||
// Releases are filtered by the languages the search index reports for
|
||||
// them. Most releases carry no Japanese track, so the primary tab hides
|
||||
// them outright. A release with no language data cannot be classified and
|
||||
// stays on the secondary tab, mirroring how unlabeled tracks are handled.
|
||||
function entryMatchesTab(entry: TsukihimeEntry, tab: 'secondary' | 'primary'): boolean {
|
||||
if (tab === 'primary') {
|
||||
return entry.sublangs.some((lang) => normalizeTsukihimeLangCode(lang) === 'ja');
|
||||
}
|
||||
if (entry.sublangs.length === 0) return true;
|
||||
return entry.sublangs.some(
|
||||
(lang) =>
|
||||
normalizeTsukihimeLangCode(lang) !== 'ja' &&
|
||||
tsukihimeTrackMatchesLanguages(lang, secondaryLanguages),
|
||||
);
|
||||
}
|
||||
|
||||
function getVisibleEntries(): TsukihimeEntry[] {
|
||||
return ctx.state.tsukihimeEntries.filter((entry) =>
|
||||
entryMatchesTab(entry, ctx.state.tsukihimeActiveTab),
|
||||
);
|
||||
}
|
||||
|
||||
function describeEmptyReleases(): string {
|
||||
const otherTab = ctx.state.tsukihimeActiveTab === 'primary' ? 'secondary' : 'primary';
|
||||
const otherTabHasReleases = ctx.state.tsukihimeEntries.some((entry) =>
|
||||
entryMatchesTab(entry, otherTab),
|
||||
);
|
||||
const language = ctx.state.tsukihimeActiveTab === 'primary' ? 'Japanese' : secondaryTabLabel();
|
||||
const otherLabel = otherTab === 'primary' ? 'Japanese' : secondaryTabLabel();
|
||||
return otherTabHasReleases
|
||||
? `No releases with ${language} subtitles. Switch to the ${otherLabel} tab.`
|
||||
: `No releases with ${language} subtitles.`;
|
||||
}
|
||||
|
||||
function clearFiles(): void {
|
||||
ctx.state.tsukihimeFiles = [];
|
||||
ctx.state.selectedTsukihimeFileIndex = 0;
|
||||
ctx.dom.tsukihimeFilesList.innerHTML = '';
|
||||
ctx.dom.tsukihimeFilesSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
function renderTabs(): void {
|
||||
const primaryActive = ctx.state.tsukihimeActiveTab === 'primary';
|
||||
ctx.dom.tsukihimeTabSecondaryButton.setAttribute(
|
||||
@@ -98,12 +145,37 @@ export function createTsukihimeModal(
|
||||
ctx.state.selectedTsukihimeFileIndex = 0;
|
||||
renderTabs();
|
||||
|
||||
if (ctx.state.tsukihimeFiles.length === 0) return;
|
||||
const currentEntry = ctx.state.tsukihimeEntries.find(
|
||||
(entry) => entry.id === ctx.state.currentTsukihimeEntryId,
|
||||
);
|
||||
if (currentEntry && !entryMatchesTab(currentEntry, tab)) {
|
||||
// The selected release is hidden on this tab; drop its tracks so the
|
||||
// list matches what the tab claims to show.
|
||||
ctx.state.currentTsukihimeEntryId = null;
|
||||
ctx.state.selectedTsukihimeEntryIndex = 0;
|
||||
clearFiles();
|
||||
renderEntries();
|
||||
setTsukihimeStatus(
|
||||
getVisibleEntries().length === 0 ? describeEmptyReleases() : 'Select a release.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleEntries = getVisibleEntries();
|
||||
ctx.state.selectedTsukihimeEntryIndex = currentEntry ? visibleEntries.indexOf(currentEntry) : 0;
|
||||
renderEntries();
|
||||
|
||||
if (ctx.state.tsukihimeFiles.length > 0) {
|
||||
renderFiles();
|
||||
if (getVisibleFiles().length === 0) {
|
||||
setTsukihimeStatus(describeEmptyTab());
|
||||
} else {
|
||||
setTsukihimeStatus('Select a subtitle track.');
|
||||
setTsukihimeStatus(
|
||||
getVisibleFiles().length === 0 ? describeEmptyTab() : 'Select a subtitle track.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!currentEntry && ctx.state.tsukihimeEntries.length > 0) {
|
||||
setTsukihimeStatus(
|
||||
visibleEntries.length === 0 ? describeEmptyReleases() : 'Select a release.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,13 +193,14 @@ export function createTsukihimeModal(
|
||||
|
||||
function renderEntries(): void {
|
||||
ctx.dom.tsukihimeEntriesList.innerHTML = '';
|
||||
if (ctx.state.tsukihimeEntries.length === 0) {
|
||||
const visibleEntries = getVisibleEntries();
|
||||
if (visibleEntries.length === 0) {
|
||||
ctx.dom.tsukihimeEntriesSection.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.dom.tsukihimeEntriesSection.classList.remove('hidden');
|
||||
ctx.state.tsukihimeEntries.forEach((entry, index) => {
|
||||
visibleEntries.forEach((entry, index) => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = entry.title;
|
||||
|
||||
@@ -210,11 +283,15 @@ export function createTsukihimeModal(
|
||||
return;
|
||||
}
|
||||
|
||||
const searchToken = ++activeSearchToken;
|
||||
resetTsukihimeLists();
|
||||
setTsukihimeStatus('Searching TsukiHime...');
|
||||
await secondaryLanguagesReady;
|
||||
if (searchToken !== activeSearchToken) return;
|
||||
|
||||
const response: TsukihimeApiResponse<TsukihimeEntry[]> =
|
||||
await window.electronAPI.tsukihimeSearchEntries({ query });
|
||||
if (searchToken !== activeSearchToken) return;
|
||||
if (!response.ok) {
|
||||
setTsukihimeStatus(response.error.error, true);
|
||||
return;
|
||||
@@ -228,20 +305,22 @@ export function createTsukihimeModal(
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleEntries = getVisibleEntries();
|
||||
if (visibleEntries.length === 0) {
|
||||
setTsukihimeStatus(describeEmptyReleases());
|
||||
return;
|
||||
}
|
||||
|
||||
setTsukihimeStatus('Select a release.');
|
||||
renderEntries();
|
||||
if (ctx.state.tsukihimeEntries.length === 1) {
|
||||
if (visibleEntries.length === 1) {
|
||||
selectEntry(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFiles(entryId: number): Promise<void> {
|
||||
setTsukihimeStatus('Loading subtitle tracks...');
|
||||
ctx.state.tsukihimeFiles = [];
|
||||
ctx.state.selectedTsukihimeFileIndex = 0;
|
||||
|
||||
ctx.dom.tsukihimeFilesList.innerHTML = '';
|
||||
ctx.dom.tsukihimeFilesSection.classList.add('hidden');
|
||||
clearFiles();
|
||||
|
||||
const response: TsukihimeApiResponse<TsukihimeSubtitleFile[]> =
|
||||
await window.electronAPI.tsukihimeListFiles({ entryId });
|
||||
@@ -279,11 +358,14 @@ export function createTsukihimeModal(
|
||||
}
|
||||
}
|
||||
|
||||
// `index` addresses the entries visible on the active tab, not the full
|
||||
// search result list.
|
||||
function selectEntry(index: number): void {
|
||||
if (index < 0 || index >= ctx.state.tsukihimeEntries.length) return;
|
||||
const visibleEntries = getVisibleEntries();
|
||||
if (index < 0 || index >= visibleEntries.length) return;
|
||||
|
||||
ctx.state.selectedTsukihimeEntryIndex = index;
|
||||
ctx.state.currentTsukihimeEntryId = ctx.state.tsukihimeEntries[index]!.id;
|
||||
ctx.state.currentTsukihimeEntryId = visibleEntries[index]!.id;
|
||||
renderEntries();
|
||||
|
||||
if (ctx.state.currentTsukihimeEntryId !== null) {
|
||||
@@ -363,16 +445,15 @@ export function createTsukihimeModal(
|
||||
resetTsukihimeLists();
|
||||
renderTabs();
|
||||
|
||||
const secondaryLanguagesReady = loadSecondaryLanguages();
|
||||
secondaryLanguagesReady = loadSecondaryLanguages();
|
||||
|
||||
window.electronAPI
|
||||
.getJimakuMediaInfo()
|
||||
.then(async (info: JimakuMediaInfo) => {
|
||||
.then((info: JimakuMediaInfo) => {
|
||||
ctx.dom.tsukihimeTitleInput.value = info.title || '';
|
||||
ctx.dom.tsukihimeEpisodeInput.value = info.episode ? String(info.episode) : '';
|
||||
|
||||
if (info.confidence === 'high' && info.title && info.episode) {
|
||||
await secondaryLanguagesReady;
|
||||
void performTsukihimeSearch();
|
||||
} else if (info.title) {
|
||||
setTsukihimeStatus('Check title/episode and press Search.');
|
||||
@@ -389,6 +470,7 @@ export function createTsukihimeModal(
|
||||
if (!ctx.state.tsukihimeModalOpen) return;
|
||||
|
||||
activeDownloadToken += 1;
|
||||
activeSearchToken += 1;
|
||||
ctx.state.tsukihimeModalOpen = false;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
ctx.dom.tsukihimeModal.classList.add('hidden');
|
||||
@@ -438,9 +520,9 @@ export function createTsukihimeModal(
|
||||
ctx.state.selectedTsukihimeFileIndex + 1,
|
||||
);
|
||||
renderFiles();
|
||||
} else if (ctx.state.tsukihimeEntries.length > 0) {
|
||||
} else if (getVisibleEntries().length > 0) {
|
||||
ctx.state.selectedTsukihimeEntryIndex = Math.min(
|
||||
ctx.state.tsukihimeEntries.length - 1,
|
||||
getVisibleEntries().length - 1,
|
||||
ctx.state.selectedTsukihimeEntryIndex + 1,
|
||||
);
|
||||
renderEntries();
|
||||
@@ -456,7 +538,7 @@ export function createTsukihimeModal(
|
||||
ctx.state.selectedTsukihimeFileIndex - 1,
|
||||
);
|
||||
renderFiles();
|
||||
} else if (ctx.state.tsukihimeEntries.length > 0) {
|
||||
} else if (getVisibleEntries().length > 0) {
|
||||
ctx.state.selectedTsukihimeEntryIndex = Math.max(
|
||||
0,
|
||||
ctx.state.selectedTsukihimeEntryIndex - 1,
|
||||
@@ -470,7 +552,7 @@ export function createTsukihimeModal(
|
||||
e.preventDefault();
|
||||
if (getVisibleFiles().length > 0) {
|
||||
void selectFile(ctx.state.selectedTsukihimeFileIndex);
|
||||
} else if (ctx.state.tsukihimeEntries.length > 0) {
|
||||
} else if (getVisibleEntries().length > 0) {
|
||||
selectEntry(ctx.state.selectedTsukihimeEntryIndex);
|
||||
} else {
|
||||
void performTsukihimeSearch();
|
||||
|
||||
Reference in New Issue
Block a user