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:
329
src/lib/components/CaptionSettingsPanel.svelte
Normal file
329
src/lib/components/CaptionSettingsPanel.svelte
Normal 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>
|
||||||
Reference in New Issue
Block a user