Add a Settings area with 4 color themes (Aurora, Light, Ocean, Sunset)

Requested directly: "lets add some themeing into it now, perhaps
under the configuration, have white/black, and some more colors."
There was no existing app-wide Settings/Configuration surface -
every prior setting lived in its own panel-specific modal (Fetch
Data Settings, subtitle provider settings) - so this adds the
app's first one: a palette-icon button in the sidebar footer opens
a new Settings modal with a 2x2 grid of theme swatches.

Four themes: Aurora (the existing dark teal/green look, unchanged,
stays the default), Light (a clean white/black theme - the literal
"white/black" ask), Ocean (a cool blue-toned dark variant), and
Sunset (a warm amber-toned dark variant) - the "some more colors"
part. Picking one applies instantly via a `data-theme` attribute on
<html> and persists through new get_theme/save_theme commands
(added to settings.rs/lib.rs alongside delivery 25's movie-naming-
scheme fix), applied on every future launch by a boot-time effect
in App.tsx so it takes effect even if Settings is never reopened.

theme.css's variable set was expanded to cover every place a color
was previously hardcoded outside the existing --dock/--surface/etc.
variables, so the whole app actually re-themes instead of leaving
dark boxes behind on the Light theme: button resting/hover/active
shades, the Load-files popup background, the primary button's hover
tint, the header's gradient stops, and five levels of white-tint
row/button hover highlights (RenamePanel.css/Header.css) all moved
to new variables (--btn-bg*, --popup-bg, --accent-hover, --header-
grad-a/b, --tint-02/03/05/06/16) with theme-specific values - the
light theme in particular needed dark tints instead of white ones
for hover states to stay visible on a white background.

Verified with a new Playwright script covering: default theme on a
fresh install, the Settings modal listing all 4 themes with the
right one marked selected, clicking a theme applying it immediately
(actual computed background color and CSS variable checked, not
just the data-theme attribute) and saving it, and a previously-saved
theme applying on boot before Settings is ever opened. Screenshots
taken of all 4 themes across the main view and the Naming Scheme
editor (whose buttons/popups were part of the hardcoded-color
cleanup) to confirm visually. All prior verification suites (47
checks across naming-scheme/match-fetch/hash-files/movie-scheme),
`cargo test --workspace` (162 tests), and `npm run build` still
pass clean.
This commit is contained in:
Renamr Scaffold
2026-08-30 05:24:48 +00:00
parent 635d57f55b
commit 03390b43f1
9 changed files with 390 additions and 26 deletions
+17 -1
View File
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import Sidebar, { PANELS } from "./components/Sidebar";
import Header from "./components/Header";
import RenamePanel from "./components/RenamePanel";
@@ -8,11 +9,26 @@ import SfvPanel from "./components/SfvPanel";
import FilterPanel from "./components/FilterPanel";
import ListPanel from "./components/ListPanel";
import PanelStub from "./components/PanelStub";
import { applyTheme, DEFAULT_THEME, type ThemeId } from "./components/SettingsModal";
const THEME_IDS: ThemeId[] = ["aurora", "light", "ocean", "sunset"];
export default function App() {
const [selected, setSelected] = useState(PANELS[0].id);
const current = PANELS.find((p) => p.id === selected) ?? PANELS[0];
// Applies the last-saved theme as soon as the app boots, regardless of whether the
// Settings modal (SettingsModal.tsx, opened from the sidebar) ever gets opened this
// session - without this, a saved theme would only ever take effect once the user
// happened to open Settings again after restarting the app.
useEffect(() => {
invoke<string | null>("get_theme")
.then((saved) => {
applyTheme(saved && (THEME_IDS as string[]).includes(saved) ? (saved as ThemeId) : DEFAULT_THEME);
})
.catch(() => applyTheme(DEFAULT_THEME));
}, []);
return (
<div className="app-shell">
<Sidebar selected={selected} onSelect={setSelected} />
+1 -1
View File
@@ -4,7 +4,7 @@
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #111e26 0%, var(--surface-alt) 62%, #0a161c 100%);
background: linear-gradient(180deg, var(--header-grad-a) 0%, var(--surface-alt) 62%, var(--header-grad-b) 100%);
border-bottom: 1px solid var(--dock-line);
position: relative;
}
+15
View File
@@ -161,3 +161,18 @@ export function HashIcon() {
</svg>
);
}
// Used for the app-wide Settings entry (theme picker) in the sidebar footer -
// deliberately a palette rather than reusing GearIcon, which already means something
// different (the per-file Naming Scheme editor's trigger button in the Rename panel).
export function PaletteIcon() {
return (
<svg {...{ ...base, width: 16, height: 16 }}>
<path d="M12 3.5c-4.7 0-8.5 3.8-8.5 8.5s3.8 8.5 8.5 8.5c1 0 1.7-.8 1.7-1.7 0-.45-.17-.86-.46-1.16-.29-.31-.46-.72-.46-1.17 0-.94.76-1.7 1.7-1.7h2c2.6 0 4.7-2.1 4.7-4.7 0-4.15-4.1-7.55-9.19-7.55Z" />
<circle cx="7.7" cy="11" r="1.15" fill="currentColor" stroke="none" />
<circle cx="10.2" cy="7.3" r="1.15" fill="currentColor" stroke="none" />
<circle cx="14.4" cy="7.6" r="1.15" fill="currentColor" stroke="none" />
<circle cx="16.6" cy="11.2" r="1.15" fill="currentColor" stroke="none" />
</svg>
);
}
+19 -19
View File
@@ -60,7 +60,7 @@
}
.list-row:hover {
background: rgba(255, 255, 255, 0.03);
background: var(--tint-03);
}
.list-row.selected {
@@ -88,7 +88,7 @@
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--muted-text);
background: rgba(255, 255, 255, 0.05);
background: var(--tint-05);
border-radius: 999px;
padding: 2px 8px;
}
@@ -114,17 +114,17 @@
justify-content: center;
border-radius: 8px;
border: 1px solid var(--surface-deep);
background: #172a31;
background: var(--btn-bg);
color: var(--text);
cursor: pointer;
}
.list-icon-button:hover {
background: #203941;
background: var(--btn-bg-hover);
}
.list-icon-button:active {
background: #28515a;
background: var(--btn-bg-active);
}
.list-icon-button:disabled {
@@ -147,7 +147,7 @@
flex-direction: column;
min-width: 150px;
padding: 4px;
background: #16262d;
background: var(--popup-bg);
border: 1px solid var(--surface-deep);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
@@ -197,7 +197,7 @@
justify-content: center;
border-radius: 10px;
border: 1px solid var(--surface-deep);
background: #172a31;
background: var(--btn-bg);
color: var(--text);
font-size: 13px;
font-weight: 600;
@@ -206,7 +206,7 @@
}
.action-button:hover {
background: #203941;
background: var(--btn-bg-hover);
}
.action-button.primary {
@@ -217,7 +217,7 @@
}
.action-button.primary:hover {
background: #70e3bb;
background: var(--accent-hover);
}
.format-editor {
@@ -264,7 +264,7 @@
.format-hint code,
.format-editor label code {
background: rgba(255, 255, 255, 0.06);
background: var(--tint-06);
border-radius: 4px;
padding: 1px 5px;
font-family: ui-monospace, "SF Mono", monospace;
@@ -307,7 +307,7 @@
}
.format-field-chip {
background: rgba(255, 255, 255, 0.06);
background: var(--tint-06);
border: none;
border-radius: 4px;
padding: 2px 6px;
@@ -319,7 +319,7 @@
}
.format-field-chip:hover {
background: rgba(255, 255, 255, 0.16);
background: var(--tint-16);
}
.format-field-chip:active {
@@ -389,7 +389,7 @@
}
.modal-plan-row:hover {
background: rgba(255, 255, 255, 0.03);
background: var(--tint-03);
}
.modal-plan-old {
@@ -431,7 +431,7 @@
}
.modal-plan-remove:hover {
background: rgba(255, 255, 255, 0.06);
background: var(--tint-06);
color: #ff8a8a;
}
@@ -558,7 +558,7 @@
.history-batch {
padding: 8px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.02);
background: var(--tint-02);
}
.history-batch-header {
@@ -583,7 +583,7 @@
font-size: 10.5px;
font-weight: 700;
color: var(--muted-text);
background: rgba(255, 255, 255, 0.05);
background: var(--tint-05);
border-radius: 999px;
padding: 2px 8px;
}
@@ -612,7 +612,7 @@
}
.preset-row:hover {
background: rgba(255, 255, 255, 0.03);
background: var(--tint-03);
}
.preset-slot-badge {
@@ -623,7 +623,7 @@
align-items: center;
justify-content: center;
border-radius: 6px;
background: rgba(255, 255, 255, 0.06);
background: var(--tint-06);
color: var(--aurora-green);
font-size: 11.5px;
font-weight: 700;
@@ -646,7 +646,7 @@
}
.preset-expression:hover {
background: rgba(255, 255, 255, 0.06);
background: var(--tint-06);
}
.preset-expression:disabled {
+63
View File
@@ -0,0 +1,63 @@
.theme-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin: 4px 0 4px;
}
.theme-card {
all: unset;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 12px;
border-radius: 10px;
border: 1px solid var(--surface-deep);
background: var(--surface);
cursor: pointer;
position: relative;
}
.theme-card:hover {
border-color: var(--dock-line);
}
.theme-card.selected {
border-color: var(--aurora-green);
box-shadow: 0 0 0 1px var(--aurora-green);
}
.theme-card:disabled {
opacity: 0.75;
cursor: default;
}
.theme-swatch {
display: flex;
width: 100%;
height: 28px;
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--surface-deep);
}
.theme-swatch-chip {
flex: 1;
}
.theme-card-name {
font-size: 12.5px;
font-weight: 600;
color: var(--text);
}
.theme-card-check {
position: absolute;
top: 8px;
right: 10px;
color: var(--aurora-green);
font-size: 13px;
font-weight: 700;
}
+108
View File
@@ -0,0 +1,108 @@
// App-wide Settings: currently just the color theme, opened from the gear-adjacent
// palette icon in the sidebar footer (see Sidebar.tsx). Everything else the app
// persists lives in its own panel-specific settings modal (Fetch Data Settings in
// RenamePanel/EpisodesPanel, subtitle provider settings in SubtitlesPanel) - this is
// the one truly app-wide setting so far, hence its own top-level entry point rather
// than living inside one of those panels.
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import "./SettingsModal.css";
export type ThemeId = "aurora" | "light" | "ocean" | "sunset";
export const DEFAULT_THEME: ThemeId = "aurora";
interface ThemeDef {
id: ThemeId;
name: string;
description: string;
// Literal swatch colors (background, primary accent, secondary accent) matching
// theme.css's own values for this theme - these can't be CSS variables, since the
// whole point is to preview a theme that isn't the currently-active one.
swatch: [string, string, string];
}
const THEMES: ThemeDef[] = [
{ id: "aurora", name: "Aurora", description: "The original dark teal & green look.", swatch: ["#08252b", "#55d7a9", "#52c7db"] },
{ id: "light", name: "Light", description: "Clean white background, black text.", swatch: ["#ffffff", "#1f9d74", "#157e9c"] },
{ id: "ocean", name: "Ocean", description: "A cool blue-toned dark theme.", swatch: ["#071c2e", "#4fd0e8", "#5aa9ff"] },
{ id: "sunset", name: "Sunset", description: "A warm amber-toned dark theme.", swatch: ["#2b1408", "#f0955a", "#f0b66b"] },
];
function isThemeId(value: string): value is ThemeId {
return THEMES.some((t) => t.id === value);
}
/// Applies a theme id to the document immediately, so switching feels instant instead
/// of waiting on a round trip through Rust. Shared by App.tsx's boot-time load and
/// this modal's click handler, so both go through the exact same logic.
export function applyTheme(theme: ThemeId) {
document.documentElement.dataset.theme = theme;
}
export default function SettingsModal({ open, onClose }: { open: boolean; onClose: () => void }) {
const [current, setCurrent] = useState<ThemeId>(DEFAULT_THEME);
const [saving, setSaving] = useState<ThemeId | null>(null);
// Keep this modal's own idea of "the active theme" in sync with whatever's really
// applied - reads the live `data-theme` attribute rather than re-fetching from disk,
// since App.tsx's boot-time effect (or a previous open of this same modal) already
// applied it there.
useEffect(() => {
if (!open) return;
const applied = document.documentElement.dataset.theme;
setCurrent(applied && isThemeId(applied) ? applied : DEFAULT_THEME);
}, [open]);
if (!open) return null;
const pick = async (theme: ThemeId) => {
if (theme === current) return;
setSaving(theme);
applyTheme(theme);
setCurrent(theme);
try {
await invoke("save_theme", { theme });
} catch {
// The theme still applied for this session even if saving it failed - not
// worth blocking or rolling back the visual change over a disk-write hiccup.
} finally {
setSaving(null);
}
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-card modal-card-compact" onClick={(e) => e.stopPropagation()}>
<h2 className="modal-title">Settings</h2>
<span className="modal-field-label">Theme</span>
<p className="modal-subtitle">Pick a color theme for the whole app. Applies immediately and remembers your choice.</p>
<div className="theme-grid">
{THEMES.map((theme) => (
<button
key={theme.id}
type="button"
className={`theme-card${current === theme.id ? " selected" : ""}`}
onClick={() => void pick(theme.id)}
disabled={saving !== null}
title={theme.description}
>
<span className="theme-swatch">
{theme.swatch.map((color, i) => (
<span key={i} className="theme-swatch-chip" style={{ background: color }} />
))}
</span>
<span className="theme-card-name">{theme.name}</span>
{current === theme.id && <span className="theme-card-check">✓</span>}
</button>
))}
</div>
<div className="modal-actions">
<button className="action-button primary" onClick={onClose}>
Done
</button>
</div>
</div>
</div>
);
}
+19
View File
@@ -72,6 +72,25 @@
padding-bottom: 6px;
}
.sidebar-settings-button {
all: unset;
box-sizing: border-box;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
color: var(--muted-text);
cursor: pointer;
margin-bottom: 6px;
}
.sidebar-settings-button:hover {
color: var(--text);
background: var(--tint-06);
}
.sidebar-wordmark {
font-weight: 700;
font-size: 13px;
+14 -1
View File
@@ -1,4 +1,6 @@
import { EpisodesIcon, FilterIcon, ListIcon, RenameIcon, SfvIcon, SubtitlesIcon } from "./Icons";
import { useState } from "react";
import { EpisodesIcon, FilterIcon, ListIcon, PaletteIcon, RenameIcon, SfvIcon, SubtitlesIcon } from "./Icons";
import SettingsModal from "./SettingsModal";
export interface PanelDef {
id: string;
@@ -25,6 +27,8 @@ export default function Sidebar({
selected: string;
onSelect: (id: string) => void;
}) {
const [settingsOpen, setSettingsOpen] = useState(false);
return (
<nav className="sidebar">
<ul className="sidebar-list">
@@ -47,9 +51,18 @@ export default function Sidebar({
</ul>
<div className="sidebar-footer">
<button
className="sidebar-settings-button"
title="Settings (theme)"
onClick={() => setSettingsOpen(true)}
>
<PaletteIcon />
</button>
<span className="sidebar-wordmark">Aurora Curator</span>
<span className="sidebar-version">v{APP_VERSION}</span>
</div>
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
</nav>
);
}
+134 -4
View File
@@ -1,7 +1,13 @@
/* Aurora Curator's UI theme — ported 1:1 (hex values) from the original FB-Mod fork's
AuroraTheme.java / HeaderPanel.java / MainFrame.java so the rewrite looks like a
continuation of that design rather than a random restyle. (The app takes its name,
"Aurora Curator", from this same Aurora theme.) */
/* Aurora Curator's UI theme. The "aurora" theme below is ported 1:1 (hex values) from
the original FB-Mod fork's AuroraTheme.java / HeaderPanel.java / MainFrame.java so
the rewrite looks like a continuation of that design rather than a random restyle
(the app takes its name, "Aurora Curator", from this same Aurora theme) - it stays
the default. The other three theme blocks below (light/ocean/sunset) are Aurora
Curator's own additions, picked via Settings (gear icon, sidebar footer) and applied
as a `data-theme` attribute on <html>; every visible color in the app is meant to
flow through one of these custom properties so switching theme is just switching
which block of them is active - see the file's own module-doc-style notes on the
less obvious variables below. */
:root {
--dock: #08252b;
--dock-deep: #051115;
@@ -19,10 +25,134 @@
--sunset: #f0b66b;
--list-selection: #1e4d54;
--dock-selection: #1d4850;
/* A button's resting/hover/active background - was hardcoded (#172a31/#203941/
#28515a) in a handful of spots in RenamePanel.css; pulled out to variables here so
every theme can supply its own shade instead of leaving a dark box behind on a
light theme. */
--btn-bg: #172a31;
--btn-bg-hover: #203941;
--btn-bg-active: #28515a;
/* A small floating popup's background (the Load-files split-button menu). */
--popup-bg: #16262d;
/* The primary action button's hover shade - a lightened version of --aurora-green. */
--accent-hover: #70e3bb;
/* The header bar's top-to-bottom gradient stops (its middle stop is --surface-alt). */
--header-grad-a: #111e26;
--header-grad-b: #0a161c;
/* Subtle highlight tints for row/button hover states, each a light-on-dark overlay
for this theme - previously hardcoded as literal rgba(255,255,255,X) across
RenamePanel.css, which is exactly backwards on a light theme (a white tint on an
already-light surface is invisible). Named by their alpha so the light theme below
can supply the mirror-image dark tints at matching intensities. */
--tint-02: rgba(255, 255, 255, 0.02);
--tint-03: rgba(255, 255, 255, 0.03);
--tint-05: rgba(255, 255, 255, 0.05);
--tint-06: rgba(255, 255, 255, 0.06);
--tint-16: rgba(255, 255, 255, 0.16);
--sidebar-width: 158px;
--header-height: 68px;
}
/* Light - a clean white/black theme, the most literal reading of "white/black" from
the theming request. --dock-deep doubles as "the text color that sits well on top
of --aurora-green" (see RenamePanel.css's .action-button.primary) - here that's
white, since this theme's --aurora-green is deliberately darkened for contrast on a
white page, the opposite of the dark themes' light-green-on-dark-text pairing. */
:root[data-theme="light"] {
--dock: #eef2f3;
--dock-deep: #ffffff;
--dock-mist: #dde7e9;
--dock-line: #c7d3d5;
--surface: #ffffff;
--surface-alt: #f5f7f8;
--surface-deep: #dfe6e8;
--card: #f7f9fa;
--text: #15191b;
--muted-text: #5c6b6e;
--aurora-green: #1f9d74;
--aurora-cyan: #157e9c;
--aurora-lilac: #7a68d9;
--sunset: #c97a1f;
--list-selection: #d7ece7;
--dock-selection: #dbeee9;
--btn-bg: #eef2f3;
--btn-bg-hover: #e2e9eb;
--btn-bg-active: #d2dcdd;
--popup-bg: #ffffff;
--accent-hover: #29b98b;
--header-grad-a: #ffffff;
--header-grad-b: #eef2f3;
--tint-02: rgba(0, 0, 0, 0.02);
--tint-03: rgba(0, 0, 0, 0.035);
--tint-05: rgba(0, 0, 0, 0.05);
--tint-06: rgba(0, 0, 0, 0.07);
--tint-16: rgba(0, 0, 0, 0.14);
}
/* Ocean - a cool blue-toned dark variant. */
:root[data-theme="ocean"] {
--dock: #071c2e;
--dock-deep: #030f1c;
--dock-mist: #0f3350;
--dock-line: #1c4a68;
--surface: #071620;
--surface-alt: #0c1e2b;
--surface-deep: #1a3548;
--card: #0f232f;
--text: #e7f2f8;
--muted-text: #93aebe;
--aurora-green: #4fd0e8;
--aurora-cyan: #5aa9ff;
--aurora-lilac: #8ea0ff;
--sunset: #ffb46b;
--list-selection: #1a3f57;
--dock-selection: #193a52;
--btn-bg: #12293a;
--btn-bg-hover: #1a3a4f;
--btn-bg-active: #234a63;
--popup-bg: #102432;
--accent-hover: #7fe0f2;
--header-grad-a: #0c1f2e;
--header-grad-b: #061420;
--tint-02: rgba(255, 255, 255, 0.02);
--tint-03: rgba(255, 255, 255, 0.03);
--tint-05: rgba(255, 255, 255, 0.05);
--tint-06: rgba(255, 255, 255, 0.06);
--tint-16: rgba(255, 255, 255, 0.16);
}
/* Sunset - a warm amber/coral-toned dark variant. */
:root[data-theme="sunset"] {
--dock: #2b1408;
--dock-deep: #150a04;
--dock-mist: #3d2011;
--dock-line: #5c3319;
--surface: #1a0f08;
--surface-alt: #23140c;
--surface-deep: #40261a;
--card: #241610;
--text: #f7ece3;
--muted-text: #c2a190;
--aurora-green: #f0955a;
--aurora-cyan: #f0b66b;
--aurora-lilac: #e0866f;
--sunset: #ffd08a;
--list-selection: #4a2c17;
--dock-selection: #452a16;
--btn-bg: #2c1a10;
--btn-bg-hover: #3c2416;
--btn-bg-active: #4f301d;
--popup-bg: #24160e;
--accent-hover: #f5b083;
--header-grad-a: #24140b;
--header-grad-b: #140b05;
--tint-02: rgba(255, 255, 255, 0.02);
--tint-03: rgba(255, 255, 255, 0.03);
--tint-05: rgba(255, 255, 255, 0.05);
--tint-06: rgba(255, 255, 255, 0.06);
--tint-16: rgba(255, 255, 255, 0.16);
}
* {
box-sizing: border-box;
}