199 lines
6.7 KiB
TypeScript
199 lines
6.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
|||
|
|
import {
|
||
|
|
CopyCheck,
|
||
|
|
Download,
|
||
|
|
FolderTree,
|
||
|
|
Image,
|
||
|
|
Music2,
|
||
|
|
Save,
|
||
|
|
Settings2,
|
||
|
|
Tags,
|
||
|
|
} from "lucide-react";
|
||
|
|
import { Button } from "../components/Button";
|
||
|
|
import { Checkbox } from "../components/Checkbox";
|
||
|
|
import { Input } from "../components/Input";
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from "../components/Select";
|
||
|
|
import {
|
||
|
|
loadSpotiRipSettings,
|
||
|
|
saveSpotiRipSettings,
|
||
|
|
type SpotiRipSettings,
|
||
|
|
} from "../helpers/spotiripSettings";
|
||
|
|
import { getDesktopBridge } from "../helpers/desktopBridge";
|
||
|
|
import styles from "./settings.module.css";
|
||
|
|
import { MobileNav } from "../components/MobileNav";
|
||
|
|
|
||
|
|
export default function SettingsPage() {
|
||
|
|
const [settings, setSettings] = useState<SpotiRipSettings>(() => loadSpotiRipSettings());
|
||
|
|
const [saved, setSaved] = useState(false);
|
||
|
|
const desktop = getDesktopBridge();
|
||
|
|
const [downloadFolder, setDownloadFolder] = useState<string>();
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
desktop?.getDownloadFolder().then(setDownloadFolder).catch(() => undefined);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
async function chooseDownloadFolder() {
|
||
|
|
const chosen = await desktop?.chooseDownloadFolder();
|
||
|
|
if (chosen) setDownloadFolder(chosen);
|
||
|
|
}
|
||
|
|
|
||
|
|
function update(changes: Partial<SpotiRipSettings>) {
|
||
|
|
setSaved(false);
|
||
|
|
setSettings((current) => ({ ...current, ...changes }));
|
||
|
|
}
|
||
|
|
|
||
|
|
function save() {
|
||
|
|
saveSpotiRipSettings(settings);
|
||
|
|
setSaved(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<main className={styles.page}>
|
||
|
|
<header className={styles.topbar}>
|
||
|
|
<span>Preferences</span>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<section className={styles.heading}>
|
||
|
|
<Settings2 size={22} />
|
||
|
|
<div>
|
||
|
|
<h1>Settings</h1>
|
||
|
|
<p>Defaults for local file repair and metadata cleanup.</p>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section className={styles.panel}>
|
||
|
|
{desktop ? (
|
||
|
|
<div className={styles.row}>
|
||
|
|
<Download size={18} />
|
||
|
|
<div className={styles.rowContent}>
|
||
|
|
<label>Download folder</label>
|
||
|
|
<div className={styles.folderRow}>
|
||
|
|
<Input value={downloadFolder || ""} readOnly aria-label="Download folder" />
|
||
|
|
<Button variant="outline" onClick={chooseDownloadFolder}>Change…</Button>
|
||
|
|
</div>
|
||
|
|
<small>Songs are saved here; each playlist gets its own subfolder. Changes apply immediately.</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
<div className={styles.row}>
|
||
|
|
<Tags size={18} />
|
||
|
|
<div className={styles.rowContent}>
|
||
|
|
<label htmlFor="filename-template">Filename template</label>
|
||
|
|
<Input
|
||
|
|
id="filename-template"
|
||
|
|
value={settings.filenameTemplate}
|
||
|
|
onChange={(event) => update({ filenameTemplate: event.target.value })}
|
||
|
|
/>
|
||
|
|
<small>Available tokens: {"{artist}"} {"{title}"} {"{album}"} {"{year}"} {"{track}"}</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className={styles.row}>
|
||
|
|
<FolderTree size={18} />
|
||
|
|
<div className={styles.rowContent}>
|
||
|
|
<label htmlFor="folder-template">Folder structure</label>
|
||
|
|
<Input
|
||
|
|
id="folder-template"
|
||
|
|
value={settings.folderTemplate}
|
||
|
|
onChange={(event) => update({ folderTemplate: event.target.value })}
|
||
|
|
/>
|
||
|
|
<small>
|
||
|
|
Tokens: {"{album_artist}"} {"{artist}"} {"{album}"} {"{year}"} {"{genre}"} {"{disc}"}
|
||
|
|
</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className={styles.row}>
|
||
|
|
<Music2 size={18} />
|
||
|
|
<div className={styles.rowContent}>
|
||
|
|
<label>Multi-disc numbering</label>
|
||
|
|
<Select
|
||
|
|
value={settings.multiDiscMode}
|
||
|
|
onValueChange={(value) =>
|
||
|
|
update({ multiDiscMode: value as SpotiRipSettings["multiDiscMode"] })
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger aria-label="Multi-disc numbering style"><SelectValue /></SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="disc-prefix">Disc prefix · 2-05</SelectItem>
|
||
|
|
<SelectItem value="offset">Disc offset · 205</SelectItem>
|
||
|
|
<SelectItem value="track-only">Track only · 05</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className={styles.row}>
|
||
|
|
<Music2 size={18} />
|
||
|
|
<div className={styles.rowContent}>
|
||
|
|
<label>MP3 conversion bitrate</label>
|
||
|
|
<Select
|
||
|
|
value={String(settings.mp3Bitrate)}
|
||
|
|
onValueChange={(value) =>
|
||
|
|
update({ mp3Bitrate: Number(value) as SpotiRipSettings["mp3Bitrate"] })
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger aria-label="Default MP3 bitrate"><SelectValue /></SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="128">128 kbps</SelectItem>
|
||
|
|
<SelectItem value="192">192 kbps</SelectItem>
|
||
|
|
<SelectItem value="256">256 kbps</SelectItem>
|
||
|
|
<SelectItem value="320">320 kbps</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<label className={styles.checkRow}>
|
||
|
|
<Checkbox
|
||
|
|
checked={settings.embedArtwork}
|
||
|
|
onChange={(event) => update({ embedArtwork: event.target.checked })}
|
||
|
|
/>
|
||
|
|
<Image size={17} />
|
||
|
|
<div>
|
||
|
|
<strong>Embed cover artwork</strong>
|
||
|
|
<span>Add recognized album artwork to repaired MP3 tags when available.</span>
|
||
|
|
</div>
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<label className={styles.checkRow}>
|
||
|
|
<Checkbox
|
||
|
|
checked={settings.smartDedup}
|
||
|
|
onChange={(event) => update({ smartDedup: event.target.checked })}
|
||
|
|
/>
|
||
|
|
<CopyCheck size={17} />
|
||
|
|
<div>
|
||
|
|
<strong>Smart deduplication</strong>
|
||
|
|
<span>Warn when an ISRC or normalized artist/title already exists in the local SpotiRip index.</span>
|
||
|
|
</div>
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<label className={styles.checkRow}>
|
||
|
|
<Checkbox
|
||
|
|
checked={settings.autoSelectMatches}
|
||
|
|
onChange={(event) => update({ autoSelectMatches: event.target.checked })}
|
||
|
|
/>
|
||
|
|
<Tags size={17} />
|
||
|
|
<div>
|
||
|
|
<strong>Auto-select confident matches</strong>
|
||
|
|
<span>Newly recognized tracks are selected for batch apply by default.</span>
|
||
|
|
</div>
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<div className={styles.footer}>
|
||
|
|
<span>{saved ? "Settings saved on this device." : "Original source files are always preserved."}</span>
|
||
|
|
<Button onClick={save}><Save size={16} /> Save settings</Button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
<MobileNav />
|
||
|
|
</main>
|
||
|
|
);
|
||
|
|
}
|