feat(captions): extend data model with font, shadow, dimmed color, bg toggle fields
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { exportClips, type ExportConfig } from '$lib/bindings/export';
|
||||
import { checkSubtitlesFilterAvailable } from '$lib/bindings/dependencies';
|
||||
import { session } from '$lib/stores/videoSession.svelte';
|
||||
import { getClips, getSelectedClipId } from '$lib/stores/clips.svelte';
|
||||
import { preferences, setOutputDirectory } from '$lib/stores/preferences.svelte';
|
||||
@@ -11,17 +12,30 @@
|
||||
let exportScope = $state<'individual' | 'merged'>('individual');
|
||||
let cutMode = $state<'lossless' | 'precise'>('lossless');
|
||||
let preciseFormat = $state('mp4');
|
||||
let includeCaptions = $state(true);
|
||||
let burnInCaptions = $state(false);
|
||||
let canBurnIn = $state(true);
|
||||
let isExporting = $state(false);
|
||||
let exportClipPercent = $state(0);
|
||||
let exportClipLabel = $state('');
|
||||
|
||||
$effect(() => {
|
||||
checkSubtitlesFilterAvailable().then((available) => {
|
||||
canBurnIn = available;
|
||||
if (!available) burnInCaptions = false;
|
||||
});
|
||||
});
|
||||
let exportProgress = $state('');
|
||||
let exportError = $state<string | null>(null);
|
||||
let exportedPaths = $state<string[]>([]);
|
||||
|
||||
let clips = $derived(getClips());
|
||||
let selectedId = $derived(getSelectedClipId());
|
||||
let isDownloadComplete = $derived(
|
||||
session.downloadProgress >= 1 && session.localFilePath !== null
|
||||
let isExportReady = $derived(
|
||||
session.exportStatus === 'complete' && session.exportFilePath !== null
|
||||
);
|
||||
let downloadPercent = $derived(Math.round(session.downloadProgress * 100));
|
||||
let exportDownloadPercent = $derived(Math.round(session.exportProgress * 100));
|
||||
let hasCaptions = $derived(session.captionFilePath !== null);
|
||||
|
||||
let clipsToExport = $derived(
|
||||
clipScope === 'selected' && selectedId
|
||||
@@ -41,7 +55,7 @@
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
if (!session.localFilePath || clipsToExport.length === 0) return;
|
||||
if (!session.exportFilePath || clipsToExport.length === 0) return;
|
||||
|
||||
isExporting = true;
|
||||
exportError = null;
|
||||
@@ -65,7 +79,31 @@
|
||||
: { scope: 'merged' },
|
||||
outputDirectory: preferences.outputDirectory,
|
||||
videoTitle: session.title,
|
||||
sourceFilePath: session.localFilePath,
|
||||
sourceFilePath: session.exportFilePath,
|
||||
includeCaptions: includeCaptions && hasCaptions,
|
||||
burnInCaptions: burnInCaptions && includeCaptions && hasCaptions,
|
||||
captionFilePath: hasCaptions ? session.captionFilePath : null,
|
||||
captionStyle: (burnInCaptions && includeCaptions && hasCaptions)
|
||||
? {
|
||||
fontFamily: preferences.captionSettings.fontFamily,
|
||||
fontSize: preferences.captionSettings.fontSize,
|
||||
textColor: preferences.captionSettings.textColor,
|
||||
bold: preferences.captionSettings.bold,
|
||||
outlineColor: preferences.captionSettings.outlineColor,
|
||||
backgroundEnabled: preferences.captionSettings.backgroundEnabled,
|
||||
backgroundOpacity: preferences.captionSettings.backgroundOpacity,
|
||||
backgroundColor: preferences.captionSettings.backgroundColor,
|
||||
textOutline: preferences.captionSettings.textOutline,
|
||||
shadowEnabled: preferences.captionSettings.shadowEnabled,
|
||||
shadowDepth: preferences.captionSettings.shadowDepth,
|
||||
shadowColor: preferences.captionSettings.shadowColor,
|
||||
dimmedColorMode: preferences.captionSettings.dimmedColorMode,
|
||||
dimmedOpacity: preferences.captionSettings.dimmedOpacity,
|
||||
dimmedColor: preferences.captionSettings.dimmedColor,
|
||||
position: preferences.captionSettings.position,
|
||||
wordHighlight: preferences.captionSettings.wordHighlight,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -73,6 +111,8 @@
|
||||
config,
|
||||
(current, total, label) => {
|
||||
exportProgress = `Exporting ${current}/${total}: ${label}`;
|
||||
exportClipPercent = 0;
|
||||
exportClipLabel = label;
|
||||
},
|
||||
(paths) => {
|
||||
exportedPaths = paths;
|
||||
@@ -81,6 +121,11 @@
|
||||
(message) => {
|
||||
exportError = message;
|
||||
isExporting = false;
|
||||
},
|
||||
(current, total, label, percent) => {
|
||||
exportClipPercent = Math.round(percent * 100);
|
||||
exportClipLabel = label;
|
||||
exportProgress = `Exporting ${current}/${total}: ${label}`;
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
@@ -95,11 +140,11 @@
|
||||
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
|
||||
<h2>Export Clips</h2>
|
||||
|
||||
{#if !isDownloadComplete}
|
||||
{#if !isExportReady}
|
||||
<div class="waiting">
|
||||
<p>Download at {downloadPercent}% — export will start when ready.</p>
|
||||
<p>Downloading best quality ({exportDownloadPercent}%)…</p>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {downloadPercent}%"></div>
|
||||
<div class="progress-fill" style="width: {exportDownloadPercent}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if exportedPaths.length > 0}
|
||||
@@ -161,6 +206,33 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if hasCaptions}
|
||||
<section>
|
||||
<h3>Captions</h3>
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={includeCaptions} />
|
||||
Include captions{session.captionsAreAuto ? ' (auto-generated)' : ''}
|
||||
</label>
|
||||
{#if includeCaptions}
|
||||
<div class="caption-sub-options">
|
||||
<label class:disabled={!canBurnIn}>
|
||||
<input type="checkbox" bind:checked={burnInCaptions} disabled={!canBurnIn} />
|
||||
Burn into video
|
||||
</label>
|
||||
{#if !canBurnIn}
|
||||
<p class="caption-note caption-note-warn">Burn-in requires ffmpeg compiled with libass (the <code>subtitles</code> filter). The standard Homebrew <code>ffmpeg</code> formula does not include it. Install the full build instead: <code>brew install ffmpeg-full</code>, then add <code>/opt/homebrew/opt/ffmpeg-full/bin</code> to your PATH ahead of the regular ffmpeg.</p>
|
||||
{:else if burnInCaptions && cutMode === 'lossless'}
|
||||
<p class="caption-note caption-note-warn">Burn-in requires re-encoding (precise mode will be used)</p>
|
||||
{:else if burnInCaptions}
|
||||
<p class="caption-note">Captions will be burned into the video with your style settings</p>
|
||||
{:else}
|
||||
<p class="caption-note">Captions will be muxed as a subtitle track</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section>
|
||||
<h3>Save to</h3>
|
||||
<div class="row">
|
||||
@@ -174,7 +246,13 @@
|
||||
{/if}
|
||||
|
||||
{#if isExporting}
|
||||
<div class="exporting">{exportProgress}</div>
|
||||
<div class="exporting">
|
||||
<div class="export-label">{exportProgress}</div>
|
||||
<div class="export-progress-bar">
|
||||
<div class="export-progress-fill" style="width: {exportClipPercent}%"></div>
|
||||
</div>
|
||||
<div class="export-percent">{exportClipPercent}%</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="actions">
|
||||
@@ -263,6 +341,25 @@
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.caption-sub-options {
|
||||
margin-left: 28px;
|
||||
}
|
||||
|
||||
label.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.caption-note {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.caption-note-warn {
|
||||
color: var(--warning, #f9e2af);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -280,9 +377,33 @@
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.exporting {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.export-progress-bar {
|
||||
height: 6px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.export-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--accent, #89b4fa);
|
||||
border-radius: 3px;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.export-percent {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.waiting p {
|
||||
|
||||
Reference in New Issue
Block a user