mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-23 00:11:28 -07:00
feat: update subtitle sidebar overlay behavior
This commit is contained in:
@@ -85,6 +85,36 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="youtubePickerModal" class="modal hidden" aria-hidden="true">
|
||||
<div class="modal-content youtube-picker-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">YouTube Subtitle Tracks</div>
|
||||
<button id="youtubePickerCloseButton" class="modal-close" type="button">
|
||||
Continue without subtitles
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body youtube-picker-body">
|
||||
<div id="youtubePickerTitle" class="youtube-picker-title"></div>
|
||||
<div class="youtube-picker-grid">
|
||||
<label class="youtube-picker-field">
|
||||
<span>Primary subtitle</span>
|
||||
<select id="youtubePickerPrimarySelect"></select>
|
||||
</label>
|
||||
<label class="youtube-picker-field">
|
||||
<span>Secondary subtitle</span>
|
||||
<select id="youtubePickerSecondarySelect"></select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="youtubePickerStatus" class="youtube-picker-status"></div>
|
||||
<ul id="youtubePickerTracks" class="youtube-picker-tracks"></ul>
|
||||
<div class="youtube-picker-footer">
|
||||
<button id="youtubePickerContinueButton" class="kiku-confirm-button" type="button">
|
||||
Use selected subtitles
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="kikuFieldGroupingModal" class="modal hidden" aria-hidden="true">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
|
||||
@@ -84,6 +84,18 @@ test('findActiveSubtitleCueIndex prefers timing match before text fallback', ()
|
||||
assert.equal(findActiveSubtitleCueIndex(cues, { text: 'same', startTime: null }), 0);
|
||||
});
|
||||
|
||||
test('findActiveSubtitleCueIndex prefers current subtitle timing over near-future clock lookahead', () => {
|
||||
const cues = [
|
||||
{ startTime: 231, endTime: 233.2, text: 'previous' },
|
||||
{ startTime: 233.05, endTime: 236, text: 'next' },
|
||||
];
|
||||
|
||||
assert.equal(
|
||||
findActiveSubtitleCueIndex(cues, { text: 'previous', startTime: 231 }, 233, 0),
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback', async () => {
|
||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||
const previousWindow = globals.window;
|
||||
|
||||
@@ -61,6 +61,18 @@ export function findActiveSubtitleCueIndex(
|
||||
return -1;
|
||||
}
|
||||
|
||||
const hasCurrentTiming =
|
||||
typeof current?.startTime === 'number' && Number.isFinite(current.startTime);
|
||||
|
||||
if (hasCurrentTiming) {
|
||||
const timingMatch = cues.findIndex(
|
||||
(cue) => current.startTime! >= cue.startTime && current.startTime! < cue.endTime,
|
||||
);
|
||||
if (timingMatch >= 0) {
|
||||
return timingMatch;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof currentTimeSec === 'number' && Number.isFinite(currentTimeSec)) {
|
||||
const activeOrUpcomingCue = cues.findIndex(
|
||||
(cue) =>
|
||||
@@ -81,15 +93,6 @@ export function findActiveSubtitleCueIndex(
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (typeof current.startTime === 'number' && Number.isFinite(current.startTime)) {
|
||||
const timingMatch = cues.findIndex(
|
||||
(cue) => current.startTime! >= cue.startTime && current.startTime! < cue.endTime,
|
||||
);
|
||||
if (timingMatch >= 0) {
|
||||
return timingMatch;
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedText = normalizeCueText(current.text);
|
||||
if (!normalizedText) {
|
||||
return -1;
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
SubtitleSidebarConfig,
|
||||
SubtitleCue,
|
||||
SubsyncSourceTrack,
|
||||
YoutubePickerOpenPayload,
|
||||
} from '../types';
|
||||
|
||||
export type KikuModalStep = 'select' | 'preview';
|
||||
@@ -40,6 +41,12 @@ export type RendererState = {
|
||||
currentEpisodeFilter: number | null;
|
||||
currentEntryId: number | null;
|
||||
|
||||
youtubePickerModalOpen: boolean;
|
||||
youtubePickerPayload: YoutubePickerOpenPayload | null;
|
||||
youtubePickerPrimaryTrackId: string | null;
|
||||
youtubePickerSecondaryTrackId: string | null;
|
||||
youtubePickerStatus: string;
|
||||
|
||||
kikuModalOpen: boolean;
|
||||
kikuSelectedCard: 1 | 2;
|
||||
kikuOriginalData: KikuDuplicateCardInfo | null;
|
||||
@@ -131,6 +138,12 @@ export function createRendererState(): RendererState {
|
||||
currentEpisodeFilter: null,
|
||||
currentEntryId: null,
|
||||
|
||||
youtubePickerModalOpen: false,
|
||||
youtubePickerPayload: null,
|
||||
youtubePickerPrimaryTrackId: null,
|
||||
youtubePickerSecondaryTrackId: null,
|
||||
youtubePickerStatus: '',
|
||||
|
||||
kikuModalOpen: false,
|
||||
kikuSelectedCard: 1,
|
||||
kikuOriginalData: null,
|
||||
|
||||
@@ -127,6 +127,10 @@ body {
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
#youtubePickerModal {
|
||||
z-index: 1110;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -138,11 +142,11 @@ body {
|
||||
.modal-content {
|
||||
width: min(720px, 92%);
|
||||
max-height: 80%;
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(36, 39, 58, 0.95);
|
||||
border: 1px solid rgba(110, 115, 141, 0.18);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
color: #cad3f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
@@ -161,16 +165,17 @@ body {
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(73, 77, 100, 0.5);
|
||||
color: #a5adcb;
|
||||
border: 1px solid rgba(110, 115, 141, 0.2);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(91, 96, 120, 0.6);
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
@@ -288,6 +293,91 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.youtube-picker-content {
|
||||
width: min(820px, 92%);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(198, 160, 246, 0.10), transparent 34%),
|
||||
linear-gradient(180deg, rgba(36, 39, 58, 0.98), rgba(30, 32, 48, 0.98));
|
||||
border-color: rgba(138, 173, 244, 0.25);
|
||||
}
|
||||
|
||||
.youtube-picker-body {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.youtube-picker-title {
|
||||
font-size: 13px;
|
||||
color: #b8c0e0;
|
||||
}
|
||||
|
||||
.youtube-picker-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.youtube-picker-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #a5adcb;
|
||||
}
|
||||
|
||||
.youtube-picker-field select {
|
||||
min-height: 36px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(110, 115, 141, 0.28);
|
||||
background: rgba(24, 25, 38, 0.92);
|
||||
color: #cad3f5;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.youtube-picker-status {
|
||||
min-height: 20px;
|
||||
font-size: 13px;
|
||||
color: #a5adcb;
|
||||
}
|
||||
|
||||
.youtube-picker-tracks {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(110, 115, 141, 0.18);
|
||||
}
|
||||
|
||||
.youtube-picker-tracks li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid rgba(110, 115, 141, 0.08);
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.youtube-picker-tracks li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.youtube-picker-track-meta {
|
||||
color: #6e738d;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.youtube-picker-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.youtube-picker-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.jimaku-form {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -982,15 +1072,15 @@ iframe[id^='yomitan-popup'] {
|
||||
.kiku-confirm-button {
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(100, 180, 255, 0.4);
|
||||
background: rgba(100, 180, 255, 0.15);
|
||||
color: rgba(100, 180, 255, 0.95);
|
||||
border: 1px solid rgba(138, 173, 244, 0.4);
|
||||
background: rgba(138, 173, 244, 0.15);
|
||||
color: #8aadf4;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.kiku-confirm-button:hover {
|
||||
background: rgba(100, 180, 255, 0.25);
|
||||
background: rgba(138, 173, 244, 0.25);
|
||||
}
|
||||
|
||||
.subsync-modal-content {
|
||||
@@ -1288,9 +1378,9 @@ iframe[id^='yomitan-popup'] {
|
||||
.btn-learn {
|
||||
padding: 5px 14px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(100, 180, 255, 0.4);
|
||||
background: rgba(100, 180, 255, 0.15);
|
||||
color: rgba(100, 180, 255, 0.95);
|
||||
border: 1px solid rgba(138, 173, 244, 0.4);
|
||||
background: rgba(138, 173, 244, 0.15);
|
||||
color: #8aadf4;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
@@ -1298,28 +1388,28 @@ iframe[id^='yomitan-popup'] {
|
||||
}
|
||||
|
||||
.btn-learn:hover {
|
||||
background: rgba(100, 180, 255, 0.25);
|
||||
background: rgba(138, 173, 244, 0.25);
|
||||
}
|
||||
|
||||
.btn-learn.active {
|
||||
border-color: rgba(100, 180, 255, 0.7);
|
||||
background: rgba(100, 180, 255, 0.25);
|
||||
border-color: rgba(138, 173, 244, 0.7);
|
||||
background: rgba(138, 173, 244, 0.25);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 5px 12px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(110, 115, 141, 0.2);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
color: #6e738d;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, color 120ms ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background: rgba(73, 77, 100, 0.4);
|
||||
color: #a5adcb;
|
||||
}
|
||||
|
||||
.controller-debug-content {
|
||||
|
||||
@@ -20,6 +20,15 @@ export type RendererDom = {
|
||||
jimakuFilesList: HTMLUListElement;
|
||||
jimakuBroadenButton: HTMLButtonElement;
|
||||
|
||||
youtubePickerModal: HTMLDivElement;
|
||||
youtubePickerTitle: HTMLDivElement;
|
||||
youtubePickerPrimarySelect: HTMLSelectElement;
|
||||
youtubePickerSecondarySelect: HTMLSelectElement;
|
||||
youtubePickerContinueButton: HTMLButtonElement;
|
||||
youtubePickerCloseButton: HTMLButtonElement;
|
||||
youtubePickerStatus: HTMLDivElement;
|
||||
youtubePickerTracks: HTMLUListElement;
|
||||
|
||||
kikuModal: HTMLDivElement;
|
||||
kikuCard1: HTMLDivElement;
|
||||
kikuCard2: HTMLDivElement;
|
||||
@@ -120,6 +129,19 @@ export function resolveRendererDom(): RendererDom {
|
||||
jimakuFilesList: getRequiredElement<HTMLUListElement>('jimakuFiles'),
|
||||
jimakuBroadenButton: getRequiredElement<HTMLButtonElement>('jimakuBroaden'),
|
||||
|
||||
youtubePickerModal: getRequiredElement<HTMLDivElement>('youtubePickerModal'),
|
||||
youtubePickerTitle: getRequiredElement<HTMLDivElement>('youtubePickerTitle'),
|
||||
youtubePickerPrimarySelect: getRequiredElement<HTMLSelectElement>('youtubePickerPrimarySelect'),
|
||||
youtubePickerSecondarySelect: getRequiredElement<HTMLSelectElement>(
|
||||
'youtubePickerSecondarySelect',
|
||||
),
|
||||
youtubePickerContinueButton: getRequiredElement<HTMLButtonElement>(
|
||||
'youtubePickerContinueButton',
|
||||
),
|
||||
youtubePickerCloseButton: getRequiredElement<HTMLButtonElement>('youtubePickerCloseButton'),
|
||||
youtubePickerStatus: getRequiredElement<HTMLDivElement>('youtubePickerStatus'),
|
||||
youtubePickerTracks: getRequiredElement<HTMLUListElement>('youtubePickerTracks'),
|
||||
|
||||
kikuModal: getRequiredElement<HTMLDivElement>('kikuFieldGroupingModal'),
|
||||
kikuCard1: getRequiredElement<HTMLDivElement>('kikuCard1'),
|
||||
kikuCard2: getRequiredElement<HTMLDivElement>('kikuCard2'),
|
||||
|
||||
Reference in New Issue
Block a user