Merge remote-tracking branch 'origin/Dev' into feat/messenger-groups-receipts

# Conflicts:
#	public/configuration/UITexts.example
#	src/css/friends/FriendsView.css
This commit is contained in:
simoleo89
2026-06-02 21:52:59 +02:00
225 changed files with 11432 additions and 1872 deletions
+8
View File
@@ -27,8 +27,16 @@ export class PageLocalization implements IPageLocalization
if(!imageName || !imageName.length) return null;
// Already a full URL (any extension) -> use it directly.
if(/^https?:\/\//i.test(imageName)) return imageName;
let assetUrl = GetConfigurationValue<string>('catalog.asset.image.url');
// The template forces ".gif" (.../%name%.gif). If the image name
// already carries its own extension (png/jpg/webp/gif), don't append
// the forced .gif so non-gif catalog images work too.
if(/\.[a-z0-9]+$/i.test(imageName)) assetUrl = assetUrl.replace(/\.gif(?=$|\?)/i, '');
assetUrl = assetUrl.replace('%name%', imageName);
return assetUrl;
+2
View File
@@ -27,6 +27,8 @@ export * from './purse';
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';
+1
View File
@@ -11,6 +11,7 @@ export class BotSkillsEnum
public static NUX_PROCEED: number = 8;
public static CHANGE_BOT_MOTTO: number = 9;
public static NUX_TAKE_TOUR: number = 10;
public static ROTATE: number = 11;
public static NO_PICK_UP: number = 12;
public static NAVIGATOR_SEARCH: number = 14;
public static DONATE_TO_USER: number = 24;
@@ -0,0 +1,7 @@
let _soundboardEnabled = false;
export const getSoundboardRoomEnabled = () => _soundboardEnabled;
export const setSoundboardRoomEnabled = (enabled: boolean) =>
{
_soundboardEnabled = enabled;
};
+1
View File
@@ -0,0 +1 @@
export * from './SoundboardRoomState';
+122
View File
@@ -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);
}
};
+1
View File
@@ -0,0 +1 @@
export * from './ThemeManager';
+3
View File
@@ -4,4 +4,7 @@ export class LocalStorageKeys
public static CATALOG_SKIP_PURCHASE_CONFIRMATION: string = 'catalogSkipPurchaseConfirmation';
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';
}