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>
|
||||||
</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 id="kikuFieldGroupingModal" class="modal hidden" aria-hidden="true">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<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);
|
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 () => {
|
test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback', async () => {
|
||||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||||
const previousWindow = globals.window;
|
const previousWindow = globals.window;
|
||||||
|
|||||||
@@ -61,6 +61,18 @@ export function findActiveSubtitleCueIndex(
|
|||||||
return -1;
|
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)) {
|
if (typeof currentTimeSec === 'number' && Number.isFinite(currentTimeSec)) {
|
||||||
const activeOrUpcomingCue = cues.findIndex(
|
const activeOrUpcomingCue = cues.findIndex(
|
||||||
(cue) =>
|
(cue) =>
|
||||||
@@ -81,15 +93,6 @@ export function findActiveSubtitleCueIndex(
|
|||||||
return -1;
|
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);
|
const normalizedText = normalizeCueText(current.text);
|
||||||
if (!normalizedText) {
|
if (!normalizedText) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type {
|
|||||||
SubtitleSidebarConfig,
|
SubtitleSidebarConfig,
|
||||||
SubtitleCue,
|
SubtitleCue,
|
||||||
SubsyncSourceTrack,
|
SubsyncSourceTrack,
|
||||||
|
YoutubePickerOpenPayload,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
export type KikuModalStep = 'select' | 'preview';
|
export type KikuModalStep = 'select' | 'preview';
|
||||||
@@ -40,6 +41,12 @@ export type RendererState = {
|
|||||||
currentEpisodeFilter: number | null;
|
currentEpisodeFilter: number | null;
|
||||||
currentEntryId: number | null;
|
currentEntryId: number | null;
|
||||||
|
|
||||||
|
youtubePickerModalOpen: boolean;
|
||||||
|
youtubePickerPayload: YoutubePickerOpenPayload | null;
|
||||||
|
youtubePickerPrimaryTrackId: string | null;
|
||||||
|
youtubePickerSecondaryTrackId: string | null;
|
||||||
|
youtubePickerStatus: string;
|
||||||
|
|
||||||
kikuModalOpen: boolean;
|
kikuModalOpen: boolean;
|
||||||
kikuSelectedCard: 1 | 2;
|
kikuSelectedCard: 1 | 2;
|
||||||
kikuOriginalData: KikuDuplicateCardInfo | null;
|
kikuOriginalData: KikuDuplicateCardInfo | null;
|
||||||
@@ -131,6 +138,12 @@ export function createRendererState(): RendererState {
|
|||||||
currentEpisodeFilter: null,
|
currentEpisodeFilter: null,
|
||||||
currentEntryId: null,
|
currentEntryId: null,
|
||||||
|
|
||||||
|
youtubePickerModalOpen: false,
|
||||||
|
youtubePickerPayload: null,
|
||||||
|
youtubePickerPrimaryTrackId: null,
|
||||||
|
youtubePickerSecondaryTrackId: null,
|
||||||
|
youtubePickerStatus: '',
|
||||||
|
|
||||||
kikuModalOpen: false,
|
kikuModalOpen: false,
|
||||||
kikuSelectedCard: 1,
|
kikuSelectedCard: 1,
|
||||||
kikuOriginalData: null,
|
kikuOriginalData: null,
|
||||||
|
|||||||
@@ -127,6 +127,10 @@ body {
|
|||||||
z-index: 1100;
|
z-index: 1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#youtubePickerModal {
|
||||||
|
z-index: 1110;
|
||||||
|
}
|
||||||
|
|
||||||
.modal.hidden {
|
.modal.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -138,11 +142,11 @@ body {
|
|||||||
.modal-content {
|
.modal-content {
|
||||||
width: min(720px, 92%);
|
width: min(720px, 92%);
|
||||||
max-height: 80%;
|
max-height: 80%;
|
||||||
background: rgba(20, 20, 20, 0.95);
|
background: rgba(36, 39, 58, 0.95);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(110, 115, 141, 0.18);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
color: #fff;
|
color: #cad3f5;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -161,16 +165,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(73, 77, 100, 0.5);
|
||||||
color: #fff;
|
color: #a5adcb;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(110, 115, 141, 0.2);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close:hover {
|
.modal-close:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(91, 96, 120, 0.6);
|
||||||
|
color: #cad3f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
@@ -288,6 +293,91 @@ body {
|
|||||||
display: none;
|
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) {
|
@media (max-width: 640px) {
|
||||||
.jimaku-form {
|
.jimaku-form {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -982,15 +1072,15 @@ iframe[id^='yomitan-popup'] {
|
|||||||
.kiku-confirm-button {
|
.kiku-confirm-button {
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid rgba(100, 180, 255, 0.4);
|
border: 1px solid rgba(138, 173, 244, 0.4);
|
||||||
background: rgba(100, 180, 255, 0.15);
|
background: rgba(138, 173, 244, 0.15);
|
||||||
color: rgba(100, 180, 255, 0.95);
|
color: #8aadf4;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kiku-confirm-button:hover {
|
.kiku-confirm-button:hover {
|
||||||
background: rgba(100, 180, 255, 0.25);
|
background: rgba(138, 173, 244, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subsync-modal-content {
|
.subsync-modal-content {
|
||||||
@@ -1288,9 +1378,9 @@ iframe[id^='yomitan-popup'] {
|
|||||||
.btn-learn {
|
.btn-learn {
|
||||||
padding: 5px 14px;
|
padding: 5px 14px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 1px solid rgba(100, 180, 255, 0.4);
|
border: 1px solid rgba(138, 173, 244, 0.4);
|
||||||
background: rgba(100, 180, 255, 0.15);
|
background: rgba(138, 173, 244, 0.15);
|
||||||
color: rgba(100, 180, 255, 0.95);
|
color: #8aadf4;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1298,28 +1388,28 @@ iframe[id^='yomitan-popup'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-learn:hover {
|
.btn-learn:hover {
|
||||||
background: rgba(100, 180, 255, 0.25);
|
background: rgba(138, 173, 244, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-learn.active {
|
.btn-learn.active {
|
||||||
border-color: rgba(100, 180, 255, 0.7);
|
border-color: rgba(138, 173, 244, 0.7);
|
||||||
background: rgba(100, 180, 255, 0.25);
|
background: rgba(138, 173, 244, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(110, 115, 141, 0.2);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: rgba(255, 255, 255, 0.55);
|
color: #6e738d;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 120ms ease, color 120ms ease;
|
transition: background 120ms ease, color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(73, 77, 100, 0.4);
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: #a5adcb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controller-debug-content {
|
.controller-debug-content {
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ export type RendererDom = {
|
|||||||
jimakuFilesList: HTMLUListElement;
|
jimakuFilesList: HTMLUListElement;
|
||||||
jimakuBroadenButton: HTMLButtonElement;
|
jimakuBroadenButton: HTMLButtonElement;
|
||||||
|
|
||||||
|
youtubePickerModal: HTMLDivElement;
|
||||||
|
youtubePickerTitle: HTMLDivElement;
|
||||||
|
youtubePickerPrimarySelect: HTMLSelectElement;
|
||||||
|
youtubePickerSecondarySelect: HTMLSelectElement;
|
||||||
|
youtubePickerContinueButton: HTMLButtonElement;
|
||||||
|
youtubePickerCloseButton: HTMLButtonElement;
|
||||||
|
youtubePickerStatus: HTMLDivElement;
|
||||||
|
youtubePickerTracks: HTMLUListElement;
|
||||||
|
|
||||||
kikuModal: HTMLDivElement;
|
kikuModal: HTMLDivElement;
|
||||||
kikuCard1: HTMLDivElement;
|
kikuCard1: HTMLDivElement;
|
||||||
kikuCard2: HTMLDivElement;
|
kikuCard2: HTMLDivElement;
|
||||||
@@ -120,6 +129,19 @@ export function resolveRendererDom(): RendererDom {
|
|||||||
jimakuFilesList: getRequiredElement<HTMLUListElement>('jimakuFiles'),
|
jimakuFilesList: getRequiredElement<HTMLUListElement>('jimakuFiles'),
|
||||||
jimakuBroadenButton: getRequiredElement<HTMLButtonElement>('jimakuBroaden'),
|
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'),
|
kikuModal: getRequiredElement<HTMLDivElement>('kikuFieldGroupingModal'),
|
||||||
kikuCard1: getRequiredElement<HTMLDivElement>('kikuCard1'),
|
kikuCard1: getRequiredElement<HTMLDivElement>('kikuCard1'),
|
||||||
kikuCard2: getRequiredElement<HTMLDivElement>('kikuCard2'),
|
kikuCard2: getRequiredElement<HTMLDivElement>('kikuCard2'),
|
||||||
|
|||||||
Reference in New Issue
Block a user