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:
@@ -9,6 +9,10 @@ pub struct VideoMetadata {
|
|||||||
pub fps: f64,
|
pub fps: f64,
|
||||||
pub thumbnail_url: Option<String>,
|
pub thumbnail_url: Option<String>,
|
||||||
pub stream_url: String,
|
pub stream_url: String,
|
||||||
|
/// Whether English captions are available (manual or auto-generated)
|
||||||
|
pub has_captions: bool,
|
||||||
|
/// True if the available captions are auto-generated (lower quality)
|
||||||
|
pub captions_are_auto: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -84,6 +88,28 @@ pub enum ExportScope {
|
|||||||
Merged,
|
Merged,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CaptionStyle {
|
||||||
|
pub font_family: String,
|
||||||
|
pub font_size: u32,
|
||||||
|
pub text_color: String,
|
||||||
|
pub bold: bool,
|
||||||
|
pub outline_color: String,
|
||||||
|
pub background_enabled: bool,
|
||||||
|
pub background_opacity: f64,
|
||||||
|
pub background_color: String,
|
||||||
|
pub text_outline: bool,
|
||||||
|
pub shadow_enabled: bool,
|
||||||
|
pub shadow_depth: u32,
|
||||||
|
pub shadow_color: String,
|
||||||
|
pub dimmed_color_mode: String,
|
||||||
|
pub dimmed_opacity: f64,
|
||||||
|
pub dimmed_color: String,
|
||||||
|
pub position: String,
|
||||||
|
pub word_highlight: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ExportConfig {
|
pub struct ExportConfig {
|
||||||
@@ -93,6 +119,25 @@ pub struct ExportConfig {
|
|||||||
pub output_directory: String,
|
pub output_directory: String,
|
||||||
pub video_title: String,
|
pub video_title: String,
|
||||||
pub source_file_path: String,
|
pub source_file_path: String,
|
||||||
|
/// Whether to include captions in the export
|
||||||
|
pub include_captions: bool,
|
||||||
|
/// Whether to burn subtitles into the video (requires re-encoding)
|
||||||
|
pub burn_in_captions: bool,
|
||||||
|
/// Path to the VTT caption file (if available)
|
||||||
|
pub caption_file_path: Option<String>,
|
||||||
|
/// Style settings for burned-in captions
|
||||||
|
pub caption_style: Option<CaptionStyle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WaveformTiers {
|
||||||
|
/// ~2,000 peaks — overview for full-duration view
|
||||||
|
pub tier0: Vec<f64>,
|
||||||
|
/// ~10,000 peaks — moderate zoom
|
||||||
|
pub tier1: Vec<f64>,
|
||||||
|
/// All extracted peaks — deep zoom
|
||||||
|
pub tier2: Vec<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
import { invoke, Channel } from '@tauri-apps/api/core';
|
import { invoke, Channel } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
export interface CaptionStyle {
|
||||||
|
fontFamily: string;
|
||||||
|
fontSize: number;
|
||||||
|
textColor: string;
|
||||||
|
bold: boolean;
|
||||||
|
outlineColor: string;
|
||||||
|
backgroundEnabled: boolean;
|
||||||
|
backgroundOpacity: number;
|
||||||
|
backgroundColor: string;
|
||||||
|
textOutline: boolean;
|
||||||
|
shadowEnabled: boolean;
|
||||||
|
shadowDepth: number;
|
||||||
|
shadowColor: string;
|
||||||
|
dimmedColorMode: string;
|
||||||
|
dimmedOpacity: number;
|
||||||
|
dimmedColor: string;
|
||||||
|
position: string;
|
||||||
|
wordHighlight: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExportConfig {
|
export interface ExportConfig {
|
||||||
clips: Array<{
|
clips: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,10 +33,15 @@ export interface ExportConfig {
|
|||||||
outputDirectory: string;
|
outputDirectory: string;
|
||||||
videoTitle: string;
|
videoTitle: string;
|
||||||
sourceFilePath: string;
|
sourceFilePath: string;
|
||||||
|
includeCaptions: boolean;
|
||||||
|
burnInCaptions: boolean;
|
||||||
|
captionFilePath: string | null;
|
||||||
|
captionStyle: CaptionStyle | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExportEvent =
|
type ExportEvent =
|
||||||
| { event: 'progress'; data: { current: number; total: number; label: string } }
|
| { event: 'progress'; data: { current: number; total: number; label: string } }
|
||||||
|
| { event: 'clipProgress'; data: { current: number; total: number; label: string; percent: number } }
|
||||||
| { event: 'finished'; data: { paths: string[] } }
|
| { event: 'finished'; data: { paths: string[] } }
|
||||||
| { event: 'error'; data: { message: string } };
|
| { event: 'error'; data: { message: string } };
|
||||||
|
|
||||||
@@ -24,7 +49,8 @@ export async function exportClips(
|
|||||||
config: ExportConfig,
|
config: ExportConfig,
|
||||||
onProgress: (current: number, total: number, label: string) => void,
|
onProgress: (current: number, total: number, label: string) => void,
|
||||||
onFinished: (paths: string[]) => void,
|
onFinished: (paths: string[]) => void,
|
||||||
onError: (message: string) => void
|
onError: (message: string) => void,
|
||||||
|
onClipProgress?: (current: number, total: number, label: string, percent: number) => void
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onEvent = new Channel<ExportEvent>();
|
const onEvent = new Channel<ExportEvent>();
|
||||||
onEvent.onmessage = (message) => {
|
onEvent.onmessage = (message) => {
|
||||||
@@ -32,6 +58,9 @@ export async function exportClips(
|
|||||||
case 'progress':
|
case 'progress':
|
||||||
onProgress(message.data.current, message.data.total, message.data.label);
|
onProgress(message.data.current, message.data.total, message.data.label);
|
||||||
break;
|
break;
|
||||||
|
case 'clipProgress':
|
||||||
|
onClipProgress?.(message.data.current, message.data.total, message.data.label, message.data.percent);
|
||||||
|
break;
|
||||||
case 'finished':
|
case 'finished':
|
||||||
onFinished(message.data.paths);
|
onFinished(message.data.paths);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { exportClips, type ExportConfig } from '$lib/bindings/export';
|
import { exportClips, type ExportConfig } from '$lib/bindings/export';
|
||||||
|
import { checkSubtitlesFilterAvailable } from '$lib/bindings/dependencies';
|
||||||
import { session } from '$lib/stores/videoSession.svelte';
|
import { session } from '$lib/stores/videoSession.svelte';
|
||||||
import { getClips, getSelectedClipId } from '$lib/stores/clips.svelte';
|
import { getClips, getSelectedClipId } from '$lib/stores/clips.svelte';
|
||||||
import { preferences, setOutputDirectory } from '$lib/stores/preferences.svelte';
|
import { preferences, setOutputDirectory } from '$lib/stores/preferences.svelte';
|
||||||
@@ -11,17 +12,30 @@
|
|||||||
let exportScope = $state<'individual' | 'merged'>('individual');
|
let exportScope = $state<'individual' | 'merged'>('individual');
|
||||||
let cutMode = $state<'lossless' | 'precise'>('lossless');
|
let cutMode = $state<'lossless' | 'precise'>('lossless');
|
||||||
let preciseFormat = $state('mp4');
|
let preciseFormat = $state('mp4');
|
||||||
|
let includeCaptions = $state(true);
|
||||||
|
let burnInCaptions = $state(false);
|
||||||
|
let canBurnIn = $state(true);
|
||||||
let isExporting = $state(false);
|
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 exportProgress = $state('');
|
||||||
let exportError = $state<string | null>(null);
|
let exportError = $state<string | null>(null);
|
||||||
let exportedPaths = $state<string[]>([]);
|
let exportedPaths = $state<string[]>([]);
|
||||||
|
|
||||||
let clips = $derived(getClips());
|
let clips = $derived(getClips());
|
||||||
let selectedId = $derived(getSelectedClipId());
|
let selectedId = $derived(getSelectedClipId());
|
||||||
let isDownloadComplete = $derived(
|
let isExportReady = $derived(
|
||||||
session.downloadProgress >= 1 && session.localFilePath !== null
|
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(
|
let clipsToExport = $derived(
|
||||||
clipScope === 'selected' && selectedId
|
clipScope === 'selected' && selectedId
|
||||||
@@ -41,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
if (!session.localFilePath || clipsToExport.length === 0) return;
|
if (!session.exportFilePath || clipsToExport.length === 0) return;
|
||||||
|
|
||||||
isExporting = true;
|
isExporting = true;
|
||||||
exportError = null;
|
exportError = null;
|
||||||
@@ -65,7 +79,31 @@
|
|||||||
: { scope: 'merged' },
|
: { scope: 'merged' },
|
||||||
outputDirectory: preferences.outputDirectory,
|
outputDirectory: preferences.outputDirectory,
|
||||||
videoTitle: session.title,
|
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 {
|
try {
|
||||||
@@ -73,6 +111,8 @@
|
|||||||
config,
|
config,
|
||||||
(current, total, label) => {
|
(current, total, label) => {
|
||||||
exportProgress = `Exporting ${current}/${total}: ${label}`;
|
exportProgress = `Exporting ${current}/${total}: ${label}`;
|
||||||
|
exportClipPercent = 0;
|
||||||
|
exportClipLabel = label;
|
||||||
},
|
},
|
||||||
(paths) => {
|
(paths) => {
|
||||||
exportedPaths = paths;
|
exportedPaths = paths;
|
||||||
@@ -81,6 +121,11 @@
|
|||||||
(message) => {
|
(message) => {
|
||||||
exportError = message;
|
exportError = message;
|
||||||
isExporting = false;
|
isExporting = false;
|
||||||
|
},
|
||||||
|
(current, total, label, percent) => {
|
||||||
|
exportClipPercent = Math.round(percent * 100);
|
||||||
|
exportClipLabel = label;
|
||||||
|
exportProgress = `Exporting ${current}/${total}: ${label}`;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -95,11 +140,11 @@
|
|||||||
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
|
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
|
||||||
<h2>Export Clips</h2>
|
<h2>Export Clips</h2>
|
||||||
|
|
||||||
{#if !isDownloadComplete}
|
{#if !isExportReady}
|
||||||
<div class="waiting">
|
<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-bar">
|
||||||
<div class="progress-fill" style="width: {downloadPercent}%"></div>
|
<div class="progress-fill" style="width: {exportDownloadPercent}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if exportedPaths.length > 0}
|
{:else if exportedPaths.length > 0}
|
||||||
@@ -161,6 +206,33 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</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>
|
<section>
|
||||||
<h3>Save to</h3>
|
<h3>Save to</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -174,7 +246,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isExporting}
|
{#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}
|
{/if}
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -263,6 +341,25 @@
|
|||||||
padding: 4px 8px;
|
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 {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -280,9 +377,33 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.exporting {
|
.exporting {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-label {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 13px;
|
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 {
|
.waiting p {
|
||||||
|
|||||||
@@ -3,14 +3,56 @@ import type { CookieSource } from '$lib/bindings/dependencies';
|
|||||||
|
|
||||||
export type { CookieSource };
|
export type { CookieSource };
|
||||||
|
|
||||||
|
export interface CaptionSettings {
|
||||||
|
fontFamily: string;
|
||||||
|
fontSize: number;
|
||||||
|
textColor: string;
|
||||||
|
bold: boolean;
|
||||||
|
outlineColor: string;
|
||||||
|
backgroundEnabled: boolean;
|
||||||
|
backgroundOpacity: number;
|
||||||
|
backgroundColor: string;
|
||||||
|
textOutline: boolean;
|
||||||
|
shadowEnabled: boolean;
|
||||||
|
shadowDepth: number;
|
||||||
|
shadowColor: string;
|
||||||
|
dimmedColorMode: 'auto' | 'custom';
|
||||||
|
dimmedOpacity: number;
|
||||||
|
dimmedColor: string;
|
||||||
|
position: 'bottom' | 'top';
|
||||||
|
wordHighlight: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_CAPTION_SETTINGS: CaptionSettings = {
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fontSize: 18,
|
||||||
|
textColor: '#ffffff',
|
||||||
|
bold: true,
|
||||||
|
outlineColor: '#000000',
|
||||||
|
backgroundEnabled: true,
|
||||||
|
backgroundOpacity: 0.75,
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
textOutline: true,
|
||||||
|
shadowEnabled: false,
|
||||||
|
shadowDepth: 2,
|
||||||
|
shadowColor: '#000000',
|
||||||
|
dimmedColorMode: 'auto',
|
||||||
|
dimmedOpacity: 0.4,
|
||||||
|
dimmedColor: '#999999',
|
||||||
|
position: 'bottom',
|
||||||
|
wordHighlight: true,
|
||||||
|
};
|
||||||
|
|
||||||
export interface Preferences {
|
export interface Preferences {
|
||||||
outputDirectory: string;
|
outputDirectory: string;
|
||||||
cookieSource: CookieSource;
|
cookieSource: CookieSource;
|
||||||
|
captionSettings: CaptionSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PREFERENCES: Preferences = {
|
const DEFAULT_PREFERENCES: Preferences = {
|
||||||
outputDirectory: '~/',
|
outputDirectory: '~/',
|
||||||
cookieSource: { type: 'browser', browser: 'firefox' },
|
cookieSource: { type: 'browser', browser: 'firefox' },
|
||||||
|
captionSettings: { ...DEFAULT_CAPTION_SETTINGS },
|
||||||
};
|
};
|
||||||
|
|
||||||
let store: Store | null = null;
|
let store: Store | null = null;
|
||||||
@@ -27,8 +69,10 @@ export async function loadPreferences(): Promise<void> {
|
|||||||
store = await load('preferences.json', { autoSave: true });
|
store = await load('preferences.json', { autoSave: true });
|
||||||
const savedDir = await store.get<string>('outputDirectory');
|
const savedDir = await store.get<string>('outputDirectory');
|
||||||
const savedCookie = await store.get<CookieSource>('cookieSource');
|
const savedCookie = await store.get<CookieSource>('cookieSource');
|
||||||
|
const savedCaptions = await store.get<CaptionSettings>('captionSettings');
|
||||||
if (savedDir) preferences.outputDirectory = savedDir;
|
if (savedDir) preferences.outputDirectory = savedDir;
|
||||||
if (savedCookie) preferences.cookieSource = savedCookie;
|
if (savedCookie) preferences.cookieSource = savedCookie;
|
||||||
|
if (savedCaptions) preferences.captionSettings = { ...DEFAULT_CAPTION_SETTINGS, ...savedCaptions };
|
||||||
})();
|
})();
|
||||||
await loadPromise;
|
await loadPromise;
|
||||||
}
|
}
|
||||||
@@ -40,6 +84,7 @@ export async function savePreferences(): Promise<void> {
|
|||||||
if (!store) return;
|
if (!store) return;
|
||||||
await store.set('outputDirectory', preferences.outputDirectory);
|
await store.set('outputDirectory', preferences.outputDirectory);
|
||||||
await store.set('cookieSource', preferences.cookieSource);
|
await store.set('cookieSource', preferences.cookieSource);
|
||||||
|
await store.set('captionSettings', preferences.captionSettings);
|
||||||
await store.save();
|
await store.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,3 +97,8 @@ export async function setCookieSource(source: CookieSource): Promise<void> {
|
|||||||
preferences.cookieSource = source;
|
preferences.cookieSource = source;
|
||||||
await savePreferences();
|
await savePreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setCaptionSettings(settings: Partial<CaptionSettings>): Promise<void> {
|
||||||
|
Object.assign(preferences.captionSettings, settings);
|
||||||
|
await savePreferences();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user