diff --git a/src/App.tsx b/src/App.tsx index 5008f41..2dd67da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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("get_theme") + .then((saved) => { + applyTheme(saved && (THEME_IDS as string[]).includes(saved) ? (saved as ThemeId) : DEFAULT_THEME); + }) + .catch(() => applyTheme(DEFAULT_THEME)); + }, []); + return (
diff --git a/src/components/Header.css b/src/components/Header.css index 468107d..337f41a 100644 --- a/src/components/Header.css +++ b/src/components/Header.css @@ -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; } diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index 9861d90..6499e96 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -161,3 +161,18 @@ export function HashIcon() { ); } + +// 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 ( + + + + + + + + ); +} diff --git a/src/components/RenamePanel.css b/src/components/RenamePanel.css index 5374d86..65c860d 100644 --- a/src/components/RenamePanel.css +++ b/src/components/RenamePanel.css @@ -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 { diff --git a/src/components/SettingsModal.css b/src/components/SettingsModal.css new file mode 100644 index 0000000..35426c2 --- /dev/null +++ b/src/components/SettingsModal.css @@ -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; +} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx new file mode 100644 index 0000000..f2e70e8 --- /dev/null +++ b/src/components/SettingsModal.tsx @@ -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(DEFAULT_THEME); + const [saving, setSaving] = useState(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 ( +
+
e.stopPropagation()}> +

Settings

+ Theme +

Pick a color theme for the whole app. Applies immediately and remembers your choice.

+
+ {THEMES.map((theme) => ( + + ))} +
+
+ +
+
+
+ ); +} diff --git a/src/components/Sidebar.css b/src/components/Sidebar.css index e1f39a9..1522f4b 100644 --- a/src/components/Sidebar.css +++ b/src/components/Sidebar.css @@ -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; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 6797ec0..3e28a32 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -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 ( ); } diff --git a/src/theme.css b/src/theme.css index e407af4..8458e53 100644 --- a/src/theme.css +++ b/src/theme.css @@ -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 ; 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; }