Add in-app template guide with live examples (v4.0.6)
This commit is contained in:
parent
1306309e6e
commit
1e5e9137ff
4
typescript-version/package-lock.json
generated
4
typescript-version/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.6",
|
||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||
"main": "dist/main.js",
|
||||
"author": "xRangerDE",
|
||||
|
||||
@ -92,6 +92,7 @@
|
||||
style="width: 100%; background: #333; border: 1px solid #444; border-radius: 4px; padding: 8px 12px; color: white; font-family: monospace;"
|
||||
oninput="updateFilenameExamples()">
|
||||
<div id="clipTemplateHelp" style="color: #888; font-size: 12px; margin-top: 6px;">Platzhalter: {title} {id} {channel} {date} {part} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}</div>
|
||||
<button class="btn-secondary" id="clipTemplateGuideBtn" style="margin-top: 8px;" onclick="openTemplateGuide('clip')">Template Guide</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -102,6 +103,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Guide Modal -->
|
||||
<div class="modal-overlay" id="templateGuideModal">
|
||||
<div class="modal template-guide-modal">
|
||||
<button class="modal-close" onclick="closeTemplateGuide()">x</button>
|
||||
<h2 id="templateGuideTitle">Template Guide</h2>
|
||||
<p id="templateGuideIntro" class="template-guide-intro">Nutze Variablen fur Dateinamen und prufe das Ergebnis als Live-Vorschau.</p>
|
||||
|
||||
<div class="template-guide-actions">
|
||||
<button class="btn-secondary" id="templateGuideUseVod" onclick="setTemplateGuidePreset('vod')">VOD Template</button>
|
||||
<button class="btn-secondary" id="templateGuideUseParts" onclick="setTemplateGuidePreset('parts')">VOD Part Template</button>
|
||||
<button class="btn-secondary" id="templateGuideUseClip" onclick="setTemplateGuidePreset('clip')">Clip Template</button>
|
||||
</div>
|
||||
|
||||
<label id="templateGuideTemplateLabel" class="template-guide-label">Template</label>
|
||||
<input type="text" id="templateGuideInput" class="template-guide-input" oninput="updateTemplateGuidePreview()" placeholder="{title}.mp4">
|
||||
|
||||
<div class="template-guide-preview-box">
|
||||
<div class="template-guide-preview-label" id="templateGuideOutputLabel">Live Vorschau</div>
|
||||
<div id="templateGuideOutput" class="template-guide-output">-</div>
|
||||
<div id="templateGuideContext" class="template-guide-context"></div>
|
||||
</div>
|
||||
|
||||
<h3 id="templateGuideVarsTitle" class="template-guide-vars-title">Verfugbare Variablen</h3>
|
||||
<div class="template-guide-table-wrap">
|
||||
<table class="template-guide-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="templateGuideVarCol">Variable</th>
|
||||
<th id="templateGuideDescCol">Beschreibung</th>
|
||||
<th id="templateGuideExampleCol">Beispiel</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="templateGuideBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="template-guide-footer">
|
||||
<button class="btn-secondary" id="templateGuideCloseBtn" onclick="closeTemplateGuide()">Schliessen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
@ -365,7 +408,10 @@
|
||||
<input type="number" id="partMinutes" value="120" min="10" max="480">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label id="filenameTemplatesTitle">Dateinamen-Templates</label>
|
||||
<div class="form-row" style="align-items:center; margin-bottom: 4px;">
|
||||
<label id="filenameTemplatesTitle" style="margin: 0;">Dateinamen-Templates</label>
|
||||
<button class="btn-secondary" id="settingsTemplateGuideBtn" type="button" onclick="openTemplateGuide('vod')">Template Guide</button>
|
||||
</div>
|
||||
<div style="display: grid; gap: 8px; margin-top: 8px;">
|
||||
<label id="vodTemplateLabel" style="font-size: 13px; color: var(--text-secondary);">VOD Template</label>
|
||||
<input type="text" id="vodFilenameTemplate" placeholder="{title}.mp4" style="font-family: monospace;">
|
||||
@ -382,7 +428,7 @@
|
||||
|
||||
<div class="settings-card">
|
||||
<h3 id="updateTitle">Updates</h3>
|
||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.0.5</p>
|
||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.0.6</p>
|
||||
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||
</div>
|
||||
|
||||
@ -414,7 +460,7 @@
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Nicht verbunden</span>
|
||||
</div>
|
||||
<span id="versionText">v4.0.5</span>
|
||||
<span id="versionText">v4.0.6</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
|
||||
// ==========================================
|
||||
// CONFIG & CONSTANTS
|
||||
// ==========================================
|
||||
const APP_VERSION = '4.0.5';
|
||||
const APP_VERSION = '4.0.6';
|
||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||
|
||||
// Paths
|
||||
|
||||
@ -48,6 +48,23 @@ const UI_TEXT_DE = {
|
||||
vodTemplatePlaceholder: '{title}.mp4',
|
||||
partsTemplatePlaceholder: '{date}_Part{part_padded}.mp4',
|
||||
defaultClipTemplatePlaceholder: '{date}_{part}.mp4',
|
||||
templateGuideButton: 'Template Guide',
|
||||
templateGuideTitle: 'Dateinamen-Template Guide',
|
||||
templateGuideIntro: 'Nutze Platzhalter fur Dateinamen und teste dein Muster mit einer Live-Vorschau.',
|
||||
templateGuideTemplateLabel: 'Template',
|
||||
templateGuideOutputLabel: 'Live-Vorschau',
|
||||
templateGuideVarsTitle: 'Verfugbare Platzhalter',
|
||||
templateGuideVarCol: 'Platzhalter',
|
||||
templateGuideDescCol: 'Beschreibung',
|
||||
templateGuideExampleCol: 'Beispiel',
|
||||
templateGuideUseVod: 'VOD-Template nutzen',
|
||||
templateGuideUseParts: 'Teile-Template nutzen',
|
||||
templateGuideUseClip: 'Clip-Template nutzen',
|
||||
templateGuideClose: 'Schliessen',
|
||||
templateGuideContextVod: 'Kontext: Beispiel fur kompletten VOD-Download',
|
||||
templateGuideContextParts: 'Kontext: Beispiel fur VOD-Teil',
|
||||
templateGuideContextClip: 'Kontext: Beispiel fur Clip-Zuschnitt',
|
||||
templateGuideContextClipLive: 'Kontext: Aktuelle Auswahl im Clip-Dialog',
|
||||
updateTitle: 'Updates',
|
||||
checkUpdates: 'Nach Updates suchen',
|
||||
preflightTitle: 'System-Check',
|
||||
|
||||
@ -48,6 +48,23 @@ const UI_TEXT_EN = {
|
||||
vodTemplatePlaceholder: '{title}.mp4',
|
||||
partsTemplatePlaceholder: '{date}_Part{part_padded}.mp4',
|
||||
defaultClipTemplatePlaceholder: '{date}_{part}.mp4',
|
||||
templateGuideButton: 'Template Guide',
|
||||
templateGuideTitle: 'Filename Template Guide',
|
||||
templateGuideIntro: 'Use placeholders for filenames and test your pattern with a live preview.',
|
||||
templateGuideTemplateLabel: 'Template',
|
||||
templateGuideOutputLabel: 'Live preview',
|
||||
templateGuideVarsTitle: 'Available placeholders',
|
||||
templateGuideVarCol: 'Placeholder',
|
||||
templateGuideDescCol: 'Description',
|
||||
templateGuideExampleCol: 'Example',
|
||||
templateGuideUseVod: 'Use VOD template',
|
||||
templateGuideUseParts: 'Use part template',
|
||||
templateGuideUseClip: 'Use clip template',
|
||||
templateGuideClose: 'Close',
|
||||
templateGuideContextVod: 'Context: Sample full VOD download',
|
||||
templateGuideContextParts: 'Context: Sample split VOD part',
|
||||
templateGuideContextClip: 'Context: Sample clip trim',
|
||||
templateGuideContextClipLive: 'Context: Current clip dialog selection',
|
||||
updateTitle: 'Updates',
|
||||
checkUpdates: 'Check for updates',
|
||||
preflightTitle: 'System Check',
|
||||
|
||||
@ -87,6 +87,21 @@ function applyLanguageToStaticUI(): void {
|
||||
setText('partsTemplateLabel', UI_TEXT.static.partsTemplateLabel);
|
||||
setText('defaultClipTemplateLabel', UI_TEXT.static.defaultClipTemplateLabel);
|
||||
setText('filenameTemplateHint', UI_TEXT.static.filenameTemplateHint);
|
||||
setText('settingsTemplateGuideBtn', UI_TEXT.static.templateGuideButton);
|
||||
setText('clipTemplateGuideBtn', UI_TEXT.static.templateGuideButton);
|
||||
setText('templateGuideTitle', UI_TEXT.static.templateGuideTitle);
|
||||
setText('templateGuideIntro', UI_TEXT.static.templateGuideIntro);
|
||||
setText('templateGuideTemplateLabel', UI_TEXT.static.templateGuideTemplateLabel);
|
||||
setText('templateGuideOutputLabel', UI_TEXT.static.templateGuideOutputLabel);
|
||||
setText('templateGuideVarsTitle', UI_TEXT.static.templateGuideVarsTitle);
|
||||
setText('templateGuideVarCol', UI_TEXT.static.templateGuideVarCol);
|
||||
setText('templateGuideDescCol', UI_TEXT.static.templateGuideDescCol);
|
||||
setText('templateGuideExampleCol', UI_TEXT.static.templateGuideExampleCol);
|
||||
setText('templateGuideUseVod', UI_TEXT.static.templateGuideUseVod);
|
||||
setText('templateGuideUseParts', UI_TEXT.static.templateGuideUseParts);
|
||||
setText('templateGuideUseClip', UI_TEXT.static.templateGuideUseClip);
|
||||
setText('templateGuideCloseBtn', UI_TEXT.static.templateGuideClose);
|
||||
setPlaceholder('templateGuideInput', UI_TEXT.static.vodTemplatePlaceholder);
|
||||
setPlaceholder('vodFilenameTemplate', UI_TEXT.static.vodTemplatePlaceholder);
|
||||
setPlaceholder('partsFilenameTemplate', UI_TEXT.static.partsTemplatePlaceholder);
|
||||
setPlaceholder('defaultClipFilenameTemplate', UI_TEXT.static.defaultClipTemplatePlaceholder);
|
||||
@ -107,6 +122,11 @@ function applyLanguageToStaticUI(): void {
|
||||
if (status === UI_TEXTS.de.static.notConnected || status === UI_TEXTS.en.static.notConnected) {
|
||||
setText('statusText', UI_TEXT.static.notConnected);
|
||||
}
|
||||
|
||||
const guideRefresh = (window as unknown as { refreshTemplateGuideTexts?: () => void }).refreshTemplateGuideTexts;
|
||||
if (typeof guideRefresh === 'function') {
|
||||
guideRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
function localizeCurrentStatusText(current: string): string {
|
||||
|
||||
@ -181,6 +181,10 @@ const DEFAULT_VOD_TEMPLATE = '{title}.mp4';
|
||||
const DEFAULT_PARTS_TEMPLATE = '{date}_Part{part_padded}.mp4';
|
||||
const DEFAULT_CLIP_TEMPLATE = '{date}_{part}.mp4';
|
||||
|
||||
type TemplateGuideSource = 'vod' | 'parts' | 'clip';
|
||||
|
||||
let templateGuideSource: TemplateGuideSource = 'vod';
|
||||
|
||||
function formatDateWithPattern(date: Date, pattern: string): string {
|
||||
const tokenMap: Record<string, string> = {
|
||||
yyyy: date.getFullYear().toString(),
|
||||
@ -237,7 +241,7 @@ function updateFilenameTemplateVisibility(): void {
|
||||
wrap.style.display = selected === 'template' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function buildTemplatePreview(template: string, context: {
|
||||
interface TemplatePreviewContext {
|
||||
title: string;
|
||||
date: Date;
|
||||
streamer: string;
|
||||
@ -245,7 +249,9 @@ function buildTemplatePreview(template: string, context: {
|
||||
startSec: number;
|
||||
durationSec: number;
|
||||
totalSec: number;
|
||||
}): string {
|
||||
}
|
||||
|
||||
function buildTemplatePreview(template: string, context: TemplatePreviewContext): string {
|
||||
const dateStr = `${context.date.getDate().toString().padStart(2, '0')}.${(context.date.getMonth() + 1).toString().padStart(2, '0')}.${context.date.getFullYear()}`;
|
||||
const normalizedPart = context.partNum || '1';
|
||||
let output = template
|
||||
@ -272,6 +278,211 @@ function buildTemplatePreview(template: string, context: {
|
||||
return output;
|
||||
}
|
||||
|
||||
function getTemplateForSource(source: TemplateGuideSource): string {
|
||||
if (source === 'vod') {
|
||||
return ((config.filename_template_vod as string) || DEFAULT_VOD_TEMPLATE).trim() || DEFAULT_VOD_TEMPLATE;
|
||||
}
|
||||
|
||||
if (source === 'parts') {
|
||||
return ((config.filename_template_parts as string) || DEFAULT_PARTS_TEMPLATE).trim() || DEFAULT_PARTS_TEMPLATE;
|
||||
}
|
||||
|
||||
const clipField = document.getElementById('clipFilenameTemplate') as HTMLInputElement | null;
|
||||
const clipFromDialog = clipField?.value.trim() || '';
|
||||
if (clipFromDialog) {
|
||||
return clipFromDialog;
|
||||
}
|
||||
|
||||
return ((config.filename_template_clip as string) || DEFAULT_CLIP_TEMPLATE).trim() || DEFAULT_CLIP_TEMPLATE;
|
||||
}
|
||||
|
||||
function getTemplateGuidePreviewContext(source: TemplateGuideSource): { context: TemplatePreviewContext; contextText: string } {
|
||||
const now = new Date();
|
||||
const sampleDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 20, 15, 8);
|
||||
const sampleStreamer = currentStreamer || 'sample_streamer';
|
||||
|
||||
if (source === 'clip' && clipDialogData) {
|
||||
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
||||
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
||||
const clipDuration = Math.max(1, endSec - startSec);
|
||||
const totalSec = Math.max(1, clipTotalSeconds || parseDurationToSeconds(clipDialogData.duration));
|
||||
|
||||
return {
|
||||
context: {
|
||||
title: clipDialogData.title || 'Clip Title',
|
||||
date: new Date(clipDialogData.date),
|
||||
streamer: clipDialogData.streamer || sampleStreamer,
|
||||
partNum: byId<HTMLInputElement>('clipStartPart').value.trim() || '1',
|
||||
startSec,
|
||||
durationSec: clipDuration,
|
||||
totalSec
|
||||
},
|
||||
contextText: UI_TEXT.static.templateGuideContextClipLive
|
||||
};
|
||||
}
|
||||
|
||||
if (source === 'parts') {
|
||||
const partLen = Math.max(60, Number(config.part_minutes ?? 120) * 60);
|
||||
return {
|
||||
context: {
|
||||
title: 'Epic Ranked Session',
|
||||
date: sampleDate,
|
||||
streamer: sampleStreamer,
|
||||
partNum: '3',
|
||||
startSec: partLen * 2,
|
||||
durationSec: partLen,
|
||||
totalSec: partLen * 5
|
||||
},
|
||||
contextText: UI_TEXT.static.templateGuideContextParts
|
||||
};
|
||||
}
|
||||
|
||||
if (source === 'clip') {
|
||||
return {
|
||||
context: {
|
||||
title: 'Funny Clip Moment',
|
||||
date: sampleDate,
|
||||
streamer: sampleStreamer,
|
||||
partNum: '1',
|
||||
startSec: 95,
|
||||
durationSec: 45,
|
||||
totalSec: 5400
|
||||
},
|
||||
contextText: UI_TEXT.static.templateGuideContextClip
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
context: {
|
||||
title: 'Epic Ranked Session',
|
||||
date: sampleDate,
|
||||
streamer: sampleStreamer,
|
||||
partNum: '1',
|
||||
startSec: 0,
|
||||
durationSec: 3 * 3600 + 12 * 60 + 5,
|
||||
totalSec: 3 * 3600 + 12 * 60 + 5
|
||||
},
|
||||
contextText: UI_TEXT.static.templateGuideContextVod
|
||||
};
|
||||
}
|
||||
|
||||
interface TemplateVariableDoc {
|
||||
placeholder: string;
|
||||
description: string;
|
||||
exampleTemplate: string;
|
||||
}
|
||||
|
||||
function getTemplateVariableDocs(): TemplateVariableDoc[] {
|
||||
const de = currentLanguage !== 'en';
|
||||
const text = (deText: string, enText: string) => de ? deText : enText;
|
||||
|
||||
return [
|
||||
{ placeholder: '{title}', description: text('Titel des VODs/Clips', 'Title of the VOD/clip'), exampleTemplate: '{title}' },
|
||||
{ placeholder: '{id}', description: text('VOD-ID', 'VOD id'), exampleTemplate: '{id}' },
|
||||
{ placeholder: '{channel}', description: text('Kanalname', 'Channel name'), exampleTemplate: '{channel}' },
|
||||
{ placeholder: '{date}', description: text('Datum (DD.MM.YYYY)', 'Date (DD.MM.YYYY)'), exampleTemplate: '{date}' },
|
||||
{ placeholder: '{part}', description: text('Teilnummer', 'Part number'), exampleTemplate: '{part}' },
|
||||
{ placeholder: '{part_padded}', description: text('Teilnummer mit 2 Stellen', 'Part number padded to 2 digits'), exampleTemplate: '{part_padded}' },
|
||||
{ placeholder: '{trim_start}', description: text('Startzeit des Ausschnitts', 'Trim start time'), exampleTemplate: '{trim_start}' },
|
||||
{ placeholder: '{trim_end}', description: text('Endzeit des Ausschnitts', 'Trim end time'), exampleTemplate: '{trim_end}' },
|
||||
{ placeholder: '{trim_length}', description: text('Lange des Ausschnitts', 'Trimmed duration'), exampleTemplate: '{trim_length}' },
|
||||
{ placeholder: '{length}', description: text('Gesamtdauer', 'Total duration'), exampleTemplate: '{length}' },
|
||||
{ placeholder: '{ext}', description: text('Dateiendung', 'File extension'), exampleTemplate: '{ext}' },
|
||||
{ placeholder: '{random_string}', description: text('Zufallsstring (8 Zeichen)', 'Random string (8 chars)'), exampleTemplate: '{random_string}' },
|
||||
{ placeholder: '{date_custom="yyyy-MM-dd"}', description: text('Datum mit eigenem Format', 'Custom-formatted date'), exampleTemplate: '{date_custom="yyyy-MM-dd"}' },
|
||||
{ placeholder: '{trim_start_custom="HH-mm-ss"}', description: text('Startzeit mit eigenem Format', 'Custom-formatted trim start'), exampleTemplate: '{trim_start_custom="HH-mm-ss"}' },
|
||||
{ placeholder: '{trim_end_custom="HH-mm-ss"}', description: text('Endzeit mit eigenem Format', 'Custom-formatted trim end'), exampleTemplate: '{trim_end_custom="HH-mm-ss"}' },
|
||||
{ placeholder: '{trim_length_custom="HH-mm-ss"}', description: text('Trim-Dauer mit eigenem Format', 'Custom-formatted trim length'), exampleTemplate: '{trim_length_custom="HH-mm-ss"}' },
|
||||
{ placeholder: '{length_custom="HH-mm-ss"}', description: text('Gesamtdauer mit eigenem Format', 'Custom-formatted total duration'), exampleTemplate: '{length_custom="HH-mm-ss"}' }
|
||||
];
|
||||
}
|
||||
|
||||
function renderTemplateGuideTable(context: TemplatePreviewContext): void {
|
||||
const body = byId('templateGuideBody');
|
||||
body.innerHTML = '';
|
||||
|
||||
for (const item of getTemplateVariableDocs()) {
|
||||
const row = document.createElement('tr');
|
||||
const varCell = document.createElement('td');
|
||||
const descCell = document.createElement('td');
|
||||
const exampleCell = document.createElement('td');
|
||||
|
||||
varCell.textContent = item.placeholder;
|
||||
descCell.textContent = item.description;
|
||||
exampleCell.textContent = buildTemplatePreview(item.exampleTemplate, context);
|
||||
|
||||
row.append(varCell, descCell, exampleCell);
|
||||
body.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTemplateGuidePresetButtons(): void {
|
||||
const activeId: Record<TemplateGuideSource, string> = {
|
||||
vod: 'templateGuideUseVod',
|
||||
parts: 'templateGuideUseParts',
|
||||
clip: 'templateGuideUseClip'
|
||||
};
|
||||
|
||||
(Object.keys(activeId) as TemplateGuideSource[]).forEach((key) => {
|
||||
const btn = byId<HTMLButtonElement>(activeId[key]);
|
||||
btn.classList.toggle('active', key === templateGuideSource);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshTemplateGuideTexts(): void {
|
||||
setText('settingsTemplateGuideBtn', UI_TEXT.static.templateGuideButton);
|
||||
setText('clipTemplateGuideBtn', UI_TEXT.static.templateGuideButton);
|
||||
setText('templateGuideTitle', UI_TEXT.static.templateGuideTitle);
|
||||
setText('templateGuideIntro', UI_TEXT.static.templateGuideIntro);
|
||||
setText('templateGuideTemplateLabel', UI_TEXT.static.templateGuideTemplateLabel);
|
||||
setText('templateGuideOutputLabel', UI_TEXT.static.templateGuideOutputLabel);
|
||||
setText('templateGuideVarsTitle', UI_TEXT.static.templateGuideVarsTitle);
|
||||
setText('templateGuideVarCol', UI_TEXT.static.templateGuideVarCol);
|
||||
setText('templateGuideDescCol', UI_TEXT.static.templateGuideDescCol);
|
||||
setText('templateGuideExampleCol', UI_TEXT.static.templateGuideExampleCol);
|
||||
setText('templateGuideUseVod', UI_TEXT.static.templateGuideUseVod);
|
||||
setText('templateGuideUseParts', UI_TEXT.static.templateGuideUseParts);
|
||||
setText('templateGuideUseClip', UI_TEXT.static.templateGuideUseClip);
|
||||
setText('templateGuideCloseBtn', UI_TEXT.static.templateGuideClose);
|
||||
setPlaceholder('templateGuideInput', getTemplateForSource(templateGuideSource));
|
||||
updateTemplateGuidePresetButtons();
|
||||
|
||||
const modal = document.getElementById('templateGuideModal');
|
||||
if (modal?.classList.contains('show')) {
|
||||
updateTemplateGuidePreview();
|
||||
}
|
||||
}
|
||||
|
||||
function openTemplateGuide(source: TemplateGuideSource = 'vod'): void {
|
||||
templateGuideSource = source;
|
||||
byId('templateGuideModal').classList.add('show');
|
||||
refreshTemplateGuideTexts();
|
||||
setTemplateGuidePreset(source);
|
||||
}
|
||||
|
||||
function closeTemplateGuide(): void {
|
||||
byId('templateGuideModal').classList.remove('show');
|
||||
}
|
||||
|
||||
function setTemplateGuidePreset(source: TemplateGuideSource): void {
|
||||
templateGuideSource = source;
|
||||
const template = getTemplateForSource(source);
|
||||
byId<HTMLInputElement>('templateGuideInput').value = template;
|
||||
setPlaceholder('templateGuideInput', template);
|
||||
updateTemplateGuidePresetButtons();
|
||||
updateTemplateGuidePreview();
|
||||
}
|
||||
|
||||
function updateTemplateGuidePreview(): void {
|
||||
const input = byId<HTMLInputElement>('templateGuideInput');
|
||||
const template = input.value.trim() || getTemplateForSource(templateGuideSource);
|
||||
const { context, contextText } = getTemplateGuidePreviewContext(templateGuideSource);
|
||||
|
||||
byId('templateGuideOutput').textContent = buildTemplatePreview(template, context);
|
||||
byId('templateGuideContext').textContent = contextText;
|
||||
renderTemplateGuideTable(context);
|
||||
}
|
||||
|
||||
function parseTimeToSeconds(timeStr: string): number {
|
||||
const parts = timeStr.split(':').map((p: string) => parseInt(p, 10) || 0);
|
||||
if (parts.length === 3) {
|
||||
@ -378,6 +589,11 @@ function updateFilenameExamples(): void {
|
||||
durationSec,
|
||||
totalSec: clipTotalSeconds
|
||||
})} ${UI_TEXT.clips.formatTemplate}`;
|
||||
|
||||
const guideModal = document.getElementById('templateGuideModal');
|
||||
if (guideModal?.classList.contains('show') && templateGuideSource === 'clip') {
|
||||
updateTemplateGuidePreview();
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmClipDialog(): Promise<void> {
|
||||
|
||||
@ -1268,3 +1268,114 @@ body.theme-apple {
|
||||
.modal-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.template-guide-modal {
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
.template-guide-intro {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.template-guide-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.template-guide-actions .btn-secondary {
|
||||
padding: 8px 12px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.template-guide-actions .btn-secondary.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.template-guide-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.template-guide-input {
|
||||
width: 100%;
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.template-guide-preview-box {
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.template-guide-preview-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.template-guide-output {
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
color: var(--text);
|
||||
word-break: break-word;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.template-guide-context {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.template-guide-vars-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.template-guide-table-wrap {
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.template-guide-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.template-guide-table th,
|
||||
.template-guide-table td {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.template-guide-table tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.template-guide-table td:first-child,
|
||||
.template-guide-table td:last-child {
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.template-guide-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user