mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
9c2dccaad6
- RGBA color picker with live preview (debounce 50ms) - 30 preset colors + 12 theme presets (Ocean, Forest, Sunset, Royal, etc.) - Header image selection from configurable image library - Export/Import theme as JSON via clipboard - CSS variable theming across all UI elements: NitroCard headers/tabs, context menus, buttons (primary/dark/gray), InfoStand, toolbar, room tools, purse, progress bars, sliders - All elements use var(--name, fallback) for zero visual change when default - Smooth 0.3s CSS transitions on theme change - Server-side persistence via WebSocket (packets 10047/10048) - Integrated Color/Image tabs into BackgroundsView panel - All strings use LocalizeText() for i18n support - Settings persisted in localStorage + server sync with 1s debounce - Added react-colorful dependency
225 lines
6.9 KiB
TypeScript
225 lines
6.9 KiB
TypeScript
import { GetCommunication, UiSettingsDataEvent, UiSettingsLoadComposer, UiSettingsSaveComposer } from '@nitrots/nitro-renderer';
|
|
import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
import { DEFAULT_UI_SETTINGS, IUiSettings } from './IUiSettings';
|
|
|
|
const STORAGE_KEY = 'nitro.ui.settings';
|
|
|
|
interface IUiSettingsContext
|
|
{
|
|
settings: IUiSettings;
|
|
isCustomActive: boolean;
|
|
updateSettings: (partial: Partial<IUiSettings>) => void;
|
|
resetSettings: () => void;
|
|
getHeaderStyle: () => React.CSSProperties;
|
|
getTabsStyle: () => React.CSSProperties;
|
|
getAccentColor: () => string;
|
|
}
|
|
|
|
const UiSettingsContext = createContext<IUiSettingsContext>({
|
|
settings: DEFAULT_UI_SETTINGS,
|
|
isCustomActive: false,
|
|
updateSettings: () => {},
|
|
resetSettings: () => {},
|
|
getHeaderStyle: () => ({}),
|
|
getTabsStyle: () => ({}),
|
|
getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor
|
|
});
|
|
|
|
const darkenColor = (hex: string, amount: number): string =>
|
|
{
|
|
const num = parseInt(hex.replace('#', ''), 16);
|
|
const r = Math.max(0, ((num >> 16) & 0xFF) - amount);
|
|
const g = Math.max(0, ((num >> 8) & 0xFF) - amount);
|
|
const b = Math.max(0, (num & 0xFF) - amount);
|
|
|
|
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
};
|
|
|
|
const loadSettings = (): IUiSettings =>
|
|
{
|
|
try
|
|
{
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) };
|
|
}
|
|
catch(e) {}
|
|
|
|
return { ...DEFAULT_UI_SETTINGS };
|
|
};
|
|
|
|
const saveSettings = (settings: IUiSettings): void =>
|
|
{
|
|
try
|
|
{
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
|
}
|
|
catch(e) {}
|
|
};
|
|
|
|
const sendComposer = (composer: any): void =>
|
|
{
|
|
try
|
|
{
|
|
GetCommunication()?.connection?.send(composer);
|
|
}
|
|
catch(e) {}
|
|
};
|
|
|
|
export const UiSettingsProvider: FC<PropsWithChildren> = ({ children }) =>
|
|
{
|
|
const [ settings, setSettings ] = useState<IUiSettings>(loadSettings);
|
|
const serverSaveTimerRef = useRef<ReturnType<typeof setTimeout>>(null);
|
|
|
|
// Carica dal server al mount e ascolta risposta
|
|
useEffect(() =>
|
|
{
|
|
sendComposer(new UiSettingsLoadComposer());
|
|
|
|
const connection = GetCommunication()?.connection;
|
|
|
|
if(!connection) return;
|
|
|
|
const handler = (event: any) =>
|
|
{
|
|
try
|
|
{
|
|
const parser = event.getParser();
|
|
const json = parser?.settingsJson;
|
|
|
|
if(json && json !== '{}')
|
|
{
|
|
const serverSettings = { ...DEFAULT_UI_SETTINGS, ...JSON.parse(json) };
|
|
setSettings(serverSettings);
|
|
saveSettings(serverSettings);
|
|
}
|
|
}
|
|
catch(e) {}
|
|
};
|
|
|
|
connection.addMessageEvent(new UiSettingsDataEvent(handler));
|
|
}, []);
|
|
|
|
const syncToServer = useCallback((settingsToSave: IUiSettings) =>
|
|
{
|
|
if(serverSaveTimerRef.current) clearTimeout(serverSaveTimerRef.current);
|
|
|
|
serverSaveTimerRef.current = setTimeout(() =>
|
|
{
|
|
sendComposer(new UiSettingsSaveComposer(JSON.stringify(settingsToSave)));
|
|
}, 1000);
|
|
}, []);
|
|
|
|
const updateSettings = useCallback((partial: Partial<IUiSettings>) =>
|
|
{
|
|
setSettings(prev =>
|
|
{
|
|
const updated = { ...prev, ...partial };
|
|
saveSettings(updated);
|
|
syncToServer(updated);
|
|
|
|
return updated;
|
|
});
|
|
}, [ syncToServer ]);
|
|
|
|
const resetSettings = useCallback(() =>
|
|
{
|
|
setSettings({ ...DEFAULT_UI_SETTINGS });
|
|
saveSettings(DEFAULT_UI_SETTINGS);
|
|
syncToServer(DEFAULT_UI_SETTINGS);
|
|
}, [ syncToServer ]);
|
|
|
|
const getHeaderStyle = useCallback((): React.CSSProperties =>
|
|
{
|
|
if(settings.colorMode === 'color')
|
|
{
|
|
const alpha = (settings.headerAlpha ?? 100) / 100;
|
|
const num = parseInt(settings.headerColor.replace('#', ''), 16);
|
|
const r = (num >> 16) & 0xFF;
|
|
const g = (num >> 8) & 0xFF;
|
|
const b = num & 0xFF;
|
|
|
|
return { backgroundColor: `rgba(${ r }, ${ g }, ${ b }, ${ alpha })` };
|
|
}
|
|
|
|
if(settings.colorMode === 'image' && settings.headerImageUrl)
|
|
{
|
|
return {
|
|
backgroundImage: `url(${ settings.headerImageUrl })`,
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center',
|
|
backgroundRepeat: 'repeat'
|
|
};
|
|
}
|
|
|
|
return {};
|
|
}, [ settings ]);
|
|
|
|
const getTabsStyle = useCallback((): React.CSSProperties =>
|
|
{
|
|
if(settings.colorMode === 'color')
|
|
{
|
|
return { backgroundColor: darkenColor(settings.headerColor, 30) };
|
|
}
|
|
|
|
if(settings.colorMode === 'image' && settings.headerImageUrl)
|
|
{
|
|
return {
|
|
backgroundImage: `url(${ settings.headerImageUrl })`,
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center bottom',
|
|
backgroundRepeat: 'repeat'
|
|
};
|
|
}
|
|
|
|
return {};
|
|
}, [ settings ]);
|
|
|
|
const getAccentColor = useCallback((): string =>
|
|
{
|
|
if(settings.colorMode === 'color') return settings.headerColor;
|
|
|
|
return DEFAULT_UI_SETTINGS.headerColor;
|
|
}, [ settings ]);
|
|
|
|
const isCustomActive = settings.colorMode !== 'default';
|
|
|
|
const ALL_CSS_VARS = [
|
|
'--ui-accent-color', '--ui-accent-dark',
|
|
'--ui-ctx-bg', '--ui-ctx-header-bg', '--ui-ctx-item-bg1', '--ui-ctx-item-bg2',
|
|
'--ui-btn-primary-bg', '--ui-btn-primary-border',
|
|
'--ui-dark-bg', '--ui-dark-border'
|
|
];
|
|
|
|
useEffect(() =>
|
|
{
|
|
const root = document.documentElement;
|
|
|
|
if(settings.colorMode === 'color')
|
|
{
|
|
const c = settings.headerColor;
|
|
root.style.setProperty('--ui-accent-color', c);
|
|
root.style.setProperty('--ui-accent-dark', darkenColor(c, 30));
|
|
root.style.setProperty('--ui-ctx-bg', darkenColor(c, 50));
|
|
root.style.setProperty('--ui-ctx-header-bg', darkenColor(c, 20));
|
|
root.style.setProperty('--ui-ctx-item-bg1', darkenColor(c, 60));
|
|
root.style.setProperty('--ui-ctx-item-bg2', darkenColor(c, 70));
|
|
root.style.setProperty('--ui-btn-primary-bg', c);
|
|
root.style.setProperty('--ui-btn-primary-border', darkenColor(c, 20));
|
|
root.style.setProperty('--ui-dark-bg', darkenColor(c, 55));
|
|
root.style.setProperty('--ui-dark-border', darkenColor(c, 60));
|
|
}
|
|
else
|
|
{
|
|
ALL_CSS_VARS.forEach(v => root.style.removeProperty(v));
|
|
}
|
|
}, [ settings ]);
|
|
|
|
return (
|
|
<UiSettingsContext.Provider value={ { settings, isCustomActive, updateSettings, resetSettings, getHeaderStyle, getTabsStyle, getAccentColor } }>
|
|
{ children }
|
|
</UiSettingsContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useUiSettings = () => useContext(UiSettingsContext);
|