Aurora SpotiRip desktop and web app
- Spotify playlist, library, album and artist browsing with in-app playback - Song, playlist and album downloads via spotdl with YouTube and Bandcamp fallbacks - Electron desktop app (castLabs Widevine) with a permanent sidebar layout - Linux and Windows packaging scripts
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user