diff --git a/custom-themes/README.md b/custom-themes/README.md new file mode 100644 index 0000000..e9f7436 --- /dev/null +++ b/custom-themes/README.md @@ -0,0 +1,40 @@ +# Custom themes (graphics-only) + +Ecosistema temi caricati a **runtime** (niente rebuild del client). Un tema = +una cartella con un manifest + "pezzi" CSS. Ogni pezzo è attivabile/disattivabile +dall'utente da **Impostazioni → Temi** (checkbox). Se un pezzo è rotto/404 → +fallback automatico al default (solo quel pezzo). + +## Dove vivono +- **Questa cartella (`custom-themes/`) è solo il TEMPLATE di riferimento**, versionata su git. +- I temi **veri** stanno sul server in `public/nitro/custom-themes/` (serviti via + l'url configurato in ui-config `theme.base.url`, es. `/client/nitro/custom-themes`). + NON vanno su git → vedi `.gitignore` (`public/custom-themes/`). + +## Struttura +``` +custom-themes/ + index.json # { "themes": [ { "id", "name", "author?" } ] } + / + theme.json # { "name", "pieces": [ { "id", "name", "file" } ] } + cards.css chat.css ... # un file per "pezzo" + assets/... # immagini referenziate dai CSS (url assoluti) +``` + +## Creare un tema +1. Copia `neon-viola/` in una nuova cartella `/`. +2. Modifica `theme.json` (nome + elenco pezzi). +3. Scrivi i CSS dei pezzi (override con `!important`, caricati dopo il base). +4. Aggiungi `{ "id": "", "name": "..." }` a `index.json`. +5. Carica la cartella in `public/nitro/custom-themes/` sul server. **Nessun rebuild.** + +## Default hotel-wide (admin) +In `ui-config.json`: +- `theme.base.url` → dove sono serviti i temi +- `theme.default` → id del tema attivo di default (vuoto = nessuno) +- `theme.default.pieces` → array di id pezzi attivi di default + +Ogni utente può comunque sovrascrivere da Impostazioni → Temi (salvato in localStorage). + +> Nota: i temi ri-skinnano solo la **grafica** (CSS). Non cambiano la struttura +> dei componenti né il comportamento. diff --git a/custom-themes/index.example.json b/custom-themes/index.example.json new file mode 100644 index 0000000..46fd0ea --- /dev/null +++ b/custom-themes/index.example.json @@ -0,0 +1,5 @@ +{ + "themes": [ + { "id": "neon-viola", "name": "Neon Viola", "author": "infinityhotel" } + ] +} diff --git a/custom-themes/neon-viola/cards.css b/custom-themes/neon-viola/cards.css new file mode 100644 index 0000000..5608fb7 --- /dev/null +++ b/custom-themes/neon-viola/cards.css @@ -0,0 +1,24 @@ +/* Tema Neon Viola — pezzo "cards" (finestre / NitroCard). + Ricolora header + cornice delle finestre. Caricato DOPO il CSS base, quindi + usa !important per vincere. Tocca solo la cornice/header (non lo sfondo del + contenuto) per non rovinare la leggibilita' del testo. */ + +.nitro-card-shell:not(.nitro-wired) { + border-color: #7c3aed !important; + box-shadow: 0 0 14px rgba(124, 58, 237, .55), 0 8px 22px rgba(0, 0, 0, .4) !important; +} + +.nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell { + background: linear-gradient(180deg, #9333ea 0%, #6d28d9 100%) !important; + border-color: #a855f7 !important; + border-bottom-color: #2a0a4a !important; +} + +.nitro-card-shell:not(.nitro-wired) .nitro-card-title { + color: #fff !important; + text-shadow: 0 0 6px #c084fc, 0 1px 0 #3b0764 !important; +} + +.nitro-card-shell:not(.nitro-wired) .nitro-card-tabs-shell .nitro-card-tab-item-active { + box-shadow: inset 0 -2px 0 #a855f7 !important; +} diff --git a/custom-themes/neon-viola/catalog.css b/custom-themes/neon-viola/catalog.css new file mode 100644 index 0000000..1f8973d --- /dev/null +++ b/custom-themes/neon-viola/catalog.css @@ -0,0 +1,10 @@ +/* Tema Neon Viola — pezzo "catalog" (catalogo Hippiehotel, .nitro-catalog). */ + +.nitro-catalog .nitro-card-header-shell { + background: linear-gradient(180deg, #9333ea 0%, #6d28d9 100%) !important; +} + +.nitro-catalog .group\/rail { + background: #1a1030 !important; + border-right-color: #7c3aed !important; +} diff --git a/custom-themes/neon-viola/chat.css b/custom-themes/neon-viola/chat.css new file mode 100644 index 0000000..c0f767e --- /dev/null +++ b/custom-themes/neon-viola/chat.css @@ -0,0 +1,12 @@ +/* Tema Neon Viola — pezzo "chat". + Accento viola sulla bubble di default (bubble-0) e sull'input chat. + (Le bubble custom hanno la loro grafica; qui tocchiamo solo l'accento base.) */ + +.chat-bubble.bubble-0 { + filter: drop-shadow(0 0 5px rgba(168, 85, 247, .8)); +} + +.nitro-chat-input-container, +.chat-input-container { + box-shadow: inset 0 0 0 1px #7c3aed !important; +} diff --git a/custom-themes/neon-viola/theme.json b/custom-themes/neon-viola/theme.json new file mode 100644 index 0000000..199fd09 --- /dev/null +++ b/custom-themes/neon-viola/theme.json @@ -0,0 +1,10 @@ +{ + "name": "Neon Viola", + "author": "infinityhotel", + "pieces": [ + { "id": "cards", "name": "Finestre / Card", "file": "cards.css" }, + { "id": "chat", "name": "Chat", "file": "chat.css" }, + { "id": "toolbar", "name": "Toolbar", "file": "toolbar.css" }, + { "id": "catalog", "name": "Catalogo", "file": "catalog.css" } + ] +} diff --git a/custom-themes/neon-viola/toolbar.css b/custom-themes/neon-viola/toolbar.css new file mode 100644 index 0000000..ed74f7f --- /dev/null +++ b/custom-themes/neon-viola/toolbar.css @@ -0,0 +1,9 @@ +/* Tema Neon Viola — pezzo "toolbar". + Best-effort: ricolora la barra strumenti in basso. Se i selettori non + matchano nella tua build, il pezzo non ha effetto (fallback sicuro). */ + +.nitro-toolbar, +[class*="toolbar-container"] { + background: linear-gradient(180deg, #2a0a4a 0%, #1a0730 100%) !important; + box-shadow: 0 -2px 10px rgba(124, 58, 237, .4) !important; +} diff --git a/src/api/index.ts b/src/api/index.ts index 424bb4f..ae86f5d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -28,6 +28,7 @@ export * from './room'; export * from './room/events'; export * from './room/widgets'; export * from './soundboard'; +export * from './theme'; export * from './ui-settings'; export * from './user'; export * from './utils'; diff --git a/src/api/theme/ThemeManager.ts b/src/api/theme/ThemeManager.ts new file mode 100644 index 0000000..cbd9e80 --- /dev/null +++ b/src/api/theme/ThemeManager.ts @@ -0,0 +1,122 @@ +import { NitroLogger } from '@nitrots/nitro-renderer'; +import { GetConfigurationValue } from '../nitro'; + +// --------------------------------------------------------------------------- +// Custom theme ecosystem (graphics-only, runtime-loaded). +// +// A "theme" is a folder on the server (NOT bundled in the build) made of: +// /index.json -> { "themes": [ { id, name, author? } ] } +// //theme.json -> { name, pieces: [ { id, name, file } ] } +// //.css -> one CSS "piece" (cards, chat, catalog, ...) +// +// Each enabled piece is injected as a in . If a piece fails to +// load (404 / network) the link removes itself, so the UI falls back to the +// default look for that piece (per-piece fallback, never breaks the client). +// +// The base url is configurable via ui-config ("theme.base.url") so themes can +// live anywhere (and never need a client rebuild to add/change them). +// --------------------------------------------------------------------------- + +export interface ThemeInfo +{ + id: string; + name: string; + author?: string; +} + +export interface ThemePiece +{ + id: string; + name: string; + file: string; +} + +export interface ThemeManifest +{ + name: string; + pieces: ThemePiece[]; +} + +const LINK_ATTR = 'data-nitro-theme'; + +export const GetThemeBaseUrl = (): string => + GetConfigurationValue('theme.base.url', 'custom-themes').replace(/\/+$/, ''); + +export const FetchThemeIndex = async (): Promise => +{ + try + { + const response = await fetch(`${ GetThemeBaseUrl() }/index.json`, { cache: 'no-cache' }); + + if(!response.ok) return []; + + const data = await response.json(); + + return Array.isArray(data?.themes) ? data.themes.filter((t: any) => t && t.id) : []; + } + catch(error) + { + NitroLogger.warn('[ThemeManager] index.json non caricabile, nessun tema custom', error); + + return []; + } +}; + +export const FetchThemeManifest = async (themeId: string): Promise => +{ + if(!themeId) return null; + + try + { + const response = await fetch(`${ GetThemeBaseUrl() }/${ themeId }/theme.json`, { cache: 'no-cache' }); + + if(!response.ok) return null; + + const data = await response.json(); + + if(!data || !Array.isArray(data.pieces)) return null; + + return { + name: data.name ?? themeId, + pieces: data.pieces.filter((p: any) => p && p.id && p.file) + }; + } + catch(error) + { + NitroLogger.warn(`[ThemeManager] manifest non valido per tema "${ themeId }" -> fallback default`, error); + + return null; + } +}; + +export const ClearTheme = (): void => +{ + document.head.querySelectorAll(`link[${ LINK_ATTR }]`).forEach(node => node.remove()); +}; + +export const ApplyThemePieces = (themeId: string, pieces: ThemePiece[]): void => +{ + ClearTheme(); + + if(!themeId || !pieces || !pieces.length) return; + + const base = GetThemeBaseUrl(); + + for(const piece of pieces) + { + const link = document.createElement('link'); + + link.rel = 'stylesheet'; + link.setAttribute(LINK_ATTR, piece.id); + link.href = `${ base }/${ themeId }/${ piece.file }`; + + // Per-piece fallback: a broken piece removes itself, leaving the default. + link.onerror = () => + { + NitroLogger.warn(`[ThemeManager] pezzo tema rotto "${ themeId }/${ piece.file }" -> fallback default`); + link.remove(); + }; + + document.head.appendChild(link); + } +}; diff --git a/src/api/theme/index.ts b/src/api/theme/index.ts new file mode 100644 index 0000000..ee14d93 --- /dev/null +++ b/src/api/theme/index.ts @@ -0,0 +1 @@ +export * from './ThemeManager'; diff --git a/src/api/utils/LocalStorageKeys.ts b/src/api/utils/LocalStorageKeys.ts index 75ecfe8..c1ee611 100644 --- a/src/api/utils/LocalStorageKeys.ts +++ b/src/api/utils/LocalStorageKeys.ts @@ -5,4 +5,6 @@ export class LocalStorageKeys public static CHAT_WINDOW_ENABLED: string = 'chatWindowEnabled'; public static CHAT_TRANSLATION_SETTINGS: string = 'chatTranslationSettings'; public static CATALOG_CLASSIC_STYLE: string = 'catalogClassicStyle'; + public static THEME_ACTIVE: string = 'nitroThemeActive'; + public static THEME_PIECES: string = 'nitroThemePieces'; } diff --git a/src/assets/images/chat/chatbubbles/bubble_253.png b/src/assets/images/chat/chatbubbles/bubble_253.png new file mode 100644 index 0000000..b727a29 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_253.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_253_pointer.png b/src/assets/images/chat/chatbubbles/bubble_253_pointer.png new file mode 100644 index 0000000..45955f6 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_253_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_254.png b/src/assets/images/chat/chatbubbles/bubble_254.png new file mode 100644 index 0000000..b4a100d Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_254.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_254_pointer.png b/src/assets/images/chat/chatbubbles/bubble_254_pointer.png new file mode 100644 index 0000000..ebd039d Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_254_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_255.png b/src/assets/images/chat/chatbubbles/bubble_255.png new file mode 100644 index 0000000..f0f9fb6 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_255.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_255_pointer.png b/src/assets/images/chat/chatbubbles/bubble_255_pointer.png new file mode 100644 index 0000000..147f587 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_255_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_256.png b/src/assets/images/chat/chatbubbles/bubble_256.png new file mode 100644 index 0000000..f6b3845 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_256.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_256_pointer.png b/src/assets/images/chat/chatbubbles/bubble_256_pointer.png new file mode 100644 index 0000000..0a95873 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_256_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_257.png b/src/assets/images/chat/chatbubbles/bubble_257.png new file mode 100644 index 0000000..9923f0e Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_257.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_257_pointer.png b/src/assets/images/chat/chatbubbles/bubble_257_pointer.png new file mode 100644 index 0000000..7afbe0b Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_257_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_258.png b/src/assets/images/chat/chatbubbles/bubble_258.png new file mode 100644 index 0000000..755e5e1 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_258.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_258_pointer.png b/src/assets/images/chat/chatbubbles/bubble_258_pointer.png new file mode 100644 index 0000000..130f8f2 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_258_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_259.png b/src/assets/images/chat/chatbubbles/bubble_259.png new file mode 100644 index 0000000..e603611 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_259.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_259_pointer.png b/src/assets/images/chat/chatbubbles/bubble_259_pointer.png new file mode 100644 index 0000000..b370fe8 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_259_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_260.png b/src/assets/images/chat/chatbubbles/bubble_260.png new file mode 100644 index 0000000..706583d Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_260.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_260_pointer.png b/src/assets/images/chat/chatbubbles/bubble_260_pointer.png new file mode 100644 index 0000000..7ed9701 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_260_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_261.png b/src/assets/images/chat/chatbubbles/bubble_261.png new file mode 100644 index 0000000..e6881b4 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_261.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_261_pointer.png b/src/assets/images/chat/chatbubbles/bubble_261_pointer.png new file mode 100644 index 0000000..11d9615 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_261_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_262.png b/src/assets/images/chat/chatbubbles/bubble_262.png new file mode 100644 index 0000000..42c68e6 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_262.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_262_pointer.png b/src/assets/images/chat/chatbubbles/bubble_262_pointer.png new file mode 100644 index 0000000..ad0803d Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_262_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_263.png b/src/assets/images/chat/chatbubbles/bubble_263.png new file mode 100644 index 0000000..eea056f Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_263.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_263_pointer.png b/src/assets/images/chat/chatbubbles/bubble_263_pointer.png new file mode 100644 index 0000000..16225fe Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_263_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_264.png b/src/assets/images/chat/chatbubbles/bubble_264.png new file mode 100644 index 0000000..da3f3c2 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_264.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_264_pointer.png b/src/assets/images/chat/chatbubbles/bubble_264_pointer.png new file mode 100644 index 0000000..b8c24f9 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_264_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_265.png b/src/assets/images/chat/chatbubbles/bubble_265.png new file mode 100644 index 0000000..7a709b0 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_265.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_265_pointer.png b/src/assets/images/chat/chatbubbles/bubble_265_pointer.png new file mode 100644 index 0000000..8e90999 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_265_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_266.png b/src/assets/images/chat/chatbubbles/bubble_266.png new file mode 100644 index 0000000..e1c5dfa Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_266.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_266_pointer.png b/src/assets/images/chat/chatbubbles/bubble_266_pointer.png new file mode 100644 index 0000000..d6e9c11 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_266_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_267.png b/src/assets/images/chat/chatbubbles/bubble_267.png new file mode 100644 index 0000000..805d7a8 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_267.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_267_pointer.png b/src/assets/images/chat/chatbubbles/bubble_267_pointer.png new file mode 100644 index 0000000..5b1bb69 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_267_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_268.png b/src/assets/images/chat/chatbubbles/bubble_268.png new file mode 100644 index 0000000..9975412 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_268.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_268_pointer.png b/src/assets/images/chat/chatbubbles/bubble_268_pointer.png new file mode 100644 index 0000000..5a4dac2 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_268_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_269.png b/src/assets/images/chat/chatbubbles/bubble_269.png new file mode 100644 index 0000000..eda2ab0 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_269.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_269_pointer.png b/src/assets/images/chat/chatbubbles/bubble_269_pointer.png new file mode 100644 index 0000000..61aaa10 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_269_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_270.png b/src/assets/images/chat/chatbubbles/bubble_270.png new file mode 100644 index 0000000..cebc39c Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_270.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_270_pointer.png b/src/assets/images/chat/chatbubbles/bubble_270_pointer.png new file mode 100644 index 0000000..ff9130e Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_270_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_271.png b/src/assets/images/chat/chatbubbles/bubble_271.png new file mode 100644 index 0000000..96166c7 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_271.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_271_pointer.png b/src/assets/images/chat/chatbubbles/bubble_271_pointer.png new file mode 100644 index 0000000..cd8c398 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_271_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_272.png b/src/assets/images/chat/chatbubbles/bubble_272.png new file mode 100644 index 0000000..80033eb Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_272.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_272_pointer.png b/src/assets/images/chat/chatbubbles/bubble_272_pointer.png new file mode 100644 index 0000000..e38a4d7 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_272_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_273.png b/src/assets/images/chat/chatbubbles/bubble_273.png new file mode 100644 index 0000000..3b21851 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_273.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_273_pointer.png b/src/assets/images/chat/chatbubbles/bubble_273_pointer.png new file mode 100644 index 0000000..84b8587 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_273_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_274.png b/src/assets/images/chat/chatbubbles/bubble_274.png new file mode 100644 index 0000000..4e511a6 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_274.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_274_pointer.png b/src/assets/images/chat/chatbubbles/bubble_274_pointer.png new file mode 100644 index 0000000..be1db00 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_274_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_275.png b/src/assets/images/chat/chatbubbles/bubble_275.png new file mode 100644 index 0000000..cd8777a Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_275.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_275_pointer.png b/src/assets/images/chat/chatbubbles/bubble_275_pointer.png new file mode 100644 index 0000000..8a61359 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_275_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_276.png b/src/assets/images/chat/chatbubbles/bubble_276.png new file mode 100644 index 0000000..b4de1a7 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_276.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_276_pointer.png b/src/assets/images/chat/chatbubbles/bubble_276_pointer.png new file mode 100644 index 0000000..6683f6e Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_276_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_277.png b/src/assets/images/chat/chatbubbles/bubble_277.png new file mode 100644 index 0000000..df7317e Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_277.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_277_pointer.png b/src/assets/images/chat/chatbubbles/bubble_277_pointer.png new file mode 100644 index 0000000..7a3b463 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_277_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_278.png b/src/assets/images/chat/chatbubbles/bubble_278.png new file mode 100644 index 0000000..c275815 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_278.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_278_pointer.png b/src/assets/images/chat/chatbubbles/bubble_278_pointer.png new file mode 100644 index 0000000..a50c119 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_278_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_279.png b/src/assets/images/chat/chatbubbles/bubble_279.png new file mode 100644 index 0000000..d1bb2e5 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_279.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_279_pointer.png b/src/assets/images/chat/chatbubbles/bubble_279_pointer.png new file mode 100644 index 0000000..1d78808 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_279_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_280.png b/src/assets/images/chat/chatbubbles/bubble_280.png new file mode 100644 index 0000000..9132a2f Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_280.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_280_pointer.png b/src/assets/images/chat/chatbubbles/bubble_280_pointer.png new file mode 100644 index 0000000..ef49fe2 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_280_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_281.png b/src/assets/images/chat/chatbubbles/bubble_281.png new file mode 100644 index 0000000..1545615 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_281.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_281_pointer.png b/src/assets/images/chat/chatbubbles/bubble_281_pointer.png new file mode 100644 index 0000000..0859bf6 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_281_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_282.png b/src/assets/images/chat/chatbubbles/bubble_282.png new file mode 100644 index 0000000..132af17 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_282.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_282_pointer.png b/src/assets/images/chat/chatbubbles/bubble_282_pointer.png new file mode 100644 index 0000000..c1ce6f8 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_282_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_283.png b/src/assets/images/chat/chatbubbles/bubble_283.png new file mode 100644 index 0000000..5d887d9 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_283.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_283_pointer.png b/src/assets/images/chat/chatbubbles/bubble_283_pointer.png new file mode 100644 index 0000000..bfc3fdb Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_283_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_284.png b/src/assets/images/chat/chatbubbles/bubble_284.png new file mode 100644 index 0000000..e359c05 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_284.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_284_pointer.png b/src/assets/images/chat/chatbubbles/bubble_284_pointer.png new file mode 100644 index 0000000..dae09b2 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_284_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_285.png b/src/assets/images/chat/chatbubbles/bubble_285.png new file mode 100644 index 0000000..9d3f2e5 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_285.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_285_pointer.png b/src/assets/images/chat/chatbubbles/bubble_285_pointer.png new file mode 100644 index 0000000..11d9615 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_285_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_286.png b/src/assets/images/chat/chatbubbles/bubble_286.png new file mode 100644 index 0000000..ee5c706 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_286.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_286_pointer.png b/src/assets/images/chat/chatbubbles/bubble_286_pointer.png new file mode 100644 index 0000000..cacb90a Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_286_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_287.png b/src/assets/images/chat/chatbubbles/bubble_287.png new file mode 100644 index 0000000..597afec Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_287.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_287_pointer.png b/src/assets/images/chat/chatbubbles/bubble_287_pointer.png new file mode 100644 index 0000000..be1db00 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_287_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_288.png b/src/assets/images/chat/chatbubbles/bubble_288.png new file mode 100644 index 0000000..62f7236 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_288.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_288_pointer.png b/src/assets/images/chat/chatbubbles/bubble_288_pointer.png new file mode 100644 index 0000000..565a77f Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_288_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_289.png b/src/assets/images/chat/chatbubbles/bubble_289.png new file mode 100644 index 0000000..f3bf1ff Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_289.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_289_pointer.png b/src/assets/images/chat/chatbubbles/bubble_289_pointer.png new file mode 100644 index 0000000..bfc3fdb Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_289_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_290.png b/src/assets/images/chat/chatbubbles/bubble_290.png new file mode 100644 index 0000000..7eaaa0c Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_290.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_290_pointer.png b/src/assets/images/chat/chatbubbles/bubble_290_pointer.png new file mode 100644 index 0000000..5077cd2 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_290_pointer.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_291.png b/src/assets/images/chat/chatbubbles/bubble_291.png new file mode 100644 index 0000000..6a7e2a3 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_291.png differ diff --git a/src/assets/images/chat/chatbubbles/bubble_291_pointer.png b/src/assets/images/chat/chatbubbles/bubble_291_pointer.png new file mode 100644 index 0000000..5283392 Binary files /dev/null and b/src/assets/images/chat/chatbubbles/bubble_291_pointer.png differ diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index e155f05..d94bbc5 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -28,6 +28,7 @@ import { HousekeepingView } from './housekeeping/HousekeepingView'; import { RareValuesView } from './rare-values/RareValuesView'; import { FortuneWheelView } from './fortune-wheel/FortuneWheelView'; import { SoundboardView } from './soundboard/SoundboardView'; +import { ThemeApplier } from './theme/ThemeApplier'; import { RadioView } from './radio/RadioView'; import { InventoryView } from './inventory/InventoryView'; import { ModToolsView } from './mod-tools/ModToolsView'; @@ -134,6 +135,7 @@ export const MainView: FC<{}> = props => return ( <> +
{ landingViewVisible && diff --git a/src/components/soundboard/SoundboardView.tsx b/src/components/soundboard/SoundboardView.tsx index 76d7a80..a3ecf40 100644 --- a/src/components/soundboard/SoundboardView.tsx +++ b/src/components/soundboard/SoundboardView.tsx @@ -10,6 +10,18 @@ export const SoundboardView: FC<{}> = () => const [ isVisible, setIsVisible ] = useState(false); const { enabled, sounds, lastPlayed, play } = useSoundboard(); + const PAGE_SIZE = 9; + const [ page, setPage ] = useState(0); + const totalPages = Math.max(1, Math.ceil(sounds.length / PAGE_SIZE)); + + // Clamp the page if the sound list shrinks (or on first load). + useEffect(() => + { + if(page > (totalPages - 1)) setPage(0); + }, [ totalPages, page ]); + + const pageSounds = sounds.slice(page * PAGE_SIZE, (page * PAGE_SIZE) + PAGE_SIZE); + useEffect(() => { const linkTracker: ILinkEventTracker = { @@ -49,18 +61,32 @@ export const SoundboardView: FC<{}> = () => { !sounds.length && { LocalizeText('soundboard.empty') } } { !!sounds.length && -
- { sounds.map(sound => ( - - )) } -
} + <> +
+ { pageSounds.map(sound => ( + + )) } +
+ { totalPages > 1 && + + + { page + 1 } / { totalPages } + + } + } { lastPlayed && diff --git a/src/components/theme/ThemeApplier.tsx b/src/components/theme/ThemeApplier.tsx new file mode 100644 index 0000000..1f02ed1 --- /dev/null +++ b/src/components/theme/ThemeApplier.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react'; +import { useThemes } from '../../hooks'; + +// Mounted once at app level: subscribing to the shared theme store triggers the +// load + apply effects, so the saved/default custom theme is applied on boot +// and kept in sync when the user changes it from Settings. Renders nothing. +export const ThemeApplier: FC<{}> = () => +{ + useThemes(); + + return null; +}; diff --git a/src/components/user-settings/UserSettingsView.tsx b/src/components/user-settings/UserSettingsView.tsx index 999ac3b..77adc46 100644 --- a/src/components/user-settings/UserSettingsView.tsx +++ b/src/components/user-settings/UserSettingsView.tsx @@ -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(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 => processAction('close_view') } /> +
+ + +
+ { activeTab === 'general' && <>
processAction('oldchat', event.target.checked) } /> @@ -207,6 +214,35 @@ export const UserSettingsView: FC<{}> = props =>
+ } + { activeTab === 'themes' &&
+
+ Tema custom + +
+ { activeThemeId && manifest && manifest.pieces.length > 0 && +
+ Pezzi attivi + { manifest.pieces.map(piece => ( +
+ togglePiece(piece.id) } /> + { piece.name } +
+ )) } +
} + { activeThemeId && !manifest && + Tema non valido o non raggiungibile — uso il default. } + { !themes.length && + Nessun tema disponibile. Aggiungi una cartella in custom-themes/ sul server. } +
} ); diff --git a/src/css/chat/Chats.css b/src/css/chat/Chats.css index 43f093f..13187d2 100644 --- a/src/css/chat/Chats.css +++ b/src/css/chat/Chats.css @@ -807,6 +807,435 @@ } } + &.bubble-253 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_253.png'); + border-image-slice: 16 22 15 27 fill; + border-image-width: 16px 22px 15px 27px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_253_pointer.png'); + } + } + + &.bubble-254 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_254.png'); + border-image-slice: 7 28 15 25 fill; + border-image-width: 7px 28px 15px 25px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_254_pointer.png'); + } + } + + &.bubble-255 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_255.png'); + border-image-slice: 12 19 22 30 fill; + border-image-width: 12px 19px 22px 30px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_255_pointer.png'); + } + } + + &.bubble-256 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_256.png'); + border-image-slice: 24 18 10 31 fill; + border-image-width: 24px 18px 10px 31px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_256_pointer.png'); + } + } + + &.bubble-257 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_257.png'); + border-image-slice: 6 17 19 36 fill; + border-image-width: 6px 17px 19px 36px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_257_pointer.png'); + } + } + + &.bubble-258 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_258.png'); + border-image-slice: 22 27 10 27 fill; + border-image-width: 22px 27px 10px 27px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_258_pointer.png'); + } + } + + &.bubble-259 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_259.png'); + border-image-slice: 21 27 18 37 fill; + border-image-width: 21px 27px 18px 37px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_259_pointer.png'); + } + } + + &.bubble-260 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_260.png'); + border-image-slice: 6 22 16 27 fill; + border-image-width: 6px 22px 16px 27px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_260_pointer.png'); + } + } + + &.bubble-261 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_261.png'); + border-image-slice: 18 27 5 22 fill; + border-image-width: 18px 27px 5px 22px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_261_pointer.png'); + } + } + + &.bubble-262 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_262.png'); + border-image-slice: 33 31 11 34 fill; + border-image-width: 33px 31px 11px 34px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_262_pointer.png'); + } + } + + &.bubble-263 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_263.png'); + border-image-slice: 15 19 10 32 fill; + border-image-width: 15px 19px 10px 32px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_263_pointer.png'); + } + } + + &.bubble-264 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_264.png'); + border-image-slice: 18 24 16 25 fill; + border-image-width: 18px 24px 16px 25px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_264_pointer.png'); + } + } + + &.bubble-265 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_265.png'); + border-image-slice: 41 40 17 18 fill; + border-image-width: 41px 40px 17px 18px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_265_pointer.png'); + } + } + + &.bubble-266 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_266.png'); + border-image-slice: 13 34 22 27 fill; + border-image-width: 13px 34px 22px 27px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_266_pointer.png'); + } + } + + &.bubble-267 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_267.png'); + border-image-slice: 17 30 22 25 fill; + border-image-width: 17px 30px 22px 25px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_267_pointer.png'); + } + } + + &.bubble-268 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_268.png'); + border-image-slice: 7 30 21 24 fill; + border-image-width: 7px 30px 21px 24px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_268_pointer.png'); + } + } + + &.bubble-269 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_269.png'); + border-image-slice: 10 23 25 35 fill; + border-image-width: 10px 23px 25px 35px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_269_pointer.png'); + } + } + + &.bubble-270 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_270.png'); + border-image-slice: 13 30 14 26 fill; + border-image-width: 13px 30px 14px 26px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_270_pointer.png'); + } + } + + &.bubble-271 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_271.png'); + border-image-slice: 23 23 9 35 fill; + border-image-width: 23px 23px 9px 35px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_271_pointer.png'); + } + } + + &.bubble-272 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_272.png'); + border-image-slice: 9 31 24 25 fill; + border-image-width: 9px 31px 24px 25px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_272_pointer.png'); + } + } + + &.bubble-273 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_273.png'); + border-image-slice: 11 16 25 37 fill; + border-image-width: 11px 16px 25px 37px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_273_pointer.png'); + } + } + + &.bubble-274 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_274.png'); + border-image-slice: 7 22 19 27 fill; + border-image-width: 7px 22px 19px 27px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_274_pointer.png'); + } + } + + &.bubble-275 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_275.png'); + border-image-slice: 8 23 14 26 fill; + border-image-width: 8px 23px 14px 26px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_275_pointer.png'); + } + } + + &.bubble-276 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_276.png'); + border-image-slice: 12 40 17 17 fill; + border-image-width: 12px 40px 17px 17px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_276_pointer.png'); + } + } + + &.bubble-277 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_277.png'); + border-image-slice: 6 39 18 17 fill; + border-image-width: 6px 39px 18px 17px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_277_pointer.png'); + } + } + + &.bubble-278 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_278.png'); + border-image-slice: 16 38 6 19 fill; + border-image-width: 16px 38px 6px 19px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_278_pointer.png'); + } + } + + &.bubble-279 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_279.png'); + border-image-slice: 6 26 16 23 fill; + border-image-width: 6px 26px 16px 23px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_279_pointer.png'); + } + } + + &.bubble-280 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_280.png'); + border-image-slice: 23 29 6 15 fill; + border-image-width: 23px 29px 6px 15px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_280_pointer.png'); + } + } + + &.bubble-281 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_281.png'); + border-image-slice: 18 42 9 18 fill; + border-image-width: 18px 42px 9px 18px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_281_pointer.png'); + } + } + + &.bubble-282 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_282.png'); + border-image-slice: 18 42 9 18 fill; + border-image-width: 18px 42px 9px 18px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_282_pointer.png'); + } + } + + &.bubble-283 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_283.png'); + border-image-slice: 17 26 13 31 fill; + border-image-width: 17px 26px 13px 31px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_283_pointer.png'); + } + } + + &.bubble-284 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_284.png'); + border-image-slice: 9 26 23 26 fill; + border-image-width: 9px 26px 23px 26px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_284_pointer.png'); + } + } + + &.bubble-285 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_285.png'); + border-image-slice: 16 35 15 15 fill; + border-image-width: 16px 35px 15px 15px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_285_pointer.png'); + } + } + + &.bubble-286 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_286.png'); + border-image-slice: 18 22 4 23 fill; + border-image-width: 18px 22px 4px 23px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_286_pointer.png'); + } + } + + &.bubble-287 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_287.png'); + border-image-slice: 6 22 18 26 fill; + border-image-width: 6px 22px 18px 26px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_287_pointer.png'); + } + } + + &.bubble-288 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_288.png'); + border-image-slice: 18 31 11 24 fill; + border-image-width: 18px 31px 11px 24px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_288_pointer.png'); + } + } + + &.bubble-289 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_289.png'); + border-image-slice: 7 54 17 24 fill; + border-image-width: 7px 54px 17px 24px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_289_pointer.png'); + } + } + + &.bubble-290 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_290.png'); + border-image-slice: 18 24 14 29 fill; + border-image-width: 18px 24px 14px 29px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_290_pointer.png'); + } + } + + &.bubble-291 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_291.png'); + border-image-slice: 9 26 11 35 fill; + border-image-width: 9px 26px 11px 35px; + border-image-repeat: stretch stretch; + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_291_pointer.png'); + } + } + &.bubble-200, &.bubble-201, &.bubble-202, @@ -1810,4 +2239,160 @@ background: center / contain no-repeat url('@/assets/images/chat/chatbubbles/bubble_252_extra.png'); } } + &.bubble-253 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_253.png'); + } + + &.bubble-254 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_254.png'); + } + + &.bubble-255 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_255.png'); + } + + &.bubble-256 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_256.png'); + } + + &.bubble-257 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_257.png'); + } + + &.bubble-258 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_258.png'); + } + + &.bubble-259 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_259.png'); + } + + &.bubble-260 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_260.png'); + } + + &.bubble-261 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_261.png'); + } + + &.bubble-262 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_262.png'); + } + + &.bubble-263 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_263.png'); + } + + &.bubble-264 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_264.png'); + } + + &.bubble-265 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_265.png'); + } + + &.bubble-266 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_266.png'); + } + + &.bubble-267 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_267.png'); + } + + &.bubble-268 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_268.png'); + } + + &.bubble-269 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_269.png'); + } + + &.bubble-270 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_270.png'); + } + + &.bubble-271 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_271.png'); + } + + &.bubble-272 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_272.png'); + } + + &.bubble-273 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_273.png'); + } + + &.bubble-274 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_274.png'); + } + + &.bubble-275 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_275.png'); + } + + &.bubble-276 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_276.png'); + } + + &.bubble-277 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_277.png'); + } + + &.bubble-278 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_278.png'); + } + + &.bubble-279 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_279.png'); + } + + &.bubble-280 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_280.png'); + } + + &.bubble-281 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_281.png'); + } + + &.bubble-282 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_282.png'); + } + + &.bubble-283 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_283.png'); + } + + &.bubble-284 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_284.png'); + } + + &.bubble-285 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_285.png'); + } + + &.bubble-286 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_286.png'); + } + + &.bubble-287 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_287.png'); + } + + &.bubble-288 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_288.png'); + } + + &.bubble-289 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_289.png'); + } + + &.bubble-290 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_290.png'); + } + + &.bubble-291 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_291.png'); + } + } diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 8a85f2a..46a0287 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -24,6 +24,7 @@ export * from './rooms/widgets'; export * from './rooms/widgets/furniture'; export * from './session'; export * from './soundboard/useSoundboard'; +export * from './theme'; export * from './translation'; export * from './useLocalStorage'; export * from './useSharedVisibility'; diff --git a/src/hooks/theme/index.ts b/src/hooks/theme/index.ts new file mode 100644 index 0000000..effac86 --- /dev/null +++ b/src/hooks/theme/index.ts @@ -0,0 +1 @@ +export * from './useThemes'; diff --git a/src/hooks/theme/useThemes.ts b/src/hooks/theme/useThemes.ts new file mode 100644 index 0000000..c2ef444 --- /dev/null +++ b/src/hooks/theme/useThemes.ts @@ -0,0 +1,108 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useBetween } from 'use-between'; +import { ApplyThemePieces, ClearTheme, FetchThemeIndex, FetchThemeManifest, GetConfigurationValue, LocalStorageKeys, ThemeInfo, ThemeManifest } from '../../api'; +import { useLocalStorage } from '../useLocalStorage'; + +// Per-user custom theme selection. +// - activeThemeId: '' = default (no custom theme). Default for new users comes +// from ui-config `theme.default` so the admin can set a hotel-wide default +// (like catalog.classic.style), while each user can override from Settings. +// - enabledPieces[themeId]: which graphic pieces of that theme are active +// (checkboxes). If absent, defaults to ui-config `theme.default.pieces` +// (when on the default theme) or ALL pieces. +const useThemesState = () => +{ + const [ activeThemeId, setActiveThemeId ] = useLocalStorage(LocalStorageKeys.THEME_ACTIVE, GetConfigurationValue('theme.default', '')); + const [ enabledPieces, setEnabledPieces ] = useLocalStorage>(LocalStorageKeys.THEME_PIECES, {}); + const [ themes, setThemes ] = useState([]); + const [ manifest, setManifest ] = useState(null); + const [ loaded, setLoaded ] = useState(false); + + // Load the theme index once. + useEffect(() => + { + let alive = true; + + FetchThemeIndex().then(list => + { + if(alive) setThemes(list); + }).finally(() => + { + if(alive) setLoaded(true); + }); + + return () => { alive = false; }; + }, []); + + // Load the manifest whenever the active theme changes. + useEffect(() => + { + let alive = true; + + if(!activeThemeId) + { + setManifest(null); + ClearTheme(); + return; + } + + FetchThemeManifest(activeThemeId).then(m => + { + if(!alive) return; + + setManifest(m); + + if(!m) ClearTheme(); // broken/missing manifest -> full fallback to default + }); + + return () => { alive = false; }; + }, [ activeThemeId ]); + + // Which pieces are enabled for the current theme. + const activeEnabled = useMemo(() => + { + if(!manifest) return [] as string[]; + + const stored = enabledPieces[activeThemeId]; + + if(stored) return stored; + + const fromConfig = GetConfigurationValue('theme.default.pieces', null); + + // Default: config list (if this is the default theme) else every piece on. + if(fromConfig && activeThemeId === GetConfigurationValue('theme.default', '')) return fromConfig; + + return manifest.pieces.map(p => p.id); + }, [ manifest, enabledPieces, activeThemeId ]); + + // Apply (inject/remove s) whenever theme or enabled pieces change. + useEffect(() => + { + if(!activeThemeId || !manifest) + { + ClearTheme(); + return; + } + + ApplyThemePieces(activeThemeId, manifest.pieces.filter(p => activeEnabled.includes(p.id))); + }, [ activeThemeId, manifest, activeEnabled ]); + + const selectTheme = (id: string) => setActiveThemeId(id || ''); + + const togglePiece = (pieceId: string) => + { + if(!activeThemeId || !manifest) return; + + setEnabledPieces(prev => + { + const current = prev[activeThemeId] ?? manifest.pieces.map(p => p.id); + const next = current.includes(pieceId) ? current.filter(x => x !== pieceId) : [ ...current, pieceId ]; + + return { ...prev, [activeThemeId]: next }; + }); + }; + + return { themes, activeThemeId, manifest, activeEnabled, loaded, selectTheme, togglePiece }; +}; + +export const useThemes = () => useBetween(useThemesState);