feat: add preferences store with cookie source and output directory
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,17 +6,29 @@
|
|||||||
import ClipList from '$lib/components/ClipList.svelte';
|
import ClipList from '$lib/components/ClipList.svelte';
|
||||||
import StatusBar from '$lib/components/StatusBar.svelte';
|
import StatusBar from '$lib/components/StatusBar.svelte';
|
||||||
import SetupWizard from '$lib/components/SetupWizard.svelte';
|
import SetupWizard from '$lib/components/SetupWizard.svelte';
|
||||||
|
import PreferencesPanel from '$lib/components/PreferencesPanel.svelte';
|
||||||
|
import { loadPreferences } from '$lib/stores/preferences.svelte';
|
||||||
|
|
||||||
let showSetupWizard = $state(true);
|
let showSetupWizard = $state(true);
|
||||||
|
let showPreferences = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
loadPreferences();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showSetupWizard}
|
{#if showSetupWizard}
|
||||||
<SetupWizard onComplete={() => (showSetupWizard = false)} />
|
<SetupWizard onComplete={() => (showSetupWizard = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showPreferences}
|
||||||
|
<PreferencesPanel onClose={() => (showPreferences = false)} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<header class="toolbar">
|
<header class="toolbar">
|
||||||
<UrlInput />
|
<UrlInput />
|
||||||
|
<button class="prefs-btn" onclick={() => (showPreferences = true)} title="Preferences">⚙</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="content">
|
<main class="content">
|
||||||
@@ -51,4 +63,9 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prefs-btn {
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
203
src/lib/components/PreferencesPanel.svelte
Normal file
203
src/lib/components/PreferencesPanel.svelte
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
|
import {
|
||||||
|
preferences,
|
||||||
|
setOutputDirectory,
|
||||||
|
setCookieSource,
|
||||||
|
type CookieSource,
|
||||||
|
} from '$lib/stores/preferences.svelte';
|
||||||
|
|
||||||
|
let { onClose }: { onClose: () => void } = $props();
|
||||||
|
|
||||||
|
let cookieType = $derived(preferences.cookieSource.type);
|
||||||
|
let cookieFilePath = $state(
|
||||||
|
preferences.cookieSource.type === 'file' ? preferences.cookieSource.path : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
async function pickOutputDirectory() {
|
||||||
|
const selected = await open({
|
||||||
|
directory: true,
|
||||||
|
defaultPath: preferences.outputDirectory,
|
||||||
|
title: 'Choose output directory',
|
||||||
|
});
|
||||||
|
if (selected) {
|
||||||
|
await setOutputDirectory(selected as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCookieTypeChange(newType: string) {
|
||||||
|
let newSource: CookieSource;
|
||||||
|
switch (newType) {
|
||||||
|
case 'browser':
|
||||||
|
newSource = { type: 'browser', browser: 'firefox' };
|
||||||
|
break;
|
||||||
|
case 'file':
|
||||||
|
newSource = { type: 'file', path: cookieFilePath || '' };
|
||||||
|
break;
|
||||||
|
case 'none':
|
||||||
|
newSource = { type: 'none' };
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
const _exhaustive: never = newType as never;
|
||||||
|
throw new Error(`Unhandled cookie type: ${_exhaustive}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await setCookieSource(newSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickCookieFile() {
|
||||||
|
const selected = await open({
|
||||||
|
filters: [{ name: 'Cookie files', extensions: ['txt'] }],
|
||||||
|
title: 'Select cookies.txt file',
|
||||||
|
});
|
||||||
|
if (selected) {
|
||||||
|
cookieFilePath = selected as string;
|
||||||
|
await setCookieSource({ type: 'file', path: cookieFilePath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overlay" role="presentation" onclick={onClose}>
|
||||||
|
<div class="panel" role="dialog" onclick={(e) => e.stopPropagation()}>
|
||||||
|
<h2>Preferences</h2>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Output Directory</h3>
|
||||||
|
<div class="row">
|
||||||
|
<code class="path">{preferences.outputDirectory}</code>
|
||||||
|
<button onclick={pickOutputDirectory}>Change…</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>YouTube Cookie Source</h3>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="cookie"
|
||||||
|
value="browser"
|
||||||
|
checked={cookieType === 'browser'}
|
||||||
|
onchange={() => handleCookieTypeChange('browser')}
|
||||||
|
/>
|
||||||
|
Firefox (recommended)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="cookie"
|
||||||
|
value="file"
|
||||||
|
checked={cookieType === 'file'}
|
||||||
|
onchange={() => handleCookieTypeChange('file')}
|
||||||
|
/>
|
||||||
|
cookies.txt file
|
||||||
|
{#if cookieType === 'file'}
|
||||||
|
<div class="file-picker">
|
||||||
|
<code class="path">{cookieFilePath || 'No file selected'}</code>
|
||||||
|
<button onclick={pickCookieFile}>Browse…</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="cookie"
|
||||||
|
value="none"
|
||||||
|
checked={cookieType === 'none'}
|
||||||
|
onchange={() => handleCookieTypeChange('none')}
|
||||||
|
/>
|
||||||
|
None (public content only)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" onclick={onClose}>Done</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 32px;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
43
src/lib/stores/preferences.svelte.ts
Normal file
43
src/lib/stores/preferences.svelte.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { load, Store } from '@tauri-apps/plugin-store';
|
||||||
|
import type { CookieSource } from '$lib/bindings/dependencies';
|
||||||
|
|
||||||
|
export type { CookieSource };
|
||||||
|
|
||||||
|
export interface Preferences {
|
||||||
|
outputDirectory: string;
|
||||||
|
cookieSource: CookieSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PREFERENCES: Preferences = {
|
||||||
|
outputDirectory: '~/',
|
||||||
|
cookieSource: { type: 'browser', browser: 'firefox' },
|
||||||
|
};
|
||||||
|
|
||||||
|
let store: Store | null = null;
|
||||||
|
|
||||||
|
export const preferences = $state<Preferences>({ ...DEFAULT_PREFERENCES });
|
||||||
|
|
||||||
|
export async function loadPreferences(): Promise<void> {
|
||||||
|
store = await load('preferences.json', { autoSave: true });
|
||||||
|
const savedDir = await store.get<string>('outputDirectory');
|
||||||
|
const savedCookie = await store.get<CookieSource>('cookieSource');
|
||||||
|
if (savedDir) preferences.outputDirectory = savedDir;
|
||||||
|
if (savedCookie) preferences.cookieSource = savedCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function savePreferences(): Promise<void> {
|
||||||
|
if (!store) return;
|
||||||
|
await store.set('outputDirectory', preferences.outputDirectory);
|
||||||
|
await store.set('cookieSource', preferences.cookieSource);
|
||||||
|
await store.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setOutputDirectory(dir: string): Promise<void> {
|
||||||
|
preferences.outputDirectory = dir;
|
||||||
|
await savePreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setCookieSource(source: CookieSource): Promise<void> {
|
||||||
|
preferences.cookieSource = source;
|
||||||
|
await savePreferences();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user