mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-12 05:16:19 -07:00
feat(anki): add multi-line subtitle selection to media timing review
This commit is contained in:
@@ -224,6 +224,57 @@
|
||||
</div>
|
||||
|
||||
<div id="mediaTimingReviewEditor" class="media-timing-review-editor">
|
||||
<div class="media-timing-review-sentence-header">
|
||||
<div class="media-timing-review-sentence-heading">
|
||||
<span class="media-timing-review-sentence-label">Sentence on card</span>
|
||||
<span id="mediaTimingReviewLineCount" class="media-timing-review-line-count">
|
||||
1 line
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
id="mediaTimingReviewLineControls"
|
||||
class="media-timing-review-line-controls hidden"
|
||||
>
|
||||
<div class="media-timing-review-line-stepper">
|
||||
<span>Prev</span>
|
||||
<button
|
||||
id="mediaTimingReviewPrevRemove"
|
||||
type="button"
|
||||
aria-label="Remove the earliest added previous subtitle line from the card sentence"
|
||||
title="Remove the earliest previous line (Shift+P)"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<button
|
||||
id="mediaTimingReviewPrevAdd"
|
||||
type="button"
|
||||
aria-label="Add the previous subtitle line to the card sentence"
|
||||
title="Add the previous line (P)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div class="media-timing-review-line-stepper">
|
||||
<span>Next</span>
|
||||
<button
|
||||
id="mediaTimingReviewNextRemove"
|
||||
type="button"
|
||||
aria-label="Remove the latest added next subtitle line from the card sentence"
|
||||
title="Remove the latest next line (Shift+N)"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<button
|
||||
id="mediaTimingReviewNextAdd"
|
||||
type="button"
|
||||
aria-label="Add the next subtitle line to the card sentence"
|
||||
title="Add the next line (N)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<blockquote id="mediaTimingReviewText" class="media-timing-review-text"></blockquote>
|
||||
|
||||
<div class="media-timing-review-readout" aria-live="polite">
|
||||
@@ -395,6 +446,7 @@
|
||||
<div class="media-timing-review-hints" aria-hidden="true">
|
||||
<span><kbd>Space</kbd> preview</span>
|
||||
<span><kbd>←</kbd><kbd>→</kbd> nudge focused edge</span>
|
||||
<span><kbd>P</kbd>/<kbd>N</kbd> add prev/next line</span>
|
||||
<span><kbd>Enter</kbd> confirm</span>
|
||||
<span><kbd>Esc</kbd> cancel</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildMediaTimingLineSelection,
|
||||
buildMediaTimingWaveformPath,
|
||||
constrainMediaTimingSelection,
|
||||
createMediaTimingPreviewRequestGuard,
|
||||
@@ -73,6 +74,35 @@ test('sliding keeps the clip length and stops at the timeline and media bounds',
|
||||
});
|
||||
});
|
||||
|
||||
test('line selection combines adjacent lines around the mined one and tracks their range', () => {
|
||||
const base = {
|
||||
previousLines: [
|
||||
{ text: '一行目', startTime: 0, endTime: 2 },
|
||||
{ text: '二行目', startTime: 2.5, endTime: 4 },
|
||||
],
|
||||
nextLines: [
|
||||
{ text: '四行目', startTime: 7.5, endTime: 9 },
|
||||
{ text: '五行目', startTime: 9.5, endTime: 11 },
|
||||
],
|
||||
text: '採掘行',
|
||||
originalStartTime: 5,
|
||||
originalEndTime: 7,
|
||||
};
|
||||
|
||||
const none = buildMediaTimingLineSelection({ ...base, previousCount: 0, nextCount: 0 });
|
||||
assert.deepEqual(none.lineTexts, ['採掘行']);
|
||||
assert.equal(none.currentLineIndex, 0);
|
||||
assert.equal(none.rangeStart, 5);
|
||||
assert.equal(none.rangeEnd, 7);
|
||||
|
||||
const expanded = buildMediaTimingLineSelection({ ...base, previousCount: 1, nextCount: 2 });
|
||||
assert.deepEqual(expanded.lineTexts, ['二行目', '採掘行', '四行目', '五行目']);
|
||||
assert.equal(expanded.currentLineIndex, 1);
|
||||
assert.equal(expanded.sentence, '二行目 採掘行 四行目 五行目');
|
||||
assert.equal(expanded.rangeStart, 2.5);
|
||||
assert.equal(expanded.rangeEnd, 11);
|
||||
});
|
||||
|
||||
test('preview request guard blocks overlap and invalidates stale responses', () => {
|
||||
const guard = createMediaTimingPreviewRequestGuard();
|
||||
const first = guard.begin();
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { MediaTimingReviewDecision, MediaTimingReviewOpenPayload } from '../../types/anki';
|
||||
import type {
|
||||
MediaTimingReviewContextLine,
|
||||
MediaTimingReviewDecision,
|
||||
MediaTimingReviewOpenPayload,
|
||||
} from '../../types/anki';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
import { createModalFocusGuard } from './modal-focus-guard';
|
||||
|
||||
@@ -6,6 +10,7 @@ const MINIMUM_CLIP_SECONDS = 0.1;
|
||||
const FINE_ADJUST_SECONDS = 0.1;
|
||||
const COARSE_ADJUST_SECONDS = 0.5;
|
||||
const TIMELINE_EXPANSION_SECONDS = 2;
|
||||
const LINE_REVEAL_MARGIN_SECONDS = 1;
|
||||
|
||||
function clamp(value: number, minimum: number, maximum: number): number {
|
||||
return Math.min(maximum, Math.max(minimum, value));
|
||||
@@ -23,6 +28,43 @@ export function formatMediaTimingTimestamp(seconds: number, includeMilliseconds
|
||||
return `${String(minutes).padStart(2, '0')}:${remaining.padStart(includeMilliseconds ? 6 : 2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves which subtitle lines the card sentence currently includes. The counts say
|
||||
* how many adjacent lines were pulled in on each side; the range covers those lines'
|
||||
* subtitle timings so the clip edges can follow them.
|
||||
*/
|
||||
export function buildMediaTimingLineSelection(options: {
|
||||
previousLines: MediaTimingReviewContextLine[];
|
||||
nextLines: MediaTimingReviewContextLine[];
|
||||
text: string;
|
||||
originalStartTime: number;
|
||||
originalEndTime: number;
|
||||
previousCount: number;
|
||||
nextCount: number;
|
||||
}): {
|
||||
lineTexts: string[];
|
||||
currentLineIndex: number;
|
||||
sentence: string;
|
||||
rangeStart: number;
|
||||
rangeEnd: number;
|
||||
} {
|
||||
const previous =
|
||||
options.previousCount > 0 ? options.previousLines.slice(-options.previousCount) : [];
|
||||
const next = options.nextCount > 0 ? options.nextLines.slice(0, options.nextCount) : [];
|
||||
const lineTexts = [
|
||||
...previous.map((line) => line.text),
|
||||
options.text,
|
||||
...next.map((line) => line.text),
|
||||
];
|
||||
return {
|
||||
lineTexts,
|
||||
currentLineIndex: previous.length,
|
||||
sentence: lineTexts.join(' '),
|
||||
rangeStart: previous[0]?.startTime ?? options.originalStartTime,
|
||||
rangeEnd: next[next.length - 1]?.endTime ?? options.originalEndTime,
|
||||
};
|
||||
}
|
||||
|
||||
export function createMediaTimingPreviewRequestGuard() {
|
||||
let sequence = 0;
|
||||
let activeRequestId: number | null = null;
|
||||
@@ -128,6 +170,10 @@ export function createMediaTimingReviewModal(
|
||||
let selectionEnd = 0;
|
||||
let timelineStart = 0;
|
||||
let timelineEnd = 0;
|
||||
let previousCount = 0;
|
||||
let nextCount = 0;
|
||||
let startPadSeconds = 0;
|
||||
let endPadSeconds = 0;
|
||||
let resolveInFlight = false;
|
||||
let previewPlaying = false;
|
||||
let previewTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -304,6 +350,86 @@ export function createMediaTimingReviewModal(
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function currentLineSelection(): ReturnType<typeof buildMediaTimingLineSelection> {
|
||||
return buildMediaTimingLineSelection({
|
||||
previousLines: payload?.previousLines ?? [],
|
||||
nextLines: payload?.nextLines ?? [],
|
||||
text: payload?.text ?? '',
|
||||
originalStartTime: payload?.originalStartTime ?? 0,
|
||||
originalEndTime: payload?.originalEndTime ?? 0,
|
||||
previousCount,
|
||||
nextCount,
|
||||
});
|
||||
}
|
||||
|
||||
function renderSentence(): void {
|
||||
if (!payload) return;
|
||||
const selection = currentLineSelection();
|
||||
ctx.dom.mediaTimingReviewText.replaceChildren(
|
||||
...selection.lineTexts.map((text, index) => {
|
||||
const line = document.createElement('span');
|
||||
line.className =
|
||||
index === selection.currentLineIndex
|
||||
? 'media-timing-review-line is-current'
|
||||
: 'media-timing-review-line';
|
||||
line.textContent = text;
|
||||
return line;
|
||||
}),
|
||||
);
|
||||
const total = selection.lineTexts.length;
|
||||
ctx.dom.mediaTimingReviewLineCount.textContent = total === 1 ? '1 line' : `${total} lines`;
|
||||
const hasContext = payload.previousLines.length > 0 || payload.nextLines.length > 0;
|
||||
ctx.dom.mediaTimingReviewLineControls.classList.toggle('hidden', !hasContext);
|
||||
ctx.dom.mediaTimingReviewPrevAdd.disabled = previousCount >= payload.previousLines.length;
|
||||
ctx.dom.mediaTimingReviewPrevRemove.disabled = previousCount <= 0;
|
||||
ctx.dom.mediaTimingReviewNextAdd.disabled = nextCount >= payload.nextLines.length;
|
||||
ctx.dom.mediaTimingReviewNextRemove.disabled = nextCount <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or removes an adjacent subtitle line from the card sentence, then follows the
|
||||
* affected clip edge to the new outermost line while keeping the user's other edge.
|
||||
*/
|
||||
function adjustLines(direction: 'previous' | 'next', delta: number): void {
|
||||
if (!payload || resolveInFlight) return;
|
||||
const available =
|
||||
direction === 'previous' ? payload.previousLines.length : payload.nextLines.length;
|
||||
const current = direction === 'previous' ? previousCount : nextCount;
|
||||
const updated = clamp(current + delta, 0, available);
|
||||
if (updated === current) return;
|
||||
if (direction === 'previous') previousCount = updated;
|
||||
else nextCount = updated;
|
||||
|
||||
const selection = currentLineSelection();
|
||||
const mediaEnd = payload.mediaDuration ?? Number.POSITIVE_INFINITY;
|
||||
let timelineChanged = false;
|
||||
let capped = false;
|
||||
if (direction === 'previous') {
|
||||
const target = Math.max(0, selection.rangeStart - startPadSeconds);
|
||||
if (target < timelineStart) {
|
||||
timelineStart = Math.max(0, target - LINE_REVEAL_MARGIN_SECONDS);
|
||||
timelineChanged = true;
|
||||
}
|
||||
updateSelection(target, selectionEnd);
|
||||
capped = selectionStart > target + 0.001;
|
||||
} else {
|
||||
const target = Math.min(mediaEnd, selection.rangeEnd + endPadSeconds);
|
||||
if (target > timelineEnd) {
|
||||
timelineEnd = Math.min(mediaEnd, target + LINE_REVEAL_MARGIN_SECONDS);
|
||||
timelineChanged = true;
|
||||
}
|
||||
updateSelection(selectionStart, target);
|
||||
capped = selectionEnd < target - 0.001;
|
||||
}
|
||||
renderSentence();
|
||||
if (capped && payload.maxMediaDuration > 0) {
|
||||
setStatus(
|
||||
`Clip length is capped at ${payload.maxMediaDuration}s, so the audio cannot cover every added line.`,
|
||||
);
|
||||
}
|
||||
if (timelineChanged) queueWaveformLoad(120);
|
||||
}
|
||||
|
||||
function updateSelection(nextStart: number, nextEnd: number): void {
|
||||
if (!payload) return;
|
||||
const mediaEnd = payload.mediaDuration ?? Number.POSITIVE_INFINITY;
|
||||
@@ -507,9 +633,21 @@ export function createMediaTimingReviewModal(
|
||||
button.disabled = false;
|
||||
});
|
||||
renderSelection();
|
||||
renderSentence();
|
||||
}
|
||||
}
|
||||
|
||||
function confirmSelection(): void {
|
||||
if (!payload) return;
|
||||
const includesAdjacentLines = previousCount > 0 || nextCount > 0;
|
||||
void resolveReview({
|
||||
action: 'confirm',
|
||||
startTime: selectionStart,
|
||||
endTime: selectionEnd,
|
||||
...(includesAdjacentLines ? { text: currentLineSelection().sentence } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async function togglePreview(): Promise<void> {
|
||||
if (!payload || resolveInFlight) return;
|
||||
if (previewPlaying) {
|
||||
@@ -554,11 +692,19 @@ export function createMediaTimingReviewModal(
|
||||
function openMediaTimingReviewModal(nextPayload: MediaTimingReviewOpenPayload): void {
|
||||
previewRequest.invalidate();
|
||||
cancelDrag();
|
||||
payload = nextPayload;
|
||||
payload = {
|
||||
...nextPayload,
|
||||
previousLines: nextPayload.previousLines ?? [],
|
||||
nextLines: nextPayload.nextLines ?? [],
|
||||
};
|
||||
selectionStart = nextPayload.selectionStartTime;
|
||||
selectionEnd = nextPayload.selectionEndTime;
|
||||
timelineStart = nextPayload.timelineStartTime;
|
||||
timelineEnd = nextPayload.timelineEndTime;
|
||||
previousCount = 0;
|
||||
nextCount = 0;
|
||||
startPadSeconds = Math.max(0, nextPayload.originalStartTime - nextPayload.selectionStartTime);
|
||||
endPadSeconds = Math.max(0, nextPayload.selectionEndTime - nextPayload.originalEndTime);
|
||||
resolveInFlight = false;
|
||||
setPreviewPlaying(false);
|
||||
ctx.dom.mediaTimingReviewKind.textContent =
|
||||
@@ -568,12 +714,12 @@ export function createMediaTimingReviewModal(
|
||||
? 'Audio card'
|
||||
: 'Sentence card';
|
||||
ctx.dom.mediaTimingReviewKind.dataset.kind = nextPayload.kind;
|
||||
ctx.dom.mediaTimingReviewText.textContent = nextPayload.text;
|
||||
ctx.dom.mediaTimingReviewDiscard.textContent =
|
||||
nextPayload.noteId !== undefined ? 'Delete card' : "Don't create card";
|
||||
setStatus('');
|
||||
showEditor();
|
||||
renderSelection();
|
||||
renderSentence();
|
||||
ctx.state.mediaTimingReviewModalOpen = true;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
@@ -625,7 +771,21 @@ export function createMediaTimingReviewModal(
|
||||
!(event.target instanceof Element && event.target.closest('button'))
|
||||
) {
|
||||
event.preventDefault();
|
||||
void resolveReview({ action: 'confirm', startTime: selectionStart, endTime: selectionEnd });
|
||||
confirmSelection();
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
(event.key === 'p' || event.key === 'P' || event.key === 'n' || event.key === 'N') &&
|
||||
ctx.dom.mediaTimingReviewCancelStep.classList.contains('hidden') &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!event.altKey
|
||||
) {
|
||||
event.preventDefault();
|
||||
adjustLines(
|
||||
event.key === 'p' || event.key === 'P' ? 'previous' : 'next',
|
||||
event.shiftKey ? -1 : 1,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -662,8 +822,17 @@ export function createMediaTimingReviewModal(
|
||||
ctx.dom.mediaTimingReviewPlay.addEventListener('click', () => void togglePreview());
|
||||
ctx.dom.mediaTimingReviewReset.addEventListener('click', () => {
|
||||
if (!payload) return;
|
||||
previousCount = 0;
|
||||
nextCount = 0;
|
||||
updateSelection(payload.selectionStartTime, payload.selectionEndTime);
|
||||
renderSentence();
|
||||
});
|
||||
ctx.dom.mediaTimingReviewPrevAdd.addEventListener('click', () => adjustLines('previous', 1));
|
||||
ctx.dom.mediaTimingReviewPrevRemove.addEventListener('click', () =>
|
||||
adjustLines('previous', -1),
|
||||
);
|
||||
ctx.dom.mediaTimingReviewNextAdd.addEventListener('click', () => adjustLines('next', 1));
|
||||
ctx.dom.mediaTimingReviewNextRemove.addEventListener('click', () => adjustLines('next', -1));
|
||||
ctx.dom.mediaTimingReviewCancel.addEventListener('click', requestCancel);
|
||||
ctx.dom.mediaTimingReviewCancelBack.addEventListener('click', showEditor);
|
||||
ctx.dom.mediaTimingReviewUseOriginal.addEventListener(
|
||||
@@ -678,11 +847,7 @@ export function createMediaTimingReviewModal(
|
||||
'click',
|
||||
() => void resolveReview({ action: 'discard' }),
|
||||
);
|
||||
ctx.dom.mediaTimingReviewConfirm.addEventListener(
|
||||
'click',
|
||||
() =>
|
||||
void resolveReview({ action: 'confirm', startTime: selectionStart, endTime: selectionEnd }),
|
||||
);
|
||||
ctx.dom.mediaTimingReviewConfirm.addEventListener('click', () => confirmSelection());
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+89
-5
@@ -1420,11 +1420,82 @@ body:focus-visible,
|
||||
padding: 14px 20px 18px;
|
||||
}
|
||||
|
||||
.media-timing-review-sentence-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.media-timing-review-sentence-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.media-timing-review-sentence-label {
|
||||
color: var(--ctp-overlay1);
|
||||
font-size: 10px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.media-timing-review-line-count {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 999px;
|
||||
background: var(--ctp-mantle);
|
||||
color: var(--ctp-subtext0);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.media-timing-review-line-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.media-timing-review-line-stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.media-timing-review-line-stepper span {
|
||||
margin-right: 2px;
|
||||
color: var(--ctp-overlay1);
|
||||
font-size: 10px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.media-timing-review-line-stepper button {
|
||||
width: 24px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--ctp-surface2);
|
||||
border-radius: 6px;
|
||||
background: var(--ctp-surface0);
|
||||
color: var(--ctp-text);
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.media-timing-review-text {
|
||||
max-height: 88px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
max-height: 108px;
|
||||
overflow: auto;
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 14px;
|
||||
padding: 8px 14px;
|
||||
border-left: 3px solid var(--ctp-mauve);
|
||||
border-radius: 0 10px 10px 0;
|
||||
background: var(--ctp-mantle);
|
||||
@@ -1435,6 +1506,19 @@ body:focus-visible,
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.media-timing-review-line {
|
||||
color: var(--ctp-overlay1);
|
||||
}
|
||||
|
||||
.media-timing-review-line.is-current {
|
||||
color: var(--ctp-text);
|
||||
}
|
||||
|
||||
/* Only dim context lines when some are actually added. */
|
||||
.media-timing-review-text .media-timing-review-line:only-child {
|
||||
color: var(--ctp-subtext1);
|
||||
}
|
||||
|
||||
.media-timing-review-readout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
@@ -1521,7 +1605,7 @@ body:focus-visible,
|
||||
--playhead-duration: 1s;
|
||||
|
||||
position: relative;
|
||||
height: 76px;
|
||||
height: 52px;
|
||||
margin: 8px 0 9px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--ctp-surface2);
|
||||
@@ -1544,9 +1628,9 @@ body:focus-visible,
|
||||
.media-timing-review-waveform {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 6px 0;
|
||||
inset: 4px 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 12px);
|
||||
height: calc(100% - 8px);
|
||||
pointer-events: none;
|
||||
transition: opacity 160ms ease;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ export type RendererDom = {
|
||||
mediaTimingReviewModal: HTMLDivElement;
|
||||
mediaTimingReviewKind: HTMLDivElement;
|
||||
mediaTimingReviewText: HTMLElement;
|
||||
mediaTimingReviewLineCount: HTMLElement;
|
||||
mediaTimingReviewLineControls: HTMLDivElement;
|
||||
mediaTimingReviewPrevAdd: HTMLButtonElement;
|
||||
mediaTimingReviewPrevRemove: HTMLButtonElement;
|
||||
mediaTimingReviewNextAdd: HTMLButtonElement;
|
||||
mediaTimingReviewNextRemove: HTMLButtonElement;
|
||||
mediaTimingReviewStartValue: HTMLElement;
|
||||
mediaTimingReviewEndValue: HTMLElement;
|
||||
mediaTimingReviewDuration: HTMLElement;
|
||||
@@ -241,6 +247,18 @@ export function resolveRendererDom(): RendererDom {
|
||||
mediaTimingReviewModal: getRequiredElement<HTMLDivElement>('mediaTimingReviewModal'),
|
||||
mediaTimingReviewKind: getRequiredElement<HTMLDivElement>('mediaTimingReviewKind'),
|
||||
mediaTimingReviewText: getRequiredElement<HTMLElement>('mediaTimingReviewText'),
|
||||
mediaTimingReviewLineCount: getRequiredElement<HTMLElement>('mediaTimingReviewLineCount'),
|
||||
mediaTimingReviewLineControls: getRequiredElement<HTMLDivElement>(
|
||||
'mediaTimingReviewLineControls',
|
||||
),
|
||||
mediaTimingReviewPrevAdd: getRequiredElement<HTMLButtonElement>('mediaTimingReviewPrevAdd'),
|
||||
mediaTimingReviewPrevRemove: getRequiredElement<HTMLButtonElement>(
|
||||
'mediaTimingReviewPrevRemove',
|
||||
),
|
||||
mediaTimingReviewNextAdd: getRequiredElement<HTMLButtonElement>('mediaTimingReviewNextAdd'),
|
||||
mediaTimingReviewNextRemove: getRequiredElement<HTMLButtonElement>(
|
||||
'mediaTimingReviewNextRemove',
|
||||
),
|
||||
mediaTimingReviewStartValue: getRequiredElement<HTMLElement>('mediaTimingReviewStartValue'),
|
||||
mediaTimingReviewEndValue: getRequiredElement<HTMLElement>('mediaTimingReviewEndValue'),
|
||||
mediaTimingReviewDuration: getRequiredElement<HTMLElement>('mediaTimingReviewDuration'),
|
||||
|
||||
Reference in New Issue
Block a user