feat(captions): expand settings panel with font, shadow, dimmed color, bg toggle controls

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 10:36:37 -04:00
parent 517f45e8fd
commit fd05ae19b5

View File

@@ -0,0 +1,329 @@
<script lang="ts">
import { onMount } from 'svelte';
import {
preferences,
setCaptionSettings,
DEFAULT_CAPTION_SETTINGS,
type CaptionSettings,
} from '$lib/stores/preferences.svelte';
import { listSystemFonts } from '$lib/bindings/mediaAnalysis';
let { onClose }: { onClose: () => void } = $props();
let settings = $derived(preferences.captionSettings);
let systemFonts = $state<string[]>(['Arial']);
onMount(async () => {
try {
systemFonts = await listSystemFonts();
} catch {
systemFonts = ['Arial', 'Helvetica', 'Verdana', 'Georgia', 'Times New Roman', 'Courier New', 'Impact'];
}
});
function update(partial: Partial<CaptionSettings>) {
// Enforce mutual exclusivity
if (partial.backgroundEnabled) {
partial.shadowEnabled = false;
} else if (partial.shadowEnabled) {
partial.backgroundEnabled = false;
}
setCaptionSettings(partial);
}
function resetToDefaults() {
setCaptionSettings({ ...DEFAULT_CAPTION_SETTINGS });
}
</script>
<div class="panel">
<div class="panel-header">
<h3>Caption Appearance</h3>
<button class="close-btn" onclick={onClose} title="Close">x</button>
</div>
<div class="scroll-area">
<!-- Font Family -->
<div class="control">
<label>Font</label>
<select
value={settings.fontFamily}
onchange={(e) => update({ fontFamily: (e.target as HTMLSelectElement).value })}
>
{#each systemFonts as font}
<option value={font} style="font-family: {font}">{font}</option>
{/each}
</select>
</div>
<!-- Font Size -->
<div class="control">
<label>
Font Size
<span class="value">{settings.fontSize}px</span>
</label>
<input
type="range" min="12" max="36" step="1"
value={settings.fontSize}
oninput={(e) => update({ fontSize: parseInt((e.target as HTMLInputElement).value, 10) })}
/>
</div>
<!-- Text Color + Bold -->
<div class="control row">
<label>Text Color</label>
<div class="row-controls">
<input
type="color" value={settings.textColor}
oninput={(e) => update({ textColor: (e.target as HTMLInputElement).value })}
/>
<label class="inline-check">
<input type="checkbox" checked={settings.bold}
onchange={(e) => update({ bold: (e.target as HTMLInputElement).checked })} />
Bold
</label>
</div>
</div>
<!-- Dimmed Text Color -->
<div class="control">
<label>Dimmed Text</label>
<div class="radio-row">
<label>
<input type="radio" name="dimmed-mode" value="auto"
checked={settings.dimmedColorMode === 'auto'}
onchange={() => update({ dimmedColorMode: 'auto' })} />
Auto
</label>
<label>
<input type="radio" name="dimmed-mode" value="custom"
checked={settings.dimmedColorMode === 'custom'}
onchange={() => update({ dimmedColorMode: 'custom' })} />
Custom
</label>
</div>
</div>
{#if settings.dimmedColorMode === 'auto'}
<div class="control">
<label>
Dim Opacity
<span class="value">{Math.round(settings.dimmedOpacity * 100)}%</span>
</label>
<input
type="range" min="10" max="90" step="5"
value={Math.round(settings.dimmedOpacity * 100)}
oninput={(e) => update({ dimmedOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
/>
</div>
{:else}
<div class="control row">
<label>Dimmed Color</label>
<div class="row-controls">
<input
type="color" value={settings.dimmedColor}
oninput={(e) => update({ dimmedColor: (e.target as HTMLInputElement).value })}
/>
</div>
</div>
{/if}
<!-- Outline -->
<div class="control row">
<label>
<input type="checkbox" checked={settings.textOutline}
onchange={(e) => update({ textOutline: (e.target as HTMLInputElement).checked })} />
Outline
</label>
<div class="row-controls">
<input
type="color" value={settings.outlineColor}
disabled={!settings.textOutline}
class:disabled={!settings.textOutline}
oninput={(e) => update({ outlineColor: (e.target as HTMLInputElement).value })}
/>
</div>
</div>
<!-- Background (mutually exclusive with Shadow) -->
<div class="control row">
<label>
<input type="checkbox" checked={settings.backgroundEnabled}
onchange={(e) => update({ backgroundEnabled: (e.target as HTMLInputElement).checked })} />
Background
</label>
<div class="row-controls">
<input
type="color" value={settings.backgroundColor}
disabled={!settings.backgroundEnabled}
class:disabled={!settings.backgroundEnabled}
oninput={(e) => update({ backgroundColor: (e.target as HTMLInputElement).value })}
/>
</div>
</div>
{#if settings.backgroundEnabled}
<div class="control">
<label>
BG Opacity
<span class="value">{Math.round(settings.backgroundOpacity * 100)}%</span>
</label>
<input
type="range" min="0" max="100" step="5"
value={Math.round(settings.backgroundOpacity * 100)}
oninput={(e) => update({ backgroundOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
/>
</div>
{/if}
<!-- Shadow (mutually exclusive with Background) -->
<div class="control row">
<label>
<input type="checkbox" checked={settings.shadowEnabled}
onchange={(e) => update({ shadowEnabled: (e.target as HTMLInputElement).checked })} />
Drop Shadow
</label>
<div class="row-controls">
<input
type="color" value={settings.shadowColor}
disabled={!settings.shadowEnabled}
class:disabled={!settings.shadowEnabled}
oninput={(e) => update({ shadowColor: (e.target as HTMLInputElement).value })}
/>
</div>
</div>
{#if settings.shadowEnabled}
<div class="control">
<label>
Shadow Depth
<span class="value">{settings.shadowDepth}px</span>
</label>
<input
type="range" min="1" max="5" step="1"
value={settings.shadowDepth}
oninput={(e) => update({ shadowDepth: parseInt((e.target as HTMLInputElement).value, 10) })}
/>
</div>
{/if}
<!-- Position -->
<div class="control">
<label>Position</label>
<div class="radio-row">
<label>
<input type="radio" name="caption-position" value="bottom"
checked={settings.position === 'bottom'}
onchange={() => update({ position: 'bottom' })} />
Bottom
</label>
<label>
<input type="radio" name="caption-position" value="top"
checked={settings.position === 'top'}
onchange={() => update({ position: 'top' })} />
Top
</label>
</div>
</div>
<!-- Word Highlight -->
<div class="control">
<label>
<input type="checkbox" checked={settings.wordHighlight}
onchange={(e) => update({ wordHighlight: (e.target as HTMLInputElement).checked })} />
Word-by-word highlight
</label>
</div>
</div>
<div class="panel-footer">
<button class="reset-btn" onclick={resetToDefaults}>Reset to defaults</button>
</div>
</div>
<style>
.panel {
position: absolute;
bottom: 44px;
right: 12px;
background: var(--bg-primary, #1e1e2e);
border: 1px solid var(--border, #353550);
border-radius: 8px;
padding: 12px 16px;
width: 320px;
max-height: 480px;
display: flex;
flex-direction: column;
z-index: 20;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
flex-shrink: 0;
}
h3 { margin: 0; font-size: 13px; font-weight: 600; color: var(--text-primary, #cdd6f4); }
.close-btn {
background: none; border: none; color: var(--text-secondary, #6c7086);
cursor: pointer; font-size: 14px; padding: 2px 6px; border-radius: 3px;
}
.close-btn:hover { background: var(--bg-tertiary, #2a2a3e); color: var(--text-primary, #cdd6f4); }
.scroll-area {
overflow-y: auto;
flex: 1;
min-height: 0;
padding-right: 4px;
}
.control { margin-bottom: 10px; }
.control label {
display: flex; align-items: center; gap: 6px;
font-size: 12px; color: var(--text-secondary, #6c7086); margin-bottom: 4px;
}
.control.row { display: flex; align-items: center; justify-content: space-between; }
.control.row > label { margin-bottom: 0; }
.row-controls { display: flex; align-items: center; gap: 8px; }
.inline-check {
display: flex; align-items: center; gap: 4px;
font-size: 12px; color: var(--text-secondary, #6c7086);
margin-bottom: 0 !important; cursor: pointer;
}
.value {
margin-left: auto; font-size: 11px;
color: var(--text-primary, #cdd6f4); font-variant-numeric: tabular-nums;
}
select {
width: 100%; padding: 4px 6px; font-size: 12px;
background: var(--bg-secondary, #252535); color: var(--text-primary, #cdd6f4);
border: 1px solid var(--border, #353550); border-radius: 4px;
}
input[type='range'] { width: 100%; accent-color: #89b4fa; }
input[type='color'] {
width: 32px; height: 24px; padding: 0;
border: 1px solid var(--border, #353550); border-radius: 3px;
cursor: pointer; background: none;
}
input[type='color'].disabled { opacity: 0.3; cursor: not-allowed; }
input[type='checkbox'] { accent-color: #89b4fa; }
input[type='radio'] { accent-color: #89b4fa; }
.radio-row { display: flex; gap: 16px; }
.radio-row label { font-size: 12px; color: var(--text-secondary, #6c7086); cursor: pointer; }
.panel-footer {
margin-top: 8px; padding-top: 8px; flex-shrink: 0;
border-top: 1px solid var(--border, #353550);
}
.reset-btn {
font-size: 11px; color: var(--text-secondary, #6c7086);
background: none; border: none; cursor: pointer; padding: 2px 0;
}
.reset-btn:hover { color: var(--text-primary, #cdd6f4); }
</style>