Add advanced clip filename templates with live preview (v4.0.4)
This commit is contained in:
parent
a29c252606
commit
9845b25d03
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.3",
|
||||
"version": "4.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.0.3",
|
||||
"version": "4.0.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.0.3",
|
||||
"version": "4.0.4",
|
||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||
"main": "dist/main.js",
|
||||
"author": "xRangerDE",
|
||||
|
||||
@ -71,15 +71,28 @@
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 10px;">Dateinamen-Format:</label>
|
||||
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
|
||||
<input type="radio" name="filenameFormat" value="simple" checked
|
||||
<input type="radio" name="filenameFormat" value="simple" checked onchange="updateFilenameExamples()"
|
||||
style="width: 18px; height: 18px; accent-color: #9146FF;">
|
||||
<span id="formatSimple" style="color: #aaa;">01.02.2026_1.mp4 (Standard)</span>
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
|
||||
<input type="radio" name="filenameFormat" value="timestamp"
|
||||
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 8px;">
|
||||
<input type="radio" name="filenameFormat" value="timestamp" onchange="updateFilenameExamples()"
|
||||
style="width: 18px; height: 18px; accent-color: #9146FF;">
|
||||
<span id="formatTimestamp" style="color: #aaa;">01.02.2026_CLIP_00-00-00_1.mp4 (mit Zeitstempel)</span>
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 10px;">
|
||||
<input type="radio" name="filenameFormat" value="template" onchange="updateFilenameExamples()"
|
||||
style="width: 18px; height: 18px; accent-color: #9146FF;">
|
||||
<span id="formatTemplate" style="color: #aaa;">{date}_{part}.mp4 (benutzerdefiniert)</span>
|
||||
</label>
|
||||
|
||||
<div id="clipFilenameTemplateWrap" style="display:none; margin-top: 10px;">
|
||||
<input type="text" id="clipFilenameTemplate" value="{date}_{part}.mp4"
|
||||
placeholder="{date}_{part}.mp4"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button -->
|
||||
@ -355,7 +368,7 @@
|
||||
|
||||
<div class="settings-card">
|
||||
<h3 id="updateTitle">Updates</h3>
|
||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.0.3</p>
|
||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.0.4</p>
|
||||
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||
</div>
|
||||
|
||||
@ -387,7 +400,7 @@
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Nicht verbunden</span>
|
||||
</div>
|
||||
<span id="versionText">v4.0.3</span>
|
||||
<span id="versionText">v4.0.4</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
|
||||
// ==========================================
|
||||
// CONFIG & CONSTANTS
|
||||
// ==========================================
|
||||
const APP_VERSION = '4.0.3';
|
||||
const APP_VERSION = '4.0.4';
|
||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||
|
||||
// Paths
|
||||
@ -61,7 +61,8 @@ interface CustomClip {
|
||||
startSec: number;
|
||||
durationSec: number;
|
||||
startPart: number;
|
||||
filenameFormat: 'simple' | 'timestamp';
|
||||
filenameFormat: 'simple' | 'timestamp' | 'template';
|
||||
filenameTemplate?: string;
|
||||
}
|
||||
|
||||
interface QueueItem {
|
||||
@ -624,6 +625,133 @@ function formatDuration(seconds: number): string {
|
||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function formatDurationDashed(seconds: number): string {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function sanitizeFilenamePart(input: string, fallback = 'unnamed'): string {
|
||||
const cleaned = (input || '')
|
||||
.replace(/[<>:"|?*\x00-\x1f]/g, '_')
|
||||
.replace(/[\\/]/g, '_')
|
||||
.trim();
|
||||
return cleaned || fallback;
|
||||
}
|
||||
|
||||
function formatDateWithPattern(date: Date, pattern: string): string {
|
||||
const tokenMap: Record<string, string> = {
|
||||
yyyy: date.getFullYear().toString(),
|
||||
yy: date.getFullYear().toString().slice(-2),
|
||||
MM: (date.getMonth() + 1).toString().padStart(2, '0'),
|
||||
M: (date.getMonth() + 1).toString(),
|
||||
dd: date.getDate().toString().padStart(2, '0'),
|
||||
d: date.getDate().toString(),
|
||||
HH: date.getHours().toString().padStart(2, '0'),
|
||||
H: date.getHours().toString(),
|
||||
hh: date.getHours().toString().padStart(2, '0'),
|
||||
h: date.getHours().toString(),
|
||||
mm: date.getMinutes().toString().padStart(2, '0'),
|
||||
m: date.getMinutes().toString(),
|
||||
ss: date.getSeconds().toString().padStart(2, '0'),
|
||||
s: date.getSeconds().toString()
|
||||
};
|
||||
|
||||
return pattern
|
||||
.replace(/yyyy|yy|MM|M|dd|d|HH|H|hh|h|mm|m|ss|s/g, (token) => tokenMap[token] ?? token)
|
||||
.replace(/\\(.)/g, '$1');
|
||||
}
|
||||
|
||||
function formatSecondsWithPattern(totalSeconds: number, pattern: string): string {
|
||||
const safe = Math.max(0, Math.floor(totalSeconds));
|
||||
const hours = Math.floor(safe / 3600);
|
||||
const minutes = Math.floor((safe % 3600) / 60);
|
||||
const seconds = safe % 60;
|
||||
|
||||
const tokenMap: Record<string, string> = {
|
||||
HH: hours.toString().padStart(2, '0'),
|
||||
H: hours.toString(),
|
||||
hh: hours.toString().padStart(2, '0'),
|
||||
h: hours.toString(),
|
||||
mm: minutes.toString().padStart(2, '0'),
|
||||
m: minutes.toString(),
|
||||
ss: seconds.toString().padStart(2, '0'),
|
||||
s: seconds.toString()
|
||||
};
|
||||
|
||||
return pattern
|
||||
.replace(/HH|H|hh|h|mm|m|ss|s/g, (token) => tokenMap[token] ?? token)
|
||||
.replace(/\\(.)/g, '$1');
|
||||
}
|
||||
|
||||
function parseVodId(url: string): string {
|
||||
const match = url.match(/videos\/(\d+)/i);
|
||||
return match?.[1] || '';
|
||||
}
|
||||
|
||||
interface ClipTemplateContext {
|
||||
template: string;
|
||||
title: string;
|
||||
vodId: string;
|
||||
channel: string;
|
||||
date: Date;
|
||||
part: number;
|
||||
trimStartSec: number;
|
||||
trimEndSec: number;
|
||||
trimLengthSec: number;
|
||||
fullLengthSec: number;
|
||||
}
|
||||
|
||||
function renderClipFilenameTemplate(context: ClipTemplateContext): string {
|
||||
const baseDate = `${context.date.getDate().toString().padStart(2, '0')}.${(context.date.getMonth() + 1).toString().padStart(2, '0')}.${context.date.getFullYear()}`;
|
||||
let rendered = context.template
|
||||
.replace(/\{title\}/g, sanitizeFilenamePart(context.title, 'untitled'))
|
||||
.replace(/\{id\}/g, sanitizeFilenamePart(context.vodId, 'unknown'))
|
||||
.replace(/\{channel\}/g, sanitizeFilenamePart(context.channel, 'unknown'))
|
||||
.replace(/\{channel_id\}/g, '')
|
||||
.replace(/\{date\}/g, baseDate)
|
||||
.replace(/\{part\}/g, String(context.part))
|
||||
.replace(/\{trim_start\}/g, formatDurationDashed(context.trimStartSec))
|
||||
.replace(/\{trim_end\}/g, formatDurationDashed(context.trimEndSec))
|
||||
.replace(/\{trim_length\}/g, formatDurationDashed(context.trimLengthSec))
|
||||
.replace(/\{length\}/g, formatDurationDashed(context.fullLengthSec))
|
||||
.replace(/\{ext\}/g, 'mp4')
|
||||
.replace(/\{random_string\}/g, Math.random().toString(36).slice(2, 10));
|
||||
|
||||
rendered = rendered.replace(/\{date_custom="(.*?)"\}/g, (_, pattern: string) => {
|
||||
return sanitizeFilenamePart(formatDateWithPattern(context.date, pattern), 'date');
|
||||
});
|
||||
rendered = rendered.replace(/\{trim_start_custom="(.*?)"\}/g, (_, pattern: string) => {
|
||||
return sanitizeFilenamePart(formatSecondsWithPattern(context.trimStartSec, pattern), '00-00-00');
|
||||
});
|
||||
rendered = rendered.replace(/\{trim_end_custom="(.*?)"\}/g, (_, pattern: string) => {
|
||||
return sanitizeFilenamePart(formatSecondsWithPattern(context.trimEndSec, pattern), '00-00-00');
|
||||
});
|
||||
rendered = rendered.replace(/\{trim_length_custom="(.*?)"\}/g, (_, pattern: string) => {
|
||||
return sanitizeFilenamePart(formatSecondsWithPattern(context.trimLengthSec, pattern), '00-00-00');
|
||||
});
|
||||
rendered = rendered.replace(/\{length_custom="(.*?)"\}/g, (_, pattern: string) => {
|
||||
return sanitizeFilenamePart(formatSecondsWithPattern(context.fullLengthSec, pattern), '00-00-00');
|
||||
});
|
||||
|
||||
const parts = rendered
|
||||
.split(/[\\/]+/)
|
||||
.map((segment) => sanitizeFilenamePart(segment, 'unnamed'))
|
||||
.filter((segment) => segment !== '.' && segment !== '..');
|
||||
|
||||
if (parts.length === 0) {
|
||||
return 'clip.mp4';
|
||||
}
|
||||
|
||||
const lastIdx = parts.length - 1;
|
||||
if (!/\.[A-Za-z0-9]{1,8}$/.test(parts[lastIdx])) {
|
||||
parts[lastIdx] = `${parts[lastIdx]}.mp4`;
|
||||
}
|
||||
|
||||
return path.join(...parts);
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
@ -1329,7 +1457,23 @@ async function downloadVOD(
|
||||
const partDuration = config.part_minutes * 60;
|
||||
|
||||
// Helper to generate filename based on format
|
||||
const makeClipFilename = (partNum: number, startOffset: number): string => {
|
||||
const makeClipFilename = (partNum: number, startOffset: number, clipLengthSec: number): string => {
|
||||
if (clip.filenameFormat === 'template' && (clip.filenameTemplate || '').trim()) {
|
||||
const relativeName = renderClipFilenameTemplate({
|
||||
template: clip.filenameTemplate as string,
|
||||
title: item.title,
|
||||
vodId: parseVodId(item.url),
|
||||
channel: item.streamer,
|
||||
date,
|
||||
part: partNum,
|
||||
trimStartSec: startOffset,
|
||||
trimEndSec: startOffset + clipLengthSec,
|
||||
trimLengthSec: clipLengthSec,
|
||||
fullLengthSec: totalDuration
|
||||
});
|
||||
return path.join(folder, relativeName);
|
||||
}
|
||||
|
||||
if (clip.filenameFormat === 'timestamp') {
|
||||
const h = Math.floor(startOffset / 3600);
|
||||
const m = Math.floor((startOffset % 3600) / 60);
|
||||
@ -1354,7 +1498,7 @@ async function downloadVOD(
|
||||
const remainingDuration = clip.durationSec - (i * partDuration);
|
||||
const thisDuration = Math.min(partDuration, remainingDuration);
|
||||
|
||||
const partFilename = makeClipFilename(partNum, startOffset);
|
||||
const partFilename = makeClipFilename(partNum, startOffset, thisDuration);
|
||||
|
||||
const result = await downloadVODPart(
|
||||
item.url,
|
||||
@ -1377,7 +1521,7 @@ async function downloadVOD(
|
||||
};
|
||||
} else {
|
||||
// Single clip file
|
||||
const filename = makeClipFilename(clip.startPart, clip.startSec);
|
||||
const filename = makeClipFilename(clip.startPart, clip.startSec, clip.durationSec);
|
||||
return await downloadVODPart(
|
||||
item.url,
|
||||
filename,
|
||||
|
||||
@ -5,7 +5,8 @@ interface CustomClip {
|
||||
startSec: number;
|
||||
durationSec: number;
|
||||
startPart: number;
|
||||
filenameFormat: 'simple' | 'timestamp';
|
||||
filenameFormat: 'simple' | 'timestamp' | 'template';
|
||||
filenameTemplate?: string;
|
||||
}
|
||||
|
||||
interface QueueItem {
|
||||
|
||||
3
typescript-version/src/renderer-globals.d.ts
vendored
3
typescript-version/src/renderer-globals.d.ts
vendored
@ -25,7 +25,8 @@ interface CustomClip {
|
||||
startSec: number;
|
||||
durationSec: number;
|
||||
startPart: number;
|
||||
filenameFormat: 'simple' | 'timestamp';
|
||||
filenameFormat: 'simple' | 'timestamp' | 'template';
|
||||
filenameTemplate?: string;
|
||||
}
|
||||
|
||||
interface QueueItem {
|
||||
|
||||
@ -118,7 +118,11 @@ const UI_TEXT_DE = {
|
||||
errorPrefix: 'Fehler: ',
|
||||
unknownError: 'Unbekannter Fehler',
|
||||
formatSimple: '(Standard)',
|
||||
formatTimestamp: '(mit Zeitstempel)'
|
||||
formatTimestamp: '(mit Zeitstempel)',
|
||||
formatTemplate: '(benutzerdefiniert)',
|
||||
templateEmpty: 'Das Template darf im benutzerdefinierten Modus nicht leer sein.',
|
||||
templatePlaceholder: '{date}_{part}.mp4',
|
||||
templateHelp: 'Platzhalter: {title} {id} {channel} {date} {part} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}'
|
||||
},
|
||||
cutter: {
|
||||
videoInfoFailed: 'Konnte Video-Informationen nicht lesen. FFprobe installiert?',
|
||||
|
||||
@ -118,7 +118,11 @@ const UI_TEXT_EN = {
|
||||
errorPrefix: 'Error: ',
|
||||
unknownError: 'Unknown error',
|
||||
formatSimple: '(default)',
|
||||
formatTimestamp: '(with timestamp)'
|
||||
formatTimestamp: '(with timestamp)',
|
||||
formatTemplate: '(custom template)',
|
||||
templateEmpty: 'Template cannot be empty in custom template mode.',
|
||||
templatePlaceholder: '{date}_{part}.mp4',
|
||||
templateHelp: 'Placeholders: {title} {id} {channel} {date} {part} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}'
|
||||
},
|
||||
cutter: {
|
||||
videoInfoFailed: 'Could not read video info. Is FFprobe installed?',
|
||||
|
||||
@ -59,6 +59,8 @@ function applyLanguageToStaticUI(): void {
|
||||
setText('clipsHeading', UI_TEXT.static.clipsHeading);
|
||||
setText('clipsInfoTitle', UI_TEXT.static.clipsInfoTitle);
|
||||
setText('clipsInfoText', UI_TEXT.static.clipsInfoText);
|
||||
setText('clipTemplateHelp', UI_TEXT.clips.templateHelp);
|
||||
setPlaceholder('clipFilenameTemplate', UI_TEXT.clips.templatePlaceholder);
|
||||
setText('cutterSelectTitle', UI_TEXT.static.cutterSelectTitle);
|
||||
setText('cutterBrowseBtn', UI_TEXT.static.cutterBrowse);
|
||||
setText('mergeTitle', UI_TEXT.static.mergeTitle);
|
||||
|
||||
@ -174,6 +174,96 @@ function formatSecondsToTimeDashed(seconds: number): string {
|
||||
return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function formatDateWithPattern(date: Date, pattern: string): string {
|
||||
const tokenMap: Record<string, string> = {
|
||||
yyyy: date.getFullYear().toString(),
|
||||
yy: date.getFullYear().toString().slice(-2),
|
||||
MM: (date.getMonth() + 1).toString().padStart(2, '0'),
|
||||
M: (date.getMonth() + 1).toString(),
|
||||
dd: date.getDate().toString().padStart(2, '0'),
|
||||
d: date.getDate().toString(),
|
||||
HH: date.getHours().toString().padStart(2, '0'),
|
||||
H: date.getHours().toString(),
|
||||
hh: date.getHours().toString().padStart(2, '0'),
|
||||
h: date.getHours().toString(),
|
||||
mm: date.getMinutes().toString().padStart(2, '0'),
|
||||
m: date.getMinutes().toString(),
|
||||
ss: date.getSeconds().toString().padStart(2, '0'),
|
||||
s: date.getSeconds().toString()
|
||||
};
|
||||
|
||||
return pattern
|
||||
.replace(/yyyy|yy|MM|M|dd|d|HH|H|hh|h|mm|m|ss|s/g, (token) => tokenMap[token] ?? token)
|
||||
.replace(/\\(.)/g, '$1');
|
||||
}
|
||||
|
||||
function formatSecondsWithPattern(totalSeconds: number, pattern: string): string {
|
||||
const safe = Math.max(0, Math.floor(totalSeconds));
|
||||
const hours = Math.floor(safe / 3600);
|
||||
const minutes = Math.floor((safe % 3600) / 60);
|
||||
const seconds = safe % 60;
|
||||
|
||||
const tokenMap: Record<string, string> = {
|
||||
HH: hours.toString().padStart(2, '0'),
|
||||
H: hours.toString(),
|
||||
hh: hours.toString().padStart(2, '0'),
|
||||
h: hours.toString(),
|
||||
mm: minutes.toString().padStart(2, '0'),
|
||||
m: minutes.toString(),
|
||||
ss: seconds.toString().padStart(2, '0'),
|
||||
s: seconds.toString()
|
||||
};
|
||||
|
||||
return pattern
|
||||
.replace(/HH|H|hh|h|mm|m|ss|s/g, (token) => tokenMap[token] ?? token)
|
||||
.replace(/\\(.)/g, '$1');
|
||||
}
|
||||
|
||||
function getSelectedFilenameFormat(): 'simple' | 'timestamp' | 'template' {
|
||||
const selected = query<HTMLInputElement>('input[name="filenameFormat"]:checked').value;
|
||||
return selected === 'template' ? 'template' : selected === 'timestamp' ? 'timestamp' : 'simple';
|
||||
}
|
||||
|
||||
function updateFilenameTemplateVisibility(): void {
|
||||
const selected = getSelectedFilenameFormat();
|
||||
const wrap = byId('clipFilenameTemplateWrap');
|
||||
wrap.style.display = selected === 'template' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function buildTemplatePreview(template: string, context: {
|
||||
title: string;
|
||||
date: Date;
|
||||
streamer: string;
|
||||
partNum: string;
|
||||
startSec: number;
|
||||
durationSec: number;
|
||||
totalSec: number;
|
||||
}): 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
|
||||
.replace(/\{title\}/g, context.title || 'Untitled')
|
||||
.replace(/\{id\}/g, '123456789')
|
||||
.replace(/\{channel\}/g, context.streamer || 'streamer')
|
||||
.replace(/\{channel_id\}/g, '0')
|
||||
.replace(/\{date\}/g, dateStr)
|
||||
.replace(/\{part\}/g, normalizedPart)
|
||||
.replace(/\{trim_start\}/g, formatSecondsToTimeDashed(context.startSec))
|
||||
.replace(/\{trim_end\}/g, formatSecondsToTimeDashed(context.startSec + context.durationSec))
|
||||
.replace(/\{trim_length\}/g, formatSecondsToTimeDashed(context.durationSec))
|
||||
.replace(/\{length\}/g, formatSecondsToTimeDashed(context.totalSec))
|
||||
.replace(/\{ext\}/g, 'mp4')
|
||||
.replace(/\{random_string\}/g, 'abcd1234');
|
||||
|
||||
output = output.replace(/\{date_custom="(.*?)"\}/g, (_, pattern: string) => formatDateWithPattern(context.date, pattern));
|
||||
output = output.replace(/\{trim_start_custom="(.*?)"\}/g, (_, pattern: string) => formatSecondsWithPattern(context.startSec, pattern));
|
||||
output = output.replace(/\{trim_end_custom="(.*?)"\}/g, (_, pattern: string) => formatSecondsWithPattern(context.startSec + context.durationSec, pattern));
|
||||
output = output.replace(/\{trim_length_custom="(.*?)"\}/g, (_, pattern: string) => formatSecondsWithPattern(context.durationSec, pattern));
|
||||
output = output.replace(/\{length_custom="(.*?)"\}/g, (_, pattern: string) => formatSecondsWithPattern(context.totalSec, pattern));
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function parseTimeToSeconds(timeStr: string): number {
|
||||
const parts = timeStr.split(':').map((p: string) => parseInt(p, 10) || 0);
|
||||
if (parts.length === 3) {
|
||||
@ -196,6 +286,9 @@ function openClipDialog(url: string, title: string, date: string, streamer: stri
|
||||
byId<HTMLInputElement>('clipStartTime').value = '00:00:00';
|
||||
byId<HTMLInputElement>('clipEndTime').value = formatSecondsToTime(Math.min(60, clipTotalSeconds));
|
||||
byId<HTMLInputElement>('clipStartPart').value = '';
|
||||
byId<HTMLInputElement>('clipFilenameTemplate').value = '{date}_{part}.mp4';
|
||||
query<HTMLInputElement>('input[name="filenameFormat"][value="simple"]').checked = true;
|
||||
updateFilenameTemplateVisibility();
|
||||
|
||||
updateClipDuration();
|
||||
updateFilenameExamples();
|
||||
@ -259,10 +352,24 @@ function updateFilenameExamples(): void {
|
||||
const dateStr = `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getFullYear()}`;
|
||||
const partNum = byId<HTMLInputElement>('clipStartPart').value || '1';
|
||||
const startSec = parseTimeToSeconds(byId<HTMLInputElement>('clipStartTime').value);
|
||||
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
||||
const durationSec = Math.max(1, endSec - startSec);
|
||||
const timeStr = formatSecondsToTimeDashed(startSec);
|
||||
const template = byId<HTMLInputElement>('clipFilenameTemplate').value.trim() || '{date}_{part}.mp4';
|
||||
|
||||
updateFilenameTemplateVisibility();
|
||||
|
||||
byId('formatSimple').textContent = `${dateStr}_${partNum}.mp4 ${UI_TEXT.clips.formatSimple}`;
|
||||
byId('formatTimestamp').textContent = `${dateStr}_CLIP_${timeStr}_${partNum}.mp4 ${UI_TEXT.clips.formatTimestamp}`;
|
||||
byId('formatTemplate').textContent = `${buildTemplatePreview(template, {
|
||||
title: clipDialogData.title,
|
||||
date,
|
||||
streamer: clipDialogData.streamer,
|
||||
partNum,
|
||||
startSec,
|
||||
durationSec,
|
||||
totalSec: clipTotalSeconds
|
||||
})} ${UI_TEXT.clips.formatTemplate}`;
|
||||
}
|
||||
|
||||
async function confirmClipDialog(): Promise<void> {
|
||||
@ -274,7 +381,8 @@ async function confirmClipDialog(): Promise<void> {
|
||||
const endSec = parseTimeToSeconds(byId<HTMLInputElement>('clipEndTime').value);
|
||||
const startPartStr = byId<HTMLInputElement>('clipStartPart').value.trim();
|
||||
const startPart = startPartStr ? parseInt(startPartStr, 10) : 1;
|
||||
const filenameFormat = query<HTMLInputElement>('input[name="filenameFormat"]:checked').value as 'simple' | 'timestamp';
|
||||
const filenameFormat = getSelectedFilenameFormat();
|
||||
const filenameTemplate = byId<HTMLInputElement>('clipFilenameTemplate').value.trim();
|
||||
|
||||
if (endSec <= startSec) {
|
||||
alert(UI_TEXT.clips.endBeforeStart);
|
||||
@ -286,6 +394,11 @@ async function confirmClipDialog(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filenameFormat === 'template' && !filenameTemplate) {
|
||||
alert(UI_TEXT.clips.templateEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
const durationSec = endSec - startSec;
|
||||
|
||||
queue = await window.api.addToQueue({
|
||||
@ -298,7 +411,8 @@ async function confirmClipDialog(): Promise<void> {
|
||||
startSec,
|
||||
durationSec,
|
||||
startPart,
|
||||
filenameFormat
|
||||
filenameFormat,
|
||||
filenameTemplate: filenameFormat === 'template' ? filenameTemplate : undefined
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user