feat: Add Dark Theme Support
This commit is contained in:
@@ -2,8 +2,11 @@ import React from 'react';
|
|||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import Home from './pages/home';
|
import Home from './pages/home';
|
||||||
import { TooltipProvider } from './components/ui/tooltip';
|
import { TooltipProvider } from './components/ui/tooltip';
|
||||||
|
import { useTheme } from './hooks/useTheme';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Copy, Zap } from 'lucide-react';
|
import { Copy, ArrowRight } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -60,7 +60,7 @@ export function NovelCard({ novel, onParse }: NovelCardProps) {
|
|||||||
className="flex-1 h-8 px-2"
|
className="flex-1 h-8 px-2"
|
||||||
onClick={() => onParse(novel.path)}
|
onClick={() => onParse(novel.path)}
|
||||||
>
|
>
|
||||||
<Zap className="w-3.5 h-3.5" />
|
<ArrowRight className="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Copy,
|
Copy,
|
||||||
Zap,
|
ArrowRight,
|
||||||
Download,
|
Download,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -506,11 +506,11 @@ export default function ParseNovelSection({
|
|||||||
handleParseChapter(chapter.path)
|
handleParseChapter(chapter.path)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Zap className="w-3.5 h-3.5" />
|
<ArrowRight className="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Parse chapter</p>
|
<p>Open in Parse Chapter tab</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Moon, Sun } from 'lucide-react';
|
||||||
|
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
|
|
||||||
type PluginHeaderProps = {
|
type PluginHeaderProps = {
|
||||||
selectedPlugin?: Plugin.PluginBase;
|
selectedPlugin?: Plugin.PluginBase;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PluginHeader({ selectedPlugin }: PluginHeaderProps) {
|
export default function PluginHeader({ selectedPlugin }: PluginHeaderProps) {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="border-b">
|
<header className="border-b border-border">
|
||||||
<div className="px-8 py-4 flex items-center justify-between">
|
<div className="px-8 py-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-sm bg-[var(--color-accent-soft)] text-xl font-semibold text-[var(--color-accent-strong)] transition-colors">
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-sm bg-[var(--color-accent-soft)] text-xl font-semibold text-[var(--color-accent-strong)] transition-colors">
|
||||||
@@ -23,6 +34,16 @@ export default function PluginHeader({ selectedPlugin }: PluginHeaderProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="h-9 w-9"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
title={`Theme: ${theme}`}
|
||||||
|
>
|
||||||
|
{isDark ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Filter, Zap } from 'lucide-react';
|
import { Filter, BookOpen } from 'lucide-react';
|
||||||
|
|
||||||
import { FiltersSheet } from '@/components/filters/filters-sheet';
|
import { FiltersSheet } from '@/components/filters/filters-sheet';
|
||||||
import { NovelCard } from '@/components/novel-card';
|
import { NovelCard } from '@/components/novel-card';
|
||||||
@@ -182,7 +182,7 @@ export default function PopularNovelsSection({
|
|||||||
) : novels.length === 0 ? (
|
) : novels.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
|
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
|
||||||
<div className="rounded-full bg-muted p-4 mb-4">
|
<div className="rounded-full bg-muted p-4 mb-4">
|
||||||
<Zap className="w-8 h-8 text-muted-foreground" />
|
<BookOpen className="w-8 h-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-foreground mb-2">
|
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||||
{plugin ? 'No novels to display' : 'No plugin selected'}
|
{plugin ? 'No novels to display' : 'No plugin selected'}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const theme = useAppStore(state => state.theme);
|
||||||
|
const setTheme = useAppStore(state => state.setTheme);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (theme === 'dark') {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
return { theme, setTheme };
|
||||||
|
}
|
||||||
+1
-1
@@ -32,7 +32,7 @@ function Home() {
|
|||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<PluginHeader selectedPlugin={plugin} />
|
<PluginHeader selectedPlugin={plugin} />
|
||||||
<div className="flex h-[calc(100vh-64px)]">
|
<div className="flex h-[calc(100vh-64px)]">
|
||||||
<aside className="w-64 border-r bg-background flex flex-col">
|
<aside className="w-64 border-r border-border bg-background flex flex-col">
|
||||||
<div className="p-6 flex-shrink-0 space-y-4">
|
<div className="p-6 flex-shrink-0 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { StoreCreator } from '.';
|
|
||||||
|
|
||||||
export enum EDialog {}
|
|
||||||
|
|
||||||
export type DialogStore = {
|
|
||||||
activeDialogs: EDialog[];
|
|
||||||
showDialog(dialog: EDialog): void;
|
|
||||||
hideDialog(dialog: EDialog): void;
|
|
||||||
hideAllDialogs(): void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param set Use this inside actions to change the state
|
|
||||||
* @param get Use this inside actions to get the current state
|
|
||||||
*/
|
|
||||||
export const DialogStore: StoreCreator<DialogStore> = set => ({
|
|
||||||
// this is initial state
|
|
||||||
activeDialogs: [],
|
|
||||||
|
|
||||||
// those are actions
|
|
||||||
showDialog(dialog: EDialog) {
|
|
||||||
set(state => ({
|
|
||||||
...state,
|
|
||||||
activeDialogs: [...state.activeDialogs, dialog],
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
hideDialog(dialog) {
|
|
||||||
set(state => ({
|
|
||||||
...state,
|
|
||||||
activeDialogs: state.activeDialogs.filter(d => d !== dialog),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
hideAllDialogs() {
|
|
||||||
set(state => ({
|
|
||||||
...state,
|
|
||||||
activeDialogs: [],
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+15
-14
@@ -1,15 +1,11 @@
|
|||||||
import { create, StateCreator } from 'zustand';
|
import { create, StateCreator } from 'zustand';
|
||||||
import { PluginStore } from './pluginStore';
|
import { PluginStore } from './pluginStore';
|
||||||
import { DialogStore } from './dialogStore';
|
|
||||||
import { OverlayStore } from './overlayStore';
|
|
||||||
import { NavigationStore } from './navigationStore';
|
import { NavigationStore } from './navigationStore';
|
||||||
|
|
||||||
export type AppStore = DialogStore &
|
export type AppStore = PluginStore &
|
||||||
OverlayStore &
|
|
||||||
PluginStore &
|
|
||||||
NavigationStore & {
|
NavigationStore & {
|
||||||
useSwitches: boolean;
|
theme: 'light' | 'dark';
|
||||||
setUseSwitches(value: boolean): void;
|
setTheme(value: 'light' | 'dark'): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper types to use "slicing" like in Redux... We could just not use slicing, but eh
|
// Helper types to use "slicing" like in Redux... We could just not use slicing, but eh
|
||||||
@@ -17,18 +13,23 @@ export type SetStore = Parameters<StateCreator<AppStore>>[0];
|
|||||||
export type GetStore = Parameters<StateCreator<AppStore>>[1];
|
export type GetStore = Parameters<StateCreator<AppStore>>[1];
|
||||||
export type StoreCreator<T> = (s: SetStore, g: GetStore) => T;
|
export type StoreCreator<T> = (s: SetStore, g: GetStore) => T;
|
||||||
|
|
||||||
|
const getInitialTheme = (): 'light' | 'dark' => {
|
||||||
|
const stored = localStorage.getItem('theme');
|
||||||
|
if (stored === 'light' || stored === 'dark') {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
return 'light';
|
||||||
|
};
|
||||||
|
|
||||||
export const useAppStore = create<AppStore>((set: SetStore, get: GetStore) => ({
|
export const useAppStore = create<AppStore>((set: SetStore, get: GetStore) => ({
|
||||||
...DialogStore(set, get),
|
|
||||||
...OverlayStore(set, get),
|
|
||||||
...PluginStore(set, get),
|
...PluginStore(set, get),
|
||||||
...NavigationStore(set, get),
|
...NavigationStore(set, get),
|
||||||
useSwitches: !!localStorage.getItem('useSwitches'),
|
theme: getInitialTheme(),
|
||||||
setUseSwitches(uS: boolean) {
|
setTheme(theme: 'light' | 'dark') {
|
||||||
set(state => ({
|
set(state => ({
|
||||||
...state,
|
...state,
|
||||||
useSwitches: uS,
|
theme,
|
||||||
}));
|
}));
|
||||||
if (uS) localStorage.setItem('useSwitches', 'true');
|
localStorage.setItem('theme', theme);
|
||||||
else localStorage.removeItem('useSwitches');
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { StoreCreator } from '.';
|
|
||||||
|
|
||||||
export type OverlayStore = {
|
|
||||||
loading: boolean;
|
|
||||||
showLoading(): void;
|
|
||||||
hideLoading(): void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param set Use this inside actions to change the state
|
|
||||||
* @param get Use this inside actions to get the current state
|
|
||||||
*/
|
|
||||||
export const OverlayStore: StoreCreator<OverlayStore> = set => ({
|
|
||||||
// this is initial state
|
|
||||||
loading: false,
|
|
||||||
|
|
||||||
// those are actions
|
|
||||||
showLoading() {
|
|
||||||
set(state => ({
|
|
||||||
...state,
|
|
||||||
loading: true,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
hideLoading() {
|
|
||||||
set(state => ({
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user