Introduce ThemeContext and apply theme tokens

Add a ThemeContext and provider, wrap the app with ThemeProvider, and sync user settings' theme into the context. Replace hardcoded color classes with design token classes (background, muted, foreground, border, card, etc.) across multiple UI components to centralize theming and enable consistent light/dark styling. Files updated include App.tsx (useTheme, setTheme, ThemeProvider, footer/background tokens), several views and components (AddMediaView, BrowseView, CastDetailView, CastView, MediaCard, MediaListItem, SettingsView, ImporterView) to use tokenized classes, and add new src/contexts/ThemeContext.tsx.
This commit is contained in:
Lars Behrends
2026-04-10 14:59:40 +02:00
parent 96593a6235
commit b29732a653
11 changed files with 392 additions and 304 deletions

View File

@@ -164,13 +164,13 @@ export default function ImporterView() {
variant="ghost"
size="icon"
onClick={() => navigate('/')}
className="text-zinc-600 hover:text-[#6d28d9]"
className="text-muted-foreground hover:text-[#6d28d9]"
>
<ArrowLeft size={20} />
</Button>
<div>
<h1 className="text-2xl font-black text-zinc-900">Media Importers</h1>
<p className="text-sm text-zinc-500 font-medium">Import media from external platforms</p>
<h1 className="text-2xl font-black text-foreground">Media Importers</h1>
<p className="text-sm text-muted-foreground font-medium">Import media from external platforms</p>
</div>
</div>
</div>
@@ -179,38 +179,38 @@ export default function ImporterView() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
{/* XBVR Importer Card */}
{xbvrConfig.url && (
<div className="bg-white border border-zinc-200 rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="bg-card border border-border rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
<Film className="text-purple-600" size={24} />
</div>
<div>
<h3 className="font-bold text-zinc-900">XBVR</h3>
<p className="text-xs text-zinc-500 font-medium">Adult Video Manager</p>
<h3 className="font-bold text-foreground">XBVR</h3>
<p className="text-xs text-muted-foreground font-medium">Adult Video Manager</p>
</div>
</div>
<Button
variant="outline"
size="icon"
className="h-8 w-8 border-zinc-200"
className="h-8 w-8 border-border"
onClick={() => {}}
>
<Settings size={16} />
</Button>
</div>
<p className="text-sm text-zinc-600 mb-4">
<p className="text-sm text-muted-foreground mb-4">
Import adult videos and actors from your XBVR database.
</p>
<div className="space-y-3">
<div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">XBVR URL</label>
<label className="text-xs font-bold text-muted-foreground mb-1 block">XBVR URL</label>
<input
type="text"
value={xbvrConfig.url}
onChange={(e) => setXbvrConfig({ ...xbvrConfig, url: e.target.value })}
disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed"
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="http://192.168.1.102:10001"
/>
</div>
@@ -237,49 +237,49 @@ export default function ImporterView() {
{/* StashAPP Importer Card */}
{stashappConfig.url && (
<div className="bg-white border border-zinc-200 rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="bg-card border border-border rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<Film className="text-blue-600" size={24} />
</div>
<div>
<h3 className="font-bold text-zinc-900">StashAPP</h3>
<p className="text-xs text-zinc-500 font-medium">Adult Content Manager</p>
<h3 className="font-bold text-foreground">StashAPP</h3>
<p className="text-xs text-muted-foreground font-medium">Adult Content Manager</p>
</div>
</div>
<Button
variant="outline"
size="icon"
className="h-8 w-8 border-zinc-200"
className="h-8 w-8 border-border"
onClick={() => {}}
>
<Settings size={16} />
</Button>
</div>
<p className="text-sm text-zinc-600 mb-4">
<p className="text-sm text-muted-foreground mb-4">
Import adult videos and performers from your StashAPP database.
</p>
<div className="space-y-3">
<div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">StashAPP URL</label>
<label className="text-xs font-bold text-muted-foreground mb-1 block">StashAPP URL</label>
<input
type="text"
value={stashappConfig.url}
onChange={(e) => setStashappConfig({ ...stashappConfig, url: e.target.value })}
disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed"
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="http://192.168.1.102:10001"
/>
</div>
<div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">API Key (optional)</label>
<label className="text-xs font-bold text-muted-foreground mb-1 block">API Key (optional)</label>
<input
type="password"
value={stashappConfig.apiKey || ''}
onChange={(e) => setStashappConfig({ ...stashappConfig, apiKey: e.target.value })}
disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed"
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="Enter API key if required"
/>
</div>
@@ -306,38 +306,38 @@ export default function ImporterView() {
{/* StashAPP Actor Updater Card */}
{stashappConfig.url && (
<div className="bg-white border border-zinc-200 rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="bg-card border border-border rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<Users className="text-green-600" size={24} />
</div>
<div>
<h3 className="font-bold text-zinc-900">StashAPP Actor Updater</h3>
<p className="text-xs text-zinc-500 font-medium">Update existing actors</p>
<h3 className="font-bold text-foreground">StashAPP Actor Updater</h3>
<p className="text-xs text-muted-foreground font-medium">Update existing actors</p>
</div>
</div>
<Button
variant="outline"
size="icon"
className="h-8 w-8 border-zinc-200"
className="h-8 w-8 border-border"
onClick={() => {}}
>
<Settings size={16} />
</Button>
</div>
<p className="text-sm text-zinc-600 mb-4">
<p className="text-sm text-muted-foreground mb-4">
Update existing actors with fresh data from StashAPP and create missing ones.
</p>
<div className="space-y-3">
<div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">API Key (optional)</label>
<label className="text-xs font-bold text-muted-foreground mb-1 block">API Key (optional)</label>
<input
type="password"
value={stashappConfig.apiKey || ''}
onChange={(e) => setStashappConfig({ ...stashappConfig, apiKey: e.target.value })}
disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed"
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="Enter API key if required"
/>
</div>
@@ -364,60 +364,60 @@ export default function ImporterView() {
{/* Playnite Importer Card */}
{playniteConfig.ip && playniteConfig.apiToken && (
<div className="bg-white border border-zinc-200 rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="bg-card border border-border rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center">
<Film className="text-orange-600" size={24} />
</div>
<div>
<h3 className="font-bold text-zinc-900">Playnite</h3>
<p className="text-xs text-zinc-500 font-medium">Game Library Manager</p>
<h3 className="font-bold text-foreground">Playnite</h3>
<p className="text-xs text-muted-foreground font-medium">Game Library Manager</p>
</div>
</div>
<Button
variant="outline"
size="icon"
className="h-8 w-8 border-zinc-200"
className="h-8 w-8 border-border"
onClick={() => {}}
>
<Settings size={16} />
</Button>
</div>
<p className="text-sm text-zinc-600 mb-4">
<p className="text-sm text-muted-foreground mb-4">
Import games from your Playnite library via Bridge API.
</p>
<div className="space-y-3">
<div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">IP Address</label>
<label className="text-xs font-bold text-muted-foreground mb-1 block">IP Address</label>
<input
type="text"
value={playniteConfig.ip}
onChange={(e) => setPlayniteConfig({ ...playniteConfig, ip: e.target.value })}
disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed"
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="localhost"
/>
</div>
<div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">Port</label>
<label className="text-xs font-bold text-muted-foreground mb-1 block">Port</label>
<input
type="number"
value={playniteConfig.port || 19821}
onChange={(e) => setPlayniteConfig({ ...playniteConfig, port: parseInt(e.target.value) || 19821 })}
disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed"
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="19821"
/>
</div>
<div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">API Token</label>
<label className="text-xs font-bold text-muted-foreground mb-1 block">API Token</label>
<input
type="password"
value={playniteConfig.apiToken}
onChange={(e) => setPlayniteConfig({ ...playniteConfig, apiToken: e.target.value })}
disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed"
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="pb_your_token_here"
/>
</div>
@@ -445,7 +445,7 @@ export default function ImporterView() {
{/* Progress Section */}
{progress.stage !== 'idle' && (
<div className="bg-white border border-zinc-200 rounded-xl p-6">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
{progress.stage === 'complete' ? (
@@ -458,12 +458,12 @@ export default function ImporterView() {
</div>
) : (
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<Loader2 className="text-purple-600 animate-spin" size={20} />
<Loader2 className="text-muted-foreground animate-spin" size={20} />
</div>
)}
<div>
<h3 className="font-bold text-zinc-900">{progress.message}</h3>
<p className="text-xs text-zinc-500 font-medium">
<h3 className="font-bold text-foreground">{progress.message}</h3>
<p className="text-xs text-muted-foreground font-medium">
{progress.stage === 'fetching' && 'Connecting to external service...'}
{progress.stage === 'importing' && `Processing items... ${getProgressPercentage()}%`}
{progress.stage === 'complete' && 'Import finished'}
@@ -476,7 +476,7 @@ export default function ImporterView() {
variant="outline"
size="sm"
onClick={resetImport}
className="gap-2 font-bold border-zinc-200"
className="gap-2 font-bold border-border"
>
<RefreshCw size={16} />
Reset
@@ -487,7 +487,7 @@ export default function ImporterView() {
{/* Progress Bar */}
{progress.stage === 'fetching' || progress.stage === 'importing' ? (
<div className="mb-6">
<div className="h-2 bg-zinc-100 rounded-full overflow-hidden">
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className={cn(
"h-full transition-all duration-300 ease-out",
@@ -496,7 +496,7 @@ export default function ImporterView() {
style={{ width: `${getProgressPercentage()}%` }}
/>
</div>
<div className="flex justify-between mt-2 text-xs text-zinc-500 font-medium">
<div className="flex justify-between mt-2 text-xs text-muted-foreground font-medium">
<span>{progress.current} / {progress.total} items</span>
<span>{getProgressPercentage()}%</span>
</div>
@@ -505,26 +505,26 @@ export default function ImporterView() {
{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-zinc-50 rounded-lg p-4">
<div className="bg-muted rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<Film size={16} className="text-zinc-400" />
<span className="text-xs font-bold text-zinc-500">{(progress as any).gamesImported !== undefined ? 'Games' : 'Videos'}</span>
<Film size={16} className="text-muted-foreground" />
<span className="text-xs font-bold text-muted-foreground">{(progress as any).gamesImported !== undefined ? 'Games' : 'Videos'}</span>
</div>
<p className="text-2xl font-black text-zinc-900">{(progress as any).gamesImported !== undefined ? (progress as any).gamesImported : progress.videosImported}</p>
<p className="text-2xl font-black text-foreground">{(progress as any).gamesImported !== undefined ? (progress as any).gamesImported : progress.videosImported}</p>
</div>
<div className="bg-zinc-50 rounded-lg p-4">
<div className="bg-muted rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<Users size={16} className="text-zinc-400" />
<span className="text-xs font-bold text-zinc-500">Actors</span>
<Users size={16} className="text-muted-foreground" />
<span className="text-xs font-bold text-muted-foreground">Actors</span>
</div>
<p className="text-2xl font-black text-zinc-900">{progress.actorsImported}</p>
<p className="text-2xl font-black text-foreground">{progress.actorsImported}</p>
</div>
<div className="bg-zinc-50 rounded-lg p-4">
<div className="bg-muted rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<AlertCircle size={16} className="text-zinc-400" />
<span className="text-xs font-bold text-zinc-500">Errors</span>
<AlertCircle size={16} className="text-muted-foreground" />
<span className="text-xs font-bold text-muted-foreground">Errors</span>
</div>
<p className="text-2xl font-black text-zinc-900">{progress.errors.length}</p>
<p className="text-2xl font-black text-foreground">{progress.errors.length}</p>
</div>
</div>