mirror of
https://github.com/Tony0410/readlater.git
synced 2026-05-24 22:01:41 +08:00
- /api/tts proxies requests to Edge TTS and Kokoro - Uses Docker container names for internal networking - Removes URL config from settings (handled server-side) - Fixes localStorage merge for new settings fields Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
296 lines
11 KiB
TypeScript
296 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { ReaderSettings, TTSSettings } from "@/lib/types";
|
|
import { X, Sun, Moon, BookOpen } from "lucide-react";
|
|
|
|
interface SettingsPanelProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
readerSettings: ReaderSettings;
|
|
onReaderSettingsChange: (settings: ReaderSettings) => void;
|
|
ttsSettings: TTSSettings;
|
|
onTTSSettingsChange: (settings: TTSSettings) => void;
|
|
availableVoices: SpeechSynthesisVoice[];
|
|
}
|
|
|
|
export function SettingsPanel({
|
|
isOpen,
|
|
onClose,
|
|
readerSettings,
|
|
onReaderSettingsChange,
|
|
ttsSettings,
|
|
onTTSSettingsChange,
|
|
availableVoices,
|
|
}: SettingsPanelProps) {
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
<div
|
|
className="fixed inset-0 bg-black/50 z-40"
|
|
onClick={onClose}
|
|
/>
|
|
|
|
{/* Panel */}
|
|
<div className="fixed right-0 top-0 h-full w-full max-w-md bg-[var(--background)] border-l border-[var(--border)] z-50 overflow-y-auto">
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-xl font-semibold">Settings</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 rounded hover:bg-[var(--surface)] transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Theme */}
|
|
<section className="mb-8">
|
|
<h3 className="text-sm font-medium text-[var(--muted)] uppercase tracking-wide mb-4">
|
|
Theme
|
|
</h3>
|
|
<div className="flex gap-2">
|
|
{[
|
|
{ value: "dark", icon: Moon, label: "Dark" },
|
|
{ value: "light", icon: Sun, label: "Light" },
|
|
{ value: "sepia", icon: BookOpen, label: "Sepia" },
|
|
].map(({ value, icon: Icon, label }) => (
|
|
<button
|
|
key={value}
|
|
onClick={() =>
|
|
onReaderSettingsChange({
|
|
...readerSettings,
|
|
theme: value as ReaderSettings["theme"],
|
|
})
|
|
}
|
|
className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border transition-colors ${
|
|
readerSettings.theme === value
|
|
? "border-[var(--accent)] bg-[var(--accent)]/10"
|
|
: "border-[var(--border)] hover:border-[var(--muted)]"
|
|
}`}
|
|
>
|
|
<Icon className="w-5 h-5" />
|
|
<span className="text-sm">{label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Font Size */}
|
|
<section className="mb-8">
|
|
<h3 className="text-sm font-medium text-[var(--muted)] uppercase tracking-wide mb-4">
|
|
Font Size: {readerSettings.fontSize}px
|
|
</h3>
|
|
<input
|
|
type="range"
|
|
min="14"
|
|
max="32"
|
|
value={readerSettings.fontSize}
|
|
onChange={(e) =>
|
|
onReaderSettingsChange({
|
|
...readerSettings,
|
|
fontSize: parseInt(e.target.value),
|
|
})
|
|
}
|
|
className="w-full accent-[var(--accent)]"
|
|
/>
|
|
<div className="flex justify-between text-xs text-[var(--muted)] mt-1">
|
|
<span>14px</span>
|
|
<span>32px</span>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Font Family */}
|
|
<section className="mb-8">
|
|
<h3 className="text-sm font-medium text-[var(--muted)] uppercase tracking-wide mb-4">
|
|
Font
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{[
|
|
{ value: "serif", label: "Serif", sample: "Georgia" },
|
|
{ value: "sans", label: "Sans", sample: "Helvetica" },
|
|
{ value: "mono", label: "Mono", sample: "Monaco" },
|
|
{ value: "system", label: "System", sample: "Default" },
|
|
].map(({ value, label, sample }) => (
|
|
<button
|
|
key={value}
|
|
onClick={() =>
|
|
onReaderSettingsChange({
|
|
...readerSettings,
|
|
fontFamily: value as ReaderSettings["fontFamily"],
|
|
})
|
|
}
|
|
className={`p-3 rounded-lg border text-left transition-colors ${
|
|
readerSettings.fontFamily === value
|
|
? "border-[var(--accent)] bg-[var(--accent)]/10"
|
|
: "border-[var(--border)] hover:border-[var(--muted)]"
|
|
}`}
|
|
>
|
|
<div className={`font-${value} text-lg`}>{label}</div>
|
|
<div className="text-xs text-[var(--muted)]">{sample}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Line Height */}
|
|
<section className="mb-8">
|
|
<h3 className="text-sm font-medium text-[var(--muted)] uppercase tracking-wide mb-4">
|
|
Line Height: {readerSettings.lineHeight}
|
|
</h3>
|
|
<input
|
|
type="range"
|
|
min="1.4"
|
|
max="2.2"
|
|
step="0.1"
|
|
value={readerSettings.lineHeight}
|
|
onChange={(e) =>
|
|
onReaderSettingsChange({
|
|
...readerSettings,
|
|
lineHeight: parseFloat(e.target.value),
|
|
})
|
|
}
|
|
className="w-full accent-[var(--accent)]"
|
|
/>
|
|
<div className="flex justify-between text-xs text-[var(--muted)] mt-1">
|
|
<span>Tight</span>
|
|
<span>Loose</span>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Content Width */}
|
|
<section className="mb-8">
|
|
<h3 className="text-sm font-medium text-[var(--muted)] uppercase tracking-wide mb-4">
|
|
Content Width: {readerSettings.maxWidth}px
|
|
</h3>
|
|
<input
|
|
type="range"
|
|
min="500"
|
|
max="900"
|
|
step="50"
|
|
value={readerSettings.maxWidth}
|
|
onChange={(e) =>
|
|
onReaderSettingsChange({
|
|
...readerSettings,
|
|
maxWidth: parseInt(e.target.value),
|
|
})
|
|
}
|
|
className="w-full accent-[var(--accent)]"
|
|
/>
|
|
<div className="flex justify-between text-xs text-[var(--muted)] mt-1">
|
|
<span>Narrow</span>
|
|
<span>Wide</span>
|
|
</div>
|
|
</section>
|
|
|
|
<hr className="border-[var(--border)] my-8" />
|
|
|
|
{/* TTS Settings */}
|
|
<section className="mb-8">
|
|
<h3 className="text-sm font-medium text-[var(--muted)] uppercase tracking-wide mb-4">
|
|
Text-to-Speech Engine
|
|
</h3>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
{[
|
|
{ value: "edge", label: "Edge", desc: "Fast, natural" },
|
|
{ value: "kokoro", label: "Kokoro", desc: "High quality" },
|
|
{ value: "browser", label: "Browser", desc: "Built-in" },
|
|
].map(({ value, label, desc }) => (
|
|
<button
|
|
key={value}
|
|
onClick={() =>
|
|
onTTSSettingsChange({
|
|
...ttsSettings,
|
|
engine: value as TTSSettings["engine"],
|
|
})
|
|
}
|
|
className={`p-3 rounded-lg border transition-colors text-left ${
|
|
ttsSettings.engine === value
|
|
? "border-[var(--accent)] bg-[var(--accent)]/10"
|
|
: "border-[var(--border)] hover:border-[var(--muted)]"
|
|
}`}
|
|
>
|
|
<div className="font-medium">{label}</div>
|
|
<div className="text-xs text-[var(--muted)]">{desc}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* TTS Speed */}
|
|
<section className="mb-8">
|
|
<h3 className="text-sm font-medium text-[var(--muted)] uppercase tracking-wide mb-4">
|
|
TTS Speed: {ttsSettings.speed}x
|
|
</h3>
|
|
<input
|
|
type="range"
|
|
min="0.5"
|
|
max="3"
|
|
step="0.1"
|
|
value={ttsSettings.speed}
|
|
onChange={(e) =>
|
|
onTTSSettingsChange({
|
|
...ttsSettings,
|
|
speed: parseFloat(e.target.value),
|
|
})
|
|
}
|
|
className="w-full accent-[var(--accent)]"
|
|
/>
|
|
<div className="flex justify-between text-xs text-[var(--muted)] mt-1">
|
|
<span>0.5x</span>
|
|
<span>3x</span>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Browser Voice Selection */}
|
|
{ttsSettings.engine === "browser" && availableVoices.length > 0 && (
|
|
<section className="mb-8">
|
|
<h3 className="text-sm font-medium text-[var(--muted)] uppercase tracking-wide mb-4">
|
|
Voice
|
|
</h3>
|
|
<select
|
|
value={ttsSettings.voice}
|
|
onChange={(e) =>
|
|
onTTSSettingsChange({
|
|
...ttsSettings,
|
|
voice: e.target.value,
|
|
})
|
|
}
|
|
className="w-full p-3 rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)]"
|
|
>
|
|
<option value="">Default</option>
|
|
{availableVoices.map((voice) => (
|
|
<option key={voice.voiceURI} value={voice.voiceURI}>
|
|
{voice.name} ({voice.lang})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</section>
|
|
)}
|
|
|
|
{/* Edge TTS info */}
|
|
{ttsSettings.engine === "edge" && (
|
|
<section className="mb-8">
|
|
<p className="text-sm text-[var(--muted)]">
|
|
Uses Microsoft neural voices via Edge TTS. Fast and natural sounding.
|
|
Requires the edge-tts Docker container running on the server.
|
|
</p>
|
|
</section>
|
|
)}
|
|
|
|
{/* Kokoro info */}
|
|
{ttsSettings.engine === "kokoro" && (
|
|
<section className="mb-8">
|
|
<p className="text-sm text-[var(--muted)]">
|
|
High quality local TTS. Slower as it generates full audio before playing.
|
|
Requires the kokoro Docker container running on the server.
|
|
</p>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|