mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-25 00:11:26 -07:00
fix: align youtube playback with shared overlay startup
This commit is contained in:
235
src/renderer/modals/youtube-track-picker.ts
Normal file
235
src/renderer/modals/youtube-track-picker.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import type { YoutubePickerOpenPayload } from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
import { YOMITAN_POPUP_COMMAND_EVENT } from '../yomitan-popup.js';
|
||||
|
||||
function createOption(value: string, label: string): HTMLOptionElement {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = label;
|
||||
return option;
|
||||
}
|
||||
|
||||
export function createYoutubeTrackPickerModal(
|
||||
ctx: RendererContext,
|
||||
options: {
|
||||
modalStateReader: Pick<ModalStateReader, 'isAnyModalOpen'>;
|
||||
restorePointerInteractionState: () => void;
|
||||
syncSettingsModalSubtitleSuppression: () => void;
|
||||
},
|
||||
) {
|
||||
function setStatus(message: string, isError = false): void {
|
||||
ctx.state.youtubePickerStatus = message;
|
||||
ctx.dom.youtubePickerStatus.textContent = message;
|
||||
ctx.dom.youtubePickerStatus.style.color = isError
|
||||
? '#ed8796'
|
||||
: '#a5adcb';
|
||||
}
|
||||
|
||||
function getTrackLabel(trackId: string): string {
|
||||
return ctx.state.youtubePickerPayload?.tracks.find((track) => track.id === trackId)?.label ?? '';
|
||||
}
|
||||
|
||||
function renderTrackList(): void {
|
||||
ctx.dom.youtubePickerTracks.innerHTML = '';
|
||||
const payload = ctx.state.youtubePickerPayload;
|
||||
if (!payload || payload.tracks.length === 0) {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = '<span>No subtitle tracks found</span><span class="youtube-picker-track-meta">Continue without subtitles</span>';
|
||||
ctx.dom.youtubePickerTracks.appendChild(li);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const track of payload.tracks) {
|
||||
const li = document.createElement('li');
|
||||
const left = document.createElement('span');
|
||||
left.textContent = track.label;
|
||||
const right = document.createElement('span');
|
||||
right.className = 'youtube-picker-track-meta';
|
||||
right.textContent = `${track.kind} · ${track.language}`;
|
||||
li.append(left, right);
|
||||
ctx.dom.youtubePickerTracks.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
function syncSecondaryOptions(): void {
|
||||
const payload = ctx.state.youtubePickerPayload;
|
||||
const primaryTrackId = ctx.dom.youtubePickerPrimarySelect.value || null;
|
||||
ctx.dom.youtubePickerSecondarySelect.innerHTML = '';
|
||||
ctx.dom.youtubePickerSecondarySelect.appendChild(createOption('', 'None'));
|
||||
if (!payload) return;
|
||||
|
||||
for (const track of payload.tracks) {
|
||||
if (track.id === primaryTrackId) continue;
|
||||
ctx.dom.youtubePickerSecondarySelect.appendChild(createOption(track.id, track.label));
|
||||
}
|
||||
if (
|
||||
primaryTrackId &&
|
||||
ctx.dom.youtubePickerSecondarySelect.value === primaryTrackId
|
||||
) {
|
||||
ctx.dom.youtubePickerSecondarySelect.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function setSelection(primaryTrackId: string | null, secondaryTrackId: string | null): void {
|
||||
ctx.state.youtubePickerPrimaryTrackId = primaryTrackId;
|
||||
ctx.state.youtubePickerSecondaryTrackId = secondaryTrackId;
|
||||
ctx.dom.youtubePickerPrimarySelect.value = primaryTrackId ?? '';
|
||||
syncSecondaryOptions();
|
||||
ctx.dom.youtubePickerSecondarySelect.value = secondaryTrackId ?? '';
|
||||
}
|
||||
|
||||
function applyPayload(payload: YoutubePickerOpenPayload): void {
|
||||
ctx.state.youtubePickerPayload = payload;
|
||||
ctx.dom.youtubePickerTitle.textContent = `${payload.mode === 'generate' ? 'Generate' : 'Download'} subtitles for ${payload.url}`;
|
||||
ctx.dom.youtubePickerPrimarySelect.innerHTML = '';
|
||||
ctx.dom.youtubePickerSecondarySelect.innerHTML = '';
|
||||
|
||||
if (payload.tracks.length === 0) {
|
||||
ctx.dom.youtubePickerPrimarySelect.appendChild(createOption('', 'No tracks available'));
|
||||
ctx.dom.youtubePickerPrimarySelect.disabled = true;
|
||||
ctx.dom.youtubePickerSecondarySelect.disabled = true;
|
||||
ctx.dom.youtubePickerContinueButton.textContent = 'Continue without subtitles';
|
||||
setSelection(null, null);
|
||||
setStatus('No subtitle tracks were found. Playback will continue without subtitles.');
|
||||
renderTrackList();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.dom.youtubePickerPrimarySelect.disabled = false;
|
||||
ctx.dom.youtubePickerSecondarySelect.disabled = false;
|
||||
ctx.dom.youtubePickerContinueButton.textContent = 'Use selected subtitles';
|
||||
for (const track of payload.tracks) {
|
||||
ctx.dom.youtubePickerPrimarySelect.appendChild(createOption(track.id, track.label));
|
||||
}
|
||||
setSelection(payload.defaultPrimaryTrackId, payload.defaultSecondaryTrackId);
|
||||
renderTrackList();
|
||||
setStatus('Select the subtitle tracks to download.');
|
||||
}
|
||||
|
||||
async function resolveSelection(action: 'use-selected' | 'continue-without-subtitles'): Promise<void> {
|
||||
const payload = ctx.state.youtubePickerPayload;
|
||||
if (!payload) return;
|
||||
if (action === 'use-selected' && payload.hasTracks && !ctx.dom.youtubePickerPrimarySelect.value) {
|
||||
setStatus('Primary subtitle selection is required.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await window.electronAPI.youtubePickerResolve({
|
||||
sessionId: payload.sessionId,
|
||||
action,
|
||||
primaryTrackId: action === 'use-selected' ? ctx.dom.youtubePickerPrimarySelect.value || null : null,
|
||||
secondaryTrackId:
|
||||
action === 'use-selected' ? ctx.dom.youtubePickerSecondarySelect.value || null : null,
|
||||
});
|
||||
if (!response.ok) {
|
||||
setStatus(response.message, true);
|
||||
return;
|
||||
}
|
||||
closeYoutubePickerModal();
|
||||
}
|
||||
|
||||
function openYoutubePickerModal(payload: YoutubePickerOpenPayload): void {
|
||||
if (ctx.state.youtubePickerModalOpen) return;
|
||||
ctx.state.youtubePickerModalOpen = true;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
applyPayload(payload);
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
ctx.dom.youtubePickerModal.classList.remove('hidden');
|
||||
ctx.dom.youtubePickerModal.setAttribute('aria-hidden', 'false');
|
||||
window.electronAPI.notifyOverlayModalOpened('youtube-track-picker');
|
||||
}
|
||||
|
||||
function closeYoutubePickerModal(): void {
|
||||
if (!ctx.state.youtubePickerModalOpen) return;
|
||||
ctx.state.youtubePickerModalOpen = false;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
ctx.state.youtubePickerPayload = null;
|
||||
ctx.state.youtubePickerPrimaryTrackId = null;
|
||||
ctx.state.youtubePickerSecondaryTrackId = null;
|
||||
ctx.state.youtubePickerStatus = '';
|
||||
ctx.dom.youtubePickerModal.classList.add('hidden');
|
||||
ctx.dom.youtubePickerModal.setAttribute('aria-hidden', 'true');
|
||||
window.electronAPI.notifyOverlayModalClosed('youtube-track-picker');
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(YOMITAN_POPUP_COMMAND_EVENT, {
|
||||
detail: {
|
||||
type: 'refreshOptions',
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (!options.modalStateReader.isAnyModalOpen()) {
|
||||
ctx.dom.overlay.classList.remove('interactive');
|
||||
}
|
||||
options.restorePointerInteractionState();
|
||||
if (typeof ctx.dom.overlay.focus === 'function') {
|
||||
ctx.dom.overlay.focus({ preventScroll: true });
|
||||
}
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else {
|
||||
window.electronAPI.setIgnoreMouseEvents(false);
|
||||
}
|
||||
}
|
||||
window.focus();
|
||||
}
|
||||
|
||||
function handleYoutubePickerKeydown(e: KeyboardEvent): boolean {
|
||||
if (!ctx.state.youtubePickerModalOpen) return false;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
void resolveSelection('continue-without-subtitles');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void resolveSelection(
|
||||
ctx.state.youtubePickerPayload?.hasTracks ? 'use-selected' : 'continue-without-subtitles',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function wireDomEvents(): void {
|
||||
ctx.dom.youtubePickerPrimarySelect.addEventListener('change', () => {
|
||||
const primaryTrackId = ctx.dom.youtubePickerPrimarySelect.value || null;
|
||||
if (ctx.dom.youtubePickerSecondarySelect.value === primaryTrackId) {
|
||||
ctx.dom.youtubePickerSecondarySelect.value = '';
|
||||
}
|
||||
setSelection(primaryTrackId, ctx.dom.youtubePickerSecondarySelect.value || null);
|
||||
});
|
||||
|
||||
ctx.dom.youtubePickerSecondarySelect.addEventListener('change', () => {
|
||||
const primaryTrackId = ctx.dom.youtubePickerPrimarySelect.value || null;
|
||||
const secondaryTrackId = ctx.dom.youtubePickerSecondarySelect.value || null;
|
||||
if (primaryTrackId && secondaryTrackId === primaryTrackId) {
|
||||
ctx.dom.youtubePickerSecondarySelect.value = '';
|
||||
setStatus('Primary and secondary subtitles must be different.', true);
|
||||
return;
|
||||
}
|
||||
setSelection(primaryTrackId, secondaryTrackId);
|
||||
setStatus('Select the subtitle tracks to download.');
|
||||
});
|
||||
|
||||
ctx.dom.youtubePickerContinueButton.addEventListener('click', () => {
|
||||
void resolveSelection(
|
||||
ctx.state.youtubePickerPayload?.hasTracks ? 'use-selected' : 'continue-without-subtitles',
|
||||
);
|
||||
});
|
||||
|
||||
ctx.dom.youtubePickerCloseButton.addEventListener('click', () => {
|
||||
void resolveSelection('continue-without-subtitles');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
closeYoutubePickerModal,
|
||||
handleYoutubePickerKeydown,
|
||||
openYoutubePickerModal,
|
||||
wireDomEvents,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user