Merge pull request #5 from simoleo89/feat/command-autocomplete-refactor
Feat/command autocomplete refactor
@@ -44,3 +44,4 @@ Thumbs.db
|
||||
# the dev server takes minutes to start with 100k+ files under public/.
|
||||
/public/nitro-assets
|
||||
/public/swf
|
||||
.superpowers/
|
||||
|
||||
@@ -6,19 +6,27 @@ the ground running.
|
||||
|
||||
## TL;DR
|
||||
|
||||
This branch — **`feat/react19-modernization`** — is a long-running modernization
|
||||
of the Nitro V3 client: bump to React 19.2 idioms, add the supporting
|
||||
infrastructure (TanStack Query, Zustand, Vitest, React Compiler, error
|
||||
boundaries), split a few god-hooks, and audit logic bugs along the way.
|
||||
PR is **#2** on `simoleo89/Nitro-V3`.
|
||||
This client carries a long-running React 19.2 modernization: React 19
|
||||
idioms + supporting infrastructure (TanStack Query, Zustand, Vitest,
|
||||
React Compiler, error boundaries), god-hook splits, and logic-bug audits.
|
||||
|
||||
Upstream `duckietm/Nitro-V3` (`origin/Dev`) is merged in through
|
||||
`b2318b9` as of 2026-05-18 (merge commit `779a98c`). That brings in
|
||||
JSON5 config support, user-settings (reset password / email / change
|
||||
username), wear-badge popup fix, login screen fix, About update, and
|
||||
the offer-selection refactor. When syncing the next batch of upstream
|
||||
commits, expect conflicts in `App.tsx` / `bootstrap.ts` / `LoginView.tsx`
|
||||
on React 19 imports — always keep the modernized local version.
|
||||
**Working base is now `main`** (tracking `duckietm/Nitro-V3`). The earlier
|
||||
`feat/react19-modernization` long-running branch was superseded — feature
|
||||
work now ships as small focused PRs against `duckietm:Dev`, staged through
|
||||
Dev then merged to main. (`feat/react19-modernization` still exists on the
|
||||
fork as backup; do not force-push it.)
|
||||
|
||||
**Navigator modernization landed** (merged to main 2026-05-28, PRs
|
||||
#168/#169/#170): the 492-line `useNavigator` god-hook was split into
|
||||
`useNavigatorStore` + `useNavigatorData`/`useNavigatorUiState`/
|
||||
`useNavigatorSearch` filters (wired-tools layout), door lifecycle extracted
|
||||
to `src/hooks/rooms/widgets/useDoorState.ts`, 9 UI flags moved to a Zustand
|
||||
`navigatorUiStore`, search migrated to a query hook, and 5 sub-views wrapped
|
||||
in `WidgetErrorBoundary`. **Caveat**: duckietm patched `useNavigatorSearch`
|
||||
post-merge (`05d71dd1`) — see the `useNitroQuery` fragility note below.
|
||||
|
||||
When syncing upstream, expect conflicts in `App.tsx` / `bootstrap.ts` /
|
||||
`LoginView.tsx` on React 19 imports — always keep the modernized version.
|
||||
|
||||
Local-dev game assets are served by a small Vite plugin (`sirv` middleware
|
||||
mounted on `/nitro-assets` and `/swf`, reading from
|
||||
@@ -236,6 +244,20 @@ and invalidates the query slot on every push, so server-driven
|
||||
refresh paths work the same as the initial request/response (e.g.
|
||||
ClubGiftInfoEvent firing again after the user claims a gift).
|
||||
|
||||
**⚠️ Fragility — do NOT use `useNitroQuery` for primary visible data.**
|
||||
The one-shot listener inside `awaitNitroResponse` (register listener →
|
||||
await one matching response → remove itself) is fragile against
|
||||
renderer-bundle quirks: for some parsers the event fires but the listener
|
||||
never matches, so the promise never resolves and `query.data` stays
|
||||
`undefined` forever — the UI shows the server's response arriving in logs
|
||||
but renders blank. This bit **ModTools Room/CFH chatlog** (reverted to
|
||||
`useMessageEvent + useEffect`) and then **Navigator search** (P2 shipped
|
||||
with `useNitroQuery`, duckietm reverted it in `05d71dd1` to the god-hook
|
||||
pattern). **Rule: reserve `useNitroQuery` for config / secondary fetches
|
||||
where a brief blank is tolerable. For anything that is the primary visible
|
||||
content of a panel, use `useMessageEvent + useState/useEffect`** — that's
|
||||
what the rest of the codebase does and it's robust.
|
||||
|
||||
### Singleton-filter split for `useBetween`-based hooks
|
||||
|
||||
When a hook backs many consumers but most only need either state OR
|
||||
@@ -339,6 +361,7 @@ into `configurePreviewServer` so `yarn preview` keeps working.
|
||||
| Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`), `WiredCreatorToolsView` (`useWiredCreatorToolsUiStore` — every panel-lifecycle-relevant flag, snapshot, selection, highlight, inline editor, picker chain hoisted; what's left in the component as `useState` is genuinely transient: keepSelected, globalClock, roomEnteredAt, selectedMonitorErrorType, selectedMonitorLogDetails) |
|
||||
| God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` |
|
||||
| God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation`, `notification`, `friends`, `catalog` (three-way: `useCatalogData` / `useCatalogUiState` / `useCatalogActions` — all 48 consumers migrated, deprecated `useCatalog` shim removed) |
|
||||
| Navigator modernization (merged to main 2026-05-28, PRs #168/#169/#170) | 492-line `useNavigator` god-hook split into `useNavigatorStore` (internal `useBetween` closure) + flat filters `useNavigatorData` / `useNavigatorUiState` / `useNavigatorSearch`; door bell/password lifecycle extracted to `src/hooks/rooms/widgets/useDoorState.ts` (dual-subscribes `GetGuestRoomResultEvent` + `GenericErrorEvent` alongside the nav store, each filtering by branch/errorCode); 9 UI flags + `currentTabCode`/`currentFilter` in Zustand `navigatorUiStore` (`src/hooks/navigator/navigatorUiStore.ts`); all 5 Navigator sub-views wrapped in `WidgetErrorBoundary`; old shim deleted. **`useNavigatorSearch` was reverted by duckietm (`05d71dd1`) from `useNitroQuery` to `useMessageEvent + useEffect`** — see the useNitroQuery fragility note. Specs/plans under `docs/superpowers/`. |
|
||||
| `WidgetErrorBoundary` | `RoomWidgetsView` umbrella + per-widget wrap on all 13 room widgets and all 20 furniture widgets (so a crash in one widget no longer takes down its siblings) |
|
||||
| Vitest | 207/207 cases — pure helpers (incl. 4 new on `getPetPackageNameError`) + 2 Zustand store suites (`navigatorRoomCreatorStore`, `wiredCreatorToolsUiStore` with 45 cases including the picker-chain hoists) + 2 component-/hook-level pilots (WidgetErrorBoundary, useDoorbellState) on top of the renderer-SDK mock at `src/nitro-renderer.mock.ts`, 34 cases on the catalog pure helpers, 4 contract cases on the catalog filters. **Tests are co-located** under `src/`, alongside their subject. |
|
||||
| Form Actions | Login / Register / Forgot (LoginView.tsx) |
|
||||
@@ -412,6 +435,11 @@ See `docs/ARCHITECTURE.md` "Recently fixed" for fix shapes.
|
||||
`useCatalogUiState` / `useCatalogActions` in
|
||||
`src/hooks/catalog/useCatalog.ts` (all 48 consumers migrated;
|
||||
deprecated `useCatalog` shim removed)
|
||||
- Navigator hooks: `src/hooks/navigator/` — `useNavigatorStore.ts`
|
||||
(internal closure), `useNavigatorData.ts` / `useNavigatorUiState.ts` /
|
||||
`useNavigatorSearch.ts` (filters), `navigatorUiStore.ts` (Zustand UI
|
||||
flags + `setTab`/`setFilter`). Door lifecycle: `src/hooks/rooms/widgets/useDoorState.ts`.
|
||||
Specs/plans: `docs/superpowers/specs/2026-05-2*-navigator-*.md`
|
||||
- Renderer-SDK mock for Vitest: `src/nitro-renderer.mock.ts`
|
||||
(aliased over `@nitrots/nitro-renderer` via `vitest.config.mts`).
|
||||
Hosts the explicit `NitroLogger` mock, the `mockEventDispatcher` /
|
||||
|
||||
@@ -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?" } ] }
|
||||
<id>/
|
||||
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 `<id>/`.
|
||||
2. Modifica `theme.json` (nome + elenco pezzi).
|
||||
3. Scrivi i CSS dei pezzi (override con `!important`, caricati dopo il base).
|
||||
4. Aggiungi `{ "id": "<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.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"themes": [
|
||||
{ "id": "neon-viola", "name": "Neon Viola", "author": "infinityhotel" }
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# Navigator — Room Settings "Base" tab: stacked-label layout
|
||||
|
||||
**Date:** 2026-05-31
|
||||
**Component:** Nitro-V3 client
|
||||
**File:** `src/components/navigator/views/room-settings/NavigatorRoomSettingsBasicTabView.tsx`
|
||||
**Type:** Layout-only refactor (no logic / data-flow change)
|
||||
|
||||
## Problem
|
||||
|
||||
The Base tab uses a horizontal two-column row layout: a fixed-width label on the
|
||||
left, the control on the right. In the narrow room-settings panel the label column
|
||||
is too tight, so multi-word Italian labels ("Visitatori massimi", "Impostazioni
|
||||
scambio") wrap onto two lines and look broken. An earlier fix replaced dead
|
||||
Bootstrap `col-3` classes with `w-1/4 shrink-0`, which stopped the crushing but
|
||||
still leaves the labels cramped and occasionally wrapping.
|
||||
|
||||
The other five room-settings tabs (Access, Rights, VIP/Chat, Mod, Misc) already use
|
||||
idiomatic vertical/grouped layouts. Base is the outlier.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt the **stacked-label** pattern (chosen from three mockup options — A stacked,
|
||||
B sectioned cards, C wider label column). Each field becomes a vertical block: bold
|
||||
label on top, full-width control below, validation message underneath. This mirrors
|
||||
the sibling **Access** tab's existing `<Column gap={1}>` + `<Text bold>` shape, so
|
||||
the two tabs become visually consistent and labels can never wrap.
|
||||
|
||||
## Layout
|
||||
|
||||
Every field → its own `<Column gap={1}>` block:
|
||||
|
||||
```tsx
|
||||
<Column gap={ 1 }>
|
||||
<Text bold>{ LocalizeText('navigator.roomname') }</Text>
|
||||
<input className="form-control form-control-sm" value={ roomName } … onBlur={ saveRoomName } />
|
||||
{ (roomName.length < ROOM_NAME_MIN_LENGTH) &&
|
||||
<Text bold small variant="danger">{ LocalizeText('navigator.roomsettings.roomnameismandatory') }</Text> }
|
||||
</Column>
|
||||
```
|
||||
|
||||
Field-by-field:
|
||||
|
||||
- **Nome stanza** — stacked block, mandatory-name validation preserved.
|
||||
- **Descrizione** — stacked block, `<textarea>` full width.
|
||||
- **Categoria** — stacked block, `<select>` from `categories`.
|
||||
- **Visitatori massimi** — stacked block, `<select>` from `GetMaxVisitorsList`.
|
||||
- **Impostazioni scambio** — stacked block, 3-option `<select>`.
|
||||
- **Tag** — one "Tag" label, then the two tag inputs side-by-side in a
|
||||
`<Flex gap={1}>`, each `fullWidth`, each keeping its own length/type validation.
|
||||
- **allow_walkthrough / allow_underpass** — remain inline `checkbox + label` rows;
|
||||
remove the empty `<Base className="w-1/4 shrink-0" />` spacers that only existed
|
||||
to align with the old label column.
|
||||
- **Delete link** — unchanged at the bottom.
|
||||
|
||||
## Explicit non-goals
|
||||
|
||||
- No change to `handleChange` field names or values.
|
||||
- No change to validation thresholds (`ROOM_NAME_MIN_LENGTH=3`,
|
||||
`ROOM_NAME_MAX_LENGTH=60`, `DESC_MAX_LENGTH=255`, `TAGS_MAX_LENGTH=15`).
|
||||
- No change to save-on-blur handlers (`saveRoomName`, `saveRoomDescription`,
|
||||
`saveTags`), the `RoomSettingsSaveErrorEvent` subscription, or `deleteRoom`.
|
||||
- No change to field order or any localization key.
|
||||
- No change to the other five tabs.
|
||||
- The `w-1/4 shrink-0` utility classes added in the prior fix are removed (labels
|
||||
are full-width now).
|
||||
|
||||
## Risk
|
||||
|
||||
Single-file, JSX-only diff. No test covers this view, so no test impact. Manual
|
||||
check: open Room Settings → Base, confirm no label wraps, all controls full width,
|
||||
validation still appears, save-on-blur still fires.
|
||||
@@ -642,6 +642,9 @@
|
||||
'wheel.buy': 'Buy spin',
|
||||
'wheel.winners': 'Latest winners',
|
||||
'wheel.winners.empty': 'No winners yet',
|
||||
'wheel.win.title': 'You won!',
|
||||
'wheel.win.jackpot': '★ Jackpot ★',
|
||||
'wheel.win.nothing': 'Better luck next time!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Soundboard
|
||||
@@ -674,6 +677,8 @@
|
||||
'rarevalues.editor.weight': 'Chance',
|
||||
'rarevalues.editor.label': 'Label',
|
||||
'rarevalues.editor.save': 'Save',
|
||||
'rarevalues.editor.add': '+ Add prize',
|
||||
'rarevalues.editor.remove': 'Remove prize',
|
||||
'rarevalues.editor.cat.item': 'Furni (ID)',
|
||||
'rarevalues.editor.cat.spin': 'Extra spins',
|
||||
'rarevalues.editor.cat.nothing': 'Nothing',
|
||||
@@ -700,4 +705,71 @@
|
||||
'chatcmd.client.ejectall': 'Eject all furni',
|
||||
'chatcmd.client.settings': 'Room settings',
|
||||
'chatcmd.client.info': 'Client info',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Me-menu settings + User account settings window
|
||||
// ------------------------------------------------------------------------
|
||||
'usersettings.tab.general': "General",
|
||||
'usersettings.tab.themes': "Themes",
|
||||
'memenu.settings.other.place.multiple.objects': "Place multiple objects",
|
||||
'memenu.settings.other.skip.purchase.confirmation': "Skip purchase confirmation",
|
||||
'memenu.settings.other.enable.chat.window': "Enable chat window",
|
||||
'memenu.settings.other.catalog.classic.style': "Catalog: classic style",
|
||||
'usersettings.open.title': "User settings",
|
||||
'usersettings.open.subtitle': "Password & account",
|
||||
'usersettings.themes.custom': "Custom theme",
|
||||
'usersettings.themes.default_option': "Default (no theme)",
|
||||
'usersettings.themes.active_pieces': "Active pieces",
|
||||
'usersettings.themes.invalid': "Theme invalid or unreachable — using the default.",
|
||||
'usersettings.themes.none': "No themes available. Add a folder in custom-themes/ on the server.",
|
||||
'usersettings.title': "User Settings",
|
||||
'usersettings.account.label': "My account",
|
||||
'usersettings.guest': "Guest",
|
||||
'usersettings.subtitle': "Manage your account and security",
|
||||
'usersettings.menu.section': "Account",
|
||||
'usersettings.menu.password.title': "Reset password",
|
||||
'usersettings.menu.password.desc': "Change the password used to log in.",
|
||||
'usersettings.menu.email.title': "Change email",
|
||||
'usersettings.menu.email.desc': "Update the email address on your account.",
|
||||
'usersettings.menu.username.title': "Change username",
|
||||
'usersettings.menu.username.desc': "Pick a new name. You'll need to log in again.",
|
||||
'usersettings.menu.soon.title': "More coming soon",
|
||||
'usersettings.menu.soon.desc': "Two-factor authentication and more.",
|
||||
'usersettings.password.hint': "Use at least %count% characters. Mix upper & lowercase, numbers and symbols for a stronger password.",
|
||||
'usersettings.email.hint': "For security we ask you to confirm your current password before changing the email on your account.",
|
||||
'usersettings.username.hint': "Renaming will log you out and you can only rename again after 30 days. Make sure your friends know your new name!",
|
||||
'usersettings.field.current_password': "Current password",
|
||||
'usersettings.field.new_password': "New password",
|
||||
'usersettings.field.retype_password': "Retype new password",
|
||||
'usersettings.field.new_email': "New email address",
|
||||
'usersettings.field.new_username': "New username",
|
||||
'usersettings.username.rules': "%min%-%max% characters. Letters, numbers, dot, underscore and dash only.",
|
||||
'usersettings.strength.weak': "Weak",
|
||||
'usersettings.strength.fair': "Fair",
|
||||
'usersettings.strength.good': "Good",
|
||||
'usersettings.strength.strong': "Strong",
|
||||
'usersettings.aria.show_password': "Show password",
|
||||
'usersettings.aria.hide_password': "Hide password",
|
||||
'usersettings.btn.cancel': "Cancel",
|
||||
'usersettings.btn.saving': "Saving…",
|
||||
'usersettings.btn.save_password': "Save password",
|
||||
'usersettings.btn.save_email': "Save email",
|
||||
'usersettings.btn.renaming': "Renaming…",
|
||||
'usersettings.btn.rename': "Rename me",
|
||||
'usersettings.error.fields_required': "All fields are required.",
|
||||
'usersettings.error.password_min': "Password must be at least %count% characters.",
|
||||
'usersettings.error.password_long': "Password is too long.",
|
||||
'usersettings.error.password_mismatch': "New passwords do not match.",
|
||||
'usersettings.error.password_same': "New password must be different from the current password.",
|
||||
'usersettings.error.not_authenticated': "You are not authenticated. Please log in again.",
|
||||
'usersettings.error.network': "Could not reach the server. Please try again.",
|
||||
'usersettings.error.request_failed': "Request failed (%status%).",
|
||||
'usersettings.error.email_long': "Email address is too long.",
|
||||
'usersettings.error.email_invalid': "Please enter a valid email address.",
|
||||
'usersettings.error.username_length': "Username must be between %min% and %max% characters.",
|
||||
'usersettings.error.username_invalid': "Username may only contain letters, numbers, dot, underscore and dash.",
|
||||
'usersettings.error.username_same': "New username must be different from the current one.",
|
||||
'usersettings.success.password': "Password updated successfully.",
|
||||
'usersettings.success.email': "Email updated successfully.",
|
||||
'usersettings.success.username': "Username updated. Please log in again with your new name.",
|
||||
}
|
||||
|
||||
@@ -642,6 +642,9 @@
|
||||
'wheel.buy': 'Acquista giro',
|
||||
'wheel.winners': 'Ultimi vincitori',
|
||||
'wheel.winners.empty': 'Ancora nessun vincitore',
|
||||
'wheel.win.title': 'Hai vinto!',
|
||||
'wheel.win.jackpot': '★ Jackpot ★',
|
||||
'wheel.win.nothing': 'Sarà per la prossima!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Soundboard
|
||||
@@ -674,6 +677,8 @@
|
||||
'rarevalues.editor.weight': 'Probabilità',
|
||||
'rarevalues.editor.label': 'Etichetta',
|
||||
'rarevalues.editor.save': 'Salva',
|
||||
'rarevalues.editor.add': '+ Aggiungi premio',
|
||||
'rarevalues.editor.remove': 'Rimuovi premio',
|
||||
'rarevalues.editor.cat.item': 'Arredo (ID)',
|
||||
'rarevalues.editor.cat.spin': 'Giri extra',
|
||||
'rarevalues.editor.cat.nothing': 'Niente',
|
||||
@@ -700,4 +705,71 @@
|
||||
'chatcmd.client.ejectall': 'Rimuovi tutti gli arredi',
|
||||
'chatcmd.client.settings': 'Impostazioni stanza',
|
||||
'chatcmd.client.info': 'Info client',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Me-menu settings + User account settings window
|
||||
// ------------------------------------------------------------------------
|
||||
'usersettings.tab.general': "Generale",
|
||||
'usersettings.tab.themes': "Temi",
|
||||
'memenu.settings.other.place.multiple.objects': "Posiziona più oggetti",
|
||||
'memenu.settings.other.skip.purchase.confirmation': "Salta la conferma d'acquisto",
|
||||
'memenu.settings.other.enable.chat.window': "Abilita finestra chat",
|
||||
'memenu.settings.other.catalog.classic.style': "Catalogo: stile classico",
|
||||
'usersettings.open.title': "Impostazioni utente",
|
||||
'usersettings.open.subtitle': "Password e account",
|
||||
'usersettings.themes.custom': "Tema personalizzato",
|
||||
'usersettings.themes.default_option': "Predefinito (nessun tema)",
|
||||
'usersettings.themes.active_pieces': "Elementi attivi",
|
||||
'usersettings.themes.invalid': "Tema non valido o non raggiungibile — uso il predefinito.",
|
||||
'usersettings.themes.none': "Nessun tema disponibile. Aggiungi una cartella in custom-themes/ sul server.",
|
||||
'usersettings.title': "Impostazioni utente",
|
||||
'usersettings.account.label': "Il mio account",
|
||||
'usersettings.guest': "Ospite",
|
||||
'usersettings.subtitle': "Gestisci il tuo account e la sicurezza",
|
||||
'usersettings.menu.section': "Account",
|
||||
'usersettings.menu.password.title': "Reimposta password",
|
||||
'usersettings.menu.password.desc': "Cambia la password che usi per accedere.",
|
||||
'usersettings.menu.email.title': "Cambia email",
|
||||
'usersettings.menu.email.desc': "Aggiorna l'indirizzo email del tuo account.",
|
||||
'usersettings.menu.username.title': "Cambia nome utente",
|
||||
'usersettings.menu.username.desc': "Scegli un nuovo nome. Dovrai accedere di nuovo.",
|
||||
'usersettings.menu.soon.title': "Altro in arrivo",
|
||||
'usersettings.menu.soon.desc': "Autenticazione a due fattori e altro.",
|
||||
'usersettings.password.hint': "Usa almeno %count% caratteri. Combina maiuscole e minuscole, numeri e simboli per una password più sicura.",
|
||||
'usersettings.email.hint': "Per sicurezza ti chiediamo di confermare la password attuale prima di cambiare l'email del tuo account.",
|
||||
'usersettings.username.hint': "Cambiando nome verrai disconnesso e potrai rinominarti di nuovo solo dopo 30 giorni. Assicurati che i tuoi amici conoscano il tuo nuovo nome!",
|
||||
'usersettings.field.current_password': "Password attuale",
|
||||
'usersettings.field.new_password': "Nuova password",
|
||||
'usersettings.field.retype_password': "Ripeti la nuova password",
|
||||
'usersettings.field.new_email': "Nuovo indirizzo email",
|
||||
'usersettings.field.new_username': "Nuovo nome utente",
|
||||
'usersettings.username.rules': "%min%-%max% caratteri. Solo lettere, numeri, punto, trattino basso e trattino.",
|
||||
'usersettings.strength.weak': "Debole",
|
||||
'usersettings.strength.fair': "Discreta",
|
||||
'usersettings.strength.good': "Buona",
|
||||
'usersettings.strength.strong': "Forte",
|
||||
'usersettings.aria.show_password': "Mostra password",
|
||||
'usersettings.aria.hide_password': "Nascondi password",
|
||||
'usersettings.btn.cancel': "Annulla",
|
||||
'usersettings.btn.saving': "Salvataggio…",
|
||||
'usersettings.btn.save_password': "Salva password",
|
||||
'usersettings.btn.save_email': "Salva email",
|
||||
'usersettings.btn.renaming': "Rinomina…",
|
||||
'usersettings.btn.rename': "Rinominami",
|
||||
'usersettings.error.fields_required': "Tutti i campi sono obbligatori.",
|
||||
'usersettings.error.password_min': "La password deve contenere almeno %count% caratteri.",
|
||||
'usersettings.error.password_long': "La password è troppo lunga.",
|
||||
'usersettings.error.password_mismatch': "Le nuove password non corrispondono.",
|
||||
'usersettings.error.password_same': "La nuova password deve essere diversa da quella attuale.",
|
||||
'usersettings.error.not_authenticated': "Non sei autenticato. Effettua di nuovo l'accesso.",
|
||||
'usersettings.error.network': "Impossibile raggiungere il server. Riprova.",
|
||||
'usersettings.error.request_failed': "Richiesta non riuscita (%status%).",
|
||||
'usersettings.error.email_long': "L'indirizzo email è troppo lungo.",
|
||||
'usersettings.error.email_invalid': "Inserisci un indirizzo email valido.",
|
||||
'usersettings.error.username_length': "Il nome utente deve contenere tra %min% e %max% caratteri.",
|
||||
'usersettings.error.username_invalid': "Il nome utente può contenere solo lettere, numeri, punto, trattino basso e trattino.",
|
||||
'usersettings.error.username_same': "Il nuovo nome utente deve essere diverso da quello attuale.",
|
||||
'usersettings.success.password': "Password aggiornata con successo.",
|
||||
'usersettings.success.email': "Email aggiornata con successo.",
|
||||
'usersettings.success.username': "Nome utente aggiornato. Accedi di nuovo con il tuo nuovo nome.",
|
||||
}
|
||||
|
||||
@@ -372,14 +372,14 @@
|
||||
// ------------------------------------------------------------------------
|
||||
// Login
|
||||
// ------------------------------------------------------------------------
|
||||
'login.username': 'Wat is jou Camwijs naam',
|
||||
'login.username': 'Wat is jou habbo naam',
|
||||
'login.forgot_password': 'Wachtwoord vergeten?',
|
||||
|
||||
// First-time visitors card
|
||||
'nitro.login.firsttime.title': 'Voor het eerst hier?',
|
||||
'nitro.login.firsttime.text': 'Heb je nog geen Camwijs account?',
|
||||
'nitro.login.firsttime.text': 'Heb je nog geen habbo account?',
|
||||
'nitro.login.firsttime.link': 'Je kunt er hier een aanmaken',
|
||||
'nitro.login.card.title': 'Aanmelden bij Camwijs',
|
||||
'nitro.login.card.title': 'Aanmelden bij habbo',
|
||||
|
||||
// Server status checks
|
||||
'nitro.login.server.offline.short': 'De gameserver draait momenteel niet. Probeer het zo meteen opnieuw.',
|
||||
@@ -388,12 +388,12 @@
|
||||
'nitro.login.server.retry': 'Opnieuw proberen',
|
||||
|
||||
// Registration flow
|
||||
'nitro.login.register.title': 'Camwijs-gegevens',
|
||||
'nitro.login.register.title': 'habbo-gegevens',
|
||||
'nitro.login.register.next': 'Volgende',
|
||||
'nitro.login.register.finish': 'Voltooien',
|
||||
'nitro.login.register.creating': 'Bezig met aanmaken…',
|
||||
'nitro.login.register.intro.credentials': 'Laten we je account aanmaken. Voer je e-mailadres in en kies een wachtwoord — we controleren of dit e-mailadres nog niet in gebruik is.',
|
||||
'nitro.login.register.intro.avatar': 'Nu is het tijd om je eigen Camwijs-personage te maken! Begin met het kiezen van je Camwijs-naam.',
|
||||
'nitro.login.register.intro.avatar': 'Nu is het tijd om je eigen habbo-personage te maken! Begin met het kiezen van je habbo-naam.',
|
||||
'nitro.login.register.intro.room': 'Laatste stap — kies een startkamer, of sla dit over en maak later je eigen kamer.',
|
||||
'nitro.login.register.confirm.label': 'Bevestig wachtwoord',
|
||||
'nitro.login.register.username.placeholder': 'HabboNaam',
|
||||
@@ -412,8 +412,8 @@
|
||||
'nitro.login.forgot.success': 'E-mail verzonden! Als er een account bij dit adres hoort, vind je binnenkort een resetlink in je inbox (controleer je spam als je binnen een minuut niets ziet).',
|
||||
|
||||
// Login errors (validation + transport)
|
||||
'nitro.login.error.missing_credentials': 'Voer zowel je Camwijs-naam als wachtwoord in.',
|
||||
'nitro.login.error.invalid_credentials': 'Ongeldige Camwijs-naam of wachtwoord.',
|
||||
'nitro.login.error.missing_credentials': 'Voer zowel je habbo-naam als wachtwoord in.',
|
||||
'nitro.login.error.invalid_credentials': 'Ongeldige habbo-naam of wachtwoord.',
|
||||
'nitro.login.error.too_many_attempts': 'Te veel pogingen. Probeer het opnieuw over %seconds%s.',
|
||||
'nitro.login.error.turnstile': 'Voltooi de beveiligingscontrole.',
|
||||
'nitro.login.error.server_offline': 'De gameserver draait niet. Probeer het later opnieuw.',
|
||||
@@ -427,9 +427,9 @@
|
||||
'nitro.login.error.password_too_short': 'Je wachtwoord moet minimaal 8 tekens lang zijn.',
|
||||
'nitro.login.error.password_mismatch': 'Wachtwoorden komen niet overeen.',
|
||||
'nitro.login.error.email_taken': 'Dit e-mailadres is al in gebruik.',
|
||||
'nitro.login.error.missing_username': 'Kies een Camwijs-naam.',
|
||||
'nitro.login.error.username_length': 'De Camwijs-naam moet 3–16 tekens bevatten.',
|
||||
'nitro.login.error.username_taken': 'Deze Camwijs-naam is al in gebruik.',
|
||||
'nitro.login.error.missing_username': 'Kies een habbo-naam.',
|
||||
'nitro.login.error.username_length': 'De habbo-naam moet 3–16 tekens bevatten.',
|
||||
'nitro.login.error.username_taken': 'Deze habbo-naam is al in gebruik.',
|
||||
'nitro.login.error.missing_email': 'Voer je e-mailadres in.',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
@@ -644,6 +644,9 @@
|
||||
'wheel.buy': 'Draaibeurt kopen',
|
||||
'wheel.winners': 'Laatste winnaars',
|
||||
'wheel.winners.empty': 'Nog geen winnaars',
|
||||
'wheel.win.title': 'Gewonnen!',
|
||||
'wheel.win.jackpot': '★ Jackpot ★',
|
||||
'wheel.win.nothing': 'Volgende keer beter!',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Soundboard
|
||||
@@ -676,6 +679,8 @@
|
||||
'rarevalues.editor.weight': 'Kans',
|
||||
'rarevalues.editor.label': 'Label',
|
||||
'rarevalues.editor.save': 'Opslaan',
|
||||
'rarevalues.editor.add': '+ Prijs toevoegen',
|
||||
'rarevalues.editor.remove': 'Prijs verwijderen',
|
||||
'rarevalues.editor.cat.item': 'Meubel (ID)',
|
||||
'rarevalues.editor.cat.spin': 'Extra draaien',
|
||||
'rarevalues.editor.cat.nothing': 'Niets',
|
||||
@@ -702,4 +707,71 @@
|
||||
'chatcmd.client.ejectall': 'Verwijder alle meubels',
|
||||
'chatcmd.client.settings': 'Kamerinstellingen',
|
||||
'chatcmd.client.info': 'Client info',
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Me-menu settings + User account settings window
|
||||
// ------------------------------------------------------------------------
|
||||
'usersettings.tab.general': "Algemeen",
|
||||
'usersettings.tab.themes': "Thema's",
|
||||
'memenu.settings.other.place.multiple.objects': "Meerdere objecten plaatsen",
|
||||
'memenu.settings.other.skip.purchase.confirmation': "Aankoopbevestiging overslaan",
|
||||
'memenu.settings.other.enable.chat.window': "Chatvenster inschakelen",
|
||||
'memenu.settings.other.catalog.classic.style': "Catalogus: klassieke stijl",
|
||||
'usersettings.open.title': "Gebruikersinstellingen",
|
||||
'usersettings.open.subtitle': "Wachtwoord & account",
|
||||
'usersettings.themes.custom': "Aangepast thema",
|
||||
'usersettings.themes.default_option': "Standaard (geen thema)",
|
||||
'usersettings.themes.active_pieces': "Actieve onderdelen",
|
||||
'usersettings.themes.invalid': "Thema ongeldig of onbereikbaar — standaard wordt gebruikt.",
|
||||
'usersettings.themes.none': "Geen thema's beschikbaar. Voeg een map toe in custom-themes/ op de server.",
|
||||
'usersettings.title': "Gebruikersinstellingen",
|
||||
'usersettings.account.label': "Mijn account",
|
||||
'usersettings.guest': "Gast",
|
||||
'usersettings.subtitle': "Beheer je account en beveiliging",
|
||||
'usersettings.menu.section': "Account",
|
||||
'usersettings.menu.password.title': "Wachtwoord wijzigen",
|
||||
'usersettings.menu.password.desc': "Wijzig het wachtwoord waarmee je inlogt.",
|
||||
'usersettings.menu.email.title': "E-mail wijzigen",
|
||||
'usersettings.menu.email.desc': "Werk het e-mailadres van je account bij.",
|
||||
'usersettings.menu.username.title': "Gebruikersnaam wijzigen",
|
||||
'usersettings.menu.username.desc': "Kies een nieuwe naam. Je moet daarna opnieuw inloggen.",
|
||||
'usersettings.menu.soon.title': "Meer komt binnenkort",
|
||||
'usersettings.menu.soon.desc': "Tweestapsverificatie en meer.",
|
||||
'usersettings.password.hint': "Gebruik minimaal %count% tekens. Combineer hoofd- en kleine letters, cijfers en symbolen voor een sterker wachtwoord.",
|
||||
'usersettings.email.hint': "Voor de veiligheid vragen we je je huidige wachtwoord te bevestigen voordat je het e-mailadres van je account wijzigt.",
|
||||
'usersettings.username.hint': "Door je naam te wijzigen word je uitgelogd en je kunt pas na 30 dagen opnieuw wijzigen. Zorg dat je vrienden je nieuwe naam kennen!",
|
||||
'usersettings.field.current_password': "Huidig wachtwoord",
|
||||
'usersettings.field.new_password': "Nieuw wachtwoord",
|
||||
'usersettings.field.retype_password': "Herhaal nieuw wachtwoord",
|
||||
'usersettings.field.new_email': "Nieuw e-mailadres",
|
||||
'usersettings.field.new_username': "Nieuwe gebruikersnaam",
|
||||
'usersettings.username.rules': "%min%-%max% tekens. Alleen letters, cijfers, punt, underscore en streepje.",
|
||||
'usersettings.strength.weak': "Zwak",
|
||||
'usersettings.strength.fair': "Redelijk",
|
||||
'usersettings.strength.good': "Goed",
|
||||
'usersettings.strength.strong': "Sterk",
|
||||
'usersettings.aria.show_password': "Wachtwoord tonen",
|
||||
'usersettings.aria.hide_password': "Wachtwoord verbergen",
|
||||
'usersettings.btn.cancel': "Annuleren",
|
||||
'usersettings.btn.saving': "Opslaan…",
|
||||
'usersettings.btn.save_password': "Wachtwoord opslaan",
|
||||
'usersettings.btn.save_email': "E-mail opslaan",
|
||||
'usersettings.btn.renaming': "Bezig met hernoemen…",
|
||||
'usersettings.btn.rename': "Hernoem mij",
|
||||
'usersettings.error.fields_required': "Alle velden zijn verplicht.",
|
||||
'usersettings.error.password_min': "Het wachtwoord moet minimaal %count% tekens bevatten.",
|
||||
'usersettings.error.password_long': "Het wachtwoord is te lang.",
|
||||
'usersettings.error.password_mismatch': "De nieuwe wachtwoorden komen niet overeen.",
|
||||
'usersettings.error.password_same': "Het nieuwe wachtwoord moet anders zijn dan het huidige wachtwoord.",
|
||||
'usersettings.error.not_authenticated': "Je bent niet ingelogd. Log opnieuw in.",
|
||||
'usersettings.error.network': "Kan de server niet bereiken. Probeer het opnieuw.",
|
||||
'usersettings.error.request_failed': "Verzoek mislukt (%status%).",
|
||||
'usersettings.error.email_long': "Het e-mailadres is te lang.",
|
||||
'usersettings.error.email_invalid': "Voer een geldig e-mailadres in.",
|
||||
'usersettings.error.username_length': "De gebruikersnaam moet tussen %min% en %max% tekens bevatten.",
|
||||
'usersettings.error.username_invalid': "De gebruikersnaam mag alleen letters, cijfers, punt, underscore en streepje bevatten.",
|
||||
'usersettings.error.username_same': "De nieuwe gebruikersnaam moet anders zijn dan de huidige.",
|
||||
'usersettings.success.password': "Wachtwoord succesvol bijgewerkt.",
|
||||
'usersettings.success.email': "E-mail succesvol bijgewerkt.",
|
||||
'usersettings.success.username': "Gebruikersnaam bijgewerkt. Log opnieuw in met je nieuwe naam.",
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"badge.asset.url": "${image.library.url}album1584/%badgename%.gif",
|
||||
"radio.url": "${gamedata.url}/radio-stations.json5?t=%timestamp%",
|
||||
"soundboard.url": "${gamedata.url}/soundboard-sounds.json5?t=%timestamp%",
|
||||
"radio_ui": false,
|
||||
"furni.rotation.bounce.steps": 20,
|
||||
"furni.rotation.bounce.height": 0.0625,
|
||||
"enable.avatar.arrow": false,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"wired.action.kick.from.room.max.length": 100,
|
||||
"wired.action.mute.user.max.length": 100,
|
||||
"game.center.enabled": false,
|
||||
"radio_ui.enabled": false,
|
||||
"guides.enabled": true,
|
||||
"housekeeping.enabled": true,
|
||||
"toolbar.hide.quests": true,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { LoginView } from './components/login/LoginView';
|
||||
import { MainView } from './components/MainView';
|
||||
import { ReconnectView } from './components/reconnect/ReconnectView';
|
||||
import { useMessageEvent, useNitroEvent } from './hooks';
|
||||
import { ensureChatCommandListener } from './hooks/rooms/widgets/useChatCommandSelector';
|
||||
|
||||
NitroVersion.UI_VERSION = GetUIVersion();
|
||||
|
||||
@@ -562,7 +563,9 @@ export const App: FC<{}> = props =>
|
||||
bumpProgress(85, taskLabel('loading.task.rooms', 'Loading rooms...'));
|
||||
await GetRoomEngine().init();
|
||||
bumpProgress(92, taskLabel('loading.task.engine', 'Loading graphics engine...'));
|
||||
ensureChatCommandListener();
|
||||
await GetCommunication().init();
|
||||
ensureChatCommandListener();
|
||||
bumpProgress(98, taskLabel('generic.reconnecting', 'Connecting to server...'));
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -5,6 +5,7 @@ export class RoomWidgetUpdateChatInputContentEvent extends RoomWidgetUpdateEvent
|
||||
public static CHAT_INPUT_CONTENT: string = 'RWUCICE_CHAT_INPUT_CONTENT';
|
||||
public static WHISPER: string = 'whisper';
|
||||
public static SHOUT: string = 'shout';
|
||||
public static TEXT: string = 'text';
|
||||
|
||||
private _chatMode: string = '';
|
||||
private _userName: string = '';
|
||||
|
||||
@@ -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:
|
||||
// <base>/index.json -> { "themes": [ { id, name, author? } ] }
|
||||
// <base>/<id>/theme.json -> { name, pieces: [ { id, name, file } ] }
|
||||
// <base>/<id>/<file>.css -> one CSS "piece" (cards, chat, catalog, ...)
|
||||
//
|
||||
// Each enabled piece is injected as a <link> in <head>. 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<string>('theme.base.url', 'custom-themes').replace(/\/+$/, '');
|
||||
|
||||
export const FetchThemeIndex = async (): Promise<ThemeInfo[]> =>
|
||||
{
|
||||
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<ThemeManifest> =>
|
||||
{
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ThemeManager';
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 695 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 806 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 551 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 880 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 381 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 891 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 798 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 645 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 632 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 656 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 534 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 625 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 661 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 325 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 455 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 533 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 408 B |
|
After Width: | Height: | Size: 112 B |
|
After Width: | Height: | Size: 825 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 831 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 408 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 906 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 573 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 879 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 113 B |
@@ -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 (
|
||||
<>
|
||||
<ThemeApplier />
|
||||
<div className="hidden" data-localization-version={ localizationVersion } />
|
||||
<AnimatePresence>
|
||||
{ landingViewVisible &&
|
||||
|
||||