feat(ui): quick theme switch from the settings cog
Right-clicking the settings cog in the title bar opens a small theme menu (Phokus / Subtle Light / Conventional Dark) with the active theme checked, anchored under the cog and dismissed on outside-click or Escape. Left-click still opens Settings. Keeps theme switching one gesture away without cluttering the title bar with another icon.
This commit is contained in:
@@ -9,6 +9,9 @@ aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **Quick theme switch** — right-click the settings cog in the title bar to
|
||||||
|
switch theme (Phokus / Subtle Light / Conventional Dark) on the spot, without
|
||||||
|
opening Settings.
|
||||||
- **Albums** — curate your own collections. A new Albums section in the sidebar
|
- **Albums** — curate your own collections. A new Albums section in the sidebar
|
||||||
(with cover thumbnails, kept visually distinct from Libraries) lets you create,
|
(with cover thumbnails, kept visually distinct from Libraries) lets you create,
|
||||||
rename, reorder (drag the row), and open albums; albums can span multiple
|
rename, reorder (drag the row), and open albums; albums can span multiple
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { useGalleryStore } from "../store";
|
import { useGalleryStore, AppTheme } from "../store";
|
||||||
import { PhokusMark } from "./PhokusMark";
|
import { PhokusMark } from "./PhokusMark";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
|
const THEME_OPTIONS: { value: AppTheme; label: string }[] = [
|
||||||
|
{ value: "phokus", label: "Phokus" },
|
||||||
|
{ value: "subtle-light", label: "Subtle Light" },
|
||||||
|
{ value: "conventional-dark", label: "Conventional Dark" },
|
||||||
|
];
|
||||||
|
|
||||||
// SVG icons for window controls
|
// SVG icons for window controls
|
||||||
function MinimizeIcon() {
|
function MinimizeIcon() {
|
||||||
return (
|
return (
|
||||||
@@ -41,11 +47,40 @@ function CloseIcon() {
|
|||||||
export function TitleBar() {
|
export function TitleBar() {
|
||||||
const [isMaximized, setIsMaximized] = useState(false);
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
const setSettingsOpen = useGalleryStore((state) => state.setSettingsOpen);
|
const setSettingsOpen = useGalleryStore((state) => state.setSettingsOpen);
|
||||||
|
const theme = useGalleryStore((state) => state.theme);
|
||||||
|
const setTheme = useGalleryStore((state) => state.setTheme);
|
||||||
const updateStatus = useGalleryStore((state) => state.updateStatus);
|
const updateStatus = useGalleryStore((state) => state.updateStatus);
|
||||||
const updateVersion = useGalleryStore((state) => state.updateVersion);
|
const updateVersion = useGalleryStore((state) => state.updateVersion);
|
||||||
const installUpdate = useGalleryStore((state) => state.installUpdate);
|
const installUpdate = useGalleryStore((state) => state.installUpdate);
|
||||||
const appWindow = getCurrentWindow();
|
const appWindow = getCurrentWindow();
|
||||||
|
|
||||||
|
// Right-clicking the settings cog opens a quick theme switcher, anchored under
|
||||||
|
// the cog so it never overflows the right edge of the window.
|
||||||
|
const settingsBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const themeMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [themeMenu, setThemeMenu] = useState<{ top: number; right: number } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!themeMenu) return;
|
||||||
|
const handleDown = (e: MouseEvent) => {
|
||||||
|
if (themeMenuRef.current && !themeMenuRef.current.contains(e.target as Node)) setThemeMenu(null);
|
||||||
|
};
|
||||||
|
const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") setThemeMenu(null); };
|
||||||
|
document.addEventListener("mousedown", handleDown);
|
||||||
|
document.addEventListener("keydown", handleKey);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleDown);
|
||||||
|
document.removeEventListener("keydown", handleKey);
|
||||||
|
};
|
||||||
|
}, [themeMenu]);
|
||||||
|
|
||||||
|
const handleSettingsContextMenu = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const rect = settingsBtnRef.current?.getBoundingClientRect();
|
||||||
|
if (!rect) return;
|
||||||
|
setThemeMenu({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Get initial maximized state
|
// Get initial maximized state
|
||||||
appWindow.isMaximized().then(setIsMaximized);
|
appWindow.isMaximized().then(setIsMaximized);
|
||||||
@@ -109,9 +144,11 @@ export function TitleBar() {
|
|||||||
className="flex items-stretch h-full"
|
className="flex items-stretch h-full"
|
||||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
|
<Tooltip label="Settings (right-click to switch theme)" anchorToCursor>
|
||||||
<button
|
<button
|
||||||
|
ref={settingsBtnRef}
|
||||||
onClick={() => setSettingsOpen(true)}
|
onClick={() => setSettingsOpen(true)}
|
||||||
title="Settings"
|
onContextMenu={handleSettingsContextMenu}
|
||||||
className="group flex h-full w-10 items-center justify-center text-gray-600 transition-colors duration-100 hover:bg-white/6 hover:text-gray-300"
|
className="group flex h-full w-10 items-center justify-center text-gray-600 transition-colors duration-100 hover:bg-white/6 hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -119,7 +156,7 @@ export function TitleBar() {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
{/* Minimize */}
|
{/* Minimize */}
|
||||||
<button
|
<button
|
||||||
onClick={handleMinimize}
|
onClick={handleMinimize}
|
||||||
@@ -147,6 +184,36 @@ export function TitleBar() {
|
|||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Quick theme switcher — opened by right-clicking the settings cog. */}
|
||||||
|
{themeMenu && (
|
||||||
|
<div
|
||||||
|
ref={themeMenuRef}
|
||||||
|
className="fixed z-50 min-w-[170px] py-1 px-1 rounded-lg bg-gray-900 border border-white/10 shadow-xl shadow-black/50"
|
||||||
|
style={{ top: themeMenu.top, right: themeMenu.right, WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<div className="px-3 pt-1 pb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-500">Theme</div>
|
||||||
|
{THEME_OPTIONS.map((opt) => {
|
||||||
|
const active = theme === opt.value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
className={`flex w-full items-center justify-between gap-3 rounded-md px-3 py-1.5 text-left text-[12px] transition-colors ${
|
||||||
|
active ? "bg-white/8 text-white" : "text-gray-300 hover:bg-white/8 hover:text-white"
|
||||||
|
}`}
|
||||||
|
onClick={() => { setTheme(opt.value); setThemeMenu(null); }}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
{active && (
|
||||||
|
<svg className="h-3.5 w-3.5 text-sky-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user