mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
feat(theme): runtime custom theme ecosystem (graphics-only)
Runtime-loaded visual re-skin system (no client rebuild, real themes never hit git). A theme = a folder on the server (theme.base.url) with a manifest + CSS "pieces"; each piece is toggled from Settings > Themes (checkboxes). A broken/404 piece auto-falls back to the default (per piece). Hotel-wide default via ui-config theme.default (+ theme.default.pieces), per-user override in localStorage (same pattern as the catalog style toggle). - api/theme/ThemeManager: fetch index/manifest + inject/remove <link> + fallback - hooks/theme/useThemes: state + persist + default-from-config + live apply - components/theme/ThemeApplier: applies on boot (mounted in MainView) - UserSettings: General/Themes tabs with theme selector + per-piece checkboxes - custom-themes/: reference template (demo theme "Neon Viola" + README) - .gitignore: public/custom-themes/ (real themes are never committed)
This commit is contained in:
@@ -3,13 +3,15 @@ import { FC, useEffect, useState } from 'react';
|
||||
import { FaUserCog, FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa';
|
||||
import { DispatchMainEvent, DispatchUiEvent, LocalizeText, SendMessageComposer } from '../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
||||
import { useCatalogClassicStyle, useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent } from '../../hooks';
|
||||
import { useCatalogClassicStyle, useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent, useThemes } from '../../hooks';
|
||||
import { classNames } from '../../layout';
|
||||
|
||||
export const UserSettingsView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ activeTab, setActiveTab ] = useState<'general' | 'themes'>('general');
|
||||
const [ userSettings, setUserSettings ] = useState<NitroSettingsEvent>(null);
|
||||
const { themes, activeThemeId, manifest, activeEnabled, selectTheme, togglePiece } = useThemes();
|
||||
const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems();
|
||||
const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation();
|
||||
const [ chatWindowEnabled, setChatWindowEnabled ] = useChatWindow();
|
||||
@@ -132,6 +134,11 @@ export const UserSettingsView: FC<{}> = props =>
|
||||
<NitroCardView className="user-settings-window" theme="primary-slim" uniqueKey="user-settings">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('widget.memenu.settings.title') } onCloseClick={ event => processAction('close_view') } />
|
||||
<NitroCardContentView className="text-black">
|
||||
<div className="flex items-center gap-1 mb-2 border-b border-black/10 pb-1">
|
||||
<button type="button" onClick={ () => setActiveTab('general') } className={ classNames('px-3 py-1 rounded text-xs font-bold cursor-pointer transition-colors', activeTab === 'general' ? 'bg-[#1e7295] text-white' : 'bg-black/5 hover:bg-black/10') }>Generale</button>
|
||||
<button type="button" onClick={ () => setActiveTab('themes') } className={ classNames('px-3 py-1 rounded text-xs font-bold cursor-pointer transition-colors', activeTab === 'themes' ? 'bg-[#1e7295] text-white' : 'bg-black/5 hover:bg-black/10') }>Temi</button>
|
||||
</div>
|
||||
{ activeTab === 'general' && <>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<input checked={ userSettings.oldChat } className="form-check-input" type="checkbox" onChange={ event => processAction('oldchat', event.target.checked) } />
|
||||
@@ -207,6 +214,35 @@ export const UserSettingsView: FC<{}> = props =>
|
||||
<span className="text-black/30 group-hover:text-[#1e7295] text-[10px]">›</span>
|
||||
</button>
|
||||
</div>
|
||||
</> }
|
||||
{ activeTab === 'themes' && <div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text bold>Tema custom</Text>
|
||||
<select
|
||||
value={ activeThemeId }
|
||||
onChange={ event => selectTheme(event.target.value) }
|
||||
className="form-select rounded border border-black/15 px-2 py-1 text-sm">
|
||||
<option value="">Default (nessun tema)</option>
|
||||
{ themes.map(theme => (
|
||||
<option key={ theme.id } value={ theme.id }>{ theme.name }{ theme.author ? ` — ${ theme.author }` : '' }</option>
|
||||
)) }
|
||||
</select>
|
||||
</div>
|
||||
{ activeThemeId && manifest && manifest.pieces.length > 0 &&
|
||||
<div className="flex flex-col gap-1 pt-1 border-t border-black/10">
|
||||
<Text bold>Pezzi attivi</Text>
|
||||
{ manifest.pieces.map(piece => (
|
||||
<div key={ piece.id } className="flex items-center gap-1">
|
||||
<input className="form-check-input" type="checkbox" checked={ activeEnabled.includes(piece.id) } onChange={ () => togglePiece(piece.id) } />
|
||||
<Text>{ piece.name }</Text>
|
||||
</div>
|
||||
)) }
|
||||
</div> }
|
||||
{ activeThemeId && !manifest &&
|
||||
<Text small className="text-black/60">Tema non valido o non raggiungibile — uso il default.</Text> }
|
||||
{ !themes.length &&
|
||||
<Text small className="text-black/60">Nessun tema disponibile. Aggiungi una cartella in custom-themes/ sul server.</Text> }
|
||||
</div> }
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user