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 Home from './pages/home';
|
||||
import { TooltipProvider } from './components/ui/tooltip';
|
||||
import { useTheme } from './hooks/useTheme';
|
||||
|
||||
function App() {
|
||||
useTheme();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<TooltipProvider>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Copy, Zap } from 'lucide-react';
|
||||
import { Copy, ArrowRight } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -60,7 +60,7 @@ export function NovelCard({ novel, onParse }: NovelCardProps) {
|
||||
className="flex-1 h-8 px-2"
|
||||
onClick={() => onParse(novel.path)}
|
||||
>
|
||||
<Zap className="w-3.5 h-3.5" />
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Zap,
|
||||
ArrowRight,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -506,11 +506,11 @@ export default function ParseNovelSection({
|
||||
handleParseChapter(chapter.path)
|
||||
}
|
||||
>
|
||||
<Zap className="w-3.5 h-3.5" />
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Parse chapter</p>
|
||||
<p>Open in Parse Chapter tab</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
type PluginHeaderProps = {
|
||||
selectedPlugin?: Plugin.PluginBase;
|
||||
};
|
||||
|
||||
export default function PluginHeader({ selectedPlugin }: PluginHeaderProps) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
<header className="border-b">
|
||||
<header className="border-b border-border">
|
||||
<div className="px-8 py-4 flex items-center justify-between">
|
||||
<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">
|
||||
@@ -23,6 +34,16 @@ export default function PluginHeader({ selectedPlugin }: PluginHeaderProps) {
|
||||
</p>
|
||||
</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>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { NovelCard } from '@/components/novel-card';
|
||||
@@ -182,7 +182,7 @@ export default function PopularNovelsSection({
|
||||
) : novels.length === 0 ? (
|
||||
<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">
|
||||
<Zap className="w-8 h-8 text-muted-foreground" />
|
||||
<BookOpen className="w-8 h-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||
{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">
|
||||
<PluginHeader selectedPlugin={plugin} />
|
||||
<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="flex items-center justify-between">
|
||||
<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 { PluginStore } from './pluginStore';
|
||||
import { DialogStore } from './dialogStore';
|
||||
import { OverlayStore } from './overlayStore';
|
||||
import { NavigationStore } from './navigationStore';
|
||||
|
||||
export type AppStore = DialogStore &
|
||||
OverlayStore &
|
||||
PluginStore &
|
||||
export type AppStore = PluginStore &
|
||||
NavigationStore & {
|
||||
useSwitches: boolean;
|
||||
setUseSwitches(value: boolean): void;
|
||||
theme: 'light' | 'dark';
|
||||
setTheme(value: 'light' | 'dark'): void;
|
||||
};
|
||||
|
||||
// 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 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) => ({
|
||||
...DialogStore(set, get),
|
||||
...OverlayStore(set, get),
|
||||
...PluginStore(set, get),
|
||||
...NavigationStore(set, get),
|
||||
useSwitches: !!localStorage.getItem('useSwitches'),
|
||||
setUseSwitches(uS: boolean) {
|
||||
theme: getInitialTheme(),
|
||||
setTheme(theme: 'light' | 'dark') {
|
||||
set(state => ({
|
||||
...state,
|
||||
useSwitches: uS,
|
||||
theme,
|
||||
}));
|
||||
if (uS) localStorage.setItem('useSwitches', 'true');
|
||||
else localStorage.removeItem('useSwitches');
|
||||
localStorage.setItem('theme', theme);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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