mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
ESLint --fix: auto-fix brace-style, indent, semi, no-trailing-spaces
Run eslint --fix across src/ to clear ~1900 mechanical lint errors surfaced by the @typescript-eslint v8 + react-hooks v7 + react-compiler upgrade in the React 19 modernization PR. Issues fixed automatically: - brace-style (Allman): try/catch one-liners reformatted to multi-line - indent: tab-vs-space and depth corrections - semi: missing trailing semicolons - no-trailing-spaces No semantic changes. Remaining 701 errors are real-code issues (set-state-in-effect, rules-of-hooks, no-unsafe-* type checks) that need manual per-file review. https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
This commit is contained in:
+24
-8
@@ -36,7 +36,8 @@ const preloadUrl = async (url: string): Promise<void> =>
|
|||||||
const response = await fetch(url, { cache: 'force-cache' });
|
const response = await fetch(url, { cache: 'force-cache' });
|
||||||
await response.arrayBuffer();
|
await response.arrayBuffer();
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
const preloadImage = (url: string): void =>
|
const preloadImage = (url: string): void =>
|
||||||
@@ -49,7 +50,8 @@ const preloadImage = (url: string): void =>
|
|||||||
image.decoding = 'async';
|
image.decoding = 'async';
|
||||||
image.src = url;
|
image.src = url;
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asStringArray = (value: unknown): string[] =>
|
const asStringArray = (value: unknown): string[] =>
|
||||||
@@ -126,8 +128,12 @@ export const App: FC<{}> = props =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
let payload: Record<string, unknown> = {};
|
let payload: Record<string, unknown> = {};
|
||||||
try { payload = await response.json(); }
|
try
|
||||||
catch {}
|
{
|
||||||
|
payload = await response.json();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
const ssoTicket = typeof payload.ssoTicket === 'string' ? payload.ssoTicket : (typeof payload.sso === 'string' ? payload.sso : '');
|
const ssoTicket = typeof payload.ssoTicket === 'string' ? payload.ssoTicket : (typeof payload.sso === 'string' ? payload.sso : '');
|
||||||
|
|
||||||
@@ -175,8 +181,12 @@ export const App: FC<{}> = props =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
let payload: Record<string, unknown> = {};
|
let payload: Record<string, unknown> = {};
|
||||||
try { payload = await response.json(); }
|
try
|
||||||
catch {}
|
{
|
||||||
|
payload = await response.json();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
if(response.ok)
|
if(response.ok)
|
||||||
{
|
{
|
||||||
@@ -320,8 +330,14 @@ export const App: FC<{}> = props =>
|
|||||||
// Configuration is loaded lazily — fetch it up-front so the login
|
// Configuration is loaded lazily — fetch it up-front so the login
|
||||||
// screen toggle and Turnstile keys are available before we decide.
|
// screen toggle and Turnstile keys are available before we decide.
|
||||||
let configInitError: unknown = null;
|
let configInitError: unknown = null;
|
||||||
try { await GetConfiguration().init(); }
|
try
|
||||||
catch(e) { configInitError = e; }
|
{
|
||||||
|
await GetConfiguration().init();
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
configInitError = e;
|
||||||
|
}
|
||||||
|
|
||||||
const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false);
|
const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false);
|
||||||
const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1;
|
const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1;
|
||||||
|
|||||||
@@ -17,13 +17,20 @@ export const setAccessToken = (token: string | null | undefined, expiresAt?: num
|
|||||||
window.localStorage.removeItem(EXPIRES_KEY);
|
window.localStorage.removeItem(EXPIRES_KEY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAccessToken = (): string =>
|
export const getAccessToken = (): string =>
|
||||||
{
|
{
|
||||||
try { return window.localStorage.getItem(STORAGE_KEY) ?? ''; }
|
try
|
||||||
catch { return ''; }
|
{
|
||||||
|
return window.localStorage.getItem(STORAGE_KEY) ?? '';
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAccessTokenExpiresAt = (): number =>
|
export const getAccessTokenExpiresAt = (): number =>
|
||||||
@@ -35,7 +42,10 @@ export const getAccessTokenExpiresAt = (): number =>
|
|||||||
const value = parseInt(raw, 10);
|
const value = parseInt(raw, 10);
|
||||||
return Number.isFinite(value) ? value : 0;
|
return Number.isFinite(value) ? value : 0;
|
||||||
}
|
}
|
||||||
catch { return 0; }
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearAccessToken = (): void =>
|
export const clearAccessToken = (): void =>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export class AvatarEditorThumbnailsHelper
|
|||||||
AvatarFigurePartType.PET,
|
AvatarFigurePartType.PET,
|
||||||
'ptl',
|
'ptl',
|
||||||
'ptr',
|
'ptr',
|
||||||
AvatarFigurePartType.MISC,
|
AvatarFigurePartType.MISC,
|
||||||
'mcl',
|
'mcl',
|
||||||
'mcr',
|
'mcr',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -31,8 +31,14 @@ export interface CustomBadgeError
|
|||||||
|
|
||||||
const interpolate = (value: string): string =>
|
const interpolate = (value: string): string =>
|
||||||
{
|
{
|
||||||
try { return GetConfiguration().interpolate(value); }
|
try
|
||||||
catch { return value; }
|
{
|
||||||
|
return GetConfiguration().interpolate(value);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getConfigUrl = (key: string, fallback: string): string =>
|
const getConfigUrl = (key: string, fallback: string): string =>
|
||||||
@@ -61,8 +67,14 @@ const parseJson = async <T>(response: Response): Promise<T> =>
|
|||||||
{
|
{
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if(!text) return {} as T;
|
if(!text) return {} as T;
|
||||||
try { return JSON.parse(text) as T; }
|
try
|
||||||
catch { throw new Error('Invalid response from server.'); }
|
{
|
||||||
|
return JSON.parse(text) as T;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new Error('Invalid response from server.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const throwOnError = async (response: Response): Promise<void> =>
|
const throwOnError = async (response: Response): Promise<void> =>
|
||||||
@@ -129,8 +141,14 @@ const injectTextsIntoLocalization = (texts: Record<string, string> | null | unde
|
|||||||
{
|
{
|
||||||
if(!texts) return;
|
if(!texts) return;
|
||||||
let manager: ReturnType<typeof GetLocalizationManager> | null = null;
|
let manager: ReturnType<typeof GetLocalizationManager> | null = null;
|
||||||
try { manager = GetLocalizationManager(); }
|
try
|
||||||
catch { return; }
|
{
|
||||||
|
manager = GetLocalizationManager();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(!manager || typeof manager.setValue !== 'function') return;
|
if(!manager || typeof manager.setValue !== 'function') return;
|
||||||
for(const key of Object.keys(texts))
|
for(const key of Object.keys(texts))
|
||||||
{
|
{
|
||||||
@@ -152,7 +170,8 @@ export const ensureCustomBadgeTexts = (): Promise<void> =>
|
|||||||
const payload = await parseJson<{ texts: Record<string, string> }>(response);
|
const payload = await parseJson<{ texts: Record<string, string> }>(response);
|
||||||
injectTextsIntoLocalization(payload.texts);
|
injectTextsIntoLocalization(payload.texts);
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
})();
|
})();
|
||||||
return customBadgeTextsLoadPromise;
|
return customBadgeTextsLoadPromise;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class FurnitureOffer implements IPurchasableOffer
|
|||||||
constructor(furniData: IFurnitureData)
|
constructor(furniData: IFurnitureData)
|
||||||
{
|
{
|
||||||
this._furniData = furniData;
|
this._furniData = furniData;
|
||||||
this._product = (new Product(this._furniData.type, this._furniData.id, this._furniData.customParams, 1, GetProductDataForLocalization(this._furniData.className), this._furniData) as IProduct);
|
this._product = (new Product(this._furniData.type, this._furniData.id, this._furniData.customParams, 1, GetProductDataForLocalization(this._furniData.className), this._furniData));
|
||||||
}
|
}
|
||||||
|
|
||||||
public activate(): void
|
public activate(): void
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ export const GetGroupChatData = (extraData: string) =>
|
|||||||
const figure = splitData[1];
|
const figure = splitData[1];
|
||||||
const userId = parseInt(splitData[2]);
|
const userId = parseInt(splitData[2]);
|
||||||
|
|
||||||
return ({ username: username, figure: figure, userId: userId } as IGroupChatData);
|
return ({ username: username, figure: figure, userId: userId });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class AvatarInfoUser implements IAvatarInfo
|
|||||||
public prefixFont: string = '';
|
public prefixFont: string = '';
|
||||||
public displayOrder: string = 'icon-prefix-name';
|
public displayOrder: string = 'icon-prefix-name';
|
||||||
public achievementScore: number = 0;
|
public achievementScore: number = 0;
|
||||||
public backgroundId: number = 0;
|
public backgroundId: number = 0;
|
||||||
public standId: number = 0;
|
public standId: number = 0;
|
||||||
public overlayId: number = 0;
|
public overlayId: number = 0;
|
||||||
public cardBackgroundId: number = 0;
|
public cardBackgroundId: number = 0;
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ export class AvatarInfoUtilities
|
|||||||
userInfo.prefixEffect = userData.prefixEffect;
|
userInfo.prefixEffect = userData.prefixEffect;
|
||||||
userInfo.prefixFont = userData.prefixFont;
|
userInfo.prefixFont = userData.prefixFont;
|
||||||
userInfo.displayOrder = userData.displayOrder;
|
userInfo.displayOrder = userData.displayOrder;
|
||||||
userInfo.backgroundId = userData.background;
|
userInfo.backgroundId = userData.background;
|
||||||
userInfo.standId = userData.stand;
|
userInfo.standId = userData.stand;
|
||||||
userInfo.overlayId = userData.overlay;
|
userInfo.overlayId = userData.overlay;
|
||||||
userInfo.cardBackgroundId = userData.cardBackground ?? 0;
|
userInfo.cardBackgroundId = userData.cardBackground ?? 0;
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ export class chooserSelectionVisualizer
|
|||||||
{
|
{
|
||||||
if (this.animationFrameId !== null) return;
|
if (this.animationFrameId !== null) return;
|
||||||
|
|
||||||
const animate = (time: number) => {
|
const animate = (time: number) =>
|
||||||
|
{
|
||||||
const elapsed = time / 1000; // Convert to seconds
|
const elapsed = time / 1000; // Convert to seconds
|
||||||
this.activeFilters.forEach(filter => {
|
this.activeFilters.forEach(filter =>
|
||||||
|
{
|
||||||
filter.time = elapsed; // Update time uniform
|
filter.time = elapsed; // Update time uniform
|
||||||
});
|
});
|
||||||
this.animationFrameId = requestAnimationFrame(animate);
|
this.animationFrameId = requestAnimationFrame(animate);
|
||||||
@@ -22,7 +24,8 @@ export class chooserSelectionVisualizer
|
|||||||
|
|
||||||
private static stopAnimation(): void
|
private static stopAnimation(): void
|
||||||
{
|
{
|
||||||
if (this.animationFrameId !== null) {
|
if (this.animationFrameId !== null)
|
||||||
|
{
|
||||||
cancelAnimationFrame(this.animationFrameId);
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
}
|
}
|
||||||
@@ -69,7 +72,8 @@ export class chooserSelectionVisualizer
|
|||||||
sprite.filters = (sprite.filters || []).filter(f => !(f instanceof ChooserSelectionFilter));
|
sprite.filters = (sprite.filters || []).filter(f => !(f instanceof ChooserSelectionFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activeFilters.size === 0) {
|
if (this.activeFilters.size === 0)
|
||||||
|
{
|
||||||
this.stopAnimation();
|
this.stopAnimation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,6 @@ export class MannequinUtilities
|
|||||||
figureContainer.removePart(part);
|
figureContainer.removePart(part);
|
||||||
}
|
}
|
||||||
|
|
||||||
figureContainer.updatePart((this.MANNEQUIN_FIGURE[0] as string), (this.MANNEQUIN_FIGURE[1] as number), (this.MANNEQUIN_FIGURE[2] as number[]));
|
figureContainer.updatePart((this.MANNEQUIN_FIGURE[0]), (this.MANNEQUIN_FIGURE[1]), (this.MANNEQUIN_FIGURE[2]));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ interface IUiSettingsContext
|
|||||||
const UiSettingsContext = createContext<IUiSettingsContext>({
|
const UiSettingsContext = createContext<IUiSettingsContext>({
|
||||||
settings: DEFAULT_UI_SETTINGS,
|
settings: DEFAULT_UI_SETTINGS,
|
||||||
isCustomActive: false,
|
isCustomActive: false,
|
||||||
updateSettings: () => {},
|
updateSettings: () =>
|
||||||
resetSettings: () => {},
|
{},
|
||||||
|
resetSettings: () =>
|
||||||
|
{},
|
||||||
getHeaderStyle: () => ({}),
|
getHeaderStyle: () => ({}),
|
||||||
getTabsStyle: () => ({}),
|
getTabsStyle: () => ({}),
|
||||||
getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor
|
getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor
|
||||||
@@ -42,7 +44,8 @@ const loadSettings = (): IUiSettings =>
|
|||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) };
|
if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) };
|
||||||
}
|
}
|
||||||
catch(e) {}
|
catch(e)
|
||||||
|
{}
|
||||||
|
|
||||||
return { ...DEFAULT_UI_SETTINGS };
|
return { ...DEFAULT_UI_SETTINGS };
|
||||||
};
|
};
|
||||||
@@ -53,7 +56,8 @@ const saveSettings = (settings: IUiSettings): void =>
|
|||||||
{
|
{
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||||
}
|
}
|
||||||
catch(e) {}
|
catch(e)
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendComposer = (composer: any): void =>
|
const sendComposer = (composer: any): void =>
|
||||||
@@ -62,7 +66,8 @@ const sendComposer = (composer: any): void =>
|
|||||||
{
|
{
|
||||||
GetCommunication()?.connection?.send(composer);
|
GetCommunication()?.connection?.send(composer);
|
||||||
}
|
}
|
||||||
catch(e) {}
|
catch(e)
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UiSettingsProvider: FC<PropsWithChildren> = ({ children }) =>
|
export const UiSettingsProvider: FC<PropsWithChildren> = ({ children }) =>
|
||||||
@@ -93,7 +98,8 @@ export const UiSettingsProvider: FC<PropsWithChildren> = ({ children }) =>
|
|||||||
saveSettings(serverSettings);
|
saveSettings(serverSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e) {}
|
catch(e)
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
connection.addMessageEvent(new UiSettingsDataEvent(handler));
|
connection.addMessageEvent(new UiSettingsDataEvent(handler));
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class ProductImageUtility
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FurnitureType.EFFECT:
|
case FurnitureType.EFFECT:
|
||||||
// fx_icon_furniClassId_png
|
// fx_icon_furniClassId_png
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,8 +53,12 @@ export const SetRememberLogin = (data: RememberLoginData): void =>
|
|||||||
{
|
{
|
||||||
if(!data?.token?.length && !data?.ssoTicket?.length) return;
|
if(!data?.token?.length && !data?.ssoTicket?.length) return;
|
||||||
|
|
||||||
try { window.localStorage.setItem(REMEMBER_LOGIN_KEY, JSON.stringify(data)); }
|
try
|
||||||
catch {}
|
{
|
||||||
|
window.localStorage.setItem(REMEMBER_LOGIN_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ClearRememberLogin = (): void =>
|
export const ClearRememberLogin = (): void =>
|
||||||
@@ -64,7 +68,8 @@ export const ClearRememberLogin = (): void =>
|
|||||||
window.localStorage.removeItem(REMEMBER_LOGIN_KEY);
|
window.localStorage.removeItem(REMEMBER_LOGIN_KEY);
|
||||||
window.localStorage.removeItem(LEGACY_REMEMBER_LOGIN_KEY);
|
window.localStorage.removeItem(LEGACY_REMEMBER_LOGIN_KEY);
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StoreRememberLoginFromPayload = (payload: Record<string, unknown>, username?: string, ssoTicket?: string): void =>
|
export const StoreRememberLoginFromPayload = (payload: Record<string, unknown>, username?: string, ssoTicket?: string): void =>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
let _youtubeEnabled = false;
|
let _youtubeEnabled = false;
|
||||||
|
|
||||||
export const getYoutubeRoomEnabled = () => _youtubeEnabled;
|
export const getYoutubeRoomEnabled = () => _youtubeEnabled;
|
||||||
export const setYoutubeRoomEnabled = (enabled: boolean) => { _youtubeEnabled = enabled; };
|
export const setYoutubeRoomEnabled = (enabled: boolean) =>
|
||||||
|
{
|
||||||
|
_youtubeEnabled = enabled;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const rawNickIcons = import.meta.glob('./*.gif', { eager: true, import: 'default' }) as Record<string, string>;
|
const rawNickIcons = import.meta.glob('./*.gif', { eager: true, import: 'default' });
|
||||||
|
|
||||||
export const NICK_ICON_URLS: Record<string, string> = Object.entries(rawNickIcons).reduce((accumulator, [ path, url ]) =>
|
export const NICK_ICON_URLS: Record<string, string> = Object.entries(rawNickIcons).reduce((accumulator, [ path, url ]) =>
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-4
@@ -26,7 +26,8 @@ const setBootDebug = (message: string) =>
|
|||||||
|
|
||||||
if(secureNode) secureNode.textContent = `${ secureNode.textContent }\n${ message }`;
|
if(secureNode) secureNode.textContent = `${ secureNode.textContent }\n${ message }`;
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
setBootDebug('boot: secure fetch installed');
|
setBootDebug('boot: secure fetch installed');
|
||||||
@@ -38,14 +39,16 @@ const deployBaseUrl = (): string =>
|
|||||||
const loaderBase = (window as any).__nitroLoaderBase;
|
const loaderBase = (window as any).__nitroLoaderBase;
|
||||||
if(typeof loaderBase === 'string' && loaderBase.length) return new URL('..', loaderBase).toString();
|
if(typeof loaderBase === 'string' && loaderBase.length) return new URL('..', loaderBase).toString();
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const moduleUrl = (import.meta as any).url;
|
const moduleUrl = (import.meta as any).url;
|
||||||
if(typeof moduleUrl === 'string' && moduleUrl.length) return new URL('..', new URL('.', moduleUrl)).toString();
|
if(typeof moduleUrl === 'string' && moduleUrl.length) return new URL('..', new URL('.', moduleUrl)).toString();
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -56,7 +59,8 @@ const deployBaseUrl = (): string =>
|
|||||||
return trimmed ? `${ window.location.origin }/${ trimmed }/` : `${ window.location.origin }/`;
|
return trimmed ? `${ window.location.origin }/${ trimmed }/` : `${ window.location.origin }/`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
return `${ window.location.origin }/`;
|
return `${ window.location.origin }/`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export const Button: FC<ButtonProps> = props =>
|
|||||||
if(variant == 'dark')
|
if(variant == 'dark')
|
||||||
newClassNames.push('text-white bg-dark [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#18181bfb] hover:border-[#161619fb]');
|
newClassNames.push('text-white bg-dark [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#18181bfb] hover:border-[#161619fb]');
|
||||||
|
|
||||||
if(variant == 'gray')
|
if(variant == 'gray')
|
||||||
newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]');
|
newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ export const ButtonGroup: FC<ButtonGroupProps> = props =>
|
|||||||
}, [ classNames ]);
|
}, [ classNames ]);
|
||||||
|
|
||||||
return <Base classNames={ getClassNames } { ...rest } />;
|
return <Base classNames={ getClassNames } { ...rest } />;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -145,4 +145,4 @@ export const Slider: FC<SliderProps> = props =>
|
|||||||
) }
|
) }
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+16
-13
@@ -20,7 +20,8 @@ export interface TextProps extends BaseProps<HTMLDivElement> {
|
|||||||
textBreak?: boolean;
|
textBreak?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Text: FC<TextProps> = props => {
|
export const Text: FC<TextProps> = props =>
|
||||||
|
{
|
||||||
const {
|
const {
|
||||||
variant = 'black',
|
variant = 'black',
|
||||||
fontWeight = null,
|
fontWeight = null,
|
||||||
@@ -40,20 +41,22 @@ export const Text: FC<TextProps> = props => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const getClassNames = useMemo(() => {
|
const getClassNames = useMemo(() =>
|
||||||
|
{
|
||||||
const newClassNames: string[] = [truncate ? 'block' : 'inline'];
|
const newClassNames: string[] = [truncate ? 'block' : 'inline'];
|
||||||
|
|
||||||
if (variant) {
|
if (variant)
|
||||||
if (variant === 'primary') newClassNames.push('text-[#1e7295]');
|
{
|
||||||
if (variant == 'secondary') newClassNames.push('text-[#185d79]');
|
if (variant === 'primary') newClassNames.push('text-[#1e7295]');
|
||||||
if (variant === 'black') newClassNames.push('text-[#000000]');
|
if (variant == 'secondary') newClassNames.push('text-[#185d79]');
|
||||||
if (variant == 'dark') newClassNames.push('text-[#18181b]');
|
if (variant === 'black') newClassNames.push('text-[#000000]');
|
||||||
if (variant === 'gray') newClassNames.push('text-[#6b7280]');
|
if (variant == 'dark') newClassNames.push('text-[#18181b]');
|
||||||
if (variant === 'white') newClassNames.push('text-[#ffffff]');
|
if (variant === 'gray') newClassNames.push('text-[#6b7280]');
|
||||||
if (variant == 'success') newClassNames.push('text-[#00800b]');
|
if (variant === 'white') newClassNames.push('text-[#ffffff]');
|
||||||
if (variant == 'danger') newClassNames.push('text-[#a81a12]');
|
if (variant == 'success') newClassNames.push('text-[#00800b]');
|
||||||
if (variant == 'warning') newClassNames.push('text-[#ffc107]');
|
if (variant == 'danger') newClassNames.push('text-[#a81a12]');
|
||||||
}
|
if (variant == 'warning') newClassNames.push('text-[#ffc107]');
|
||||||
|
}
|
||||||
|
|
||||||
if (bold) newClassNames.push('font-bold');
|
if (bold) newClassNames.push('font-bold');
|
||||||
if (fontWeight) newClassNames.push('font-' + fontWeight);
|
if (fontWeight) newClassNames.push('font-' + fontWeight);
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export interface DraggableWindowProps {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
export const DraggableWindow: FC<DraggableWindowProps> = props =>
|
||||||
|
{
|
||||||
const { uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, dragStyle = {}, children = null, offsetLeft = 0, offsetTop = 0 } = props;
|
const { uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, dragStyle = {}, children = null, offsetLeft = 0, offsetTop = 0 } = props;
|
||||||
const [delta, setDelta] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
|
const [delta, setDelta] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
|
||||||
const [offset, setOffset] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
|
const [offset, setOffset] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
|
||||||
@@ -31,49 +32,61 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
const [dragHandler, setDragHandler] = useState<HTMLElement>(null);
|
const [dragHandler, setDragHandler] = useState<HTMLElement>(null);
|
||||||
const elementRef = useRef<HTMLDivElement>();
|
const elementRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
const bringToTop = useCallback(() => {
|
const bringToTop = useCallback(() =>
|
||||||
|
{
|
||||||
let zIndex = 400;
|
let zIndex = 400;
|
||||||
for (const existingWindow of CURRENT_WINDOWS) {
|
for (const existingWindow of CURRENT_WINDOWS)
|
||||||
|
{
|
||||||
zIndex += 1;
|
zIndex += 1;
|
||||||
existingWindow.style.zIndex = zIndex.toString();
|
existingWindow.style.zIndex = zIndex.toString();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const moveCurrentWindow = useCallback(() => {
|
const moveCurrentWindow = useCallback(() =>
|
||||||
|
{
|
||||||
const index = CURRENT_WINDOWS.indexOf(elementRef.current);
|
const index = CURRENT_WINDOWS.indexOf(elementRef.current);
|
||||||
if (index === -1) {
|
if (index === -1)
|
||||||
|
{
|
||||||
CURRENT_WINDOWS.push(elementRef.current);
|
CURRENT_WINDOWS.push(elementRef.current);
|
||||||
} else if (index === (CURRENT_WINDOWS.length - 1)) return;
|
}
|
||||||
else if (index >= 0) {
|
else if (index === (CURRENT_WINDOWS.length - 1)) return;
|
||||||
|
else if (index >= 0)
|
||||||
|
{
|
||||||
CURRENT_WINDOWS.splice(index, 1);
|
CURRENT_WINDOWS.splice(index, 1);
|
||||||
CURRENT_WINDOWS.push(elementRef.current);
|
CURRENT_WINDOWS.push(elementRef.current);
|
||||||
}
|
}
|
||||||
bringToTop();
|
bringToTop();
|
||||||
}, [bringToTop]);
|
}, [bringToTop]);
|
||||||
|
|
||||||
const onMouseDown = useCallback((event: ReactMouseEvent<HTMLDivElement>) => {
|
const onMouseDown = useCallback((event: ReactMouseEvent<HTMLDivElement>) =>
|
||||||
|
{
|
||||||
moveCurrentWindow();
|
moveCurrentWindow();
|
||||||
}, [moveCurrentWindow]);
|
}, [moveCurrentWindow]);
|
||||||
|
|
||||||
const onTouchStart = useCallback((event: ReactTouchEvent<HTMLDivElement>) => {
|
const onTouchStart = useCallback((event: ReactTouchEvent<HTMLDivElement>) =>
|
||||||
|
{
|
||||||
moveCurrentWindow();
|
moveCurrentWindow();
|
||||||
}, [moveCurrentWindow]);
|
}, [moveCurrentWindow]);
|
||||||
|
|
||||||
const startDragging = useCallback((startX: number, startY: number) => {
|
const startDragging = useCallback((startX: number, startY: number) =>
|
||||||
|
{
|
||||||
setStart({ x: startX, y: startY });
|
setStart({ x: startX, y: startY });
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onDragMouseDown = useCallback((event: MouseEvent) => {
|
const onDragMouseDown = useCallback((event: MouseEvent) =>
|
||||||
|
{
|
||||||
startDragging(event.clientX, event.clientY);
|
startDragging(event.clientX, event.clientY);
|
||||||
}, [startDragging]);
|
}, [startDragging]);
|
||||||
|
|
||||||
const onTouchDown = useCallback((event: TouchEvent) => {
|
const onTouchDown = useCallback((event: TouchEvent) =>
|
||||||
|
{
|
||||||
const touch = event.touches[0];
|
const touch = event.touches[0];
|
||||||
startDragging(touch.clientX, touch.clientY);
|
startDragging(touch.clientX, touch.clientY);
|
||||||
}, [startDragging]);
|
}, [startDragging]);
|
||||||
|
|
||||||
const clampPosition = useCallback((newX: number, newY: number) => {
|
const clampPosition = useCallback((newX: number, newY: number) =>
|
||||||
|
{
|
||||||
if (!elementRef.current) return { x: newX, y: newY };
|
if (!elementRef.current) return { x: newX, y: newY };
|
||||||
|
|
||||||
const windowWidth = elementRef.current.offsetWidth;
|
const windowWidth = elementRef.current.offsetWidth;
|
||||||
@@ -88,7 +101,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
return { x: clampedX, y: clampedY };
|
return { x: clampedX, y: clampedY };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onDragMouseMove = useCallback((event: MouseEvent) => {
|
const onDragMouseMove = useCallback((event: MouseEvent) =>
|
||||||
|
{
|
||||||
if (!elementRef.current || !isDragging) return;
|
if (!elementRef.current || !isDragging) return;
|
||||||
|
|
||||||
const newDeltaX = event.clientX - start.x;
|
const newDeltaX = event.clientX - start.x;
|
||||||
@@ -100,7 +114,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y });
|
setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y });
|
||||||
}, [start, offset, clampPosition, isDragging]);
|
}, [start, offset, clampPosition, isDragging]);
|
||||||
|
|
||||||
const onDragTouchMove = useCallback((event: TouchEvent) => {
|
const onDragTouchMove = useCallback((event: TouchEvent) =>
|
||||||
|
{
|
||||||
if (!elementRef.current || !isDragging) return;
|
if (!elementRef.current || !isDragging) return;
|
||||||
|
|
||||||
const touch = event.touches[0];
|
const touch = event.touches[0];
|
||||||
@@ -113,7 +128,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y });
|
setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y });
|
||||||
}, [start, offset, clampPosition, isDragging]);
|
}, [start, offset, clampPosition, isDragging]);
|
||||||
|
|
||||||
const completeDrag = useCallback(() => {
|
const completeDrag = useCallback(() =>
|
||||||
|
{
|
||||||
if (!elementRef.current || !dragHandler || !isDragging) return;
|
if (!elementRef.current || !dragHandler || !isDragging) return;
|
||||||
|
|
||||||
const finalOffsetX = offset.x + delta.x;
|
const finalOffsetX = offset.x + delta.x;
|
||||||
@@ -124,29 +140,34 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
setOffset({ x: clampedPos.x, y: clampedPos.y });
|
setOffset({ x: clampedPos.x, y: clampedPos.y });
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
|
||||||
if (uniqueKey !== null) {
|
if (uniqueKey !== null)
|
||||||
const newStorage = { ...GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`) } as WindowSaveOptions;
|
{
|
||||||
|
const newStorage = { ...GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`) };
|
||||||
newStorage.offset = { x: clampedPos.x, y: clampedPos.y };
|
newStorage.offset = { x: clampedPos.x, y: clampedPos.y };
|
||||||
SetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`, newStorage);
|
SetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`, newStorage);
|
||||||
}
|
}
|
||||||
}, [dragHandler, delta, offset, uniqueKey, clampPosition, isDragging]);
|
}, [dragHandler, delta, offset, uniqueKey, clampPosition, isDragging]);
|
||||||
|
|
||||||
const onDragMouseUp = useCallback((event: MouseEvent) => {
|
const onDragMouseUp = useCallback((event: MouseEvent) =>
|
||||||
|
{
|
||||||
completeDrag();
|
completeDrag();
|
||||||
}, [completeDrag]);
|
}, [completeDrag]);
|
||||||
|
|
||||||
const onDragTouchUp = useCallback((event: TouchEvent) => {
|
const onDragTouchUp = useCallback((event: TouchEvent) =>
|
||||||
|
{
|
||||||
completeDrag();
|
completeDrag();
|
||||||
}, [completeDrag]);
|
}, [completeDrag]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() =>
|
||||||
|
{
|
||||||
const element = elementRef.current as HTMLElement;
|
const element = elementRef.current as HTMLElement;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
CURRENT_WINDOWS.push(element);
|
CURRENT_WINDOWS.push(element);
|
||||||
bringToTop();
|
bringToTop();
|
||||||
|
|
||||||
if (!disableDrag) {
|
if (!disableDrag)
|
||||||
|
{
|
||||||
const handle = element.querySelector(handleSelector);
|
const handle = element.querySelector(handleSelector);
|
||||||
if (handle) setDragHandler(handle as HTMLElement);
|
if (handle) setDragHandler(handle as HTMLElement);
|
||||||
}
|
}
|
||||||
@@ -156,7 +177,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
let offsetY = 0;
|
let offsetY = 0;
|
||||||
|
|
||||||
switch (windowPosition) {
|
switch (windowPosition)
|
||||||
|
{
|
||||||
case DraggableWindowPosition.TOP_CENTER:
|
case DraggableWindowPosition.TOP_CENTER:
|
||||||
offsetY = 50 + offsetTop;
|
offsetY = 50 + offsetTop;
|
||||||
offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft;
|
offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft;
|
||||||
@@ -176,25 +198,29 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
setDelta({ x: 0, y: 0 });
|
setDelta({ x: 0, y: 0 });
|
||||||
setIsPositioned(true);
|
setIsPositioned(true);
|
||||||
|
|
||||||
return () => {
|
return () =>
|
||||||
|
{
|
||||||
const index = CURRENT_WINDOWS.indexOf(element);
|
const index = CURRENT_WINDOWS.indexOf(element);
|
||||||
if (index >= 0) CURRENT_WINDOWS.splice(index, 1);
|
if (index >= 0) CURRENT_WINDOWS.splice(index, 1);
|
||||||
};
|
};
|
||||||
}, [handleSelector, windowPosition, uniqueKey, disableDrag, offsetLeft, offsetTop, bringToTop]);
|
}, [handleSelector, windowPosition, uniqueKey, disableDrag, offsetLeft, offsetTop, bringToTop]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
if (!dragHandler) return;
|
if (!dragHandler) return;
|
||||||
|
|
||||||
dragHandler.addEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
|
dragHandler.addEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
|
||||||
dragHandler.addEventListener(TouchEventType.TOUCH_START, onTouchDown);
|
dragHandler.addEventListener(TouchEventType.TOUCH_START, onTouchDown);
|
||||||
|
|
||||||
return () => {
|
return () =>
|
||||||
|
{
|
||||||
dragHandler.removeEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
|
dragHandler.removeEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
|
||||||
dragHandler.removeEventListener(TouchEventType.TOUCH_START, onTouchDown);
|
dragHandler.removeEventListener(TouchEventType.TOUCH_START, onTouchDown);
|
||||||
};
|
};
|
||||||
}, [dragHandler, onDragMouseDown, onTouchDown]);
|
}, [dragHandler, onDragMouseDown, onTouchDown]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
document.addEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
|
document.addEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
|
||||||
@@ -202,7 +228,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
document.addEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
|
document.addEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
|
||||||
document.addEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove);
|
document.addEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove);
|
||||||
|
|
||||||
return () => {
|
return () =>
|
||||||
|
{
|
||||||
document.removeEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
|
document.removeEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
|
||||||
document.removeEventListener(TouchEventType.TOUCH_END, onDragTouchUp);
|
document.removeEventListener(TouchEventType.TOUCH_END, onDragTouchUp);
|
||||||
document.removeEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
|
document.removeEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
|
||||||
@@ -210,7 +237,8 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
|||||||
};
|
};
|
||||||
}, [isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove]);
|
}, [isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
if (!uniqueKey) return;
|
if (!uniqueKey) return;
|
||||||
|
|
||||||
const localStorage = GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`);
|
const localStorage = GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`);
|
||||||
|
|||||||
+1
-1
@@ -19,5 +19,5 @@ export * from './draggable-window';
|
|||||||
export * from './layout';
|
export * from './layout';
|
||||||
export * from './layout/limited-edition';
|
export * from './layout/limited-edition';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from "./Slider";
|
export * from './Slider';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props =>
|
|||||||
{
|
{
|
||||||
isMounted.current = true;
|
isMounted.current = true;
|
||||||
|
|
||||||
return () => { isMounted.current = false; };
|
return () =>
|
||||||
|
{
|
||||||
|
isMounted.current = false;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateImage = useCallback(async (texture: any) =>
|
const updateImage = useCallback(async (texture: any) =>
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ interface LayoutMiniCameraViewProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LayoutMiniCameraView: FC<LayoutMiniCameraViewProps> = props => {
|
export const LayoutMiniCameraView: FC<LayoutMiniCameraViewProps> = props =>
|
||||||
|
{
|
||||||
const { roomId = -1, textureReceiver = null, onClose = null } = props;
|
const { roomId = -1, textureReceiver = null, onClose = null } = props;
|
||||||
const elementRef = useRef<HTMLDivElement>();
|
const elementRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
const getCameraBounds = () => {
|
const getCameraBounds = () =>
|
||||||
|
{
|
||||||
if (!elementRef || !elementRef.current) return null;
|
if (!elementRef || !elementRef.current) return null;
|
||||||
|
|
||||||
const frameBounds = elementRef.current.getBoundingClientRect();
|
const frameBounds = elementRef.current.getBoundingClientRect();
|
||||||
@@ -26,7 +28,8 @@ export const LayoutMiniCameraView: FC<LayoutMiniCameraViewProps> = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const takePicture = () => {
|
const takePicture = () =>
|
||||||
|
{
|
||||||
PlaySound(SoundNames.CAMERA_SHUTTER);
|
PlaySound(SoundNames.CAMERA_SHUTTER);
|
||||||
textureReceiver(GetRoomEngine().createTextureFromRoom(roomId, 1, getCameraBounds()));
|
textureReceiver(GetRoomEngine().createTextureFromRoom(roomId, 1, getCameraBounds()));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ export const LayoutRoomObjectImageView: FC<LayoutRoomObjectImageViewProps> = pro
|
|||||||
{
|
{
|
||||||
isMounted.current = true;
|
isMounted.current = true;
|
||||||
|
|
||||||
return () => { isMounted.current = false; };
|
return () =>
|
||||||
|
{
|
||||||
|
isMounted.current = false;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getStyle = useMemo(() =>
|
const getStyle = useMemo(() =>
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export const MainView: FC<{}> = props =>
|
|||||||
<AvatarEffectsView />
|
<AvatarEffectsView />
|
||||||
<AchievementsView />
|
<AchievementsView />
|
||||||
<NavigatorView />
|
<NavigatorView />
|
||||||
<NitrobubbleHiddenView />
|
<NitrobubbleHiddenView />
|
||||||
<InventoryView />
|
<InventoryView />
|
||||||
<CatalogView />
|
<CatalogView />
|
||||||
<FriendsView />
|
<FriendsView />
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ interface AdsenseConfig {
|
|||||||
fullWidthResponsive?: boolean;
|
fullWidthResponsive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsePublisherIdFromAdsTxt = (text: string): string | null => {
|
const parsePublisherIdFromAdsTxt = (text: string): string | null =>
|
||||||
for (const rawLine of text.split(/\r?\n/)) {
|
{
|
||||||
|
for (const rawLine of text.split(/\r?\n/))
|
||||||
|
{
|
||||||
const line = rawLine.split('#')[0].trim();
|
const line = rawLine.split('#')[0].trim();
|
||||||
if (!line) continue;
|
if (!line) continue;
|
||||||
const parts = line.split(',').map(part => part.trim());
|
const parts = line.split(',').map(part => part.trim());
|
||||||
@@ -22,7 +24,8 @@ const parsePublisherIdFromAdsTxt = (text: string): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GoogleAdsView: FC<{}> = () => {
|
export const GoogleAdsView: FC<{}> = () =>
|
||||||
|
{
|
||||||
const adsEnabled = GetConfigurationValue<boolean>('show.google.ads', false);
|
const adsEnabled = GetConfigurationValue<boolean>('show.google.ads', false);
|
||||||
const [ isOpen, setIsOpen ] = useState(false);
|
const [ isOpen, setIsOpen ] = useState(false);
|
||||||
const [ publisherId, setPublisherId ] = useState<string | null>(null);
|
const [ publisherId, setPublisherId ] = useState<string | null>(null);
|
||||||
@@ -32,7 +35,8 @@ export const GoogleAdsView: FC<{}> = () => {
|
|||||||
const pushedRef = useRef(false);
|
const pushedRef = useRef(false);
|
||||||
const autoOpenedRef = useRef(false);
|
const autoOpenedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
if (!adsEnabled) return;
|
if (!adsEnabled) return;
|
||||||
const handler = () => setIsOpen(prev => !prev);
|
const handler = () => setIsOpen(prev => !prev);
|
||||||
window.addEventListener('ads:toggle', handler);
|
window.addEventListener('ads:toggle', handler);
|
||||||
@@ -42,7 +46,8 @@ export const GoogleAdsView: FC<{}> = () => {
|
|||||||
// Auto-open once on initial mount (the login / landing stage).
|
// Auto-open once on initial mount (the login / landing stage).
|
||||||
// Subsequent toggles are driven by the "ads:toggle" window event
|
// Subsequent toggles are driven by the "ads:toggle" window event
|
||||||
// (e.g. the Show Ad button in NitroSystemAlertView).
|
// (e.g. the Show Ad button in NitroSystemAlertView).
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
if (!adsEnabled) return;
|
if (!adsEnabled) return;
|
||||||
if (autoOpenedRef.current) return;
|
if (autoOpenedRef.current) return;
|
||||||
autoOpenedRef.current = true;
|
autoOpenedRef.current = true;
|
||||||
@@ -50,11 +55,14 @@ export const GoogleAdsView: FC<{}> = () => {
|
|||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [ adsEnabled ]);
|
}, [ adsEnabled ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
(async () => {
|
(async () =>
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
const [ adsTxtRes, configRes ] = await Promise.all([
|
const [ adsTxtRes, configRes ] = await Promise.all([
|
||||||
fetch('/ads.txt', { cache: 'no-cache' }),
|
fetch('/ads.txt', { cache: 'no-cache' }),
|
||||||
fetch(configFileUrl('adsense.json', true), { cache: 'no-cache' })
|
fetch(configFileUrl('adsense.json', true), { cache: 'no-cache' })
|
||||||
@@ -73,39 +81,54 @@ export const GoogleAdsView: FC<{}> = () => {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setPublisherId(pubId);
|
setPublisherId(pubId);
|
||||||
setConfig(cfg);
|
setConfig(cfg);
|
||||||
} catch (err) {
|
}
|
||||||
|
catch (err)
|
||||||
|
{
|
||||||
if (!cancelled) setLoadError((err as Error).message);
|
if (!cancelled) setLoadError((err as Error).message);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
if (!isOpen) {
|
{
|
||||||
|
if (!isOpen)
|
||||||
|
{
|
||||||
pushedRef.current = false;
|
pushedRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!insRef.current || pushedRef.current) return;
|
if (!insRef.current || pushedRef.current) return;
|
||||||
if (!publisherId || !config?.slot) return;
|
if (!publisherId || !config?.slot) return;
|
||||||
|
|
||||||
const tryPush = () => {
|
const tryPush = () =>
|
||||||
try {
|
{
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
try
|
||||||
|
{
|
||||||
|
|
||||||
const w = window as any;
|
const w = window as any;
|
||||||
w.adsbygoogle = w.adsbygoogle || [];
|
w.adsbygoogle = w.adsbygoogle || [];
|
||||||
w.adsbygoogle.push({});
|
w.adsbygoogle.push({});
|
||||||
pushedRef.current = true;
|
pushedRef.current = true;
|
||||||
} catch {
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
// AdSense script may not be ready yet; retry once
|
// AdSense script may not be ready yet; retry once
|
||||||
setTimeout(() => {
|
setTimeout(() =>
|
||||||
try {
|
{
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
try
|
||||||
|
{
|
||||||
|
|
||||||
const w = window as any;
|
const w = window as any;
|
||||||
w.adsbygoogle = w.adsbygoogle || [];
|
w.adsbygoogle = w.adsbygoogle || [];
|
||||||
w.adsbygoogle.push({});
|
w.adsbygoogle.push({});
|
||||||
pushedRef.current = true;
|
pushedRef.current = true;
|
||||||
} catch { /* give up */ }
|
}
|
||||||
|
catch
|
||||||
|
{ /* give up */ }
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ const findNearestColor = (hex: string, colors: IPartColor[]): IPartColor | null
|
|||||||
const cb = color.rgb & 0xFF;
|
const cb = color.rgb & 0xFF;
|
||||||
const dist = (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
|
const dist = (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
|
||||||
|
|
||||||
if(dist < minDist) { minDist = dist; nearest = color; }
|
if(dist < minDist)
|
||||||
|
{
|
||||||
|
minDist = dist; nearest = color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nearest;
|
return nearest;
|
||||||
@@ -40,7 +43,10 @@ export const AvatarEditorAdvancedColorView: FC<{
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
return () => { if(debounceRef.current) clearTimeout(debounceRef.current); };
|
return () =>
|
||||||
|
{
|
||||||
|
if(debounceRef.current) clearTimeout(debounceRef.current);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const selectedColor = useMemo(() =>
|
const selectedColor = useMemo(() =>
|
||||||
@@ -52,7 +58,7 @@ export const AvatarEditorAdvancedColorView: FC<{
|
|||||||
|
|
||||||
const hexColor = useMemo(() =>
|
const hexColor = useMemo(() =>
|
||||||
ColorUtils.makeColorNumberHex((selectedColor?.rgb ?? 0) & 0xFFFFFF),
|
ColorUtils.makeColorNumberHex((selectedColor?.rgb ?? 0) & 0xFFFFFF),
|
||||||
[ selectedColor ]);
|
[ selectedColor ]);
|
||||||
|
|
||||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) =>
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export const AvatarEffectsView: FC<{}> = () =>
|
|||||||
|
|
||||||
switch(parts[1])
|
switch(parts[1])
|
||||||
{
|
{
|
||||||
case 'show': setIsVisible(true); return;
|
case 'show': setIsVisible(true); return;
|
||||||
case 'hide': setIsVisible(false); return;
|
case 'hide': setIsVisible(false); return;
|
||||||
case 'toggle': setIsVisible(prev => !prev); return;
|
case 'toggle': setIsVisible(prev => !prev); return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -83,7 +83,10 @@ export const AvatarEffectsView: FC<{}> = () =>
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [ isVisible, effects.length, loadError ]);
|
}, [ isVisible, effects.length, loadError ]);
|
||||||
|
|
||||||
const session = GetSessionDataManager();
|
const session = GetSessionDataManager();
|
||||||
|
|||||||
@@ -49,18 +49,21 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
|
|||||||
setSelectedOverlay,
|
setSelectedOverlay,
|
||||||
selectedCardBackground,
|
selectedCardBackground,
|
||||||
setSelectedCardBackground
|
setSelectedCardBackground
|
||||||
}) => {
|
}) =>
|
||||||
|
{
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('backgrounds');
|
const [activeTab, setActiveTab] = useState<TabType>('backgrounds');
|
||||||
const remoteData = use(fetchBackgroundsData());
|
const remoteData = use(fetchBackgroundsData());
|
||||||
const { roomSession } = useRoom();
|
const { roomSession } = useRoom();
|
||||||
|
|
||||||
const processData = useCallback((configData: any[], idField: string): ItemData[] => {
|
const processData = useCallback((configData: any[], idField: string): ItemData[] =>
|
||||||
|
{
|
||||||
if (!configData?.length) return [];
|
if (!configData?.length) return [];
|
||||||
|
|
||||||
return configData.map(item => ({ id: typeof item === 'number' ? item : item[idField] }));
|
return configData.map(item => ({ id: typeof item === 'number' ? item : item[idField] }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const readData = useCallback((key: 'backgrounds.data' | 'stands.data' | 'overlays.data' | 'cards.data'): any[] => {
|
const readData = useCallback((key: 'backgrounds.data' | 'stands.data' | 'overlays.data' | 'cards.data'): any[] =>
|
||||||
|
{
|
||||||
const fromRemote = remoteData?.[key];
|
const fromRemote = remoteData?.[key];
|
||||||
if(Array.isArray(fromRemote)) return fromRemote;
|
if(Array.isArray(fromRemote)) return fromRemote;
|
||||||
return GetOptionalConfigurationValue<any[]>(key, []) || [];
|
return GetOptionalConfigurationValue<any[]>(key, []) || [];
|
||||||
@@ -73,7 +76,8 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
|
|||||||
cards: processData(readData('cards.data').length ? readData('cards.data') : readData('backgrounds.data'), 'backgroundId')
|
cards: processData(readData('cards.data').length ? readData('cards.data') : readData('backgrounds.data'), 'backgroundId')
|
||||||
}), [processData, readData]);
|
}), [processData, readData]);
|
||||||
|
|
||||||
const handleSelection = useCallback((id: number) => {
|
const handleSelection = useCallback((id: number) =>
|
||||||
|
{
|
||||||
if (!roomSession) return;
|
if (!roomSession) return;
|
||||||
|
|
||||||
const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay, cards: setSelectedCardBackground };
|
const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay, cards: setSelectedCardBackground };
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const t = (key: string, fallback: string, params?: string[], replacements?: stri
|
|||||||
const value = LocalizeText(key, params ?? null, replacements ?? null);
|
const value = LocalizeText(key, params ?? null, replacements ?? null);
|
||||||
if(value && value !== key) return value;
|
if(value && value !== key) return value;
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
if(!params || !replacements) return fallback;
|
if(!params || !replacements) return fallback;
|
||||||
let out = fallback;
|
let out = fallback;
|
||||||
@@ -38,8 +39,8 @@ const PALETTE: number[] = [
|
|||||||
const currencyName = (type: number): string =>
|
const currencyName = (type: number): string =>
|
||||||
{
|
{
|
||||||
if(type === -1) return 'credits';
|
if(type === -1) return 'credits';
|
||||||
if(type === 0) return 'duckets';
|
if(type === 0) return 'duckets';
|
||||||
if(type === 5) return 'diamonds';
|
if(type === 5) return 'diamonds';
|
||||||
return `currency #${ type }`;
|
return `currency #${ type }`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,14 +59,14 @@ const floodFill = (grid: Uint32Array, w: number, h: number, startX: number, star
|
|||||||
const stack: number[] = [ startIdx ];
|
const stack: number[] = [ startIdx ];
|
||||||
while(stack.length)
|
while(stack.length)
|
||||||
{
|
{
|
||||||
const idx = stack.pop() as number;
|
const idx = stack.pop();
|
||||||
if(next[idx] !== target) continue;
|
if(next[idx] !== target) continue;
|
||||||
next[idx] = replacement;
|
next[idx] = replacement;
|
||||||
const x = idx % w;
|
const x = idx % w;
|
||||||
const y = (idx - x) / w;
|
const y = (idx - x) / w;
|
||||||
if(x > 0) stack.push(idx - 1);
|
if(x > 0) stack.push(idx - 1);
|
||||||
if(x < w - 1) stack.push(idx + 1);
|
if(x < w - 1) stack.push(idx + 1);
|
||||||
if(y > 0) stack.push(idx - w);
|
if(y > 0) stack.push(idx - w);
|
||||||
if(y < h - 1) stack.push(idx + w);
|
if(y < h - 1) stack.push(idx + w);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
@@ -119,7 +120,7 @@ const gridToPngBase64 = async (grid: Uint32Array): Promise<{ b64: string; bytes:
|
|||||||
{
|
{
|
||||||
const argb = grid[i];
|
const argb = grid[i];
|
||||||
const o = i * 4;
|
const o = i * 4;
|
||||||
image.data[o] = (argb >>> 16) & 0xff;
|
image.data[o] = (argb >>> 16) & 0xff;
|
||||||
image.data[o + 1] = (argb >>> 8) & 0xff;
|
image.data[o + 1] = (argb >>> 8) & 0xff;
|
||||||
image.data[o + 2] = argb & 0xff;
|
image.data[o + 2] = argb & 0xff;
|
||||||
image.data[o + 3] = (argb >>> 24) & 0xff;
|
image.data[o + 3] = (argb >>> 24) & 0xff;
|
||||||
@@ -157,12 +158,18 @@ const loadGridFromUrl = (url: string): Promise<Uint32Array> =>
|
|||||||
{
|
{
|
||||||
const o = i * 4;
|
const o = i * 4;
|
||||||
const a = data[o + 3];
|
const a = data[o + 3];
|
||||||
if(a === 0) { grid[i] = 0; continue; }
|
if(a === 0)
|
||||||
|
{
|
||||||
|
grid[i] = 0; continue;
|
||||||
|
}
|
||||||
grid[i] = ((a & 0xff) << 24) | ((data[o] & 0xff) << 16) | ((data[o + 1] & 0xff) << 8) | (data[o + 2] & 0xff);
|
grid[i] = ((a & 0xff) << 24) | ((data[o] & 0xff) << 16) | ((data[o + 1] & 0xff) << 8) | (data[o + 2] & 0xff);
|
||||||
}
|
}
|
||||||
resolve(grid);
|
resolve(grid);
|
||||||
}
|
}
|
||||||
catch(err) { reject(err); }
|
catch(err)
|
||||||
|
{
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
image.onerror = () => reject(new Error('Could not load badge image (CORS?).'));
|
image.onerror = () => reject(new Error('Could not load badge image (CORS?).'));
|
||||||
image.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
|
image.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
|
||||||
@@ -216,8 +223,8 @@ export const BadgeCreatorView: FC<{}> = () =>
|
|||||||
if(parts.length < 2) return;
|
if(parts.length < 2) return;
|
||||||
switch(parts[1])
|
switch(parts[1])
|
||||||
{
|
{
|
||||||
case 'show': setIsVisible(true); return;
|
case 'show': setIsVisible(true); return;
|
||||||
case 'hide': setIsVisible(false); return;
|
case 'hide': setIsVisible(false); return;
|
||||||
case 'toggle': setIsVisible(v => !v); return;
|
case 'toggle': setIsVisible(v => !v); return;
|
||||||
case 'edit':
|
case 'edit':
|
||||||
if(!parts[2]) return;
|
if(!parts[2]) return;
|
||||||
@@ -232,7 +239,13 @@ export const BadgeCreatorView: FC<{}> = () =>
|
|||||||
return () => RemoveLinkEventTracker(tracker);
|
return () => RemoveLinkEventTracker(tracker);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { if(isVisible) { refresh(); ensureCustomBadgeTexts(); } }, [ isVisible, refresh ]);
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if(isVisible)
|
||||||
|
{
|
||||||
|
refresh(); ensureCustomBadgeTexts();
|
||||||
|
}
|
||||||
|
}, [ isVisible, refresh ]);
|
||||||
|
|
||||||
const resetEditor = useCallback(() =>
|
const resetEditor = useCallback(() =>
|
||||||
{
|
{
|
||||||
@@ -316,9 +329,9 @@ export const BadgeCreatorView: FC<{}> = () =>
|
|||||||
{
|
{
|
||||||
const v = grid[i];
|
const v = grid[i];
|
||||||
const o = i * 4;
|
const o = i * 4;
|
||||||
buffer[o] = (v >>> 16) & 0xff;
|
buffer[o] = (v >>> 16) & 0xff;
|
||||||
buffer[o + 1] = (v >>> 8) & 0xff;
|
buffer[o + 1] = (v >>> 8) & 0xff;
|
||||||
buffer[o + 2] = v & 0xff;
|
buffer[o + 2] = v & 0xff;
|
||||||
buffer[o + 3] = (v >>> 24) & 0xff;
|
buffer[o + 3] = (v >>> 24) & 0xff;
|
||||||
}
|
}
|
||||||
ctx.putImageData(image, 0, 0);
|
ctx.putImageData(image, 0, 0);
|
||||||
@@ -365,7 +378,10 @@ export const BadgeCreatorView: FC<{}> = () =>
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
const stopDrag = () => { isDraggingRef.current = false; };
|
const stopDrag = () =>
|
||||||
|
{
|
||||||
|
isDraggingRef.current = false;
|
||||||
|
};
|
||||||
window.addEventListener('mouseup', stopDrag);
|
window.addEventListener('mouseup', stopDrag);
|
||||||
return () => window.removeEventListener('mouseup', stopDrag);
|
return () => window.removeEventListener('mouseup', stopDrag);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -385,7 +401,10 @@ export const BadgeCreatorView: FC<{}> = () =>
|
|||||||
const handleSave = useCallback(async () =>
|
const handleSave = useCallback(async () =>
|
||||||
{
|
{
|
||||||
if(submitting) return;
|
if(submitting) return;
|
||||||
if(isEmpty) { setError(t('badgecreator.error.empty', 'Draw something first.')); return; }
|
if(isEmpty)
|
||||||
|
{
|
||||||
|
setError(t('badgecreator.error.empty', 'Draw something first.')); return;
|
||||||
|
}
|
||||||
if(!editingBadgeId && !canCreateMore)
|
if(!editingBadgeId && !canCreateMore)
|
||||||
{
|
{
|
||||||
setError(t('badgecreator.error.limit', 'You already have %max% custom badges.', [ 'max' ], [ String(maxBadges) ]));
|
setError(t('badgecreator.error.limit', 'You already have %max% custom badges.', [ 'max' ], [ String(maxBadges) ]));
|
||||||
@@ -506,7 +525,10 @@ export const BadgeCreatorView: FC<{}> = () =>
|
|||||||
<button
|
<button
|
||||||
key={ idx }
|
key={ idx }
|
||||||
type="button"
|
type="button"
|
||||||
onClick={ () => { setSelectedColor(color); setTool('paint'); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
setSelectedColor(color); setTool('paint');
|
||||||
|
} }
|
||||||
title={ isTransparent ? 'Transparent' : argbToCss(color) }
|
title={ isTransparent ? 'Transparent' : argbToCss(color) }
|
||||||
style={ {
|
style={ {
|
||||||
width: 22,
|
width: 22,
|
||||||
|
|||||||
@@ -10,40 +10,46 @@ export interface CameraWidgetShowPhotoViewProps {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = props => {
|
export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = props =>
|
||||||
|
{
|
||||||
const { currentIndex = -1, currentPhotos = null, onClick = null } = props;
|
const { currentIndex = -1, currentPhotos = null, onClick = null } = props;
|
||||||
const [imageIndex, setImageIndex] = useState(0);
|
const [imageIndex, setImageIndex] = useState(0);
|
||||||
|
|
||||||
const currentImage = currentPhotos && currentPhotos.length ? currentPhotos[imageIndex] : null;
|
const currentImage = currentPhotos && currentPhotos.length ? currentPhotos[imageIndex] : null;
|
||||||
|
|
||||||
const next = () => {
|
const next = () =>
|
||||||
setImageIndex(prevValue => {
|
{
|
||||||
|
setImageIndex(prevValue =>
|
||||||
|
{
|
||||||
let newIndex = prevValue + 1;
|
let newIndex = prevValue + 1;
|
||||||
if (newIndex >= currentPhotos.length) newIndex = 0;
|
if (newIndex >= currentPhotos.length) newIndex = 0;
|
||||||
return newIndex;
|
return newIndex;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const previous = () => {
|
const previous = () =>
|
||||||
setImageIndex(prevValue => {
|
{
|
||||||
|
setImageIndex(prevValue =>
|
||||||
|
{
|
||||||
let newIndex = prevValue - 1;
|
let newIndex = prevValue - 1;
|
||||||
if (newIndex < 0) newIndex = currentPhotos.length - 1;
|
if (newIndex < 0) newIndex = currentPhotos.length - 1;
|
||||||
return newIndex;
|
return newIndex;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
setImageIndex(currentIndex);
|
setImageIndex(currentIndex);
|
||||||
}, [currentIndex]);
|
}, [currentIndex]);
|
||||||
|
|
||||||
if (!currentImage) return null;
|
if (!currentImage) return null;
|
||||||
|
|
||||||
const getUserData = (roomId: number, objectId: number, type: string): number | string =>
|
const getUserData = (roomId: number, objectId: number, type: string): number | string =>
|
||||||
{
|
{
|
||||||
const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.WALL);
|
const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.WALL);
|
||||||
if (!roomObject) return;
|
if (!roomObject) return;
|
||||||
return type == 'username' ? roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
|
return type == 'username' ? roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid style={{ display: 'flex', flexDirection: 'column' }}>
|
<Grid style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
@@ -53,13 +59,13 @@ export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = pro
|
|||||||
{currentImage.m && currentImage.m.length && <Text center>{currentImage.m}</Text>}
|
{currentImage.m && currentImage.m.length && <Text center>{currentImage.m}</Text>}
|
||||||
<div className="flex items-center center justify-between">
|
<div className="flex items-center center justify-between">
|
||||||
<Text>{currentImage.n || ''}</Text>
|
<Text>{currentImage.n || ''}</Text>
|
||||||
<Text onClick={() => GetUserProfile(Number(getUserData(currentImage.s, Number(currentImage.u), 'id')))}> { getUserData(currentImage.s, Number(currentImage.u), 'username') } </Text>
|
<Text onClick={() => GetUserProfile(Number(getUserData(currentImage.s, Number(currentImage.u), 'id')))}> { getUserData(currentImage.s, Number(currentImage.u), 'username') } </Text>
|
||||||
<Text className="cursor-pointer" onClick={() => GetUserProfile(currentImage.oi)}>{currentImage.o}</Text>
|
<Text className="cursor-pointer" onClick={() => GetUserProfile(currentImage.oi)}>{currentImage.o}</Text>
|
||||||
<Text>{new Date(currentImage.t * 1000).toLocaleDateString()}</Text>
|
<Text>{new Date(currentImage.t * 1000).toLocaleDateString()}</Text>
|
||||||
</div>
|
</div>
|
||||||
{currentPhotos.length > 1 && (
|
{currentPhotos.length > 1 && (
|
||||||
<Flex className="picture-preview-buttons">
|
<Flex className="picture-preview-buttons">
|
||||||
<FaArrowLeft onClick={previous} />
|
<FaArrowLeft onClick={previous} />
|
||||||
<FaArrowRight className="cursor-pointer"onClick={next} />
|
<FaArrowRight className="cursor-pointer"onClick={next} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export interface CameraWidgetEditorViewProps {
|
|||||||
|
|
||||||
const TABS: string[] = [ CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE ];
|
const TABS: string[] = [ CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE ];
|
||||||
|
|
||||||
export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props => {
|
export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||||
|
{
|
||||||
const { picture = null, availableEffects = null, myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props;
|
const { picture = null, availableEffects = null, myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props;
|
||||||
const [ currentTab, setCurrentTab ] = useState(TABS[0]);
|
const [ currentTab, setCurrentTab ] = useState(TABS[0]);
|
||||||
const [ selectedEffectName, setSelectedEffectName ] = useState<string>(null);
|
const [ selectedEffectName, setSelectedEffectName ] = useState<string>(null);
|
||||||
@@ -35,37 +36,45 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
|||||||
img.src = picture.imageUrl;
|
img.src = picture.imageUrl;
|
||||||
}, [ picture ]);
|
}, [ picture ]);
|
||||||
|
|
||||||
const getColorMatrixEffects = useMemo(() => {
|
const getColorMatrixEffects = useMemo(() =>
|
||||||
|
{
|
||||||
return availableEffects.filter(effect => effect.colorMatrix);
|
return availableEffects.filter(effect => effect.colorMatrix);
|
||||||
}, [ availableEffects ]);
|
}, [ availableEffects ]);
|
||||||
|
|
||||||
const getCompositeEffects = useMemo(() => {
|
const getCompositeEffects = useMemo(() =>
|
||||||
|
{
|
||||||
return availableEffects.filter(effect => effect.texture);
|
return availableEffects.filter(effect => effect.texture);
|
||||||
}, [ availableEffects ]);
|
}, [ availableEffects ]);
|
||||||
|
|
||||||
const getEffectList = useCallback(() => {
|
const getEffectList = useCallback(() =>
|
||||||
|
{
|
||||||
return currentTab === CameraEditorTabs.COLORMATRIX ? getColorMatrixEffects : getCompositeEffects;
|
return currentTab === CameraEditorTabs.COLORMATRIX ? getColorMatrixEffects : getCompositeEffects;
|
||||||
}, [ currentTab, getColorMatrixEffects, getCompositeEffects ]);
|
}, [ currentTab, getColorMatrixEffects, getCompositeEffects ]);
|
||||||
|
|
||||||
const getSelectedEffectIndex = useCallback((name: string) => {
|
const getSelectedEffectIndex = useCallback((name: string) =>
|
||||||
|
{
|
||||||
if (!name || !name.length || !selectedEffects || !selectedEffects.length) return -1;
|
if (!name || !name.length || !selectedEffects || !selectedEffects.length) return -1;
|
||||||
return selectedEffects.findIndex(effect => effect.effect.name === name);
|
return selectedEffects.findIndex(effect => effect.effect.name === name);
|
||||||
}, [ selectedEffects ]);
|
}, [ selectedEffects ]);
|
||||||
|
|
||||||
const getCurrentEffectIndex = useMemo(() => {
|
const getCurrentEffectIndex = useMemo(() =>
|
||||||
|
{
|
||||||
return getSelectedEffectIndex(selectedEffectName);
|
return getSelectedEffectIndex(selectedEffectName);
|
||||||
}, [ selectedEffectName, getSelectedEffectIndex ]);
|
}, [ selectedEffectName, getSelectedEffectIndex ]);
|
||||||
|
|
||||||
const getCurrentEffect = useMemo(() => {
|
const getCurrentEffect = useMemo(() =>
|
||||||
|
{
|
||||||
if (!selectedEffectName) return null;
|
if (!selectedEffectName) return null;
|
||||||
return selectedEffects[getCurrentEffectIndex] || null;
|
return selectedEffects[getCurrentEffectIndex] || null;
|
||||||
}, [ selectedEffectName, getCurrentEffectIndex, selectedEffects ]);
|
}, [ selectedEffectName, getCurrentEffectIndex, selectedEffects ]);
|
||||||
|
|
||||||
const setSelectedEffectAlpha = useCallback((alpha: number) => {
|
const setSelectedEffectAlpha = useCallback((alpha: number) =>
|
||||||
|
{
|
||||||
const index = getCurrentEffectIndex;
|
const index = getCurrentEffectIndex;
|
||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
|
|
||||||
setSelectedEffects(prevValue => {
|
setSelectedEffects(prevValue =>
|
||||||
|
{
|
||||||
const clone = [ ...prevValue ];
|
const clone = [ ...prevValue ];
|
||||||
const currentEffect = clone[index];
|
const currentEffect = clone[index];
|
||||||
clone[index] = new RoomCameraWidgetSelectedEffect(currentEffect.effect, alpha);
|
clone[index] = new RoomCameraWidgetSelectedEffect(currentEffect.effect, alpha);
|
||||||
@@ -73,8 +82,10 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
|||||||
});
|
});
|
||||||
}, [ getCurrentEffectIndex ]);
|
}, [ getCurrentEffectIndex ]);
|
||||||
|
|
||||||
const processAction = useCallback((type: string, effectName: string = null) => {
|
const processAction = useCallback((type: string, effectName: string = null) =>
|
||||||
switch (type) {
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
case 'close':
|
case 'close':
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
@@ -102,7 +113,8 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
|||||||
const existingIndex = getSelectedEffectIndex(effectName);
|
const existingIndex = getSelectedEffectIndex(effectName);
|
||||||
if (existingIndex === -1) return;
|
if (existingIndex === -1) return;
|
||||||
|
|
||||||
setSelectedEffects(prevValue => {
|
setSelectedEffects(prevValue =>
|
||||||
|
{
|
||||||
const clone = [ ...prevValue ];
|
const clone = [ ...prevValue ];
|
||||||
clone.splice(existingIndex, 1);
|
clone.splice(existingIndex, 1);
|
||||||
return clone;
|
return clone;
|
||||||
@@ -141,10 +153,12 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
|||||||
}
|
}
|
||||||
}, [ availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose ]);
|
}, [ availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
if(!stableTexture) return;
|
if(!stableTexture) return;
|
||||||
|
|
||||||
const processThumbnails = async () => {
|
const processThumbnails = async () =>
|
||||||
|
{
|
||||||
const renderedEffects = await Promise.all(
|
const renderedEffects = await Promise.all(
|
||||||
availableEffects.map(effect =>
|
availableEffects.map(effect =>
|
||||||
GetRoomCameraWidgetManager().applyEffects(stableTexture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
|
GetRoomCameraWidgetManager().applyEffects(stableTexture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
|
||||||
@@ -155,24 +169,28 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
|||||||
processThumbnails();
|
processThumbnails();
|
||||||
}, [ stableTexture, availableEffects ]);
|
}, [ stableTexture, availableEffects ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
if(!stableTexture) return;
|
if(!stableTexture) return;
|
||||||
|
|
||||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
||||||
|
|
||||||
debounceTimerRef.current = setTimeout(() => {
|
debounceTimerRef.current = setTimeout(() =>
|
||||||
|
{
|
||||||
const id = ++requestIdRef.current;
|
const id = ++requestIdRef.current;
|
||||||
|
|
||||||
GetRoomCameraWidgetManager()
|
GetRoomCameraWidgetManager()
|
||||||
.applyEffects(stableTexture, selectedEffects, false)
|
.applyEffects(stableTexture, selectedEffects, false)
|
||||||
.then(imageElement => {
|
.then(imageElement =>
|
||||||
|
{
|
||||||
if (id !== requestIdRef.current) return;
|
if (id !== requestIdRef.current) return;
|
||||||
setCurrentPictureUrl(imageElement.src);
|
setCurrentPictureUrl(imageElement.src);
|
||||||
})
|
})
|
||||||
.catch(error => NitroLogger.error('Failed to apply effects to picture', error));
|
.catch(error => NitroLogger.error('Failed to apply effects to picture', error));
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
return () => {
|
return () =>
|
||||||
|
{
|
||||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
||||||
};
|
};
|
||||||
}, [ stableTexture, selectedEffects ]);
|
}, [ stableTexture, selectedEffects ]);
|
||||||
|
|||||||
@@ -19,22 +19,22 @@ export const CameraWidgetEffectListItemView: FC<CameraWidgetEffectListItemViewPr
|
|||||||
const { effect = null, thumbnailUrl = null, isActive = false, isLocked = false, selectEffect = null, removeEffect = null } = props;
|
const { effect = null, thumbnailUrl = null, isActive = false, isLocked = false, selectEffect = null, removeEffect = null } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutGridItem itemActive={ isActive } title={ LocalizeText(!isLocked ? (`camera.effect.name.${ effect.name }`) : `camera.effect.required.level ${ effect.minLevel }`) } onClick={ event => (!isActive && selectEffect()) }>
|
<LayoutGridItem itemActive={ isActive } title={ LocalizeText(!isLocked ? (`camera.effect.name.${ effect.name }`) : `camera.effect.required.level ${ effect.minLevel }`) } onClick={ event => (!isActive && selectEffect()) }>
|
||||||
{ isActive &&
|
{ isActive &&
|
||||||
<Button className="rounded-circle remove-effect" variant="danger" onClick={ removeEffect }>
|
<Button className="rounded-circle remove-effect" variant="danger" onClick={ removeEffect }>
|
||||||
<FaTimes className="fa-icon" />
|
<FaTimes className="fa-icon" />
|
||||||
</Button> }
|
</Button> }
|
||||||
{ !isLocked && (thumbnailUrl && thumbnailUrl.length > 0) &&
|
{ !isLocked && (thumbnailUrl && thumbnailUrl.length > 0) &&
|
||||||
<div className="effect-thumbnail-image border">
|
<div className="effect-thumbnail-image border">
|
||||||
<img alt="" src={ thumbnailUrl } />
|
<img alt="" src={ thumbnailUrl } />
|
||||||
</div> }
|
</div> }
|
||||||
{ isLocked &&
|
{ isLocked &&
|
||||||
<Text bold center>
|
<Text bold center>
|
||||||
<div>
|
<div>
|
||||||
<FaLock className="fa-icon" />
|
<FaLock className="fa-icon" />
|
||||||
</div>
|
</div>
|
||||||
{ effect.minLevel }
|
{ effect.minLevel }
|
||||||
</Text> }
|
</Text> }
|
||||||
</LayoutGridItem>
|
</LayoutGridItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const CameraWidgetEffectListView: FC<CameraWidgetEffectListViewProps> = p
|
|||||||
|
|
||||||
// return <CameraWidgetEffectListItemView key={ index } effect={ effect } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } removeEffect={ () => processAction('remove_effect', effect.name) } selectEffect={ () => processAction('select_effect', effect.name) } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } />;
|
// return <CameraWidgetEffectListItemView key={ index } effect={ effect } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } removeEffect={ () => processAction('remove_effect', effect.name) } selectEffect={ () => processAction('select_effect', effect.name) } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } />;
|
||||||
|
|
||||||
return <CameraWidgetEffectListItemView key={ index } effect={ effect } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } selectEffect={ () => processAction('select_effect', effect.name) } removeEffect={ () => processAction('remove_effect', effect.name) } />
|
return <CameraWidgetEffectListItemView key={ index } effect={ effect } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } selectEffect={ () => processAction('select_effect', effect.name) } removeEffect={ () => processAction('remove_effect', effect.name) } />;
|
||||||
}) }
|
}) }
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -97,7 +97,10 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
|||||||
{
|
{
|
||||||
if(e.key === 'Escape')
|
if(e.key === 'Escape')
|
||||||
{
|
{
|
||||||
if(editingOffer) { setEditingOffer(null); e.preventDefault(); return; }
|
if(editingOffer)
|
||||||
|
{
|
||||||
|
setEditingOffer(null); e.preventDefault(); return;
|
||||||
|
}
|
||||||
if(editingPageData || editingRootPage || editingPageNode)
|
if(editingPageData || editingRootPage || editingPageNode)
|
||||||
{
|
{
|
||||||
setEditingPageData(false);
|
setEditingPageData(false);
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ const CatalogClassicViewInner: FC<{}> = () =>
|
|||||||
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
|
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||||
const catalogAdmin = useCatalogAdmin();
|
const catalogAdmin = useCatalogAdmin();
|
||||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||||
const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {});
|
const setAdminMode = catalogAdmin?.setAdminMode ?? (() =>
|
||||||
|
{});
|
||||||
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
|
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
|
||||||
const publishCatalog = catalogAdmin?.publishCatalog ?? (() => {});
|
const publishCatalog = catalogAdmin?.publishCatalog ?? (() =>
|
||||||
|
{});
|
||||||
const loading = catalogAdmin?.loading ?? false;
|
const loading = catalogAdmin?.loading ?? false;
|
||||||
|
|
||||||
const isMod = GetSessionDataManager().isModerator;
|
const isMod = GetSessionDataManager().isModerator;
|
||||||
@@ -148,13 +150,19 @@ const CatalogClassicViewInner: FC<{}> = () =>
|
|||||||
{ adminMode &&
|
{ adminMode &&
|
||||||
<div className="flex items-center gap-0.5 ml-1" onClick={ e => e.stopPropagation() }>
|
<div className="flex items-center gap-0.5 ml-1" onClick={ e => e.stopPropagation() }>
|
||||||
<FaEdit className="text-[8px] text-primary cursor-pointer hover:text-dark" title={ LocalizeText('catalog.admin.edit.title') }
|
<FaEdit className="text-[8px] text-primary cursor-pointer hover:text-dark" title={ LocalizeText('catalog.admin.edit.title') }
|
||||||
onClick={ () => { catalogAdmin.setEditingPageNode(child); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } } />
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
catalogAdmin.setEditingPageNode(child); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
|
||||||
|
} } />
|
||||||
<span className="cursor-pointer" title={ isHidden ? LocalizeText('catalog.admin.show') : LocalizeText('catalog.admin.hide') }
|
<span className="cursor-pointer" title={ isHidden ? LocalizeText('catalog.admin.show') : LocalizeText('catalog.admin.hide') }
|
||||||
onClick={ () => catalogAdmin.togglePageVisible(child.pageId) }>
|
onClick={ () => catalogAdmin.togglePageVisible(child.pageId) }>
|
||||||
{ isHidden ? <FaEye className="text-[8px] text-success" /> : <FaEyeSlash className="text-[8px] text-muted" /> }
|
{ isHidden ? <FaEye className="text-[8px] text-success" /> : <FaEyeSlash className="text-[8px] text-muted" /> }
|
||||||
</span>
|
</span>
|
||||||
<FaTrash className="text-[8px] text-danger cursor-pointer hover:text-red-800" title={ LocalizeText('catalog.admin.delete.title') }
|
<FaTrash className="text-[8px] text-danger cursor-pointer hover:text-red-800" title={ LocalizeText('catalog.admin.delete.title') }
|
||||||
onClick={ () => { if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) catalogAdmin.deletePage(child.pageId); } } />
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) catalogAdmin.deletePage(child.pageId);
|
||||||
|
} } />
|
||||||
</div> }
|
</div> }
|
||||||
</div>
|
</div>
|
||||||
</NitroCardTabsItemView>
|
</NitroCardTabsItemView>
|
||||||
@@ -180,7 +188,10 @@ const CatalogClassicViewInner: FC<{}> = () =>
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
|
className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
|
||||||
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
<FaEdit className="text-[8px]" />
|
<FaEdit className="text-[8px]" />
|
||||||
<span>{ LocalizeText('catalog.admin.root') }</span>
|
<span>{ LocalizeText('catalog.admin.root') }</span>
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ const CatalogModernViewInner: FC<{}> = () =>
|
|||||||
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
|
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||||
const catalogAdmin = useCatalogAdmin();
|
const catalogAdmin = useCatalogAdmin();
|
||||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||||
const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {});
|
const setAdminMode = catalogAdmin?.setAdminMode ?? (() =>
|
||||||
|
{});
|
||||||
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
|
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
|
||||||
const publishCatalog = catalogAdmin?.publishCatalog ?? (() => {});
|
const publishCatalog = catalogAdmin?.publishCatalog ?? (() =>
|
||||||
|
{});
|
||||||
const loading = catalogAdmin?.loading ?? false;
|
const loading = catalogAdmin?.loading ?? false;
|
||||||
const { favoriteOfferIds, favoritePageIds } = useCatalogFavorites();
|
const { favoriteOfferIds, favoritePageIds } = useCatalogFavorites();
|
||||||
const [ showFavorites, setShowFavorites ] = useState(false);
|
const [ showFavorites, setShowFavorites ] = useState(false);
|
||||||
@@ -170,7 +172,10 @@ const CatalogModernViewInner: FC<{}> = () =>
|
|||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
|
className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
|
||||||
title={ LocalizeText('catalog.admin.edit.root') }
|
title={ LocalizeText('catalog.admin.edit.root') }
|
||||||
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
<FaEdit className="text-[8px]" />
|
<FaEdit className="text-[8px]" />
|
||||||
<span className="whitespace-nowrap">{ LocalizeText('catalog.admin.root') }</span>
|
<span className="whitespace-nowrap">{ LocalizeText('catalog.admin.root') }</span>
|
||||||
|
|||||||
@@ -93,13 +93,19 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
|
|||||||
<div
|
<div
|
||||||
key={ page.pageId }
|
key={ page.pageId }
|
||||||
className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100"
|
className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100"
|
||||||
onClick={ () => { activateNode(page.node); onClose(); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
activateNode(page.node); onClose();
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
<CatalogIconView icon={ page.iconId } />
|
<CatalogIconView icon={ page.iconId } />
|
||||||
<span className="text-[11px] flex-1 truncate font-medium">{ page.name }</span>
|
<span className="text-[11px] flex-1 truncate font-medium">{ page.name }</span>
|
||||||
<FaTimes
|
<FaTimes
|
||||||
className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer"
|
className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer"
|
||||||
onClick={ e => { e.stopPropagation(); toggleFavoritePage(page.pageId); } }
|
onClick={ e =>
|
||||||
|
{
|
||||||
|
e.stopPropagation(); toggleFavoritePage(page.pageId);
|
||||||
|
} }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
@@ -118,7 +124,10 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
|
|||||||
<div
|
<div
|
||||||
key={ fav.offerId }
|
key={ fav.offerId }
|
||||||
className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100"
|
className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100"
|
||||||
onClick={ () => { openPageByOfferId(fav.offerId); onClose(); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
openPageByOfferId(fav.offerId); onClose();
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ /* Furni icon */ }
|
{ /* Furni icon */ }
|
||||||
<div className="w-7 h-7 flex items-center justify-center shrink-0 bg-white rounded border border-card-grid-item-border overflow-hidden">
|
<div className="w-7 h-7 flex items-center justify-center shrink-0 bg-white rounded border border-card-grid-item-border overflow-hidden">
|
||||||
@@ -132,7 +141,10 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
|
|||||||
<span className="text-[11px] flex-1 truncate font-medium">{ fav.displayName }</span>
|
<span className="text-[11px] flex-1 truncate font-medium">{ fav.displayName }</span>
|
||||||
<FaTimes
|
<FaTimes
|
||||||
className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer"
|
className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer"
|
||||||
onClick={ e => { e.stopPropagation(); toggleFavoriteOffer(fav.offerId); } }
|
onClick={ e =>
|
||||||
|
{
|
||||||
|
e.stopPropagation(); toggleFavoriteOffer(fav.offerId);
|
||||||
|
} }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
|
|||||||
@@ -133,7 +133,10 @@ export const CatalogGiftView: FC<{}> = props =>
|
|||||||
if(isBuyingGift) return;
|
if(isBuyingGift) return;
|
||||||
|
|
||||||
isBuyingGift = true;
|
isBuyingGift = true;
|
||||||
setTimeout(() => { isBuyingGift = false; }, 10000);
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
isBuyingGift = false;
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace));
|
SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -126,7 +126,10 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
|
|||||||
{ !adminMode && node.pageId > 0 &&
|
{ !adminMode && node.pageId > 0 &&
|
||||||
<FaStar
|
<FaStar
|
||||||
className={ `text-[8px] transition-all duration-100 cursor-pointer shrink-0 ${ isFav ? 'text-warning opacity-100' : 'text-muted opacity-0 group-hover/nav:opacity-100 hover:text-warning' }` }
|
className={ `text-[8px] transition-all duration-100 cursor-pointer shrink-0 ${ isFav ? 'text-warning opacity-100' : 'text-muted opacity-0 group-hover/nav:opacity-100 hover:text-warning' }` }
|
||||||
onClick={ e => { e.stopPropagation(); toggleFavoritePage(node.pageId); } }
|
onClick={ e =>
|
||||||
|
{
|
||||||
|
e.stopPropagation(); toggleFavoritePage(node.pageId);
|
||||||
|
} }
|
||||||
/> }
|
/> }
|
||||||
{ node.isBranch &&
|
{ node.isBranch &&
|
||||||
<span className="text-[9px] text-muted shrink-0">
|
<span className="text-[9px] text-muted shrink-0">
|
||||||
|
|||||||
@@ -77,7 +77,10 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
|
|||||||
<LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> }
|
<LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> }
|
||||||
<div
|
<div
|
||||||
className={ `absolute top-0 right-0 z-10 p-0.5 cursor-pointer transition-opacity duration-100 ${ isFav ? 'opacity-100' : 'opacity-0 group-hover/tile:opacity-100' }` }
|
className={ `absolute top-0 right-0 z-10 p-0.5 cursor-pointer transition-opacity duration-100 ${ isFav ? 'opacity-100' : 'opacity-0 group-hover/tile:opacity-100' }` }
|
||||||
onClick={ e => { e.stopPropagation(); e.preventDefault(); toggleFavoriteOffer(offer.offerId, offer.localizationName, iconUrl); } }
|
onClick={ e =>
|
||||||
|
{
|
||||||
|
e.stopPropagation(); e.preventDefault(); toggleFavoriteOffer(offer.offerId, offer.localizationName, iconUrl);
|
||||||
|
} }
|
||||||
onMouseDown={ e => e.stopPropagation() }
|
onMouseDown={ e => e.stopPropagation() }
|
||||||
>
|
>
|
||||||
<FaHeart className={ `text-[10px] drop-shadow transition-colors duration-100 ${ isFav ? 'text-danger' : 'text-muted hover:text-danger' }` } />
|
<FaHeart className={ `text-[10px] drop-shadow transition-colors duration-100 ${ isFav ? 'text-danger' : 'text-muted hover:text-danger' }` } />
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const CatalogSearchView: FC<{}> = () =>
|
|||||||
FilterCatalogNode(search, foundFurniLines, rootNode, nodes);
|
FilterCatalogNode(search, foundFurniLines, rootNode, nodes);
|
||||||
|
|
||||||
setSearchResult(new SearchResult(search, offers, nodes.filter(node => (node.isVisible))));
|
setSearchResult(new SearchResult(search, offers, nodes.filter(node => (node.isVisible))));
|
||||||
setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1) as ICatalogPage));
|
setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1)));
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
|
|||||||
@@ -142,44 +142,45 @@ export const CatalogLayoutBuildersClubBuyView: FC<CatalogLayoutProps> = () =>
|
|||||||
{ currentPage?.localization?.getImage(0) &&
|
{ currentPage?.localization?.getImage(0) &&
|
||||||
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
|
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
|
||||||
<Grid>
|
<Grid>
|
||||||
<Column fullHeight justifyContent="between" overflow="hidden" size={ 7 }>
|
<Column fullHeight justifyContent="between" overflow="hidden" size={ 7 }>
|
||||||
<Column gap={ 1 } overflow="auto">
|
<Column gap={ 1 } overflow="auto">
|
||||||
{ offers && (offers.length > 0) && offers.map((offer, index) =>
|
{ offers && (offers.length > 0) && offers.map((offer, index) =>
|
||||||
{
|
{
|
||||||
const meta = getOfferMeta(offer);
|
const meta = getOfferMeta(offer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-2" column={ false } itemActive={ pendingOffer?.offerId === offer.offerId } justifyContent="between" onClick={ () => {
|
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-2" column={ false } itemActive={ pendingOffer?.offerId === offer.offerId } justifyContent="between" onClick={ () =>
|
||||||
setPurchaseState(CatalogPurchaseState.NONE);
|
{
|
||||||
setPendingOffer(offer);
|
setPurchaseState(CatalogPurchaseState.NONE);
|
||||||
} }>
|
setPendingOffer(offer);
|
||||||
<Column gap={ 0 }>
|
} }>
|
||||||
<Text fontWeight="bold">{ getOfferName(offer) }</Text>
|
<Column gap={ 0 }>
|
||||||
{ meta.length > 0 && <Text small>{ meta }</Text> }
|
<Text fontWeight="bold">{ getOfferName(offer) }</Text>
|
||||||
</Column>
|
{ meta.length > 0 && <Text small>{ meta }</Text> }
|
||||||
<div className="flex flex-col gap-1">
|
</Column>
|
||||||
{ (offer.priceCredits > 0) &&
|
<div className="flex flex-col gap-1">
|
||||||
|
{ (offer.priceCredits > 0) &&
|
||||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||||
<Text>{ offer.priceCredits }</Text>
|
<Text>{ offer.priceCredits }</Text>
|
||||||
<LayoutCurrencyIcon type={ -1 } />
|
<LayoutCurrencyIcon type={ -1 } />
|
||||||
</Flex> }
|
</Flex> }
|
||||||
{ (offer.priceActivityPoints > 0) &&
|
{ (offer.priceActivityPoints > 0) &&
|
||||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||||
<Text>{ offer.priceActivityPoints }</Text>
|
<Text>{ offer.priceActivityPoints }</Text>
|
||||||
<LayoutCurrencyIcon type={ offer.priceActivityPointsType } />
|
<LayoutCurrencyIcon type={ offer.priceActivityPointsType } />
|
||||||
</Flex> }
|
</Flex> }
|
||||||
</div>
|
</div>
|
||||||
</LayoutGridItem>
|
</LayoutGridItem>
|
||||||
);
|
);
|
||||||
}) }
|
}) }
|
||||||
|
</Column>
|
||||||
</Column>
|
</Column>
|
||||||
</Column>
|
<Column gap={ 2 } overflow="hidden" size={ 5 }>
|
||||||
<Column gap={ 2 } overflow="hidden" size={ 5 }>
|
<Column center grow overflow="hidden">
|
||||||
<Column center grow overflow="hidden">
|
{ currentPage?.localization.getImage(1) && <img alt="" src={ currentPage.localization.getImage(1) } /> }
|
||||||
{ currentPage?.localization.getImage(1) && <img alt="" src={ currentPage.localization.getImage(1) } /> }
|
{ pageDescription.length > 0 && <Text center dangerouslySetInnerHTML={ { __html: SanitizeHtml(pageDescription) } } overflow="auto" /> }
|
||||||
{ pageDescription.length > 0 && <Text center dangerouslySetInnerHTML={ { __html: SanitizeHtml(pageDescription) } } overflow="auto" /> }
|
</Column>
|
||||||
</Column>
|
{ pendingOffer &&
|
||||||
{ pendingOffer &&
|
|
||||||
<Column fullWidth gap={ 1 }>
|
<Column fullWidth gap={ 1 }>
|
||||||
<Text fontWeight="bold">{ getOfferName(pendingOffer) }</Text>
|
<Text fontWeight="bold">{ getOfferName(pendingOffer) }</Text>
|
||||||
{ getOfferMeta(pendingOffer).length > 0 && <Text>{ getOfferMeta(pendingOffer) }</Text> }
|
{ getOfferMeta(pendingOffer).length > 0 && <Text>{ getOfferMeta(pendingOffer) }</Text> }
|
||||||
@@ -202,7 +203,7 @@ export const CatalogLayoutBuildersClubBuyView: FC<CatalogLayoutProps> = () =>
|
|||||||
</Flex>
|
</Flex>
|
||||||
{ getPurchaseButton() }
|
{ getPurchaseButton() }
|
||||||
</Column> }
|
</Column> }
|
||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -117,7 +117,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
|||||||
if(!prefixText.length) return;
|
if(!prefixText.length) return;
|
||||||
|
|
||||||
const newColors: Record<number, string> = {};
|
const newColors: Record<number, string> = {};
|
||||||
[ ...prefixText ].forEach((_, i) => { newColors[i] = customColorInput; });
|
[ ...prefixText ].forEach((_, i) =>
|
||||||
|
{
|
||||||
|
newColors[i] = customColorInput;
|
||||||
|
});
|
||||||
setLetterColors(newColors);
|
setLetterColors(newColors);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -222,7 +225,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
|||||||
<Picker
|
<Picker
|
||||||
data={ data }
|
data={ data }
|
||||||
locale="it"
|
locale="it"
|
||||||
onEmojiSelect={ (emoji: { native: string }) => { setSelectedIcon(emoji.native); setShowIconPicker(false); } }
|
onEmojiSelect={ (emoji: { native: string }) =>
|
||||||
|
{
|
||||||
|
setSelectedIcon(emoji.native); setShowIconPicker(false);
|
||||||
|
} }
|
||||||
theme="dark"
|
theme="dark"
|
||||||
previewPosition="none"
|
previewPosition="none"
|
||||||
skinTonePosition="search"
|
skinTonePosition="search"
|
||||||
@@ -268,7 +274,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
|||||||
borderRight: '1px solid rgba(0,0,0,0.1)',
|
borderRight: '1px solid rgba(0,0,0,0.1)',
|
||||||
opacity: colorMode === 'single' ? 1 : 0.6
|
opacity: colorMode === 'single' ? 1 : 0.6
|
||||||
} }
|
} }
|
||||||
onClick={ () => { setColorMode('single'); setSelectedLetterIndex(null); } }>
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
setColorMode('single'); setSelectedLetterIndex(null);
|
||||||
|
} }>
|
||||||
{ LocalizeText('catalog.prefix.color.single') }
|
{ LocalizeText('catalog.prefix.color.single') }
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -277,7 +286,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
|||||||
background: colorMode === 'perLetter' ? 'rgba(59,130,246,0.25)' : 'rgba(0,0,0,0.1)',
|
background: colorMode === 'perLetter' ? 'rgba(59,130,246,0.25)' : 'rgba(0,0,0,0.1)',
|
||||||
opacity: colorMode === 'perLetter' ? 1 : 0.6
|
opacity: colorMode === 'perLetter' ? 1 : 0.6
|
||||||
} }
|
} }
|
||||||
onClick={ () => { setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0); } }>
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0);
|
||||||
|
} }>
|
||||||
{ LocalizeText('catalog.prefix.color.per.letter') }
|
{ LocalizeText('catalog.prefix.color.per.letter') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,7 +340,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
|||||||
zIndex: isSelected ? 10 : 1,
|
zIndex: isSelected ? 10 : 1,
|
||||||
boxShadow: isSelected ? '0 0 8px rgba(59,130,246,0.3)' : 'none'
|
boxShadow: isSelected ? '0 0 8px rgba(59,130,246,0.3)' : 'none'
|
||||||
} }
|
} }
|
||||||
onClick={ () => { setSelectedLetterIndex(i); setCustomColorInput(charColor); } }>
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
setSelectedLetterIndex(i); setCustomColorInput(charColor);
|
||||||
|
} }>
|
||||||
<span className="text-sm font-black" style={ { color: charColor } }>
|
<span className="text-sm font-black" style={ { color: charColor } }>
|
||||||
{ char }
|
{ char }
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
|
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
|
||||||
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
|
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ export const CatalogLayoutTrophiesView: FC<CatalogLayoutProps> = props =>
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
|
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
|
||||||
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
|
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -202,7 +202,10 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
|
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
|
||||||
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
|
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
|||||||
isPurchasingCatalogItem = true;
|
isPurchasingCatalogItem = true;
|
||||||
setPurchaseState(CatalogPurchaseState.PURCHASE);
|
setPurchaseState(CatalogPurchaseState.PURCHASE);
|
||||||
|
|
||||||
setTimeout(() => { isPurchasingCatalogItem = false; }, 10000);
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
isPurchasingCatalogItem = false;
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
if(purchaseCallback)
|
if(purchaseCallback)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,19 +5,22 @@ import { Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text }
|
|||||||
import { useChatHistory, useOnClickChat } from '../../hooks';
|
import { useChatHistory, useOnClickChat } from '../../hooks';
|
||||||
import { NitroInput } from '../../layout';
|
import { NitroInput } from '../../layout';
|
||||||
|
|
||||||
export const ChatHistoryView: FC<{}> = props => {
|
export const ChatHistoryView: FC<{}> = props =>
|
||||||
|
{
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [searchText, setSearchText] = useState<string>('');
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
const {chatHistory = []} = useChatHistory();
|
const { chatHistory = [] } = useChatHistory();
|
||||||
const { onClickChat } = useOnClickChat();
|
const { onClickChat } = useOnClickChat();
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
const isFirstRender = useRef(true);
|
const isFirstRender = useRef(true);
|
||||||
const prevChatLength = useRef<number>(0);
|
const prevChatLength = useRef<number>(0);
|
||||||
|
|
||||||
const filteredChatHistory = useMemo(() => {
|
const filteredChatHistory = useMemo(() =>
|
||||||
|
{
|
||||||
let result = chatHistory;
|
let result = chatHistory;
|
||||||
|
|
||||||
if (searchText.length > 0) {
|
if (searchText.length > 0)
|
||||||
|
{
|
||||||
const text = searchText.toLowerCase();
|
const text = searchText.toLowerCase();
|
||||||
result = chatHistory.filter(entry =>
|
result = chatHistory.filter(entry =>
|
||||||
(entry.message && entry.message.toLowerCase().includes(text)) ||
|
(entry.message && entry.message.toLowerCase().includes(text)) ||
|
||||||
@@ -28,31 +31,38 @@ export const ChatHistoryView: FC<{}> = props => {
|
|||||||
return [...result];
|
return [...result];
|
||||||
}, [chatHistory, searchText]);
|
}, [chatHistory, searchText]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
if (!elementRef.current || !isVisible) return;
|
if (!elementRef.current || !isVisible) return;
|
||||||
|
|
||||||
const element = elementRef.current;
|
const element = elementRef.current;
|
||||||
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
||||||
const isAtBottom = maxScrollTop === 0 || Math.abs(element.scrollTop - maxScrollTop) <= 50;
|
const isAtBottom = maxScrollTop === 0 || Math.abs(element.scrollTop - maxScrollTop) <= 50;
|
||||||
|
|
||||||
if (isFirstRender.current) {
|
if (isFirstRender.current)
|
||||||
|
{
|
||||||
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
|
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
|
||||||
isFirstRender.current = false;
|
isFirstRender.current = false;
|
||||||
} else if (filteredChatHistory.length > prevChatLength.current) {
|
}
|
||||||
|
else if (filteredChatHistory.length > prevChatLength.current)
|
||||||
|
{
|
||||||
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
|
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
||||||
prevChatLength.current = filteredChatHistory.length;
|
prevChatLength.current = filteredChatHistory.length;
|
||||||
}, [filteredChatHistory, isVisible]);
|
}, [filteredChatHistory, isVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
const linkTracker: ILinkEventTracker = {
|
const linkTracker: ILinkEventTracker = {
|
||||||
linkReceived: (url: string) => {
|
linkReceived: (url: string) =>
|
||||||
|
{
|
||||||
const parts = url.split('/');
|
const parts = url.split('/');
|
||||||
|
|
||||||
if (parts.length < 2) return;
|
if (parts.length < 2) return;
|
||||||
|
|
||||||
switch (parts[1]) {
|
switch (parts[1])
|
||||||
|
{
|
||||||
case 'show':
|
case 'show':
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
return;
|
return;
|
||||||
@@ -84,18 +94,18 @@ export const ChatHistoryView: FC<{}> = props => {
|
|||||||
<Flex key={index} alignItems="center" className="p-1" gap={2}>
|
<Flex key={index} alignItems="center" className="p-1" gap={2}>
|
||||||
<Text variant="gray">{row.timestamp}</Text>
|
<Text variant="gray">{row.timestamp}</Text>
|
||||||
{row.type === ChatEntryType.TYPE_CHAT && (
|
{row.type === ChatEntryType.TYPE_CHAT && (
|
||||||
<div className="bubble-container" style={{position: 'relative', display: 'inline-flex', alignItems: 'center'}}>
|
<div className="bubble-container" style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}>
|
||||||
<div
|
<div
|
||||||
className={`chat-bubble bubble-${row.style} type-${row.chatType}`}
|
className={`chat-bubble bubble-${row.style} type-${row.chatType}`}
|
||||||
style={{ maxWidth: '100%', backgroundColor: row.style === 0 ? row.color : 'transparent', position: 'relative', zIndex: 1 }}>
|
style={{ maxWidth: '100%', backgroundColor: row.style === 0 ? row.color : 'transparent', position: 'relative', zIndex: 1 }}>
|
||||||
<div className="user-container">
|
<div className="user-container">
|
||||||
{row.imageUrl && row.imageUrl.length > 0 && (
|
{row.imageUrl && row.imageUrl.length > 0 && (
|
||||||
<div className="user-image" style={{backgroundImage: `url(${row.imageUrl})`}} />
|
<div className="user-image" style={{ backgroundImage: `url(${row.imageUrl})` }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="chat-content">
|
<div className="chat-content">
|
||||||
<b className="mr-1 username" dangerouslySetInnerHTML={{__html: `${row.name}: `}} />
|
<b className="mr-1 username" dangerouslySetInnerHTML={{ __html: `${row.name}: ` }} />
|
||||||
<span className="message" dangerouslySetInnerHTML={{__html: `${row.message}`}} onClick={ onClickChat } />
|
<span className="message" dangerouslySetInnerHTML={{ __html: `${row.message}` }} onClick={ onClickChat } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -367,38 +367,38 @@ export const CustomizeNickIconView: FC<{}> = () =>
|
|||||||
|
|
||||||
{ activePrefixSubTab === 'library' &&
|
{ activePrefixSubTab === 'library' &&
|
||||||
<>
|
<>
|
||||||
<div className="rounded border border-black/10 bg-black/5 p-2 text-[11px] leading-4">
|
<div className="rounded border border-black/10 bg-black/5 p-2 text-[11px] leading-4">
|
||||||
Choose a preset or custom prefix for your bubble identity.
|
Choose a preset or custom prefix for your bubble identity.
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{ combinedPrefixes.map(item => (
|
|
||||||
<div key={ `${ item.catalogPrefixId || 'custom' }-${ item.ownedPrefixId || item.id }` } className={ `relative flex min-h-[96px] flex-col gap-2 rounded border p-2.5 ${ item.active ? 'border-[#1e7295] bg-[#dff3fb]' : 'border-black/10 bg-black/5' }` }>
|
|
||||||
{ item.active && <span className="absolute right-1 top-1 rounded bg-[#1e7295] px-1.5 py-0.5 text-[9px] font-bold uppercase text-white">Active</span> }
|
|
||||||
<UserIdentityView
|
|
||||||
displayOrder={ displayOrder }
|
|
||||||
nickIcon={ activeIcon?.iconKey || '' }
|
|
||||||
prefixColor={ item.color }
|
|
||||||
prefixEffect={ item.effect }
|
|
||||||
prefixFont={ item.font || '' }
|
|
||||||
prefixIcon={ item.icon }
|
|
||||||
prefixText={ item.text }
|
|
||||||
username="Username" />
|
|
||||||
<div className="flex flex-col gap-1 text-[11px]">
|
|
||||||
<span>{ item.owned ? (item.active ? 'Owned - Active' : 'Owned') : 'Locked' }</span>
|
|
||||||
<span className="truncate">{ item.displayName || item.text }{ item.isCustom ? ' - Custom' : '' }</span>
|
|
||||||
<span className="inline-flex items-center gap-1">
|
|
||||||
<LayoutCurrencyIcon type={ item.pointsType } />
|
|
||||||
{ item.points }
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button disabled={ isLoading } onClick={ () => handleCombinedPrefixAction(item) }>
|
|
||||||
{ !item.owned && 'Buy' }
|
|
||||||
{ item.owned && !item.active && 'Activate' }
|
|
||||||
{ item.owned && item.active && 'Deactivate' }
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)) }
|
<div className="grid grid-cols-2 gap-2">
|
||||||
</div>
|
{ combinedPrefixes.map(item => (
|
||||||
|
<div key={ `${ item.catalogPrefixId || 'custom' }-${ item.ownedPrefixId || item.id }` } className={ `relative flex min-h-[96px] flex-col gap-2 rounded border p-2.5 ${ item.active ? 'border-[#1e7295] bg-[#dff3fb]' : 'border-black/10 bg-black/5' }` }>
|
||||||
|
{ item.active && <span className="absolute right-1 top-1 rounded bg-[#1e7295] px-1.5 py-0.5 text-[9px] font-bold uppercase text-white">Active</span> }
|
||||||
|
<UserIdentityView
|
||||||
|
displayOrder={ displayOrder }
|
||||||
|
nickIcon={ activeIcon?.iconKey || '' }
|
||||||
|
prefixColor={ item.color }
|
||||||
|
prefixEffect={ item.effect }
|
||||||
|
prefixFont={ item.font || '' }
|
||||||
|
prefixIcon={ item.icon }
|
||||||
|
prefixText={ item.text }
|
||||||
|
username="Username" />
|
||||||
|
<div className="flex flex-col gap-1 text-[11px]">
|
||||||
|
<span>{ item.owned ? (item.active ? 'Owned - Active' : 'Owned') : 'Locked' }</span>
|
||||||
|
<span className="truncate">{ item.displayName || item.text }{ item.isCustom ? ' - Custom' : '' }</span>
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<LayoutCurrencyIcon type={ item.pointsType } />
|
||||||
|
{ item.points }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button disabled={ isLoading } onClick={ () => handleCombinedPrefixAction(item) }>
|
||||||
|
{ !item.owned && 'Buy' }
|
||||||
|
{ item.owned && !item.active && 'Activate' }
|
||||||
|
{ item.owned && item.active && 'Deactivate' }
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)) }
|
||||||
|
</div>
|
||||||
</> }
|
</> }
|
||||||
|
|
||||||
{ activePrefixSubTab === 'custom' &&
|
{ activePrefixSubTab === 'custom' &&
|
||||||
@@ -430,14 +430,14 @@ export const CustomizeNickIconView: FC<{}> = () =>
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-6 gap-2">
|
<div className="grid grid-cols-6 gap-2">
|
||||||
{ PRESET_COLORS.map(color => (
|
{ PRESET_COLORS.map(color => (
|
||||||
<button
|
<button
|
||||||
key={ color }
|
key={ color }
|
||||||
className={ `flex h-[28px] items-center justify-center rounded border text-[10px] font-bold uppercase ${ customPrefixColor === color ? 'border-[#1e7295] ring-1 ring-[#1e7295]' : 'border-black/10' }` }
|
className={ `flex h-[28px] items-center justify-center rounded border text-[10px] font-bold uppercase ${ customPrefixColor === color ? 'border-[#1e7295] ring-1 ring-[#1e7295]' : 'border-black/10' }` }
|
||||||
style={ { backgroundColor: color } }
|
style={ { backgroundColor: color } }
|
||||||
type="button"
|
type="button"
|
||||||
onClick={ () => setCustomPrefixColor(color) }>
|
onClick={ () => setCustomPrefixColor(color) }>
|
||||||
{ customPrefixColor === color ? 'ON' : '' }
|
{ customPrefixColor === color ? 'ON' : '' }
|
||||||
</button>
|
</button>
|
||||||
)) }
|
)) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -572,7 +572,10 @@ export const CustomizeNickIconView: FC<{}> = () =>
|
|||||||
<Picker
|
<Picker
|
||||||
data={ data }
|
data={ data }
|
||||||
locale="en"
|
locale="en"
|
||||||
onEmojiSelect={ (emoji: { native: string }) => { setCustomPrefixIcon(emoji.native); setShowEmojiPicker(false); } }
|
onEmojiSelect={ (emoji: { native: string }) =>
|
||||||
|
{
|
||||||
|
setCustomPrefixIcon(emoji.native); setShowEmojiPicker(false);
|
||||||
|
} }
|
||||||
previewPosition="none"
|
previewPosition="none"
|
||||||
set="native"
|
set="native"
|
||||||
theme="dark" />
|
theme="dark" />
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const FloorplanImportExportView: FC<FloorplanImportExportViewProps> = pro
|
|||||||
convertNumbersForSaving(originalFloorplanSettings.thicknessFloor),
|
convertNumbersForSaving(originalFloorplanSettings.thicknessFloor),
|
||||||
originalFloorplanSettings.wallHeight - 1
|
originalFloorplanSettings.wallHeight - 1
|
||||||
));
|
));
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
@@ -52,4 +52,4 @@ export const FloorplanImportExportView: FC<FloorplanImportExportViewProps> = pro
|
|||||||
</NitroCardContentView>
|
</NitroCardContentView>
|
||||||
</NitroCardView>
|
</NitroCardView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { FriendsMessengerView } from './views/messenger/FriendsMessengerView';
|
|||||||
|
|
||||||
const FRIEND_BAR_TARGET_IDS = [ 'toolbar-friend-bar-container-desktop' ];
|
const FRIEND_BAR_TARGET_IDS = [ 'toolbar-friend-bar-container-desktop' ];
|
||||||
|
|
||||||
export const FriendsView: FC<{}> = props => {
|
export const FriendsView: FC<{}> = props =>
|
||||||
|
{
|
||||||
const { settings = null, onlineFriends = [], requests = [] } = useFriends();
|
const { settings = null, onlineFriends = [], requests = [] } = useFriends();
|
||||||
const [ portalTarget, setPortalTarget ] = useState<HTMLElement | null>(null);
|
const [ portalTarget, setPortalTarget ] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -5,17 +5,21 @@ import { LayoutAvatarImageView, LayoutBadgeImageView } from '../../../../common'
|
|||||||
import { useFriends } from '../../../../hooks';
|
import { useFriends } from '../../../../hooks';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
|
export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props =>
|
||||||
|
{
|
||||||
const { friend = null } = props;
|
const { friend = null } = props;
|
||||||
const [isVisible, setVisible] = useState(false);
|
const [isVisible, setVisible] = useState(false);
|
||||||
const { followFriend = null } = useFriends();
|
const { followFriend = null } = useFriends();
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
const onClick = (event: MouseEvent) => {
|
{
|
||||||
|
const onClick = (event: MouseEvent) =>
|
||||||
|
{
|
||||||
const element = elementRef.current;
|
const element = elementRef.current;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
if ((event.target !== element) && !element.contains((event.target as Node))) {
|
if ((event.target !== element) && !element.contains((event.target as Node)))
|
||||||
|
{
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -23,7 +27,8 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
|
|||||||
return () => document.removeEventListener(MouseEventType.MOUSE_CLICK, onClick);
|
return () => document.removeEventListener(MouseEventType.MOUSE_CLICK, onClick);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!friend) {
|
if (!friend)
|
||||||
|
{
|
||||||
return (
|
return (
|
||||||
<div ref={elementRef} className="relative">
|
<div ref={elementRef} className="relative">
|
||||||
<motion.button
|
<motion.button
|
||||||
@@ -42,14 +47,17 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
|
|||||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
|
||||||
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel whitespace-nowrap z-[80] flex flex-col items-center gap-2 pointer-events-auto min-w-[170px]"
|
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel whitespace-nowrap z-[80] flex flex-col items-center gap-2 pointer-events-auto min-w-[170px]"
|
||||||
>
|
>
|
||||||
<div className="text-white text-[13px] drop-shadow-[1px_1px_0_#000]">{LocalizeText('friend.bar.find.title')}</div>
|
<div className="text-white text-[13px] drop-shadow-[1px_1px_0_#000]">{LocalizeText('friend.bar.find.title')}</div>
|
||||||
<div className="text-white/80 text-xs px-2">{LocalizeText('friend.bar.find.text')}</div>
|
<div className="text-white/80 text-xs px-2">{LocalizeText('friend.bar.find.text')}</div>
|
||||||
<button
|
<button
|
||||||
className="px-3 py-1 bg-black/40 hover:bg-black/60 border border-white/10 rounded-lg text-white text-[11px] transition-colors cursor-pointer mt-1"
|
className="px-3 py-1 bg-black/40 hover:bg-black/60 border border-white/10 rounded-lg text-white text-[11px] transition-colors cursor-pointer mt-1"
|
||||||
onClick={event => { event.stopPropagation(); SendMessageComposer(new FindNewFriendsMessageComposer()); setVisible(false); }}
|
onClick={event =>
|
||||||
|
{
|
||||||
|
event.stopPropagation(); SendMessageComposer(new FindNewFriendsMessageComposer()); setVisible(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{LocalizeText('friend.bar.find.button')}
|
{LocalizeText('friend.bar.find.button')}
|
||||||
</button>
|
</button>
|
||||||
@@ -92,15 +100,24 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
|
|||||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
|
||||||
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel flex flex-col items-center gap-2 z-[80] pointer-events-auto min-w-[110px]"
|
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel flex flex-col items-center gap-2 z-[80] pointer-events-auto min-w-[110px]"
|
||||||
>
|
>
|
||||||
<div className="text-white font-bold text-[13px] drop-shadow-[1px_1px_0_#000] truncate max-w-[120px] px-1">{friend.name}</div>
|
<div className="text-white font-bold text-[13px] drop-shadow-[1px_1px_0_#000] truncate max-w-[120px] px-1">{friend.name}</div>
|
||||||
<div className="flex justify-center gap-3 px-2">
|
<div className="flex justify-center gap-3 px-2">
|
||||||
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-chat hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); OpenMessengerChat(friend.id); setVisible(false); }} />
|
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-chat hover:-translate-y-1 transition-transform" onClick={event =>
|
||||||
|
{
|
||||||
|
event.stopPropagation(); OpenMessengerChat(friend.id); setVisible(false);
|
||||||
|
}} />
|
||||||
{friend.online &&
|
{friend.online &&
|
||||||
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-visit hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); followFriend(friend); setVisible(false); }} />}
|
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-visit hover:-translate-y-1 transition-transform" onClick={event =>
|
||||||
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-profile hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); GetUserProfile(friend.id); setVisible(false); }} />
|
{
|
||||||
|
event.stopPropagation(); followFriend(friend); setVisible(false);
|
||||||
|
}} />}
|
||||||
|
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-profile hover:-translate-y-1 transition-transform" onClick={event =>
|
||||||
|
{
|
||||||
|
event.stopPropagation(); GetUserProfile(friend.id); setVisible(false);
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const itemVariants = {
|
|||||||
exit: { opacity: 0, y: 6, scale: 0.85, transition: { duration: 0.1 } },
|
exit: { opacity: 0, y: 6, scale: 0.85, transition: { duration: 0.1 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount?: number }> = props => {
|
export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount?: number }> = props =>
|
||||||
|
{
|
||||||
const { onlineFriends = [], requestsCount = 0 } = props;
|
const { onlineFriends = [], requestsCount = 0 } = props;
|
||||||
const [ indexOffset, setIndexOffset ] = useState(0);
|
const [ indexOffset, setIndexOffset ] = useState(0);
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -44,7 +45,10 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
|
|||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<div
|
<div
|
||||||
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || (indexOffset <= 0)) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || (indexOffset <= 0)) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
||||||
onClick={ () => { if(indexOffset > 0) setIndexOffset(indexOffset - 1); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
if(indexOffset > 0) setIndexOffset(indexOffset - 1);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
<FaChevronLeft className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
<FaChevronLeft className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +95,10 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
|
|||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<div
|
<div
|
||||||
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || !((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1)))) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || !((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1)))) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
||||||
onClick={ () => { if((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) setIndexOffset(indexOffset + 1); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
if((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) setIndexOffset(indexOffset + 1);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
<FaChevronRight className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
<FaChevronRight className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -148,7 +148,10 @@ export const FriendsListView: FC<{}> = props =>
|
|||||||
<NitroCardAccordionView fullHeight overflow="hidden">
|
<NitroCardAccordionView fullHeight overflow="hidden">
|
||||||
<NitroCardAccordionSetView className="friends-list-section" headerText={ LocalizeText('friendlist.friends') + ` (${ onlineFriends.length })` } isExpanded={ true }>
|
<NitroCardAccordionSetView className="friends-list-section" headerText={ LocalizeText('friendlist.friends') + ` (${ onlineFriends.length })` } isExpanded={ true }>
|
||||||
<Flex className="friends-list-toolbar px-2 py-1" justifyContent="end">
|
<Flex className="friends-list-toolbar px-2 py-1" justifyContent="end">
|
||||||
<span className="friends-list-toolbar-link" onClick={ event => { event.stopPropagation(); toggleSelectFriends(onlineFriends.map(friend => friend.id)); } }>
|
<span className="friends-list-toolbar-link" onClick={ event =>
|
||||||
|
{
|
||||||
|
event.stopPropagation(); toggleSelectFriends(onlineFriends.map(friend => friend.id));
|
||||||
|
} }>
|
||||||
{ onlineFriends.length && onlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0))
|
{ onlineFriends.length && onlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0))
|
||||||
? LocalizeText('friendlist.unselect_all')
|
? LocalizeText('friendlist.unselect_all')
|
||||||
: LocalizeText('friendlist.select_all') }
|
: LocalizeText('friendlist.select_all') }
|
||||||
@@ -158,7 +161,10 @@ export const FriendsListView: FC<{}> = props =>
|
|||||||
</NitroCardAccordionSetView>
|
</NitroCardAccordionSetView>
|
||||||
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${ offlineFriends.length })` }>
|
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${ offlineFriends.length })` }>
|
||||||
<Flex className="friends-list-toolbar px-2 py-1" justifyContent="end">
|
<Flex className="friends-list-toolbar px-2 py-1" justifyContent="end">
|
||||||
<span className="friends-list-toolbar-link" onClick={ event => { event.stopPropagation(); toggleSelectFriends(offlineFriends.map(friend => friend.id)); } }>
|
<span className="friends-list-toolbar-link" onClick={ event =>
|
||||||
|
{
|
||||||
|
event.stopPropagation(); toggleSelectFriends(offlineFriends.map(friend => friend.id));
|
||||||
|
} }>
|
||||||
{ offlineFriends.length && offlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0))
|
{ offlineFriends.length && offlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0))
|
||||||
? LocalizeText('friendlist.unselect_all')
|
? LocalizeText('friendlist.unselect_all')
|
||||||
: LocalizeText('friendlist.select_all') }
|
: LocalizeText('friendlist.select_all') }
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ export const FurniEditorView: FC<{}> = () =>
|
|||||||
loadBySpriteId(spriteId);
|
loadBySpriteId(spriteId);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('furni-editor:open', handler as EventListener);
|
window.addEventListener('furni-editor:open', handler);
|
||||||
|
|
||||||
return () => window.removeEventListener('furni-editor:open', handler as EventListener);
|
return () => window.removeEventListener('furni-editor:open', handler);
|
||||||
}, [ isMod, loadBySpriteId ]);
|
}, [ isMod, loadBySpriteId ]);
|
||||||
|
|
||||||
const handleSelect = useCallback((id: number) =>
|
const handleSelect = useCallback((id: number) =>
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ export const GroupForumListView: FC<GroupForumListViewProps> = props =>
|
|||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
value={ listMode }
|
value={ listMode }
|
||||||
onChange={ e => { setListMode(parseInt(e.target.value)); setStartIndex(0); } }>
|
onChange={ e =>
|
||||||
|
{
|
||||||
|
setListMode(parseInt(e.target.value)); setStartIndex(0);
|
||||||
|
} }>
|
||||||
<option value={ 0 }>{ LocalizeText('groupforum.list.tab.most_active') }</option>
|
<option value={ 0 }>{ LocalizeText('groupforum.list.tab.most_active') }</option>
|
||||||
<option value={ 2 }>{ LocalizeText('groupforum.list.tab.my_forums') }</option>
|
<option value={ 2 }>{ LocalizeText('groupforum.list.tab.my_forums') }</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -89,7 +92,10 @@ export const GroupForumListView: FC<GroupForumListViewProps> = props =>
|
|||||||
<Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }>
|
<Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }>
|
||||||
{ (forum.lastMessageAuthorId > 0) && <>
|
{ (forum.lastMessageAuthorId > 0) && <>
|
||||||
<Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text>
|
<Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text>
|
||||||
<Text small pointer underline onClick={ e => { e.stopPropagation(); GetUserProfile(forum.lastMessageAuthorId); } }>
|
<Text small pointer underline onClick={ e =>
|
||||||
|
{
|
||||||
|
e.stopPropagation(); GetUserProfile(forum.lastMessageAuthorId);
|
||||||
|
} }>
|
||||||
{ forum.lastMessageAuthorName }
|
{ forum.lastMessageAuthorName }
|
||||||
</Text>
|
</Text>
|
||||||
<Text small variant="muted">{ formatTimeAgo(forum.lastMessageTimeAsSecondsAgo) }</Text>
|
<Text small variant="muted">{ formatTimeAgo(forum.lastMessageTimeAsSecondsAgo) }</Text>
|
||||||
|
|||||||
@@ -165,7 +165,10 @@ export const GroupForumThreadListView: FC<GroupForumThreadListViewProps> = props
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap={ 1 }>
|
<Flex gap={ 1 }>
|
||||||
<Text small variant="muted">{ LocalizeText('messageboard.started.by') }</Text>
|
<Text small variant="muted">{ LocalizeText('messageboard.started.by') }</Text>
|
||||||
<Text small pointer underline onClick={ e => { e.stopPropagation(); GetUserProfile(thread.authorId); } }>
|
<Text small pointer underline onClick={ e =>
|
||||||
|
{
|
||||||
|
e.stopPropagation(); GetUserProfile(thread.authorId);
|
||||||
|
} }>
|
||||||
{ thread.authorName }
|
{ thread.authorName }
|
||||||
</Text>
|
</Text>
|
||||||
<Text small variant="muted">- { formatTimeAgo(thread.creationTimeAsSecondsAgo) }</Text>
|
<Text small variant="muted">- { formatTimeAgo(thread.creationTimeAsSecondsAgo) }</Text>
|
||||||
@@ -182,7 +185,10 @@ export const GroupForumThreadListView: FC<GroupForumThreadListViewProps> = props
|
|||||||
</Column> }
|
</Column> }
|
||||||
<Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }>
|
<Column className="flex-shrink-0 text-end min-w-[100px]" gap={ 0 }>
|
||||||
<Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text>
|
<Text small variant="muted">{ LocalizeText('messageboard.last.message') }</Text>
|
||||||
<Text small pointer underline onClick={ e => { e.stopPropagation(); GetUserProfile(thread.lastUserId); } }>
|
<Text small pointer underline onClick={ e =>
|
||||||
|
{
|
||||||
|
e.stopPropagation(); GetUserProfile(thread.lastUserId);
|
||||||
|
} }>
|
||||||
{ thread.lastUserName }
|
{ thread.lastUserName }
|
||||||
</Text>
|
</Text>
|
||||||
<Text small variant="muted">{ formatTimeAgo(thread.lastCommentTime) }</Text>
|
<Text small variant="muted">{ formatTimeAgo(thread.lastCommentTime) }</Text>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ function getHourInTimezone(timezone: string): number
|
|||||||
*/
|
*/
|
||||||
function getTimeOfDay(hour: number): string
|
function getTimeOfDay(hour: number): string
|
||||||
{
|
{
|
||||||
if(hour > 5 && hour <= 9) return 'morning';
|
if(hour > 5 && hour <= 9) return 'morning';
|
||||||
if(hour > 9 && hour <= 16) return 'day';
|
if(hour > 9 && hour <= 16) return 'day';
|
||||||
if(hour > 16 && hour <= 19) return 'sunset';
|
if(hour > 16 && hour <= 19) return 'sunset';
|
||||||
if(hour > 19 && hour <= 23) return 'evening';
|
if(hour > 19 && hour <= 23) return 'evening';
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export const HotelView: FC<{}> = props =>
|
|||||||
return getTimeOfDay(hour);
|
return getTimeOfDay(hour);
|
||||||
}, [ timezone ]);
|
}, [ timezone ]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
const timeOfDay = 'sunset';
|
const timeOfDay = 'sunset';
|
||||||
For debuging the diff views
|
For debuging the diff views
|
||||||
*/
|
*/
|
||||||
@@ -73,9 +73,9 @@ export const HotelView: FC<{}> = props =>
|
|||||||
const skyColor = SKY_COLORS[timeOfDay] ?? configBgColor ?? '#000';
|
const skyColor = SKY_COLORS[timeOfDay] ?? configBgColor ?? '#000';
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isDragging = useRef(false);
|
const isDragging = useRef(false);
|
||||||
const lastMouseX = useRef(0);
|
const lastMouseX = useRef(0);
|
||||||
const lastMouseY = useRef(0);
|
const lastMouseY = useRef(0);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
@@ -85,7 +85,7 @@ export const HotelView: FC<{}> = props =>
|
|||||||
|
|
||||||
const centerView = () =>
|
const centerView = () =>
|
||||||
{
|
{
|
||||||
const viewportWidth = window.innerWidth;
|
const viewportWidth = window.innerWidth;
|
||||||
const viewportHeight = window.innerHeight - 55;
|
const viewportHeight = window.innerHeight - 55;
|
||||||
|
|
||||||
const lobbyEl = container.querySelector<HTMLElement>('.nitro-hotel-view-lobby');
|
const lobbyEl = container.querySelector<HTMLElement>('.nitro-hotel-view-lobby');
|
||||||
@@ -93,18 +93,18 @@ export const HotelView: FC<{}> = props =>
|
|||||||
if(lobbyEl)
|
if(lobbyEl)
|
||||||
{
|
{
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
const lobbyRect = lobbyEl.getBoundingClientRect();
|
const lobbyRect = lobbyEl.getBoundingClientRect();
|
||||||
|
|
||||||
const lobbyCenterX = (lobbyRect.left - containerRect.left) + container.scrollLeft + lobbyRect.width / 2;
|
const lobbyCenterX = (lobbyRect.left - containerRect.left) + container.scrollLeft + lobbyRect.width / 2;
|
||||||
const lobbyCenterY = (lobbyRect.top - containerRect.top) + container.scrollTop + lobbyRect.height / 2;
|
const lobbyCenterY = (lobbyRect.top - containerRect.top) + container.scrollTop + lobbyRect.height / 2;
|
||||||
|
|
||||||
container.scrollLeft = Math.max(0, lobbyCenterX - viewportWidth / 2);
|
container.scrollLeft = Math.max(0, lobbyCenterX - viewportWidth / 2);
|
||||||
container.scrollTop = Math.max(0, lobbyCenterY - viewportHeight / 2);
|
container.scrollTop = Math.max(0, lobbyCenterY - viewportHeight / 2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
container.scrollLeft = Math.max(0, (2600 - viewportWidth) / 2);
|
container.scrollLeft = Math.max(0, (2600 - viewportWidth) / 2);
|
||||||
container.scrollTop = Math.max(0, (1425 - viewportHeight) / 2);
|
container.scrollTop = Math.max(0, (1425 - viewportHeight) / 2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ export const HotelView: FC<{}> = props =>
|
|||||||
if(containerRef.current)
|
if(containerRef.current)
|
||||||
{
|
{
|
||||||
containerRef.current.scrollLeft -= dx;
|
containerRef.current.scrollLeft -= dx;
|
||||||
containerRef.current.scrollTop -= dy;
|
containerRef.current.scrollTop -= dy;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Base } from '../../common';
|
|||||||
|
|
||||||
export interface RoomWidgetViewProps {}
|
export interface RoomWidgetViewProps {}
|
||||||
|
|
||||||
export const RoomWidgetView: FC<RoomWidgetViewProps> = props => {
|
export const RoomWidgetView: FC<RoomWidgetViewProps> = props =>
|
||||||
|
{
|
||||||
const poolId = GetConfigurationValue<string>('hotelview')['room.pool'];
|
const poolId = GetConfigurationValue<string>('hotelview')['room.pool'];
|
||||||
const picnicId = GetConfigurationValue<string>('hotelview')['room.picnic'];
|
const picnicId = GetConfigurationValue<string>('hotelview')['room.picnic'];
|
||||||
const rooftopId = GetConfigurationValue<string>('hotelview')['room.rooftop'];
|
const rooftopId = GetConfigurationValue<string>('hotelview')['room.rooftop'];
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ export const InterfaceColorTabView: FC<{}> = () =>
|
|||||||
setImportValue('');
|
setImportValue('');
|
||||||
setShowImport(false);
|
setShowImport(false);
|
||||||
}
|
}
|
||||||
catch(e) {}
|
catch(e)
|
||||||
|
{}
|
||||||
}, [ importValue, updateSettings ]);
|
}, [ importValue, updateSettings ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -107,8 +107,14 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { refreshOwnCustomBadges(); }, [ refreshOwnCustomBadges ]);
|
useEffect(() =>
|
||||||
useEffect(() => { ensureCustomBadgeTexts(); }, []);
|
{
|
||||||
|
refreshOwnCustomBadges();
|
||||||
|
}, [ refreshOwnCustomBadges ]);
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
ensureCustomBadgeTexts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const baseCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes);
|
const baseCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes);
|
||||||
const customCount = useMemo(() => baseCodes.filter(c => isCustomBadgeCode(c)).length, [ baseCodes ]);
|
const customCount = useMemo(() => baseCodes.filter(c => isCustomBadgeCode(c)).length, [ baseCodes ]);
|
||||||
@@ -138,7 +144,8 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
|||||||
await refreshOwnCustomBadges();
|
await refreshOwnCustomBadges();
|
||||||
refreshCustomBadgeTexts();
|
refreshCustomBadgeTexts();
|
||||||
}
|
}
|
||||||
catch { /* error already surfaced server-side */ }
|
catch
|
||||||
|
{ /* error already surfaced server-side */ }
|
||||||
},
|
},
|
||||||
null, null, null,
|
null, null, null,
|
||||||
LocalizeText('inventory.delete.confirm_delete.title')
|
LocalizeText('inventory.delete.confirm_delete.title')
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ interface LoadingViewProps {
|
|||||||
homeUrl?: string;
|
homeUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoadingView: FC<LoadingViewProps> = props => {
|
export const LoadingView: FC<LoadingViewProps> = props =>
|
||||||
|
{
|
||||||
const { isError = false, message = '', homeUrl = '' } = props;
|
const { isError = false, message = '', homeUrl = '' } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ const interpolate = (value: string | null | undefined): string =>
|
|||||||
|
|
||||||
let output = value;
|
let output = value;
|
||||||
|
|
||||||
try { output = GetConfiguration().interpolate(value) || value; }
|
try
|
||||||
catch {}
|
{
|
||||||
|
output = GetConfiguration().interpolate(value) || value;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
return output.replace(/\$\{([^}]+)\}/g, (_, key: string) =>
|
return output.replace(/\$\{([^}]+)\}/g, (_, key: string) =>
|
||||||
{
|
{
|
||||||
@@ -45,7 +49,8 @@ const interpolate = (value: string | null | undefined): string =>
|
|||||||
|
|
||||||
if(configValue) return configValue;
|
if(configValue) return configValue;
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -53,7 +58,8 @@ const interpolate = (value: string | null | undefined): string =>
|
|||||||
|
|
||||||
if(configValue) return configValue;
|
if(configValue) return configValue;
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
@@ -100,13 +106,20 @@ const readLock = (): AttemptState =>
|
|||||||
if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 };
|
if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 };
|
||||||
return JSON.parse(raw);
|
return JSON.parse(raw);
|
||||||
}
|
}
|
||||||
catch { return { attempts: 0, firstAt: 0, lockedUntil: 0 }; }
|
catch
|
||||||
|
{
|
||||||
|
return { attempts: 0, firstAt: 0, lockedUntil: 0 };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const writeLock = (state: AttemptState) =>
|
const writeLock = (state: AttemptState) =>
|
||||||
{
|
{
|
||||||
try { sessionStorage.setItem(LOCK_KEY, JSON.stringify(state)); }
|
try
|
||||||
catch { }
|
{
|
||||||
|
sessionStorage.setItem(LOCK_KEY, JSON.stringify(state));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ }
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeLanguageCode = (value: string): string =>
|
const normalizeLanguageCode = (value: string): string =>
|
||||||
@@ -150,7 +163,8 @@ const readCachedLocale = (): LoginLocale =>
|
|||||||
|
|
||||||
if(typeof settings.uiTextLanguage === 'string' && settings.uiTextLanguage.length) return resolveLoginLocale(settings.uiTextLanguage);
|
if(typeof settings.uiTextLanguage === 'string' && settings.uiTextLanguage.length) return resolveLoginLocale(settings.uiTextLanguage);
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
return getBrowserLocale();
|
return getBrowserLocale();
|
||||||
};
|
};
|
||||||
@@ -170,7 +184,8 @@ const applyLocaleSelection = (locale: LoginLocale): void =>
|
|||||||
|
|
||||||
localStorage.setItem(CHAT_TRANSLATION_SETTINGS_KEY, JSON.stringify(nextSettings));
|
localStorage.setItem(CHAT_TRANSLATION_SETTINGS_KEY, JSON.stringify(nextSettings));
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoginSubmitButton: FC<{ isEntering: boolean; isLocked: boolean; loginPingingServer: boolean }> = ({ isEntering, isLocked, loginPingingServer }) =>
|
const LoginSubmitButton: FC<{ isEntering: boolean; isLocked: boolean; loginPingingServer: boolean }> = ({ isEntering, isLocked, loginPingingServer }) =>
|
||||||
@@ -411,8 +426,12 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
|||||||
});
|
});
|
||||||
|
|
||||||
let payload: Record<string, unknown> = {};
|
let payload: Record<string, unknown> = {};
|
||||||
try { payload = await response.json(); }
|
try
|
||||||
catch { }
|
{
|
||||||
|
payload = await response.json();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ }
|
||||||
|
|
||||||
return { ok: response.ok, status: response.status, payload };
|
return { ok: response.ok, status: response.status, payload };
|
||||||
}, []);
|
}, []);
|
||||||
@@ -705,7 +724,10 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="login-widget-button"
|
className="login-widget-button"
|
||||||
onClick={ () => { if(btnLink) window.location.href = btnLink; } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
if(btnLink) window.location.href = btnLink;
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ btnText }
|
{ btnText }
|
||||||
</button> }
|
</button> }
|
||||||
@@ -957,7 +979,12 @@ const renderAvatarPreview = (figure: string, gender: GenderKey, setType: string)
|
|||||||
if(resolved) return;
|
if(resolved) return;
|
||||||
resolved = true;
|
resolved = true;
|
||||||
if(timer !== null) window.clearTimeout(timer);
|
if(timer !== null) window.clearTimeout(timer);
|
||||||
try { avatarImage?.dispose(); } catch {}
|
try
|
||||||
|
{
|
||||||
|
avatarImage?.dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{}
|
||||||
avatarImage = null;
|
avatarImage = null;
|
||||||
if(url)
|
if(url)
|
||||||
{
|
{
|
||||||
@@ -976,17 +1003,26 @@ const renderAvatarPreview = (figure: string, gender: GenderKey, setType: string)
|
|||||||
const attempt = () =>
|
const attempt = () =>
|
||||||
{
|
{
|
||||||
if(resolved) return;
|
if(resolved) return;
|
||||||
if(attempts >= AVATAR_PREVIEW_MAX_ATTEMPTS) { finish(''); return; }
|
if(attempts >= AVATAR_PREVIEW_MAX_ATTEMPTS)
|
||||||
|
{
|
||||||
|
finish(''); return;
|
||||||
|
}
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
try { avatarImage?.dispose(); } catch {}
|
try
|
||||||
|
{
|
||||||
|
avatarImage?.dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{}
|
||||||
avatarImage = null;
|
avatarImage = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, {
|
avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, {
|
||||||
resetFigure: () => attempt(),
|
resetFigure: () => attempt(),
|
||||||
dispose: () => {},
|
dispose: () =>
|
||||||
|
{},
|
||||||
disposed: false
|
disposed: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -996,7 +1032,10 @@ const renderAvatarPreview = (figure: string, gender: GenderKey, setType: string)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!avatarImage) { finish(''); return; }
|
if(!avatarImage)
|
||||||
|
{
|
||||||
|
finish(''); return;
|
||||||
|
}
|
||||||
|
|
||||||
if(avatarImage.isPlaceholder()) return;
|
if(avatarImage.isPlaceholder()) return;
|
||||||
|
|
||||||
@@ -1036,7 +1075,10 @@ const useAvatarPreview = (figure: string, gender: GenderKey, setType: string): s
|
|||||||
{
|
{
|
||||||
if(!cancelled) setUrl(result);
|
if(!cancelled) setUrl(result);
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [ figure, gender, setType ]);
|
}, [ figure, gender, setType ]);
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
@@ -1061,7 +1103,10 @@ const AvatarPartRow: FC<AvatarPartRowProps> = ({ setType, selection, gender, onP
|
|||||||
<div className="avatar-part-row">
|
<div className="avatar-part-row">
|
||||||
<button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` } onClick={ onPrev }>‹</button>
|
<button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` } onClick={ onPrev }>‹</button>
|
||||||
<div className={ `part-preview part-preview-${ setType }` }>
|
<div className={ `part-preview part-preview-${ setType }` }>
|
||||||
{ url && <img src={ url } alt={ `${ setType } preview` } onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> }
|
{ url && <img src={ url } alt={ `${ setType } preview` } onError={ e =>
|
||||||
|
{
|
||||||
|
(e.currentTarget).style.visibility = 'hidden';
|
||||||
|
} } /> }
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` } onClick={ onNext }>›</button>
|
<button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` } onClick={ onNext }>›</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1108,7 +1153,10 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
const ok = await onCheckServer();
|
const ok = await onCheckServer();
|
||||||
if(!cancelled) setServerReachable(ok);
|
if(!cancelled) setServerReachable(ok);
|
||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [ onCheckServer ]);
|
}, [ onCheckServer ]);
|
||||||
|
|
||||||
const resetWidget = useCallback(() =>
|
const resetWidget = useCallback(() =>
|
||||||
@@ -1117,15 +1165,24 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
setResetSignal(prev => prev + 1);
|
setResetSignal(prev => prev + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { setLocalError(null); }, [ step ]);
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setLocalError(null);
|
||||||
|
}, [ step ]);
|
||||||
|
|
||||||
const [ figureData, setFigureData ] = useState<FigureData | null>(null);
|
const [ figureData, setFigureData ] = useState<FigureData | null>(null);
|
||||||
const figureDataUrlRaw = GetConfigurationValue<string>('avatar.figuredata.url', '');
|
const figureDataUrlRaw = GetConfigurationValue<string>('avatar.figuredata.url', '');
|
||||||
const figureDataUrl = useMemo(() =>
|
const figureDataUrl = useMemo(() =>
|
||||||
{
|
{
|
||||||
if(!figureDataUrlRaw) return '';
|
if(!figureDataUrlRaw) return '';
|
||||||
try { return GetConfiguration().interpolate(figureDataUrlRaw); }
|
try
|
||||||
catch { return figureDataUrlRaw; }
|
{
|
||||||
|
return GetConfiguration().interpolate(figureDataUrlRaw);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return figureDataUrlRaw;
|
||||||
|
}
|
||||||
}, [ figureDataUrlRaw ]);
|
}, [ figureDataUrlRaw ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@@ -1134,9 +1191,16 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
fetch(figureDataUrl, { credentials: 'omit' })
|
fetch(figureDataUrl, { credentials: 'omit' })
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.then(json => { if(!cancelled && json) setFigureData(json as FigureData); })
|
.then(json =>
|
||||||
.catch(() => { });
|
{
|
||||||
return () => { cancelled = true; };
|
if(!cancelled && json) setFigureData(json as FigureData);
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
{ });
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [ step, figureData, figureDataUrl ]);
|
}, [ step, figureData, figureDataUrl ]);
|
||||||
|
|
||||||
const partOptions = useMemo(() =>
|
const partOptions = useMemo(() =>
|
||||||
@@ -1162,7 +1226,10 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
{
|
{
|
||||||
if(!PART_ROWS.includes(st.type)) continue;
|
if(!PART_ROWS.includes(st.type)) continue;
|
||||||
const palette = figureData.palettes.find(p => p.id === st.paletteId);
|
const palette = figureData.palettes.find(p => p.id === st.paletteId);
|
||||||
if(!palette) { result[st.type] = []; continue; }
|
if(!palette)
|
||||||
|
{
|
||||||
|
result[st.type] = []; continue;
|
||||||
|
}
|
||||||
result[st.type] = palette.colors
|
result[st.type] = palette.colors
|
||||||
.filter(c => c.selectable && c.club === 0)
|
.filter(c => c.selectable && c.club === 0)
|
||||||
.map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() }));
|
.map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() }));
|
||||||
@@ -1199,12 +1266,16 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : '';
|
const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : '';
|
||||||
const figure = typeof entry._figure === 'string' ? entry._figure : '';
|
const figure = typeof entry._figure === 'string' ? entry._figure : '';
|
||||||
if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue;
|
if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue;
|
||||||
parsed.push({ gender: rawGender as GenderKey, figure });
|
parsed.push({ gender: rawGender, figure });
|
||||||
}
|
}
|
||||||
if(parsed.length) setHotLooks(parsed);
|
if(parsed.length) setHotLooks(parsed);
|
||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() =>
|
||||||
return () => { cancelled = true; };
|
{ });
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [ step, hotLooks.length ]);
|
}, [ step, hotLooks.length ]);
|
||||||
|
|
||||||
const applyLook = useCallback((figure: string, lookGender: GenderKey) =>
|
const applyLook = useCallback((figure: string, lookGender: GenderKey) =>
|
||||||
@@ -1498,11 +1569,15 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="avatar-preview">
|
<div className="avatar-preview">
|
||||||
{ previewSrc && <img src={ previewSrc } alt="Habbo preview" onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> }
|
{ previewSrc && <img src={ previewSrc } alt="Habbo preview" onError={ e =>
|
||||||
|
{
|
||||||
|
(e.currentTarget).style.visibility = 'hidden';
|
||||||
|
} } /> }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="avatar-color-col">
|
<div className="avatar-color-col">
|
||||||
{ PART_ROWS.map(setType => {
|
{ PART_ROWS.map(setType =>
|
||||||
|
{
|
||||||
const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0;
|
const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0;
|
||||||
const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor;
|
const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor;
|
||||||
const swatchHex = hexFor(setType, currentColor);
|
const swatchHex = hexFor(setType, currentColor);
|
||||||
|
|||||||
@@ -67,7 +67,12 @@ export const TurnstileWidget: FC<TurnstileWidgetProps> = props =>
|
|||||||
{
|
{
|
||||||
if(widgetIdRef.current && window.turnstile)
|
if(widgetIdRef.current && window.turnstile)
|
||||||
{
|
{
|
||||||
try { window.turnstile.remove(widgetIdRef.current); } catch { }
|
try
|
||||||
|
{
|
||||||
|
window.turnstile.remove(widgetIdRef.current);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ }
|
||||||
widgetIdRef.current = null;
|
widgetIdRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -78,7 +83,12 @@ export const TurnstileWidget: FC<TurnstileWidgetProps> = props =>
|
|||||||
if(resetSignal <= 0) return;
|
if(resetSignal <= 0) return;
|
||||||
if(widgetIdRef.current && window.turnstile)
|
if(widgetIdRef.current && window.turnstile)
|
||||||
{
|
{
|
||||||
try { window.turnstile.reset(widgetIdRef.current); } catch { }
|
try
|
||||||
|
{
|
||||||
|
window.turnstile.reset(widgetIdRef.current);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ }
|
||||||
}
|
}
|
||||||
}, [ resetSignal ]);
|
}, [ resetSignal ]);
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(!newsUrl) { setFailed(true); return; }
|
if(!newsUrl)
|
||||||
|
{
|
||||||
|
setFailed(true); return;
|
||||||
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
@@ -63,7 +66,10 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
|
|||||||
: Array.isArray(json) ? (json as RawNewsItem[]) : [];
|
: Array.isArray(json) ? (json as RawNewsItem[]) : [];
|
||||||
setItems(rawList.map((raw, idx) => normalizeNewsItem(raw, idx + 1)));
|
setItems(rawList.map((raw, idx) => normalizeNewsItem(raw, idx + 1)));
|
||||||
})
|
})
|
||||||
.catch(() => { if(!cancelled) setFailed(true); });
|
.catch(() =>
|
||||||
|
{
|
||||||
|
if(!cancelled) setFailed(true);
|
||||||
|
});
|
||||||
return () =>
|
return () =>
|
||||||
{
|
{
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
@@ -87,8 +93,14 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
|
|||||||
const current = items[Math.min(index, items.length - 1)];
|
const current = items[Math.min(index, items.length - 1)];
|
||||||
const hasMany = items.length > 1;
|
const hasMany = items.length > 1;
|
||||||
const bumpAuto = () => setAutoTick(t => t + 1);
|
const bumpAuto = () => setAutoTick(t => t + 1);
|
||||||
const prev = () => { setIndex(i => (i - 1 + items.length) % items.length); bumpAuto(); };
|
const prev = () =>
|
||||||
const next = () => { setIndex(i => (i + 1) % items.length); bumpAuto(); };
|
{
|
||||||
|
setIndex(i => (i - 1 + items.length) % items.length); bumpAuto();
|
||||||
|
};
|
||||||
|
const next = () =>
|
||||||
|
{
|
||||||
|
setIndex(i => (i + 1) % items.length); bumpAuto();
|
||||||
|
};
|
||||||
|
|
||||||
const safeLinkUrl = resolveNewsLink(current.linkUrl);
|
const safeLinkUrl = resolveNewsLink(current.linkUrl);
|
||||||
const safeImageSrc = resolveNewsImage(current.image);
|
const safeImageSrc = resolveNewsImage(current.image);
|
||||||
@@ -119,7 +131,10 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
|
|||||||
<img
|
<img
|
||||||
src={ safeImageSrc }
|
src={ safeImageSrc }
|
||||||
alt={ current.title || 'news' }
|
alt={ current.title || 'news' }
|
||||||
onError={ e => { (e.currentTarget as HTMLImageElement).style.display = 'none'; } }
|
onError={ e =>
|
||||||
|
{
|
||||||
|
(e.currentTarget).style.display = 'none';
|
||||||
|
} }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,10 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
const ok = await onCheckServer();
|
const ok = await onCheckServer();
|
||||||
if(!cancelled) setServerReachable(ok);
|
if(!cancelled) setServerReachable(ok);
|
||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [ onCheckServer ]);
|
}, [ onCheckServer ]);
|
||||||
|
|
||||||
const resetWidget = useCallback(() =>
|
const resetWidget = useCallback(() =>
|
||||||
@@ -81,7 +84,10 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
setResetSignal(prev => prev + 1);
|
setResetSignal(prev => prev + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { setLocalError(null); }, [ step ]);
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setLocalError(null);
|
||||||
|
}, [ step ]);
|
||||||
|
|
||||||
const [ roomTemplates, setRoomTemplates ] = useState<RoomTemplate[] | null>(null);
|
const [ roomTemplates, setRoomTemplates ] = useState<RoomTemplate[] | null>(null);
|
||||||
const [ roomTemplatesError, setRoomTemplatesError ] = useState<string | null>(null);
|
const [ roomTemplatesError, setRoomTemplatesError ] = useState<string | null>(null);
|
||||||
@@ -92,8 +98,14 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
const figureDataUrl = useMemo(() =>
|
const figureDataUrl = useMemo(() =>
|
||||||
{
|
{
|
||||||
if(!figureDataUrlRaw) return '';
|
if(!figureDataUrlRaw) return '';
|
||||||
try { return GetConfiguration().interpolate(figureDataUrlRaw); }
|
try
|
||||||
catch { return figureDataUrlRaw; }
|
{
|
||||||
|
return GetConfiguration().interpolate(figureDataUrlRaw);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return figureDataUrlRaw;
|
||||||
|
}
|
||||||
}, [ figureDataUrlRaw ]);
|
}, [ figureDataUrlRaw ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@@ -102,9 +114,16 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
fetch(figureDataUrl, { credentials: 'omit' })
|
fetch(figureDataUrl, { credentials: 'omit' })
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.then(json => { if(!cancelled && json) setFigureData(json as FigureData); })
|
.then(json =>
|
||||||
.catch(() => { });
|
{
|
||||||
return () => { cancelled = true; };
|
if(!cancelled && json) setFigureData(json as FigureData);
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
{ });
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [ step, figureData, figureDataUrl ]);
|
}, [ step, figureData, figureDataUrl ]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@@ -113,23 +132,29 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setRoomTemplatesError(null);
|
setRoomTemplatesError(null);
|
||||||
fetch(roomTemplatesUrl, { credentials: 'include' })
|
fetch(roomTemplatesUrl, { credentials: 'include' })
|
||||||
.then(async r => {
|
.then(async r =>
|
||||||
|
{
|
||||||
if(!r.ok) throw new Error(`status ${ r.status }`);
|
if(!r.ok) throw new Error(`status ${ r.status }`);
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
.then(json => {
|
.then(json =>
|
||||||
|
{
|
||||||
if(cancelled) return;
|
if(cancelled) return;
|
||||||
const list = Array.isArray((json as { templates?: unknown })?.templates)
|
const list = Array.isArray((json as { templates?: unknown })?.templates)
|
||||||
? (json as { templates: RoomTemplate[] }).templates
|
? (json as { templates: RoomTemplate[] }).templates
|
||||||
: [];
|
: [];
|
||||||
setRoomTemplates(list);
|
setRoomTemplates(list);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() =>
|
||||||
|
{
|
||||||
if(cancelled) return;
|
if(cancelled) return;
|
||||||
setRoomTemplates([]);
|
setRoomTemplates([]);
|
||||||
setRoomTemplatesError(t('nitro.login.register.room.error', 'Could not load room options. You can still skip this step.'));
|
setRoomTemplatesError(t('nitro.login.register.room.error', 'Could not load room options. You can still skip this step.'));
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [ step, roomTemplates, roomTemplatesUrl ]);
|
}, [ step, roomTemplates, roomTemplatesUrl ]);
|
||||||
|
|
||||||
const partOptions = useMemo(() =>
|
const partOptions = useMemo(() =>
|
||||||
@@ -155,7 +180,10 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
{
|
{
|
||||||
if(!PART_ROWS.includes(st.type)) continue;
|
if(!PART_ROWS.includes(st.type)) continue;
|
||||||
const palette = figureData.palettes.find(p => p.id === st.paletteId);
|
const palette = figureData.palettes.find(p => p.id === st.paletteId);
|
||||||
if(!palette) { result[st.type] = []; continue; }
|
if(!palette)
|
||||||
|
{
|
||||||
|
result[st.type] = []; continue;
|
||||||
|
}
|
||||||
result[st.type] = palette.colors
|
result[st.type] = palette.colors
|
||||||
.filter(c => c.selectable && c.club === 0)
|
.filter(c => c.selectable && c.club === 0)
|
||||||
.map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() }));
|
.map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() }));
|
||||||
@@ -192,12 +220,16 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : '';
|
const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : '';
|
||||||
const figure = typeof entry._figure === 'string' ? entry._figure : '';
|
const figure = typeof entry._figure === 'string' ? entry._figure : '';
|
||||||
if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue;
|
if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue;
|
||||||
parsed.push({ gender: rawGender as GenderKey, figure });
|
parsed.push({ gender: rawGender, figure });
|
||||||
}
|
}
|
||||||
if(parsed.length) setHotLooks(parsed);
|
if(parsed.length) setHotLooks(parsed);
|
||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() =>
|
||||||
return () => { cancelled = true; };
|
{ });
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [ step, hotLooks.length ]);
|
}, [ step, hotLooks.length ]);
|
||||||
|
|
||||||
const applyLook = useCallback((figure: string, lookGender: GenderKey) =>
|
const applyLook = useCallback((figure: string, lookGender: GenderKey) =>
|
||||||
@@ -499,14 +531,18 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
|
|
||||||
<div className="avatar-builder">
|
<div className="avatar-builder">
|
||||||
<div className="avatar-part-col">
|
<div className="avatar-part-col">
|
||||||
{ PART_ROWS.map(setType => {
|
{ PART_ROWS.map(setType =>
|
||||||
|
{
|
||||||
const partPreviewSrc = buildPartPreviewUrl(imagingUrl, setType, selection, gender);
|
const partPreviewSrc = buildPartPreviewUrl(imagingUrl, setType, selection, gender);
|
||||||
return (
|
return (
|
||||||
<div className="avatar-part-row" key={ `part-${ setType }` }>
|
<div className="avatar-part-row" key={ `part-${ setType }` }>
|
||||||
<button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` }
|
<button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` }
|
||||||
onClick={ () => cyclePart(setType, -1) }>‹</button>
|
onClick={ () => cyclePart(setType, -1) }>‹</button>
|
||||||
<div className={ `part-preview part-preview-${ setType }` }>
|
<div className={ `part-preview part-preview-${ setType }` }>
|
||||||
<img src={ partPreviewSrc } alt={ `${ setType } preview` } onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } />
|
<img src={ partPreviewSrc } alt={ `${ setType } preview` } onError={ e =>
|
||||||
|
{
|
||||||
|
(e.currentTarget).style.visibility = 'hidden';
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` }
|
<button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` }
|
||||||
onClick={ () => cyclePart(setType, 1) }>›</button>
|
onClick={ () => cyclePart(setType, 1) }>›</button>
|
||||||
@@ -516,11 +552,15 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="avatar-preview">
|
<div className="avatar-preview">
|
||||||
<img src={ previewSrc } alt="Habbo preview" onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } />
|
<img src={ previewSrc } alt="Habbo preview" onError={ e =>
|
||||||
|
{
|
||||||
|
(e.currentTarget).style.visibility = 'hidden';
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="avatar-color-col">
|
<div className="avatar-color-col">
|
||||||
{ PART_ROWS.map(setType => {
|
{ PART_ROWS.map(setType =>
|
||||||
|
{
|
||||||
const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0;
|
const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0;
|
||||||
const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor;
|
const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor;
|
||||||
const swatchHex = hexFor(setType, currentColor);
|
const swatchHex = hexFor(setType, currentColor);
|
||||||
@@ -603,7 +643,10 @@ export const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
onChange={ () => setSelectedTemplateId(template.templateId) } />
|
onChange={ () => setSelectedTemplateId(template.templateId) } />
|
||||||
{ template.thumbnail &&
|
{ template.thumbnail &&
|
||||||
<img className="room-template-thumb" src={ template.thumbnail } alt={ template.title }
|
<img className="room-template-thumb" src={ template.thumbnail } alt={ template.title }
|
||||||
onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> }
|
onError={ e =>
|
||||||
|
{
|
||||||
|
(e.currentTarget).style.visibility = 'hidden';
|
||||||
|
} } /> }
|
||||||
<div className="room-template-body">
|
<div className="room-template-body">
|
||||||
<div className="room-template-title">{ template.title }</div>
|
<div className="room-template-title">{ template.title }</div>
|
||||||
{ template.description &&
|
{ template.description &&
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export const t = (key: string, fallback: string, params?: string[], replacements
|
|||||||
const value = LocalizeText(key, params ?? null, replacements ?? null);
|
const value = LocalizeText(key, params ?? null, replacements ?? null);
|
||||||
if(value && value !== key) return value;
|
if(value && value !== key) return value;
|
||||||
}
|
}
|
||||||
catch {}
|
catch
|
||||||
|
{}
|
||||||
|
|
||||||
if(!params || !replacements) return fallback;
|
if(!params || !replacements) return fallback;
|
||||||
let out = fallback;
|
let out = fallback;
|
||||||
@@ -22,6 +23,12 @@ export const t = (key: string, fallback: string, params?: string[], replacements
|
|||||||
export const interpolate = (value: string | null | undefined): string =>
|
export const interpolate = (value: string | null | undefined): string =>
|
||||||
{
|
{
|
||||||
if(!value) return '';
|
if(!value) return '';
|
||||||
try { return GetConfiguration().interpolate(value); }
|
try
|
||||||
catch { return value; }
|
{
|
||||||
|
return GetConfiguration().interpolate(value);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,11 +13,18 @@ export const readLock = (): AttemptState =>
|
|||||||
if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 };
|
if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 };
|
||||||
return JSON.parse(raw);
|
return JSON.parse(raw);
|
||||||
}
|
}
|
||||||
catch { return { attempts: 0, firstAt: 0, lockedUntil: 0 }; }
|
catch
|
||||||
|
{
|
||||||
|
return { attempts: 0, firstAt: 0, lockedUntil: 0 };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writeLock = (state: AttemptState) =>
|
export const writeLock = (state: AttemptState) =>
|
||||||
{
|
{
|
||||||
try { sessionStorage.setItem(LOCK_KEY, JSON.stringify(state)); }
|
try
|
||||||
catch { }
|
{
|
||||||
|
sessionStorage.setItem(LOCK_KEY, JSON.stringify(state));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,8 +12,14 @@ export const resolveNewsImage = (raw: string | null | undefined): string =>
|
|||||||
if(value.startsWith('//')) return window.location.protocol + value;
|
if(value.startsWith('//')) return window.location.protocol + value;
|
||||||
if(value.startsWith('/'))
|
if(value.startsWith('/'))
|
||||||
{
|
{
|
||||||
try { return new URL(value, window.location.origin).href; }
|
try
|
||||||
catch { return window.location.origin + value; }
|
{
|
||||||
|
return new URL(value, window.location.origin).href;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return window.location.origin + value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(value.startsWith('data:'))
|
if(value.startsWith('data:'))
|
||||||
{
|
{
|
||||||
@@ -46,5 +52,8 @@ export const resolveNewsLink = (raw: string | null | undefined): string =>
|
|||||||
if(proto !== 'http:' && proto !== 'https:') return '';
|
if(proto !== 'http:' && proto !== 'https:') return '';
|
||||||
return url.href;
|
return url.href;
|
||||||
}
|
}
|
||||||
catch { return ''; }
|
catch
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NitroCardView className="nitro-mod-tools-chatlog min-w-[400px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
<NitroCardView className="nitro-mod-tools-chatlog min-w-[400px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
||||||
<NitroCardHeaderView headerText={ `Room Chatlog` } onCloseClick={ onCloseClick } />
|
<NitroCardHeaderView headerText={ 'Room Chatlog' } onCloseClick={ onCloseClick } />
|
||||||
<NitroCardContentView className="text-black" overflow="auto">
|
<NitroCardContentView className="text-black" overflow="auto">
|
||||||
{ roomChatlog &&
|
{ roomChatlog &&
|
||||||
<ChatlogView records={ [ roomChatlog ] } /> }
|
<ChatlogView records={ [ roomChatlog ] } /> }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const NavigatorRoomSettingsAccessTabView: FC<NavigatorRoomSettingsTabView
|
|||||||
if(!isTryingPassword || ((password.length <= 0) || (confirmPassword.length <= 0) || (password !== confirmPassword))) return;
|
if(!isTryingPassword || ((password.length <= 0) || (confirmPassword.length <= 0) || (password !== confirmPassword))) return;
|
||||||
|
|
||||||
handleChange('password', password);
|
handleChange('password', password);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,21 +59,21 @@ export const NavigatorRoomSettingsBasicTabView: FC<NavigatorRoomSettingsTabViewP
|
|||||||
CreateLinkEvent('navigator/search/myworld_view');
|
CreateLinkEvent('navigator/search/myworld_view');
|
||||||
},
|
},
|
||||||
null, null, null, LocalizeText('navigator.roomsettings.deleteroom.confirm.title'));
|
null, null, null, LocalizeText('navigator.roomsettings.deleteroom.confirm.title'));
|
||||||
}
|
};
|
||||||
|
|
||||||
const saveRoomName = () =>
|
const saveRoomName = () =>
|
||||||
{
|
{
|
||||||
if((roomName === roomData.roomName) || (roomName.length < ROOM_NAME_MIN_LENGTH) || (roomName.length > ROOM_NAME_MAX_LENGTH)) return;
|
if((roomName === roomData.roomName) || (roomName.length < ROOM_NAME_MIN_LENGTH) || (roomName.length > ROOM_NAME_MAX_LENGTH)) return;
|
||||||
|
|
||||||
handleChange('name', roomName);
|
handleChange('name', roomName);
|
||||||
}
|
};
|
||||||
|
|
||||||
const saveRoomDescription = () =>
|
const saveRoomDescription = () =>
|
||||||
{
|
{
|
||||||
if((roomDescription === roomData.roomDescription) || (roomDescription.length > DESC_MAX_LENGTH)) return;
|
if((roomDescription === roomData.roomDescription) || (roomDescription.length > DESC_MAX_LENGTH)) return;
|
||||||
|
|
||||||
handleChange('description', roomDescription);
|
handleChange('description', roomDescription);
|
||||||
}
|
};
|
||||||
|
|
||||||
const saveTags = (index: number) =>
|
const saveTags = (index: number) =>
|
||||||
{
|
{
|
||||||
@@ -86,7 +86,7 @@ export const NavigatorRoomSettingsBasicTabView: FC<NavigatorRoomSettingsTabViewP
|
|||||||
setTypeError('');
|
setTypeError('');
|
||||||
setTagIndex(index);
|
setTagIndex(index);
|
||||||
handleChange('tags', (roomTag1 === '' && roomTag2 !== '') ? [ roomTag2 ] : [ roomTag1, roomTag2 ]);
|
handleChange('tags', (roomTag1 === '' && roomTag2 !== '') ? [ roomTag2 ] : [ roomTag1, roomTag2 ]);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ export const NavigatorRoomSettingsModTabView: FC<NavigatorRoomSettingsTabViewPro
|
|||||||
if(index >= 0) newValue.splice(index, 1);
|
if(index >= 0) newValue.splice(index, 1);
|
||||||
|
|
||||||
return newValue;
|
return newValue;
|
||||||
})
|
});
|
||||||
|
|
||||||
SendMessageComposer(new RoomUnbanUserComposer(userId, roomData.roomId));
|
SendMessageComposer(new RoomUnbanUserComposer(userId, roomData.roomId));
|
||||||
|
|
||||||
setSelectedUserId(-1);
|
setSelectedUserId(-1);
|
||||||
}
|
};
|
||||||
|
|
||||||
useMessageEvent<BannedUsersFromRoomEvent>(BannedUsersFromRoomEvent, event =>
|
useMessageEvent<BannedUsersFromRoomEvent>(BannedUsersFromRoomEvent, event =>
|
||||||
{
|
{
|
||||||
@@ -115,4 +115,4 @@ export const NavigatorRoomSettingsModTabView: FC<NavigatorRoomSettingsTabViewPro
|
|||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
|
|||||||
{
|
{
|
||||||
if(friend.id === STAFF_CHAT_ID) return false;
|
if(friend.id === STAFF_CHAT_ID) return false;
|
||||||
if(friend.name === STAFF_CHAT_NAME) return false;
|
if(friend.name === STAFF_CHAT_NAME) return false;
|
||||||
if(friend.id <= 0) return false;
|
if(friend.id <= 0) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -46,7 +46,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
|
|||||||
{
|
{
|
||||||
if(id === STAFF_CHAT_ID) return false;
|
if(id === STAFF_CHAT_ID) return false;
|
||||||
if(name === STAFF_CHAT_NAME) return false;
|
if(name === STAFF_CHAT_NAME) return false;
|
||||||
if(id <= 0) return false;
|
if(id <= 0) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -189,7 +189,10 @@ export const NavigatorRoomSettingsView: FC<{}> = props =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NitroCardView className="nitro-room-settings" uniqueKey="nitro-room-settings">
|
<NitroCardView className="nitro-room-settings" uniqueKey="nitro-room-settings">
|
||||||
<NitroCardHeaderView headerText={ LocalizeText('navigator.roomsettings') } isInfoToHabboPages={ currentTab === TABS[3] } onClickInfoHabboPages={ () => { if(currentTab === TABS[3]) CreateLinkEvent('habbopages/chat/options'); } } onCloseClick={ onClose } />
|
<NitroCardHeaderView headerText={ LocalizeText('navigator.roomsettings') } isInfoToHabboPages={ currentTab === TABS[3] } onClickInfoHabboPages={ () =>
|
||||||
|
{
|
||||||
|
if(currentTab === TABS[3]) CreateLinkEvent('habbopages/chat/options');
|
||||||
|
} } onCloseClick={ onClose } />
|
||||||
<NitroCardTabsView>
|
<NitroCardTabsView>
|
||||||
{ TABS.map(tab =>
|
{ TABS.map(tab =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -100,8 +100,14 @@ export const NavigatorSearchResultItemInfoView: FC<NavigatorSearchResultItemInfo
|
|||||||
ref={ elementRef }
|
ref={ elementRef }
|
||||||
className="cursor-pointer nitro-icon icon-navigator-info"
|
className="cursor-pointer nitro-icon icon-navigator-info"
|
||||||
onClick={ handleIconClick }
|
onClick={ handleIconClick }
|
||||||
onMouseOver={ () => { if(!isControlled) setInternalVisible(true); } }
|
onMouseOver={ () =>
|
||||||
onMouseLeave={ () => { if(!isControlled) setInternalVisible(false); } }
|
{
|
||||||
|
if(!isControlled) setInternalVisible(true);
|
||||||
|
} }
|
||||||
|
onMouseLeave={ () =>
|
||||||
|
{
|
||||||
|
if(!isControlled) setInternalVisible(false);
|
||||||
|
} }
|
||||||
/>
|
/>
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
|
|||||||
@@ -54,6 +54,6 @@ export const NitrobubbleHiddenView: FC<{}> = props =>
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if(!isVisible) return null;
|
if(!isVisible) return null;
|
||||||
var stylecssnew = "<style>.newbubblehe { visibility: hidden !important; }</style>";
|
var stylecssnew = '<style>.newbubblehe { visibility: hidden !important; }</style>';
|
||||||
return ( <div dangerouslySetInnerHTML={ { __html: stylecssnew }} />);
|
return ( <div dangerouslySetInnerHTML={ { __html: stylecssnew }} />);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -28,15 +28,15 @@ export const NitroSystemAlertView: FC<NotificationDefaultAlertViewProps> = props
|
|||||||
<Button fullWidth onClick={ () => window.dispatchEvent(new CustomEvent('ads:toggle')) }>Show Ad</Button> }
|
<Button fullWidth onClick={ () => window.dispatchEvent(new CustomEvent('ads:toggle')) }>Show Ad</Button> }
|
||||||
</Column>
|
</Column>
|
||||||
</Column>
|
</Column>
|
||||||
<div className="alertView_nitro-coolui-logo"></div>
|
<div className="alertView_nitro-coolui-logo"></div>
|
||||||
<Column size={ 12 }>
|
<Column size={ 12 }>
|
||||||
<Column alignItems="center" gap={ 0 }>
|
<Column alignItems="center" gap={ 0 }>
|
||||||
<Text center bold fontSize={ 5 }>Cool UI</Text>
|
<Text center bold fontSize={ 5 }>Cool UI</Text>
|
||||||
<Text>- DuckieTM (Design)</Text>
|
<Text>- DuckieTM (Design)</Text>
|
||||||
<Text center bold small>v3.0.0</Text>
|
<Text center bold small>v3.0.0</Text>
|
||||||
<Button fullWidth onClick={ event => window.open('https://github.com/duckietm/Nitro-Cool-UI') }>Cool UI Git</Button>
|
<Button fullWidth onClick={ event => window.open('https://github.com/duckietm/Nitro-Cool-UI') }>Cool UI Git</Button>
|
||||||
</Column>
|
</Column>
|
||||||
</Column>
|
</Column>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ const pluginApi: INitroPluginApi = {
|
|||||||
// Create overlay container
|
// Create overlay container
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.id = `nitro-plugin-window-${id}`;
|
overlay.id = `nitro-plugin-window-${id}`;
|
||||||
overlay.style.cssText = `position:fixed;z-index:500;top:50%;left:50%;transform:translate(-50%,-50%)`;
|
overlay.style.cssText = 'position:fixed;z-index:500;top:50%;left:50%;transform:translate(-50%,-50%)';
|
||||||
|
|
||||||
// Card wrapper
|
// Card wrapper
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
@@ -165,14 +165,14 @@ const pluginApi: INitroPluginApi = {
|
|||||||
|
|
||||||
// Header (draggable)
|
// Header (draggable)
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.style.cssText = `display:flex;align-items:center;justify-content:center;position:relative;min-height:33px;background:linear-gradient(180deg,#3c6a8e 0%,#2a4f6e 100%);cursor:move;user-select:none`;
|
header.style.cssText = 'display:flex;align-items:center;justify-content:center;position:relative;min-height:33px;background:linear-gradient(180deg,#3c6a8e 0%,#2a4f6e 100%);cursor:move;user-select:none';
|
||||||
|
|
||||||
const titleEl = document.createElement('span');
|
const titleEl = document.createElement('span');
|
||||||
titleEl.textContent = title;
|
titleEl.textContent = title;
|
||||||
titleEl.style.cssText = `color:#fff;font-size:16px;text-shadow:0 1px 2px rgba(0,0,0,0.5)`;
|
titleEl.style.cssText = 'color:#fff;font-size:16px;text-shadow:0 1px 2px rgba(0,0,0,0.5)';
|
||||||
|
|
||||||
const closeBtn = document.createElement('div');
|
const closeBtn = document.createElement('div');
|
||||||
closeBtn.style.cssText = `position:absolute;right:8px;width:20px;height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;border-radius:50%;background:rgba(255,255,255,0.1)`;
|
closeBtn.style.cssText = 'position:absolute;right:8px;width:20px;height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;border-radius:50%;background:rgba(255,255,255,0.1)';
|
||||||
closeBtn.innerHTML = '✕';
|
closeBtn.innerHTML = '✕';
|
||||||
closeBtn.addEventListener('click', () => pluginApi.destroyWindow(id));
|
closeBtn.addEventListener('click', () => pluginApi.destroyWindow(id));
|
||||||
|
|
||||||
@@ -201,7 +201,10 @@ const pluginApi: INitroPluginApi = {
|
|||||||
overlay.style.top = (e.clientY - offsetY) + 'px';
|
overlay.style.top = (e.clientY - offsetY) + 'px';
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = () => { isDragging = false; };
|
const onMouseUp = () =>
|
||||||
|
{
|
||||||
|
isDragging = false;
|
||||||
|
};
|
||||||
|
|
||||||
header.addEventListener('mousedown', onMouseDown);
|
header.addEventListener('mousedown', onMouseDown);
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
@@ -209,7 +212,7 @@ const pluginApi: INitroPluginApi = {
|
|||||||
|
|
||||||
// Content area
|
// Content area
|
||||||
const content = document.createElement('div');
|
const content = document.createElement('div');
|
||||||
content.style.cssText = `padding:16px`;
|
content.style.cssText = 'padding:16px';
|
||||||
|
|
||||||
card.appendChild(header);
|
card.appendChild(header);
|
||||||
card.appendChild(content);
|
card.appendChild(content);
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import purseIcon from '../../assets/images/rightside/purse.gif';
|
|||||||
import { CurrencyView } from './views/CurrencyView';
|
import { CurrencyView } from './views/CurrencyView';
|
||||||
import { SeasonalView } from './views/SeasonalView';
|
import { SeasonalView } from './views/SeasonalView';
|
||||||
|
|
||||||
export const PurseView: FC<{}> = props => {
|
export const PurseView: FC<{}> = props =>
|
||||||
|
{
|
||||||
const { purse = null, hcDisabled = false } = usePurse();
|
const { purse = null, hcDisabled = false } = usePurse();
|
||||||
const [ isOpen, setIsOpen ] = useState(true);
|
const [ isOpen, setIsOpen ] = useState(true);
|
||||||
const [ isCompact, setIsCompact ] = useState(false);
|
const [ isCompact, setIsCompact ] = useState(false);
|
||||||
@@ -16,7 +17,8 @@ export const PurseView: FC<{}> = props => {
|
|||||||
const displayedCurrencies = useMemo(() => GetConfigurationValue<number[]>('system.currency.types', []), []);
|
const displayedCurrencies = useMemo(() => GetConfigurationValue<number[]>('system.currency.types', []), []);
|
||||||
const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue<boolean>('currency.display.number.short', false), []);
|
const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue<boolean>('currency.display.number.short', false), []);
|
||||||
|
|
||||||
const getClubText = (() => {
|
const getClubText = (() =>
|
||||||
|
{
|
||||||
if (!purse) return null;
|
if (!purse) return null;
|
||||||
|
|
||||||
const totalDays = ((purse.clubPeriods * 31) + purse.clubDays);
|
const totalDays = ((purse.clubPeriods * 31) + purse.clubDays);
|
||||||
@@ -27,11 +29,13 @@ export const PurseView: FC<{}> = props => {
|
|||||||
else return FriendlyTime.shortFormat(totalDays * 86400);
|
else return FriendlyTime.shortFormat(totalDays * 86400);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const currencyTypes = useMemo(() => {
|
const currencyTypes = useMemo(() =>
|
||||||
|
{
|
||||||
if (!purse || !purse.activityPoints || !purse.activityPoints.size) return [];
|
if (!purse || !purse.activityPoints || !purse.activityPoints.size) return [];
|
||||||
|
|
||||||
const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0));
|
const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0));
|
||||||
types.sort((a, b) => {
|
types.sort((a, b) =>
|
||||||
|
{
|
||||||
if (a === 0) return -1;
|
if (a === 0) return -1;
|
||||||
if (b === 0) return 1;
|
if (b === 0) return 1;
|
||||||
if (a === 5) return -1;
|
if (a === 5) return -1;
|
||||||
@@ -80,7 +84,8 @@ export const PurseView: FC<{}> = props => {
|
|||||||
body: JSON.stringify({ ssoTicket, rememberToken })
|
body: JSON.stringify({ ssoTicket, rememberToken })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch { /* best-effort — proceed with local logout regardless */ }
|
catch
|
||||||
|
{ /* best-effort — proceed with local logout regardless */ }
|
||||||
|
|
||||||
ClearRememberLogin();
|
ClearRememberLogin();
|
||||||
if(window.NitroConfig) window.NitroConfig['sso.ticket'] = '';
|
if(window.NitroConfig) window.NitroConfig['sso.ticket'] = '';
|
||||||
@@ -104,13 +109,16 @@ export const PurseView: FC<{}> = props => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={ `nitro-purse__content ${ isOpen ? 'is-open' : 'is-closed' }` }>
|
<div className={ `nitro-purse__content ${ isOpen ? 'is-open' : 'is-closed' }` }>
|
||||||
<div className={ `nitro-purse__summary nitro-purse__summary--compact ${ hcDisabled ? 'is-no-hc' : '' }` }>
|
<div className={ `nitro-purse__summary nitro-purse__summary--compact ${ hcDisabled ? 'is-no-hc' : '' }` }>
|
||||||
<div className="nitro-purse__primary">
|
<div className="nitro-purse__primary">
|
||||||
<CurrencyView type={ -1 } amount={ purse.credits } short={ currencyDisplayNumberShort } />
|
<CurrencyView type={ -1 } amount={ purse.credits } short={ currencyDisplayNumberShort } />
|
||||||
{ primaryCurrencies.map(type => <CurrencyView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } short={ currencyDisplayNumberShort } />) }
|
{ primaryCurrencies.map(type => <CurrencyView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } short={ currencyDisplayNumberShort } />) }
|
||||||
</div>
|
</div>
|
||||||
{ !hcDisabled &&
|
{ !hcDisabled &&
|
||||||
<div className="nitro-purse-subscription" onClick={ event => { event.stopPropagation(); CreateLinkEvent('habboUI/open/hccenter'); } }>
|
<div className="nitro-purse-subscription" onClick={ event =>
|
||||||
|
{
|
||||||
|
event.stopPropagation(); CreateLinkEvent('habboUI/open/hccenter');
|
||||||
|
} }>
|
||||||
<div className="nitro-purse-subscription__icon">
|
<div className="nitro-purse-subscription__icon">
|
||||||
<LayoutCurrencyIcon type="hc" />
|
<LayoutCurrencyIcon type="hc" />
|
||||||
</div>
|
</div>
|
||||||
@@ -119,22 +127,31 @@ export const PurseView: FC<{}> = props => {
|
|||||||
<Text variant="white" className="nitro-purse-subscription__value">{ getClubText }</Text>
|
<Text variant="white" className="nitro-purse-subscription__value">{ getClubText }</Text>
|
||||||
</div>
|
</div>
|
||||||
</div> }
|
</div> }
|
||||||
<div className="nitro-purse__actions">
|
<div className="nitro-purse__actions">
|
||||||
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--translate" onClick={ event => { event.stopPropagation(); CreateLinkEvent('translation-settings/toggle'); } } title="Google Translate">
|
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--translate" onClick={ event =>
|
||||||
<FaLanguage />
|
{
|
||||||
</button>
|
event.stopPropagation(); CreateLinkEvent('translation-settings/toggle');
|
||||||
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--help" onClick={ event => { event.stopPropagation(); CreateLinkEvent('help/show'); } } title={ LocalizeText('help.button.name') }>
|
} } title="Google Translate">
|
||||||
<FaQuestionCircle />
|
<FaLanguage />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--settings" onClick={ event => { event.stopPropagation(); CreateLinkEvent('user-settings/toggle'); } } title={ LocalizeText('widget.memenu.settings.title') }>
|
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--help" onClick={ event =>
|
||||||
<i className="nitro-icon icon-cog" />
|
{
|
||||||
</button>
|
event.stopPropagation(); CreateLinkEvent('help/show');
|
||||||
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--logout" onClick={ handleLogout } title="Log out">
|
} } title={ LocalizeText('help.button.name') }>
|
||||||
<FaSignOutAlt />
|
<FaQuestionCircle />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--settings" onClick={ event =>
|
||||||
|
{
|
||||||
|
event.stopPropagation(); CreateLinkEvent('user-settings/toggle');
|
||||||
|
} } title={ LocalizeText('widget.memenu.settings.title') }>
|
||||||
|
<i className="nitro-icon icon-cog" />
|
||||||
|
</button>
|
||||||
|
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--logout" onClick={ handleLogout } title="Log out">
|
||||||
|
<FaSignOutAlt />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{ seasonalCurrencies.length > 0 &&
|
</div>
|
||||||
|
{ seasonalCurrencies.length > 0 &&
|
||||||
<div className="nitro-purse__seasonal">
|
<div className="nitro-purse__seasonal">
|
||||||
{ seasonalCurrencies.map(type => <SeasonalView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } />) }
|
{ seasonalCurrencies.map(type => <SeasonalView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } />) }
|
||||||
</div> }
|
</div> }
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ export const CurrencyView: FC<CurrencyViewProps> = props =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -7,33 +7,34 @@ interface SeasonalViewProps {
|
|||||||
amount: number;
|
amount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SeasonalView: FC<SeasonalViewProps> = props => {
|
export const SeasonalView: FC<SeasonalViewProps> = props =>
|
||||||
const { type = -1, amount = -1 } = props;
|
{
|
||||||
const seasonalColor = GetConfigurationValue<string>('currency.seasonal.color', 'blue');
|
const { type = -1, amount = -1 } = props;
|
||||||
const formattedAmount = LocalizeFormattedNumber(amount);
|
const seasonalColor = GetConfigurationValue<string>('currency.seasonal.color', 'blue');
|
||||||
const iconUrl = GetConfigurationValue<string>('currency.asset.icon.url', '').replace('%type%', type.toString());
|
const formattedAmount = LocalizeFormattedNumber(amount);
|
||||||
|
const iconUrl = GetConfigurationValue<string>('currency.asset.icon.url', '').replace('%type%', type.toString());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
fullWidth
|
fullWidth
|
||||||
justifyContent="between"
|
justifyContent="between"
|
||||||
className={`nitro-purse-seasonal-currency nitro-notification ${seasonalColor}`}
|
className={`nitro-purse-seasonal-currency nitro-notification ${seasonalColor}`}
|
||||||
>
|
|
||||||
<Flex fullWidth className="seasonal-row">
|
|
||||||
<Flex className="nitro-seasonal-box seasonal-image-padding">
|
|
||||||
<img src={ iconUrl } alt="" className="seasonal-image" />
|
|
||||||
</Flex>
|
|
||||||
<Text truncate fullWidth variant="white" className="seasonal-text-padding seasonal-text">
|
|
||||||
{LocalizeText(`purse.seasonal.currency.${type}`)}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
variant="white"
|
|
||||||
className="seasonal-amount text-end"
|
|
||||||
title={formattedAmount}
|
|
||||||
>
|
>
|
||||||
{formattedAmount}
|
<Flex fullWidth className="seasonal-row">
|
||||||
</Text>
|
<Flex className="nitro-seasonal-box seasonal-image-padding">
|
||||||
</Flex>
|
<img src={ iconUrl } alt="" className="seasonal-image" />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
<Text truncate fullWidth variant="white" className="seasonal-text-padding seasonal-text">
|
||||||
|
{LocalizeText(`purse.seasonal.currency.${type}`)}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
variant="white"
|
||||||
|
className="seasonal-amount text-end"
|
||||||
|
title={formattedAmount}
|
||||||
|
>
|
||||||
|
{formattedAmount}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const RoomView: FC<{}> = (props) =>
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(!roomSession) return;
|
if(!roomSession) return;
|
||||||
|
|
||||||
const canvas = GetRenderer().canvas;
|
const canvas = GetRenderer().canvas;
|
||||||
|
|
||||||
@@ -109,10 +109,10 @@ export const RoomView: FC<{}> = (props) =>
|
|||||||
};
|
};
|
||||||
}, [roomSession]);
|
}, [roomSession]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{
|
{
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
initial={ { opacity: 0 }}
|
initial={ { opacity: 0 }}
|
||||||
animate={ { opacity: 1 }}
|
animate={ { opacity: 1 }}
|
||||||
|
|||||||
@@ -577,7 +577,10 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
|
||||||
<path d="M5.127 3.502 5.25 3.5h9.5c.041 0 .082 0 .123.002A2.251 2.251 0 0 0 12.75 2h-5.5a2.25 2.25 0 0 0-2.123 1.502ZM1 10.25A2.25 2.25 0 0 1 3.25 8h13.5A2.25 2.25 0 0 1 19 10.25v5.5A2.25 2.25 0 0 1 16.75 18H3.25A2.25 2.25 0 0 1 1 15.75v-5.5ZM3.25 6.5c-.04 0-.082 0-.123.002A2.25 2.25 0 0 1 5.25 5h9.5c.98 0 1.814.627 2.123 1.502a3.819 3.819 0 0 0-.123-.002H3.25Z" />
|
<path d="M5.127 3.502 5.25 3.5h9.5c.041 0 .082 0 .123.002A2.251 2.251 0 0 0 12.75 2h-5.5a2.25 2.25 0 0 0-2.123 1.502ZM1 10.25A2.25 2.25 0 0 1 3.25 8h13.5A2.25 2.25 0 0 1 19 10.25v5.5A2.25 2.25 0 0 1 16.75 18H3.25A2.25 2.25 0 0 1 1 15.75v-5.5ZM3.25 6.5c-.04 0-.082 0-.123.002A2.25 2.25 0 0 1 5.25 5h9.5c.98 0 1.814.627 2.123 1.502a3.819 3.819 0 0 0-.123-.002H3.25Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<Text small wrap variant="white">Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() }</Text>
|
<Text small wrap variant="white">Sprite: { (() =>
|
||||||
|
{
|
||||||
|
const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?';
|
||||||
|
})() }</Text>
|
||||||
</div>
|
</div>
|
||||||
</div> }
|
</div> }
|
||||||
{ (!avatarInfo.isWallItem && canMove) &&
|
{ (!avatarInfo.isWallItem && canMove) &&
|
||||||
|
|||||||
@@ -39,23 +39,23 @@ interface InfoStandWidgetPetViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PetHeader: FC<{ name: string; petType: number; petBreed: number; onClose: () => void }> = ({ name, petType, petBreed, onClose }) => (
|
const PetHeader: FC<{ name: string; petType: number; petBreed: number; onClose: () => void }> = ({ name, petType, petBreed, onClose }) => (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Flex alignItems="center" gap={1} justifyContent="between">
|
<Flex alignItems="center" gap={1} justifyContent="between">
|
||||||
<Text small wrap variant="white">
|
<Text small wrap variant="white">
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<FaTimes
|
<FaTimes
|
||||||
className="cursor-pointer fa-icon"
|
className="cursor-pointer fa-icon"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label={LocalizeText('generic.close')}
|
aria-label={LocalizeText('generic.close')}
|
||||||
title={LocalizeText('generic.close')}
|
title={LocalizeText('generic.close')}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Text small wrap variant="white">
|
<Text small wrap variant="white">
|
||||||
{LocalizeText(`pet.breed.${petType}.${petBreed}`)}
|
{LocalizeText(`pet.breed.${petType}.${petBreed}`)}
|
||||||
</Text>
|
</Text>
|
||||||
<hr className="m-0" />
|
<hr className="m-0" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const MonsterplantStats: FC<{
|
const MonsterplantStats: FC<{
|
||||||
@@ -63,281 +63,290 @@ const MonsterplantStats: FC<{
|
|||||||
remainingGrowTime: number;
|
remainingGrowTime: number;
|
||||||
remainingTimeToLive: number;
|
remainingTimeToLive: number;
|
||||||
}> = ({ avatarInfo, remainingGrowTime, remainingTimeToLive }) => (
|
}> = ({ avatarInfo, remainingGrowTime, remainingTimeToLive }) => (
|
||||||
<>
|
<>
|
||||||
<Column center gap={1}>
|
<Column center gap={1}>
|
||||||
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
||||||
<hr className="m-0" />
|
<hr className="m-0" />
|
||||||
</Column>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{!avatarInfo.dead && (
|
|
||||||
<Column alignItems="center" gap={1}>
|
|
||||||
<Text center small wrap variant="white">
|
|
||||||
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
|
||||||
</Text>
|
|
||||||
</Column>
|
</Column>
|
||||||
)}
|
<div className="flex flex-col gap-2">
|
||||||
<Column alignItems="center" gap={1}>
|
{!avatarInfo.dead && (
|
||||||
<Text small truncate variant="white">
|
<Column alignItems="center" gap={1}>
|
||||||
{LocalizeText('infostand.pet.text.wellbeing')}
|
<Text center small wrap variant="white">
|
||||||
</Text>
|
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
||||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
</Text>
|
||||||
<div className="flex justify-center items-center size-full absolute">
|
</Column>
|
||||||
<Text small variant="white">
|
)}
|
||||||
{avatarInfo.dead || remainingTimeToLive <= 0
|
<Column alignItems="center" gap={1}>
|
||||||
? '00:00:00'
|
<Text small truncate variant="white">
|
||||||
: `${ConvertSeconds(remainingTimeToLive).split(':')[1]}:${ConvertSeconds(remainingTimeToLive).split(':')[2]}:${ConvertSeconds(remainingTimeToLive).split(':')[3]}`}
|
{LocalizeText('infostand.pet.text.wellbeing')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||||
<div
|
<div className="flex justify-center items-center size-full absolute">
|
||||||
className="bg-success rounded pet-stats"
|
<Text small variant="white">
|
||||||
style={{
|
{avatarInfo.dead || remainingTimeToLive <= 0
|
||||||
width: avatarInfo.dead || remainingTimeToLive <= 0 ? '0' : `${(remainingTimeToLive / avatarInfo.maximumTimeToLive) * 100}%`,
|
? '00:00:00'
|
||||||
}}
|
: `${ConvertSeconds(remainingTimeToLive).split(':')[1]}:${ConvertSeconds(remainingTimeToLive).split(':')[2]}:${ConvertSeconds(remainingTimeToLive).split(':')[3]}`}
|
||||||
/>
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="bg-success rounded pet-stats"
|
||||||
|
style={{
|
||||||
|
width: avatarInfo.dead || remainingTimeToLive <= 0 ? '0' : `${(remainingTimeToLive / avatarInfo.maximumTimeToLive) * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
{remainingGrowTime > 0 && (
|
||||||
|
<Column alignItems="center" gap={1}>
|
||||||
|
<Text small truncate variant="white">
|
||||||
|
{LocalizeText('infostand.pet.text.growth')}
|
||||||
|
</Text>
|
||||||
|
<LayoutCounterTimeView
|
||||||
|
className="top-2 inset-e-2"
|
||||||
|
day={ConvertSeconds(remainingGrowTime).split(':')[0]}
|
||||||
|
hour={ConvertSeconds(remainingGrowTime).split(':')[1]}
|
||||||
|
minutes={ConvertSeconds(remainingGrowTime).split(':')[2]}
|
||||||
|
seconds={ConvertSeconds(remainingGrowTime).split(':')[3]}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
|
<Column alignItems="center" gap={1}>
|
||||||
|
<Text small truncate variant="white">
|
||||||
|
{LocalizeText('infostand.pet.text.raritylevel', ['level'], [LocalizeText(`infostand.pet.raritylevel.${avatarInfo.rarityLevel}`)])}
|
||||||
|
</Text>
|
||||||
|
<LayoutRarityLevelView className="top-2 inset-e-2" level={avatarInfo.rarityLevel} />
|
||||||
|
</Column>
|
||||||
|
<hr className="m-0" />
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
<div className="flex flex-col gap-1">
|
||||||
{remainingGrowTime > 0 && (
|
<Text small wrap variant="white">
|
||||||
<Column alignItems="center" gap={1}>
|
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
||||||
<Text small truncate variant="white">
|
</Text>
|
||||||
{LocalizeText('infostand.pet.text.growth')}
|
<hr className="m-0" />
|
||||||
</Text>
|
</div>
|
||||||
<LayoutCounterTimeView
|
</>
|
||||||
className="top-2 inset-e-2"
|
|
||||||
day={ConvertSeconds(remainingGrowTime).split(':')[0]}
|
|
||||||
hour={ConvertSeconds(remainingGrowTime).split(':')[1]}
|
|
||||||
minutes={ConvertSeconds(remainingGrowTime).split(':')[2]}
|
|
||||||
seconds={ConvertSeconds(remainingGrowTime).split(':')[3]}
|
|
||||||
/>
|
|
||||||
</Column>
|
|
||||||
)}
|
|
||||||
<Column alignItems="center" gap={1}>
|
|
||||||
<Text small truncate variant="white">
|
|
||||||
{LocalizeText('infostand.pet.text.raritylevel', ['level'], [LocalizeText(`infostand.pet.raritylevel.${avatarInfo.rarityLevel}`)])}
|
|
||||||
</Text>
|
|
||||||
<LayoutRarityLevelView className="top-2 inset-e-2" level={avatarInfo.rarityLevel} />
|
|
||||||
</Column>
|
|
||||||
<hr className="m-0" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Text small wrap variant="white">
|
|
||||||
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
|
||||||
</Text>
|
|
||||||
<hr className="m-0" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sub-component: Regular Pet Stats
|
// Sub-component: Regular Pet Stats
|
||||||
const RegularPetStats: FC<{ avatarInfo: AvatarInfoPet }> = ({ avatarInfo }) => (
|
const RegularPetStats: FC<{ avatarInfo: AvatarInfoPet }> = ({ avatarInfo }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Column fullWidth className="body-image pet p-1" overflow="hidden">
|
<Column fullWidth className="body-image pet p-1" overflow="hidden">
|
||||||
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
||||||
</Column>
|
</Column>
|
||||||
<Column grow gap={1}>
|
<Column grow gap={1}>
|
||||||
<Text center small wrap variant="white">
|
<Text center small wrap variant="white">
|
||||||
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
||||||
</Text>
|
</Text>
|
||||||
<Column alignItems="center" gap={1}>
|
<Column alignItems="center" gap={1}>
|
||||||
<Text small truncate variant="white">
|
<Text small truncate variant="white">
|
||||||
{LocalizeText('infostand.pet.text.happiness')}
|
{LocalizeText('infostand.pet.text.happiness')}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||||
<div className="flex justify-center items-center size-full absolute">
|
<div className="flex justify-center items-center size-full absolute">
|
||||||
<Text small variant="white">
|
<Text small variant="white">
|
||||||
{avatarInfo.happyness + '/' + avatarInfo.maximumHappyness}
|
{avatarInfo.happyness + '/' + avatarInfo.maximumHappyness}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="bg-info rounded pet-stats"
|
className="bg-info rounded pet-stats"
|
||||||
style={{ width: (avatarInfo.happyness / avatarInfo.maximumHappyness) * 100 + '%' }}
|
style={{ width: (avatarInfo.happyness / avatarInfo.maximumHappyness) * 100 + '%' }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
<Column alignItems="center" gap={1}>
|
||||||
|
<Text small truncate variant="white">
|
||||||
|
{LocalizeText('infostand.pet.text.experience')}
|
||||||
|
</Text>
|
||||||
|
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||||
|
<div className="flex justify-center items-center size-full absolute">
|
||||||
|
<Text small variant="white">
|
||||||
|
{avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="bg-purple rounded pet-stats"
|
||||||
|
style={{ width: (avatarInfo.experience / avatarInfo.levelExperienceGoal) * 100 + '%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
<Column alignItems="center" gap={1}>
|
||||||
|
<Text small truncate variant="white">
|
||||||
|
{LocalizeText('infostand.pet.text.energy')}
|
||||||
|
</Text>
|
||||||
|
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||||
|
<div className="flex justify-center items-center size-full absolute">
|
||||||
|
<Text small variant="white">
|
||||||
|
{avatarInfo.energy + '/' + avatarInfo.maximumEnergy}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="bg-success rounded pet-stats"
|
||||||
|
style={{ width: (avatarInfo.energy / avatarInfo.maximumEnergy) * 100 + '%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
</Column>
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
<hr className="m-0" />
|
||||||
<Column alignItems="center" gap={1}>
|
</div>
|
||||||
<Text small truncate variant="white">
|
<div className="flex flex-col gap-1">
|
||||||
{LocalizeText('infostand.pet.text.experience')}
|
<Text small wrap variant="white">
|
||||||
|
{LocalizeText('infostand.text.petrespect', ['count'], [avatarInfo.respect.toString()])}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
<Text small wrap variant="white">
|
||||||
<div className="flex justify-center items-center size-full absolute">
|
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
||||||
<Text small variant="white">
|
|
||||||
{avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="bg-purple rounded pet-stats"
|
|
||||||
style={{ width: (avatarInfo.experience / avatarInfo.levelExperienceGoal) * 100 + '%' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Column>
|
|
||||||
<Column alignItems="center" gap={1}>
|
|
||||||
<Text small truncate variant="white">
|
|
||||||
{LocalizeText('infostand.pet.text.energy')}
|
|
||||||
</Text>
|
</Text>
|
||||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
<hr className="m-0" />
|
||||||
<div className="flex justify-center items-center size-full absolute">
|
</div>
|
||||||
<Text small variant="white">
|
</>
|
||||||
{avatarInfo.energy + '/' + avatarInfo.maximumEnergy}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="bg-success rounded pet-stats"
|
|
||||||
style={{ width: (avatarInfo.energy / avatarInfo.maximumEnergy) * 100 + '%' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Column>
|
|
||||||
</Column>
|
|
||||||
</div>
|
|
||||||
<hr className="m-0" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Text small wrap variant="white">
|
|
||||||
{LocalizeText('infostand.text.petrespect', ['count'], [avatarInfo.respect.toString()])}
|
|
||||||
</Text>
|
|
||||||
<Text small wrap variant="white">
|
|
||||||
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
|
||||||
</Text>
|
|
||||||
<hr className="m-0" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatarInfo, onClose }) => {
|
export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatarInfo, onClose }) =>
|
||||||
const [remainingGrowTime, setRemainingGrowTime] = useState(0);
|
{
|
||||||
const [remainingTimeToLive, setRemainingTimeToLive] = useState(0);
|
const [remainingGrowTime, setRemainingGrowTime] = useState(0);
|
||||||
const { roomSession = null } = useRoom();
|
const [remainingTimeToLive, setRemainingTimeToLive] = useState(0);
|
||||||
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
|
const { roomSession = null } = useRoom();
|
||||||
|
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
setRemainingGrowTime(avatarInfo.remainingGrowTime || 0);
|
|
||||||
setRemainingTimeToLive(avatarInfo.remainingTimeToLive || 0);
|
|
||||||
}, [avatarInfo]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (avatarInfo.petType !== PetType.MONSTERPLANT || avatarInfo.dead) return;
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setRemainingGrowTime((prev) => (prev <= 0 ? 0 : prev - 1));
|
|
||||||
setRemainingTimeToLive((prev) => (prev <= 0 ? 0 : prev - 1));
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [avatarInfo]);
|
|
||||||
|
|
||||||
const processButtonAction = useCallback(
|
|
||||||
async (action: string) => {
|
|
||||||
try {
|
|
||||||
let hideMenu = true;
|
|
||||||
if (!action) return;
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'respect':
|
|
||||||
await respectPet(avatarInfo.id);
|
|
||||||
if (petRespectRemaining - 1 >= 1) hideMenu = false;
|
|
||||||
break;
|
|
||||||
case 'buyfood':
|
|
||||||
CreateLinkEvent('catalog/open/' + GetConfigurationValue('catalog.links')['pets.buy_food']);
|
|
||||||
break;
|
|
||||||
case 'train':
|
|
||||||
roomSession?.requestPetCommands(avatarInfo.id);
|
|
||||||
break;
|
|
||||||
case 'treat':
|
|
||||||
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
|
|
||||||
break;
|
|
||||||
case 'compost':
|
|
||||||
roomSession?.compostPlant(avatarInfo.id);
|
|
||||||
break;
|
|
||||||
case 'pick_up':
|
|
||||||
roomSession?.pickupPet(avatarInfo.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hideMenu) onClose();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to process action ${action}:`, error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[avatarInfo, petRespectRemaining, respectPet, roomSession, onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
const buttons = [
|
|
||||||
{
|
{
|
||||||
action: 'buyfood',
|
setRemainingGrowTime(avatarInfo.remainingGrowTime || 0);
|
||||||
label: LocalizeText('infostand.button.buyfood'),
|
setRemainingTimeToLive(avatarInfo.remainingTimeToLive || 0);
|
||||||
condition: avatarInfo.petType !== PetType.MONSTERPLANT,
|
}, [avatarInfo]);
|
||||||
},
|
|
||||||
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
action: 'train',
|
if (avatarInfo.petType !== PetType.MONSTERPLANT || avatarInfo.dead) return;
|
||||||
label: LocalizeText('infostand.button.train'),
|
|
||||||
condition: avatarInfo.isOwner && avatarInfo.petType !== PetType.MONSTERPLANT,
|
const interval = setInterval(() =>
|
||||||
},
|
{
|
||||||
{
|
setRemainingGrowTime((prev) => (prev <= 0 ? 0 : prev - 1));
|
||||||
action: 'treat',
|
setRemainingTimeToLive((prev) => (prev <= 0 ? 0 : prev - 1));
|
||||||
label: LocalizeText('infostand.button.pettreat'),
|
}, 1000);
|
||||||
condition:
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [avatarInfo]);
|
||||||
|
|
||||||
|
const processButtonAction = useCallback(
|
||||||
|
async (action: string) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
let hideMenu = true;
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case 'respect':
|
||||||
|
await respectPet(avatarInfo.id);
|
||||||
|
if (petRespectRemaining - 1 >= 1) hideMenu = false;
|
||||||
|
break;
|
||||||
|
case 'buyfood':
|
||||||
|
CreateLinkEvent('catalog/open/' + GetConfigurationValue('catalog.links')['pets.buy_food']);
|
||||||
|
break;
|
||||||
|
case 'train':
|
||||||
|
roomSession?.requestPetCommands(avatarInfo.id);
|
||||||
|
break;
|
||||||
|
case 'treat':
|
||||||
|
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
|
||||||
|
break;
|
||||||
|
case 'compost':
|
||||||
|
roomSession?.compostPlant(avatarInfo.id);
|
||||||
|
break;
|
||||||
|
case 'pick_up':
|
||||||
|
roomSession?.pickupPet(avatarInfo.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hideMenu) onClose();
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error(`Failed to process action ${action}:`, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[avatarInfo, petRespectRemaining, respectPet, roomSession, onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
action: 'buyfood',
|
||||||
|
label: LocalizeText('infostand.button.buyfood'),
|
||||||
|
condition: avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'train',
|
||||||
|
label: LocalizeText('infostand.button.train'),
|
||||||
|
condition: avatarInfo.isOwner && avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'treat',
|
||||||
|
label: LocalizeText('infostand.button.pettreat'),
|
||||||
|
condition:
|
||||||
!avatarInfo.dead &&
|
!avatarInfo.dead &&
|
||||||
avatarInfo.petType === PetType.MONSTERPLANT &&
|
avatarInfo.petType === PetType.MONSTERPLANT &&
|
||||||
avatarInfo.energy / avatarInfo.maximumEnergy < 0.98,
|
avatarInfo.energy / avatarInfo.maximumEnergy < 0.98,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'compost',
|
action: 'compost',
|
||||||
label: LocalizeText('infostand.button.compost'),
|
label: LocalizeText('infostand.button.compost'),
|
||||||
condition: roomSession?.isRoomOwner && avatarInfo.petType === PetType.MONSTERPLANT,
|
condition: roomSession?.isRoomOwner && avatarInfo.petType === PetType.MONSTERPLANT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'pick_up',
|
action: 'pick_up',
|
||||||
label: LocalizeText('inventory.pets.pickup'),
|
label: LocalizeText('inventory.pets.pickup'),
|
||||||
condition: avatarInfo.isOwner,
|
condition: avatarInfo.isOwner,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'respect',
|
action: 'respect',
|
||||||
label: LocalizeText('infostand.button.petrespect', ['count'], [petRespectRemaining.toString()]),
|
label: LocalizeText('infostand.button.petrespect', ['count'], [petRespectRemaining.toString()]),
|
||||||
condition: petRespectRemaining > 0 && avatarInfo.petType !== PetType.MONSTERPLANT,
|
condition: petRespectRemaining > 0 && avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!avatarInfo) return <Text variant="white">{LocalizeText('generic.loading')}</Text>;
|
if (!avatarInfo) return <Text variant="white">{LocalizeText('generic.loading')}</Text>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column alignItems="end" gap={1}>
|
<Column alignItems="end" gap={1}>
|
||||||
<Column className="nitro-infostand rounded">
|
<Column className="nitro-infostand rounded">
|
||||||
<Column className="container-fluid content-area" gap={1} overflow="visible">
|
<Column className="container-fluid content-area" gap={1} overflow="visible">
|
||||||
<PetHeader
|
<PetHeader
|
||||||
name={avatarInfo.name}
|
name={avatarInfo.name}
|
||||||
petType={avatarInfo.petType}
|
petType={avatarInfo.petType}
|
||||||
petBreed={avatarInfo.petBreed}
|
petBreed={avatarInfo.petBreed}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
{avatarInfo.petType === PetType.MONSTERPLANT ? (
|
{avatarInfo.petType === PetType.MONSTERPLANT ? (
|
||||||
<MonsterplantStats
|
<MonsterplantStats
|
||||||
avatarInfo={avatarInfo}
|
avatarInfo={avatarInfo}
|
||||||
remainingGrowTime={remainingGrowTime}
|
remainingGrowTime={remainingGrowTime}
|
||||||
remainingTimeToLive={remainingTimeToLive}
|
remainingTimeToLive={remainingTimeToLive}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RegularPetStats avatarInfo={avatarInfo} />
|
<RegularPetStats avatarInfo={avatarInfo} />
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<UserProfileIconView userId={avatarInfo.ownerId} />
|
<UserProfileIconView userId={avatarInfo.ownerId} />
|
||||||
<Text small wrap variant="white">
|
<Text small wrap variant="white">
|
||||||
{LocalizeText('infostand.text.petowner', ['name'], [avatarInfo.ownerName])}
|
{LocalizeText('infostand.text.petowner', ['name'], [avatarInfo.ownerName])}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Column>
|
||||||
|
</Column>
|
||||||
|
<Flex gap={1} justifyContent="end">
|
||||||
|
{buttons.map(
|
||||||
|
(button) =>
|
||||||
|
button.condition && (
|
||||||
|
<Button key={button.action} variant="dark" onClick={() => processButtonAction(button.action)}>
|
||||||
|
{button.label}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
</Column>
|
</Column>
|
||||||
</Column>
|
);
|
||||||
<Flex gap={1} justifyContent="end">
|
|
||||||
{buttons.map(
|
|
||||||
(button) =>
|
|
||||||
button.condition && (
|
|
||||||
<Button key={button.action} variant="dark" onClick={() => processButtonAction(button.action)}>
|
|
||||||
{button.label}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
@@ -16,286 +16,312 @@ interface InfoStandWidgetUserViewProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props => {
|
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =>
|
||||||
const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props;
|
{
|
||||||
const [motto, setMotto] = useState<string>(null);
|
const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props;
|
||||||
const [isEditingMotto, setIsEditingMotto] = useState(false);
|
const [motto, setMotto] = useState<string>(null);
|
||||||
const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null);
|
const [isEditingMotto, setIsEditingMotto] = useState(false);
|
||||||
const [backgroundId, setBackgroundId] = useState<number>(null);
|
const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null);
|
||||||
const [standId, setStandId] = useState<number>(null);
|
const [backgroundId, setBackgroundId] = useState<number>(null);
|
||||||
const [overlayId, setOverlayId] = useState<number>(null);
|
const [standId, setStandId] = useState<number>(null);
|
||||||
const [cardBackgroundId, setCardBackgroundId] = useState<number>(null);
|
const [overlayId, setOverlayId] = useState<number>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [cardBackgroundId, setCardBackgroundId] = useState<number>(null);
|
||||||
const { roomSession = null } = useRoom();
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const { roomSession = null } = useRoom();
|
||||||
|
|
||||||
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
|
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
|
||||||
const infostandStandClass = `stand-${standId ?? 'default'}`;
|
const infostandStandClass = `stand-${standId ?? 'default'}`;
|
||||||
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
|
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
|
||||||
const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : '';
|
const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : '';
|
||||||
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
|
const handleProfileClick = useCallback(() =>
|
||||||
|
{
|
||||||
|
GetUserProfile(avatarInfo.webID);
|
||||||
|
}, [avatarInfo.webID]);
|
||||||
|
|
||||||
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
|
const handleEditClick = useCallback((event: React.MouseEvent) =>
|
||||||
|
{
|
||||||
|
event.stopPropagation(); setIsVisible(prev => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const saveMotto = (motto: string) => {
|
const saveMotto = (motto: string) =>
|
||||||
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
|
{
|
||||||
|
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
|
||||||
|
|
||||||
roomSession.sendMottoMessage(motto);
|
roomSession.sendMottoMessage(motto);
|
||||||
setIsEditingMotto(false);
|
setIsEditingMotto(false);
|
||||||
};
|
|
||||||
|
|
||||||
const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value);
|
|
||||||
|
|
||||||
const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
case 'Enter':
|
|
||||||
saveMotto((event.target as HTMLInputElement).value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event => {
|
|
||||||
if (!avatarInfo || avatarInfo.webID !== event.userId) return;
|
|
||||||
|
|
||||||
// Deduplicate badges from server
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const dedupedBadges = event.badges.map(code => {
|
|
||||||
if (!code || seen.has(code)) return '';
|
|
||||||
seen.add(code);
|
|
||||||
return code;
|
|
||||||
});
|
|
||||||
|
|
||||||
const oldBadges = avatarInfo.badges.join('');
|
|
||||||
|
|
||||||
if (oldBadges === dedupedBadges.join('')) return;
|
|
||||||
|
|
||||||
setAvatarInfo(prevValue => {
|
|
||||||
if (!prevValue) return prevValue;
|
|
||||||
|
|
||||||
const newValue = CloneObject(prevValue);
|
|
||||||
newValue.badges = dedupedBadges;
|
|
||||||
return newValue;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event => {
|
|
||||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
|
||||||
|
|
||||||
setAvatarInfo(prevValue => {
|
|
||||||
if (!prevValue) return prevValue;
|
|
||||||
|
|
||||||
const newValue = CloneObject(prevValue);
|
|
||||||
newValue.figure = event.figure;
|
|
||||||
newValue.motto = event.customInfo;
|
|
||||||
newValue.achievementScore = event.activityPoints;
|
|
||||||
newValue.nickIcon = event.nickIcon;
|
|
||||||
newValue.prefixText = event.prefixText;
|
|
||||||
newValue.prefixColor = event.prefixColor;
|
|
||||||
newValue.prefixIcon = event.prefixIcon;
|
|
||||||
newValue.prefixEffect = event.prefixEffect;
|
|
||||||
newValue.displayOrder = event.displayOrder;
|
|
||||||
newValue.backgroundId = event.backgroundId;
|
|
||||||
newValue.standId = event.standId;
|
|
||||||
newValue.overlayId = event.overlayId;
|
|
||||||
newValue.cardBackgroundId = event.cardBackgroundId ?? 0;
|
|
||||||
return newValue;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event => {
|
|
||||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
|
||||||
|
|
||||||
setAvatarInfo(prevValue => {
|
|
||||||
if (!prevValue) return prevValue;
|
|
||||||
|
|
||||||
const newValue = CloneObject(prevValue);
|
|
||||||
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
|
|
||||||
|
|
||||||
newValue.groupId = clearGroup ? -1 : event.habboGroupId;
|
|
||||||
newValue.groupName = clearGroup ? null : event.habboGroupName;
|
|
||||||
newValue.groupBadgeId = clearGroup ? null : GetSessionDataManager().getGroupBadge(event.habboGroupId);
|
|
||||||
return newValue;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event => {
|
|
||||||
const parser = event.getParser();
|
|
||||||
|
|
||||||
if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
|
|
||||||
|
|
||||||
setRelationships(parser);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsEditingMotto(false);
|
|
||||||
setMotto(avatarInfo.motto);
|
|
||||||
setBackgroundId(avatarInfo.backgroundId);
|
|
||||||
setStandId(avatarInfo.standId);
|
|
||||||
setOverlayId(avatarInfo.overlayId);
|
|
||||||
setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0);
|
|
||||||
|
|
||||||
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setRelationships(null);
|
|
||||||
};
|
};
|
||||||
}, [avatarInfo]);
|
|
||||||
|
|
||||||
if (!avatarInfo) return null;
|
const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value);
|
||||||
|
|
||||||
return (
|
const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) =>
|
||||||
<>
|
{
|
||||||
<Column className={`relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto ${cardBackgroundId ? '' : 'bg-[rgba(28,28,32,0.95)]'} [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded overflow-hidden profile-card-background ${infostandCardBackgroundClass}`}>
|
event.stopPropagation();
|
||||||
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<UserProfileIconView userId={avatarInfo.webID} />
|
|
||||||
<UserIdentityView
|
|
||||||
className="text-[12px]"
|
|
||||||
displayOrder={ avatarInfo.displayOrder }
|
|
||||||
nameClassName="text-white"
|
|
||||||
nickIcon={ avatarInfo.nickIcon }
|
|
||||||
prefixColor={ avatarInfo.prefixColor }
|
|
||||||
prefixEffect={ avatarInfo.prefixEffect }
|
|
||||||
prefixFont={ avatarInfo.prefixFont }
|
|
||||||
prefixIcon={ avatarInfo.prefixIcon }
|
|
||||||
prefixText={ avatarInfo.prefixText }
|
|
||||||
username={ avatarInfo.name } />
|
|
||||||
</div>
|
|
||||||
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
|
|
||||||
</div>
|
|
||||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Column
|
|
||||||
fullWidth
|
|
||||||
className={`flex items-center w-full max-w-[68px] rounded-sm relative overflow-hidden profile-background ${infostandBackgroundClass}`}
|
|
||||||
onClick={handleProfileClick}
|
|
||||||
>
|
|
||||||
<Base position="absolute" className={`profile-stand ${infostandStandClass}`} />
|
|
||||||
<LayoutAvatarImageView direction={2} figure={avatarInfo.figure} />
|
|
||||||
<Base position="absolute" className={`profile-overlay ${infostandOverlayClass}`} />
|
|
||||||
</Column>
|
|
||||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
|
||||||
<Base
|
|
||||||
className="background-edit-icon background-edit-position"
|
|
||||||
style={{ pointerEvents: 'auto', cursor: 'pointer' }}
|
|
||||||
onClick={handleEditClick}
|
|
||||||
aria-label="Edit profile background"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Column grow alignItems="center" gap={0}>
|
|
||||||
{ (() => {
|
|
||||||
const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
|
|
||||||
const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
|
|
||||||
const showGroup = maxSlots <= 5;
|
|
||||||
|
|
||||||
const items: React.ReactNode[] = [];
|
switch (event.key)
|
||||||
items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />);
|
{
|
||||||
|
case 'Enter':
|
||||||
|
saveMotto((event.target as HTMLInputElement).value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if(showGroup) {
|
useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event =>
|
||||||
items.push(
|
{
|
||||||
<Flex key="group" center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
|
if (!avatarInfo || avatarInfo.webID !== event.userId) return;
|
||||||
{avatarInfo.groupId > 0 && <LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
const startIdx = showGroup ? 1 : 2;
|
// Deduplicate badges from server
|
||||||
for(let i = startIdx; i < maxSlots; i++) {
|
const seen = new Set<string>();
|
||||||
items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />);
|
const dedupedBadges = event.badges.map(code =>
|
||||||
}
|
{
|
||||||
|
if (!code || seen.has(code)) return '';
|
||||||
|
seen.add(code);
|
||||||
|
return code;
|
||||||
|
});
|
||||||
|
|
||||||
const rows: React.ReactNode[][] = [];
|
const oldBadges = avatarInfo.badges.join('');
|
||||||
for(let i = 0; i < items.length; i += 2) {
|
|
||||||
rows.push(items.slice(i, i + 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows.map((row, idx) => (
|
if (oldBadges === dedupedBadges.join('')) return;
|
||||||
<Flex key={idx} center gap={1}>{row}</Flex>
|
|
||||||
));
|
setAvatarInfo(prevValue =>
|
||||||
})() }
|
{
|
||||||
</Column>
|
if (!prevValue) return prevValue;
|
||||||
</div>
|
|
||||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
const newValue = CloneObject(prevValue);
|
||||||
</div>
|
newValue.badges = dedupedBadges;
|
||||||
<div className="flex flex-col gap-1">
|
return newValue;
|
||||||
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
|
});
|
||||||
{avatarInfo.type !== AvatarInfoUser.OWN_USER && (
|
});
|
||||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
|
||||||
<Text fullWidth pointer small textBreak wrap variant="white">{motto}</Text>
|
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event =>
|
||||||
</Flex>
|
{
|
||||||
)}
|
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
||||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
|
||||||
<Flex grow alignItems="center" gap={2}>
|
setAvatarInfo(prevValue =>
|
||||||
<FaPencilAlt className="small fa-icon" />
|
{
|
||||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
if (!prevValue) return prevValue;
|
||||||
{!isEditingMotto && (
|
|
||||||
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
|
const newValue = CloneObject(prevValue);
|
||||||
{motto}
|
newValue.figure = event.figure;
|
||||||
</Text>
|
newValue.motto = event.customInfo;
|
||||||
|
newValue.achievementScore = event.activityPoints;
|
||||||
|
newValue.nickIcon = event.nickIcon;
|
||||||
|
newValue.prefixText = event.prefixText;
|
||||||
|
newValue.prefixColor = event.prefixColor;
|
||||||
|
newValue.prefixIcon = event.prefixIcon;
|
||||||
|
newValue.prefixEffect = event.prefixEffect;
|
||||||
|
newValue.displayOrder = event.displayOrder;
|
||||||
|
newValue.backgroundId = event.backgroundId;
|
||||||
|
newValue.standId = event.standId;
|
||||||
|
newValue.overlayId = event.overlayId;
|
||||||
|
newValue.cardBackgroundId = event.cardBackgroundId ?? 0;
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event =>
|
||||||
|
{
|
||||||
|
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
||||||
|
|
||||||
|
setAvatarInfo(prevValue =>
|
||||||
|
{
|
||||||
|
if (!prevValue) return prevValue;
|
||||||
|
|
||||||
|
const newValue = CloneObject(prevValue);
|
||||||
|
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
|
||||||
|
|
||||||
|
newValue.groupId = clearGroup ? -1 : event.habboGroupId;
|
||||||
|
newValue.groupName = clearGroup ? null : event.habboGroupName;
|
||||||
|
newValue.groupBadgeId = clearGroup ? null : GetSessionDataManager().getGroupBadge(event.habboGroupId);
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event =>
|
||||||
|
{
|
||||||
|
const parser = event.getParser();
|
||||||
|
|
||||||
|
if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
|
||||||
|
|
||||||
|
setRelationships(parser);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setIsEditingMotto(false);
|
||||||
|
setMotto(avatarInfo.motto);
|
||||||
|
setBackgroundId(avatarInfo.backgroundId);
|
||||||
|
setStandId(avatarInfo.standId);
|
||||||
|
setOverlayId(avatarInfo.overlayId);
|
||||||
|
setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0);
|
||||||
|
|
||||||
|
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
setRelationships(null);
|
||||||
|
};
|
||||||
|
}, [avatarInfo]);
|
||||||
|
|
||||||
|
if (!avatarInfo) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Column className={`relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto ${cardBackgroundId ? '' : 'bg-[rgba(28,28,32,0.95)]'} [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded overflow-hidden profile-card-background ${infostandCardBackgroundClass}`}>
|
||||||
|
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<UserProfileIconView userId={avatarInfo.webID} />
|
||||||
|
<UserIdentityView
|
||||||
|
className="text-[12px]"
|
||||||
|
displayOrder={ avatarInfo.displayOrder }
|
||||||
|
nameClassName="text-white"
|
||||||
|
nickIcon={ avatarInfo.nickIcon }
|
||||||
|
prefixColor={ avatarInfo.prefixColor }
|
||||||
|
prefixEffect={ avatarInfo.prefixEffect }
|
||||||
|
prefixFont={ avatarInfo.prefixFont }
|
||||||
|
prefixIcon={ avatarInfo.prefixIcon }
|
||||||
|
prefixText={ avatarInfo.prefixText }
|
||||||
|
username={ avatarInfo.name } />
|
||||||
|
</div>
|
||||||
|
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Column
|
||||||
|
fullWidth
|
||||||
|
className={`flex items-center w-full max-w-[68px] rounded-sm relative overflow-hidden profile-background ${infostandBackgroundClass}`}
|
||||||
|
onClick={handleProfileClick}
|
||||||
|
>
|
||||||
|
<Base position="absolute" className={`profile-stand ${infostandStandClass}`} />
|
||||||
|
<LayoutAvatarImageView direction={2} figure={avatarInfo.figure} />
|
||||||
|
<Base position="absolute" className={`profile-overlay ${infostandOverlayClass}`} />
|
||||||
|
</Column>
|
||||||
|
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||||
|
<Base
|
||||||
|
className="background-edit-icon background-edit-position"
|
||||||
|
style={{ pointerEvents: 'auto', cursor: 'pointer' }}
|
||||||
|
onClick={handleEditClick}
|
||||||
|
aria-label="Edit profile background"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Column grow alignItems="center" gap={0}>
|
||||||
|
{ (() =>
|
||||||
|
{
|
||||||
|
const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
|
||||||
|
const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
|
||||||
|
const showGroup = maxSlots <= 5;
|
||||||
|
|
||||||
|
const items: React.ReactNode[] = [];
|
||||||
|
items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />);
|
||||||
|
|
||||||
|
if(showGroup)
|
||||||
|
{
|
||||||
|
items.push(
|
||||||
|
<Flex key="group" center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
|
||||||
|
{avatarInfo.groupId > 0 && <LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIdx = showGroup ? 1 : 2;
|
||||||
|
for(let i = startIdx; i < maxSlots; i++)
|
||||||
|
{
|
||||||
|
items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: React.ReactNode[][] = [];
|
||||||
|
for(let i = 0; i < items.length; i += 2)
|
||||||
|
{
|
||||||
|
rows.push(items.slice(i, i + 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.map((row, idx) => (
|
||||||
|
<Flex key={idx} center gap={1}>{row}</Flex>
|
||||||
|
));
|
||||||
|
})() }
|
||||||
|
</Column>
|
||||||
|
</div>
|
||||||
|
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
|
||||||
|
{avatarInfo.type !== AvatarInfoUser.OWN_USER && (
|
||||||
|
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||||
|
<Text fullWidth pointer small textBreak wrap variant="white">{motto}</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||||
|
<Flex grow alignItems="center" gap={2}>
|
||||||
|
<FaPencilAlt className="small fa-icon" />
|
||||||
|
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||||
|
{!isEditingMotto && (
|
||||||
|
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
|
||||||
|
{motto}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{isEditingMotto && (
|
||||||
|
<input
|
||||||
|
autoFocus={true}
|
||||||
|
className="w-full h-full text-[12px] p-0 outline-0 border-0 text-[#fff] relative bg-transparent resize-none focus:italic border-transparent focus:border-transparent focus:ring-0"
|
||||||
|
maxLength={GetConfigurationValue<number>('motto.max.length', 38)}
|
||||||
|
type="text"
|
||||||
|
value={motto}
|
||||||
|
onBlur={onMottoBlur}
|
||||||
|
onChange={event => setMotto(event.target.value)}
|
||||||
|
onKeyDown={onMottoKeyDown}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Text small wrap variant="white">
|
||||||
|
{LocalizeText('infostand.text.achievement_score') + ' ' + avatarInfo.achievementScore}
|
||||||
|
</Text>
|
||||||
|
{avatarInfo.carryItem > 0 && (
|
||||||
|
<>
|
||||||
|
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||||
|
<Text small wrap variant="white">
|
||||||
|
{LocalizeText('infostand.text.handitem', ['item'], [LocalizeText('handitem' + avatarInfo.carryItem)])}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<InfoStandWidgetUserRelationshipsView relationships={relationships} />
|
||||||
|
</div>
|
||||||
|
{GetConfigurationValue('user.tags.enabled') && (
|
||||||
|
<Column className="mt-1" gap={1}>
|
||||||
|
<InfoStandWidgetUserTagsView tags={GetSessionDataManager().tags} />
|
||||||
|
</Column>
|
||||||
)}
|
)}
|
||||||
{isEditingMotto && (
|
</Column>
|
||||||
<input
|
|
||||||
autoFocus={true}
|
|
||||||
className="w-full h-full text-[12px] p-0 outline-0 border-0 text-[#fff] relative bg-transparent resize-none focus:italic border-transparent focus:border-transparent focus:ring-0"
|
|
||||||
maxLength={GetConfigurationValue<number>('motto.max.length', 38)}
|
|
||||||
type="text"
|
|
||||||
value={motto}
|
|
||||||
onBlur={onMottoBlur}
|
|
||||||
onChange={event => setMotto(event.target.value)}
|
|
||||||
onKeyDown={onMottoKeyDown}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Text small wrap variant="white">
|
|
||||||
{LocalizeText('infostand.text.achievement_score') + ' ' + avatarInfo.achievementScore}
|
|
||||||
</Text>
|
|
||||||
{avatarInfo.carryItem > 0 && (
|
|
||||||
<>
|
|
||||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
|
||||||
<Text small wrap variant="white">
|
|
||||||
{LocalizeText('infostand.text.handitem', ['item'], [LocalizeText('handitem' + avatarInfo.carryItem)])}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<InfoStandWidgetUserRelationshipsView relationships={relationships} />
|
|
||||||
</div>
|
|
||||||
{GetConfigurationValue('user.tags.enabled') && (
|
|
||||||
<Column className="mt-1" gap={1}>
|
|
||||||
<InfoStandWidgetUserTagsView tags={GetSessionDataManager().tags} />
|
|
||||||
</Column>
|
</Column>
|
||||||
)}
|
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||||
</Column>
|
<div className="backgrounds-view-container">
|
||||||
</Column>
|
<BackgroundsView
|
||||||
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
setIsVisible={setIsVisible}
|
||||||
<div className="backgrounds-view-container">
|
selectedBackground={backgroundId}
|
||||||
<BackgroundsView
|
setSelectedBackground={setBackgroundId}
|
||||||
setIsVisible={setIsVisible}
|
selectedStand={standId}
|
||||||
selectedBackground={backgroundId}
|
setSelectedStand={setStandId}
|
||||||
setSelectedBackground={setBackgroundId}
|
selectedOverlay={overlayId}
|
||||||
selectedStand={standId}
|
setSelectedOverlay={setOverlayId}
|
||||||
setSelectedStand={setStandId}
|
selectedCardBackground={cardBackgroundId}
|
||||||
selectedOverlay={overlayId}
|
setSelectedCardBackground={setCardBackgroundId}
|
||||||
setSelectedOverlay={setOverlayId}
|
/>
|
||||||
selectedCardBackground={cardBackgroundId}
|
</div>
|
||||||
setSelectedCardBackground={setCardBackgroundId}
|
)}
|
||||||
/>
|
</>
|
||||||
</div>
|
);
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,42 +11,42 @@ interface ChatInputStyleSelectorViewProps
|
|||||||
|
|
||||||
export const ChatInputStyleSelectorView: FC<ChatInputStyleSelectorViewProps> = props =>
|
export const ChatInputStyleSelectorView: FC<ChatInputStyleSelectorViewProps> = props =>
|
||||||
{
|
{
|
||||||
const { chatStyleIds = null, selectChatStyleId = null } = props;
|
const { chatStyleIds = null, selectChatStyleId = null } = props;
|
||||||
const [ selectorVisible, setSelectorVisible ] = useState(false);
|
const [ selectorVisible, setSelectorVisible ] = useState(false);
|
||||||
|
|
||||||
const selectStyle = (styleId: number) =>
|
const selectStyle = (styleId: number) =>
|
||||||
{
|
{
|
||||||
selectChatStyleId(styleId);
|
selectChatStyleId(styleId);
|
||||||
setSelectorVisible(false);
|
setSelectorVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={selectorVisible} onOpenChange={setSelectorVisible}>
|
<Popover.Root open={selectorVisible} onOpenChange={setSelectorVisible}>
|
||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<div className="chatstyles-anchor">
|
<div className="chatstyles-anchor">
|
||||||
<div className="nitro-icon chatstyles-icon" />
|
<div className="nitro-icon chatstyles-icon" />
|
||||||
</div>
|
</div>
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
side="top"
|
side="top"
|
||||||
sideOffset={12}
|
sideOffset={12}
|
||||||
className="max-w-[276px] not-italic font-normal leading-normal text-left no-underline normal-case tracking-normal whitespace-normal text-[.7875rem] [word-wrap:break-word] bg-[#dfdfdf] bg-clip-padding border border-solid border-[#283F5D] rounded-[.25rem] [box-shadow:0_2px_#00000073] z-[1070]"
|
className="max-w-[276px] not-italic font-normal leading-normal text-left no-underline normal-case tracking-normal whitespace-normal text-[.7875rem] [word-wrap:break-word] bg-[#dfdfdf] bg-clip-padding border border-solid border-[#283F5D] rounded-[.25rem] [box-shadow:0_2px_#00000073] z-[1070]"
|
||||||
>
|
>
|
||||||
<NitroCardContentView className="bg-transparent max-h-[210px]!" overflow="hidden">
|
<NitroCardContentView className="bg-transparent max-h-[210px]!" overflow="hidden">
|
||||||
<Grid columnCount={3} overflow="auto">
|
<Grid columnCount={3} overflow="auto">
|
||||||
{chatStyleIds && chatStyleIds.length > 0 && chatStyleIds.map(styleId => (
|
{chatStyleIds && chatStyleIds.length > 0 && chatStyleIds.map(styleId => (
|
||||||
<Flex key={styleId} center pointer className="h-[35px] w-[65px]" onClick={() => selectStyle(styleId)}>
|
<Flex key={styleId} center pointer className="h-[35px] w-[65px]" onClick={() => selectStyle(styleId)}>
|
||||||
<div className="bubble-container relative w-[50px]">
|
<div className="bubble-container relative w-[50px]">
|
||||||
<div className={`relative max-w-[65px] min-h-[26px] text-[14px] chat-bubble bubble-${styleId}`} />
|
<div className={`relative max-w-[65px] min-h-[26px] text-[14px] chat-bubble bubble-${styleId}`} />
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</NitroCardContentView>
|
</NitroCardContentView>
|
||||||
<Popover.Arrow className="fill-black" width={14} height={7} />
|
<Popover.Arrow className="fill-black" width={14} height={7} />
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -284,7 +284,10 @@ export const ChatInputView: FC<{}> = props =>
|
|||||||
<ChatInputCommandSelectorView
|
<ChatInputCommandSelectorView
|
||||||
commands={ filteredCommands }
|
commands={ filteredCommands }
|
||||||
selectedIndex={ selectedIndex }
|
selectedIndex={ selectedIndex }
|
||||||
onSelect={ (cmd) => { setChatValue(':' + cmd.key + ' '); inputRef.current?.focus(); } }
|
onSelect={ (cmd) =>
|
||||||
|
{
|
||||||
|
setChatValue(':' + cmd.key + ' '); inputRef.current?.focus();
|
||||||
|
} }
|
||||||
onHover={ setSelectedIndex }
|
onHover={ setSelectedIndex }
|
||||||
/> }
|
/> }
|
||||||
<div className="flex-1 items-center input-sizer">
|
<div className="flex-1 items-center input-sizer">
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
|
|||||||
chooserSelectionVisualizer.clearAll();
|
chooserSelectionVisualizer.clearAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const isChecked = (id: number) => checkedIds.includes(id);
|
const isChecked = (id: number) => checkedIds.includes(id);
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
|
|||||||
setCheckAll(false);
|
setCheckAll(false);
|
||||||
chooserSelectionVisualizer.clearAll();
|
chooserSelectionVisualizer.clearAll();
|
||||||
setSelectedItems([]);
|
setSelectedItems([]);
|
||||||
}
|
};
|
||||||
|
|
||||||
const filteredItems = useMemo(() =>
|
const filteredItems = useMemo(() =>
|
||||||
{
|
{
|
||||||
@@ -179,7 +179,10 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
className={ classNames('rounded p-1', selectedItems.some(item => item.id === row.id) && 'bg-muted') }
|
className={ classNames('rounded p-1', selectedItems.some(item => item.id === row.id) && 'bg-muted') }
|
||||||
pointer
|
pointer
|
||||||
onClick={ () => { toggleItemSelection(row); if(pickallFurni) checkedId(row.id); } }
|
onClick={ () =>
|
||||||
|
{
|
||||||
|
toggleItemSelection(row); if(pickallFurni) checkedId(row.id);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ pickallFurni && (
|
{ pickallFurni && (
|
||||||
<input
|
<input
|
||||||
@@ -187,7 +190,10 @@ export const ChooserWidgetView: FC<ChooserWidgetViewProps> = props =>
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={ isChecked(row.id) }
|
checked={ isChecked(row.id) }
|
||||||
onChange={ () => checkedId(row.id) }
|
onChange={ () => checkedId(row.id) }
|
||||||
onClick={ e => { e.stopPropagation(); toggleItemSelection(row); } }
|
onClick={ e =>
|
||||||
|
{
|
||||||
|
e.stopPropagation(); toggleItemSelection(row);
|
||||||
|
} }
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Text truncate>
|
<Text truncate>
|
||||||
|
|||||||
@@ -20,133 +20,146 @@ const FADE_LENGTH = 75;
|
|||||||
const SPACE_AROUND_EDGES = 10;
|
const SPACE_AROUND_EDGES = 10;
|
||||||
|
|
||||||
export const ContextMenuView: FC<ContextMenuViewProps> = ({
|
export const ContextMenuView: FC<ContextMenuViewProps> = ({
|
||||||
objectId = -1,
|
objectId = -1,
|
||||||
category = -1,
|
category = -1,
|
||||||
userType = -1,
|
userType = -1,
|
||||||
fades = false,
|
fades = false,
|
||||||
onClose,
|
onClose,
|
||||||
classNames = [],
|
classNames = [],
|
||||||
style = {},
|
style = {},
|
||||||
children = null,
|
children = null,
|
||||||
collapsable = false,
|
collapsable = false,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) =>
|
||||||
const [pos, setPos] = useState<{ x: number; y: number }>({ x: null, y: null });
|
{
|
||||||
const [opacity, setOpacity] = useState(1);
|
const [pos, setPos] = useState<{ x: number; y: number }>({ x: null, y: null });
|
||||||
const [isFading, setIsFading] = useState(false);
|
const [opacity, setOpacity] = useState(1);
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isFading, setIsFading] = useState(false);
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
const stackRef = useRef<FixedSizeStack>(new FixedSizeStack(LOCATION_STACK_SIZE));
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
const maxStackRef = useRef(-1000000);
|
const stackRef = useRef<FixedSizeStack>(new FixedSizeStack(LOCATION_STACK_SIZE));
|
||||||
|
const maxStackRef = useRef(-1000000);
|
||||||
|
|
||||||
const updatePosition = useCallback(
|
const updatePosition = useCallback(
|
||||||
(bounds: NitroRectangle, location: { x: number; y: number }) => {
|
(bounds: NitroRectangle, location: { x: number; y: number }) =>
|
||||||
if (!bounds || !location || !elementRef.current) return;
|
{
|
||||||
|
if (!bounds || !location || !elementRef.current) return;
|
||||||
|
|
||||||
let offset = -elementRef.current.offsetHeight;
|
let offset = -elementRef.current.offsetHeight;
|
||||||
if (userType > -1 && [RoomObjectType.USER, RoomObjectType.BOT, RoomObjectType.RENTABLE_BOT].includes(userType)) {
|
if (userType > -1 && [RoomObjectType.USER, RoomObjectType.BOT, RoomObjectType.RENTABLE_BOT].includes(userType))
|
||||||
offset += bounds.height > 50 ? 15 : 0;
|
{
|
||||||
} else {
|
offset += bounds.height > 50 ? 15 : 0;
|
||||||
offset -= 14;
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
|
offset -= 14;
|
||||||
|
}
|
||||||
|
|
||||||
stackRef.current.addValue(location.y - bounds.top);
|
stackRef.current.addValue(location.y - bounds.top);
|
||||||
let maxStack = stackRef.current.getMax();
|
let maxStack = stackRef.current.getMax();
|
||||||
if (maxStack < maxStackRef.current - BUBBLE_DROP_SPEED) {
|
if (maxStack < maxStackRef.current - BUBBLE_DROP_SPEED)
|
||||||
maxStack = maxStackRef.current - BUBBLE_DROP_SPEED;
|
{
|
||||||
}
|
maxStack = maxStackRef.current - BUBBLE_DROP_SPEED;
|
||||||
maxStackRef.current = maxStack;
|
}
|
||||||
|
maxStackRef.current = maxStack;
|
||||||
|
|
||||||
const deltaY = location.y - maxStack;
|
const deltaY = location.y - maxStack;
|
||||||
let x = Math.round(location.x - elementRef.current.offsetWidth / 2);
|
let x = Math.round(location.x - elementRef.current.offsetWidth / 2);
|
||||||
let y = Math.round(deltaY + offset);
|
let y = Math.round(deltaY + offset);
|
||||||
|
|
||||||
const stage = GetStage();
|
const stage = GetStage();
|
||||||
const maxLeft = stage.width - elementRef.current.offsetWidth - SPACE_AROUND_EDGES;
|
const maxLeft = stage.width - elementRef.current.offsetWidth - SPACE_AROUND_EDGES;
|
||||||
const maxTop = stage.height - elementRef.current.offsetHeight - SPACE_AROUND_EDGES;
|
const maxTop = stage.height - elementRef.current.offsetHeight - SPACE_AROUND_EDGES;
|
||||||
|
|
||||||
x = Math.max(SPACE_AROUND_EDGES, Math.min(x, maxLeft));
|
x = Math.max(SPACE_AROUND_EDGES, Math.min(x, maxLeft));
|
||||||
y = Math.max(SPACE_AROUND_EDGES, Math.min(y, maxTop));
|
y = Math.max(SPACE_AROUND_EDGES, Math.min(y, maxTop));
|
||||||
|
|
||||||
setPos({ x, y });
|
setPos({ x, y });
|
||||||
},
|
},
|
||||||
[userType]
|
[userType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getClassNames = useMemo(() => {
|
const getClassNames = useMemo(() =>
|
||||||
const classes = [
|
{
|
||||||
'nitro-context-menu',
|
const classes = [
|
||||||
'p-[2px]!',
|
'nitro-context-menu',
|
||||||
'bg-[#1c323f]',
|
'p-[2px]!',
|
||||||
'border-2',
|
'bg-[#1c323f]',
|
||||||
'border-[solid]',
|
'border-2',
|
||||||
'border-[rgba(255,255,255,.5)]',
|
'border-[solid]',
|
||||||
'rounded-[.25rem]',
|
'border-[rgba(255,255,255,.5)]',
|
||||||
'text-[.7875rem]',
|
'rounded-[.25rem]',
|
||||||
|
'text-[.7875rem]',
|
||||||
'text-white',
|
'text-white',
|
||||||
'z-40',
|
'z-40',
|
||||||
'pointer-events-auto',
|
'pointer-events-auto',
|
||||||
'absolute',
|
'absolute',
|
||||||
pos.x !== null ? 'visible' : 'invisible',
|
pos.x !== null ? 'visible' : 'invisible',
|
||||||
];
|
];
|
||||||
if (isCollapsed) classes.push('menu-hidden');
|
if (isCollapsed) classes.push('menu-hidden');
|
||||||
return [...classes, ...classNames];
|
return [...classes, ...classNames];
|
||||||
}, [pos.x, isCollapsed, classNames]);
|
}, [pos.x, isCollapsed, classNames]);
|
||||||
|
|
||||||
const getStyle = useMemo(
|
const getStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
left: pos.x ?? 0,
|
left: pos.x ?? 0,
|
||||||
top: pos.y ?? 0,
|
top: pos.y ?? 0,
|
||||||
transition: isFading ? 'opacity 75ms linear' : undefined,
|
transition: isFading ? 'opacity 75ms linear' : undefined,
|
||||||
opacity,
|
opacity,
|
||||||
...style,
|
...style,
|
||||||
}),
|
}),
|
||||||
[pos, opacity, isFading, style]
|
[pos, opacity, isFading, style]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
if (!elementRef.current) return;
|
{
|
||||||
|
if (!elementRef.current) return;
|
||||||
|
|
||||||
const update = () => {
|
const update = () =>
|
||||||
if (!elementRef.current) return;
|
{
|
||||||
const roomSession = GetRoomSession();
|
if (!elementRef.current) return;
|
||||||
|
const roomSession = GetRoomSession();
|
||||||
|
|
||||||
if (!roomSession) {
|
if (!roomSession)
|
||||||
onClose();
|
{
|
||||||
return;
|
onClose();
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bounds = GetRoomObjectBounds(roomSession.roomId, objectId, category);
|
const bounds = GetRoomObjectBounds(roomSession.roomId, objectId, category);
|
||||||
const location = GetRoomObjectScreenLocation(roomSession.roomId, objectId, category);
|
const location = GetRoomObjectScreenLocation(roomSession.roomId, objectId, category);
|
||||||
updatePosition(bounds, location);
|
updatePosition(bounds, location);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ticker = GetTicker();
|
const ticker = GetTicker();
|
||||||
ticker.add(update);
|
ticker.add(update);
|
||||||
|
|
||||||
return () => ticker.remove(update);
|
return () => ticker.remove(update);
|
||||||
}, [objectId, category, updatePosition, onClose]);
|
}, [objectId, category, updatePosition, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
if (!fades) return;
|
{
|
||||||
|
if (!fades) return;
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() =>
|
||||||
setIsFading(true);
|
{
|
||||||
setTimeout(onClose, FADE_LENGTH);
|
setIsFading(true);
|
||||||
}, FADE_DELAY);
|
setTimeout(onClose, FADE_LENGTH);
|
||||||
|
}, FADE_DELAY);
|
||||||
|
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [fades, onClose]);
|
}, [fades, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
if (!isFading) return;
|
{
|
||||||
setOpacity(0);
|
if (!isFading) return;
|
||||||
}, [isFading]);
|
setOpacity(0);
|
||||||
|
}, [isFading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={elementRef} className={getClassNames.join(' ')} style={getStyle} {...rest}>
|
<div ref={elementRef} className={getClassNames.join(' ')} style={getStyle} {...rest}>
|
||||||
{!(collapsable && isCollapsed) && children}
|
{!(collapsable && isCollapsed) && children}
|
||||||
{collapsable && <ContextMenuCaretView collapsed={isCollapsed} onClick={() => setIsCollapsed((prev) => !prev)} />}
|
{collapsable && <ContextMenuCaretView collapsed={isCollapsed} onClick={() => setIsCollapsed((prev) => !prev)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../
|
|||||||
import { useFurnitureExternalImageWidget, useHelp } from '../../../../hooks';
|
import { useFurnitureExternalImageWidget, useHelp } from '../../../../hooks';
|
||||||
import { CameraWidgetShowPhotoView } from '../../../camera/views/CameraWidgetShowPhotoView';
|
import { CameraWidgetShowPhotoView } from '../../../camera/views/CameraWidgetShowPhotoView';
|
||||||
|
|
||||||
export const FurnitureExternalImageView: FC<{}> = props => {
|
export const FurnitureExternalImageView: FC<{}> = props =>
|
||||||
|
{
|
||||||
const { objectId = -1, currentPhotoIndex = -1, currentPhotos = null, onClose = null } = useFurnitureExternalImageWidget();
|
const { objectId = -1, currentPhotoIndex = -1, currentPhotos = null, onClose = null } = useFurnitureExternalImageWidget();
|
||||||
const { report = null } = useHelp();
|
const { report = null } = useHelp();
|
||||||
|
|
||||||
if (objectId === -1 || currentPhotoIndex === -1) return null;
|
if (objectId === -1 || currentPhotoIndex === -1) return null;
|
||||||
|
|
||||||
const handleOpenFullPhoto = () => {
|
const handleOpenFullPhoto = () =>
|
||||||
|
{
|
||||||
const photoUrl = currentPhotos[currentPhotoIndex].w.replace('_small.png', '.png');
|
const photoUrl = currentPhotos[currentPhotoIndex].w.replace('_small.png', '.png');
|
||||||
if (photoUrl) {
|
if (photoUrl)
|
||||||
console.log("Opened photo URL:", photoUrl);
|
{
|
||||||
|
console.log('Opened photo URL:', photoUrl);
|
||||||
window.open(photoUrl, '_blank');
|
window.open(photoUrl, '_blank');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Text } from '../../../../common';
|
|||||||
import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks';
|
import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks';
|
||||||
import { getRegisteredPlugins, INitroPlugin, subscribePlugins } from '../../../plugins/NitroPluginApi';
|
import { getRegisteredPlugins, INitroPlugin, subscribePlugins } from '../../../plugins/NitroPluginApi';
|
||||||
|
|
||||||
export const RoomToolsWidgetView: FC<{}> = props => {
|
export const RoomToolsWidgetView: FC<{}> = props =>
|
||||||
|
{
|
||||||
const [areBubblesMuted, setAreBubblesMuted] = useState(false);
|
const [areBubblesMuted, setAreBubblesMuted] = useState(false);
|
||||||
const [isZoomedIn, setIsZoomedIn] = useState<boolean>(false);
|
const [isZoomedIn, setIsZoomedIn] = useState<boolean>(false);
|
||||||
const [roomName, setRoomName] = useState<string>(null);
|
const [roomName, setRoomName] = useState<string>(null);
|
||||||
@@ -27,19 +28,25 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
|||||||
return subscribePlugins(() => setPlugins(getRegisteredPlugins()));
|
return subscribePlugins(() => setPlugins(getRegisteredPlugins()));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleToolClick = (action: string, value?: string) => {
|
const handleToolClick = (action: string, value?: string) =>
|
||||||
if (!roomSession) return;
|
{
|
||||||
|
if (!roomSession) return;
|
||||||
|
|
||||||
switch (action) {
|
switch (action)
|
||||||
|
{
|
||||||
case 'settings':
|
case 'settings':
|
||||||
CreateLinkEvent('navigator/toggle-room-info');
|
CreateLinkEvent('navigator/toggle-room-info');
|
||||||
return;
|
return;
|
||||||
case 'zoom':
|
case 'zoom':
|
||||||
setIsZoomedIn(prevValue => {
|
setIsZoomedIn(prevValue =>
|
||||||
if (GetConfigurationValue('room.zoom.enabled', true)) {
|
{
|
||||||
|
if (GetConfigurationValue('room.zoom.enabled', true))
|
||||||
|
{
|
||||||
const scale = GetRoomEngine().getRoomInstanceRenderingCanvasScale(roomSession.roomId, 1);
|
const scale = GetRoomEngine().getRoomInstanceRenderingCanvasScale(roomSession.roomId, 1);
|
||||||
GetRoomEngine().setRoomInstanceRenderingCanvasScale(roomSession.roomId, 1, scale === 1 ? 0.5 : 1);
|
GetRoomEngine().setRoomInstanceRenderingCanvasScale(roomSession.roomId, 1, scale === 1 ? 0.5 : 1);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
const geometry = GetRoomEngine().getRoomInstanceGeometry(roomSession.roomId, 1);
|
const geometry = GetRoomEngine().getRoomInstanceGeometry(roomSession.roomId, 1);
|
||||||
if (geometry) geometry.performZoom();
|
if (geometry) geometry.performZoom();
|
||||||
}
|
}
|
||||||
@@ -77,7 +84,8 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeRoomHistory = (roomId: number, roomName: string) => {
|
const onChangeRoomHistory = (roomId: number, roomName: string) =>
|
||||||
|
{
|
||||||
let newStorage = JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]');
|
let newStorage = JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]');
|
||||||
if (newStorage.some((room: { roomId: number }) => room.roomId === roomId)) return;
|
if (newStorage.some((room: { roomId: number }) => room.roomId === roomId)) return;
|
||||||
|
|
||||||
@@ -88,7 +96,8 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
|||||||
SetLocalStorage('nitro.room.history', newStorage);
|
SetLocalStorage('nitro.room.history', newStorage);
|
||||||
};
|
};
|
||||||
|
|
||||||
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event => {
|
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, event =>
|
||||||
|
{
|
||||||
const parser = event.getParser();
|
const parser = event.getParser();
|
||||||
if (!parser.roomEnter || (parser.data.roomId !== roomSession.roomId)) return;
|
if (!parser.roomEnter || (parser.data.roomId !== roomSession.roomId)) return;
|
||||||
|
|
||||||
@@ -98,18 +107,22 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
|||||||
onChangeRoomHistory(parser.data.roomId, parser.data.roomName);
|
onChangeRoomHistory(parser.data.roomId, parser.data.roomName);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
const timeout = setTimeout(() => setIsOpen(false), 5000);
|
const timeout = setTimeout(() => setIsOpen(false), 5000);
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [roomName, roomOwner, roomTags]);
|
}, [roomName, roomOwner, roomTags]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
setRoomHistory(JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]'));
|
setRoomHistory(JSON.parse(window.localStorage.getItem('nitro.room.history') || '[]'));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
const handleTabClose = () => {
|
{
|
||||||
|
const handleTabClose = () =>
|
||||||
|
{
|
||||||
window.localStorage.removeItem('nitro.room.history');
|
window.localStorage.removeItem('nitro.room.history');
|
||||||
};
|
};
|
||||||
window.addEventListener('beforeunload', handleTabClose);
|
window.addEventListener('beforeunload', handleTabClose);
|
||||||
@@ -119,10 +132,10 @@ export const RoomToolsWidgetView: FC<{}> = props => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex space-x-2 nitro-room-tools-container">
|
<div className="flex space-x-2 nitro-room-tools-container">
|
||||||
<div className="flex flex-col items-center justify-center p-2 nitro-room-tools">
|
<div className="flex flex-col items-center justify-center p-2 nitro-room-tools">
|
||||||
<div className="cursor-pointer nitro-icon icon-cog" title={LocalizeText('room.settings.button.text')} onClick={() => handleToolClick('settings')} />
|
<div className="cursor-pointer nitro-icon icon-cog" title={LocalizeText('room.settings.button.text')} onClick={() => handleToolClick('settings')} />
|
||||||
<div className={classNames('cursor-pointer', 'nitro-icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more'))} title={LocalizeText('room.zoom.button.text')} onClick={() => handleToolClick('zoom')} />
|
<div className={classNames('cursor-pointer', 'nitro-icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more'))} title={LocalizeText('room.zoom.button.text')} onClick={() => handleToolClick('zoom')} />
|
||||||
<div className="cursor-pointer nitro-icon icon-chat-history" title={LocalizeText('room.chathistory.button.text')} onClick={() => handleToolClick('chat_history')} />
|
<div className="cursor-pointer nitro-icon icon-chat-history" title={LocalizeText('room.chathistory.button.text')} onClick={() => handleToolClick('chat_history')} />
|
||||||
<div className={classNames('cursor-pointer', 'nitro-icon', (areBubblesMuted ? 'icon-chat-disablebubble' : 'icon-chat-enablebubble'))} title={areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')} onClick={() => handleToolClick('hiddenbubbles')} />
|
<div className={classNames('cursor-pointer', 'nitro-icon', (areBubblesMuted ? 'icon-chat-disablebubble' : 'icon-chat-enablebubble'))} title={areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')} onClick={() => handleToolClick('hiddenbubbles')} />
|
||||||
|
|
||||||
{navigatorData.canRate && (
|
{navigatorData.canRate && (
|
||||||
<div className="cursor-pointer nitro-icon icon-like-room" title={LocalizeText('room.like.button.text')} onClick={() => handleToolClick('like_room')} />
|
<div className="cursor-pointer nitro-icon icon-like-room" title={LocalizeText('room.like.button.text')} onClick={() => handleToolClick('like_room')} />
|
||||||
|
|||||||
@@ -203,7 +203,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
|||||||
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
|
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
|
||||||
</motion.div> }
|
</motion.div> }
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
<motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event => { setMeExpanded(value => !value); event.stopPropagation(); } }>
|
<motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event =>
|
||||||
|
{
|
||||||
|
setMeExpanded(value => !value); event.stopPropagation();
|
||||||
|
} }>
|
||||||
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } />
|
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
{ (getTotalUnseen > 0) &&
|
{ (getTotalUnseen > 0) &&
|
||||||
@@ -239,20 +242,20 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
|||||||
exit={ { opacity: 0, x: 10 } }
|
exit={ { opacity: 0, x: 10 } }
|
||||||
transition={ { type: 'spring', stiffness: 300, damping: 28 } }
|
transition={ { type: 'spring', stiffness: 300, damping: 28 } }
|
||||||
className={ `fixed bottom-0 z-40 h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pr-3 pointer-events-auto ${ desktopFlexClasses } ${ isInRoom ? 'right-0' : 'right-3' }` }>
|
className={ `fixed bottom-0 z-40 h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pr-3 pointer-events-auto ${ desktopFlexClasses } ${ isInRoom ? 'right-0' : 'right-3' }` }>
|
||||||
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className="tb-open-shell flex h-[52px] max-w-full items-center gap-3 overflow-visible bg-transparent px-[8px] pt-[10px] pb-[2px]">
|
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className="tb-open-shell flex h-[52px] max-w-full items-center gap-3 overflow-visible bg-transparent px-[8px] pt-[10px] pb-[2px]">
|
||||||
<motion.div variants={ itemVariants } className="relative">
|
<motion.div variants={ itemVariants } className="relative">
|
||||||
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
|
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
|
||||||
{ (requests.length > 0) &&
|
{ (requests.length > 0) &&
|
||||||
<LayoutItemCountView count={ requests.length } className="absolute -right-2 -top-1" /> }
|
<LayoutItemCountView count={ requests.length } className="absolute -right-2 -top-1" /> }
|
||||||
</motion.div>
|
</motion.div>
|
||||||
{ ((iconState === MessengerIconState.SHOW) || (iconState === MessengerIconState.UNREAD)) &&
|
{ ((iconState === MessengerIconState.SHOW) || (iconState === MessengerIconState.UNREAD)) &&
|
||||||
<motion.div variants={ itemVariants }>
|
<motion.div variants={ itemVariants }>
|
||||||
<ToolbarItemView className={ `tb-icon ${ iconState === MessengerIconState.UNREAD ? 'is-unseen animate-pulse' : '' }` } icon="message" onClick={ () => OpenMessengerChat() } />
|
<ToolbarItemView className={ `tb-icon ${ iconState === MessengerIconState.UNREAD ? 'is-unseen animate-pulse' : '' }` } icon="message" onClick={ () => OpenMessengerChat() } />
|
||||||
</motion.div> }
|
</motion.div> }
|
||||||
<div className={ `mx-1 h-5 w-[1px] bg-white/20 ${ desktopBlockClasses }` } />
|
<div className={ `mx-1 h-5 w-[1px] bg-white/20 ${ desktopBlockClasses }` } />
|
||||||
<div className={ `h-full shrink-0 ${ desktopBlockClasses }` } id="toolbar-friend-bar-container-desktop" />
|
<div className={ `h-full shrink-0 ${ desktopBlockClasses }` } id="toolbar-friend-bar-container-desktop" />
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
key="mobile-nav"
|
key="mobile-nav"
|
||||||
@@ -297,13 +300,16 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
|||||||
className="pointer-events-auto absolute bottom-[calc(100%+10px)] left-1/2 z-[70] -translate-x-1/2">
|
className="pointer-events-auto absolute bottom-[calc(100%+10px)] left-1/2 z-[70] -translate-x-1/2">
|
||||||
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
|
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
|
||||||
</motion.div> }
|
</motion.div> }
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
<motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event => { setMeExpanded(value => !value); event.stopPropagation(); } }>
|
<motion.div whileHover={ { scale: 1.08 } } whileTap={ { scale: 0.95 } } className="cursor-pointer" onClick={ event =>
|
||||||
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } />
|
{
|
||||||
</motion.div>
|
setMeExpanded(value => !value); event.stopPropagation();
|
||||||
{ (getTotalUnseen > 0) &&
|
} }>
|
||||||
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
|
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[44px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: '4px' } } />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
{ (getTotalUnseen > 0) &&
|
||||||
|
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
|
||||||
|
</motion.div>
|
||||||
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className="tb-bar-scroll flex h-full items-center gap-2 overflow-x-auto overflow-y-visible px-1">
|
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className="tb-bar-scroll flex h-full items-center gap-2 overflow-x-auto overflow-y-visible px-1">
|
||||||
{ (isInRoom && showToolbarButton) &&
|
{ (isInRoom && showToolbarButton) &&
|
||||||
<motion.div variants={ itemVariants }>
|
<motion.div variants={ itemVariants }>
|
||||||
|
|||||||
@@ -1,31 +1,34 @@
|
|||||||
import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomSettingsEvent, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from "@nitrots/nitro-renderer";
|
import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomSettingsEvent, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from '@nitrots/nitro-renderer';
|
||||||
import { FC, useEffect, useRef, useState } from "react";
|
import { FC, useEffect, useRef, useState } from 'react';
|
||||||
import ReactPlayer from "react-player/youtube";
|
import ReactPlayer from 'react-player/youtube';
|
||||||
import { GetRoomSession, getYoutubeRoomEnabled, GetSessionDataManager, LocalizeText, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from "../../api";
|
import { GetRoomSession, getYoutubeRoomEnabled, GetSessionDataManager, LocalizeText, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from '../../api';
|
||||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, LayoutAvatarImageView } from "../../common";
|
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, LayoutAvatarImageView } from '../../common';
|
||||||
import { useFurnitureYoutubeWidget, useMessageEvent } from "../../hooks";
|
import { useFurnitureYoutubeWidget, useMessageEvent } from '../../hooks';
|
||||||
|
|
||||||
const CONTROL_COMMAND_PREVIOUS_VIDEO = 0;
|
const CONTROL_COMMAND_PREVIOUS_VIDEO = 0;
|
||||||
const CONTROL_COMMAND_NEXT_VIDEO = 1;
|
const CONTROL_COMMAND_NEXT_VIDEO = 1;
|
||||||
const CONTROL_COMMAND_PAUSE_VIDEO = 2;
|
const CONTROL_COMMAND_PAUSE_VIDEO = 2;
|
||||||
const CONTROL_COMMAND_CONTINUE_VIDEO = 3;
|
const CONTROL_COMMAND_CONTINUE_VIDEO = 3;
|
||||||
|
|
||||||
const extractVideoId = (input: string): string => {
|
const extractVideoId = (input: string): string =>
|
||||||
|
{
|
||||||
const patterns = [
|
const patterns = [
|
||||||
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
|
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
|
||||||
/^([a-zA-Z0-9_-]{11})$/,
|
/^([a-zA-Z0-9_-]{11})$/,
|
||||||
];
|
];
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns)
|
||||||
|
{
|
||||||
const match = input.match(pattern);
|
const match = input.match(pattern);
|
||||||
if (match) return match[1];
|
if (match) return match[1];
|
||||||
}
|
}
|
||||||
return input;
|
return input;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const YouTubePlayerView: FC<{}> = () => {
|
export const YouTubePlayerView: FC<{}> = () =>
|
||||||
|
{
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [tab, setTab] = useState< | "player" | "playlist" | "spectators" | "settings" | "history" | "share" >("player");
|
const [tab, setTab] = useState< | 'player' | 'playlist' | 'spectators' | 'settings' | 'history' | 'share' >('player');
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [isRoomMode, setIsRoomMode] = useState(false);
|
const [isRoomMode, setIsRoomMode] = useState(false);
|
||||||
const [volume, setVolume] = useState(100);
|
const [volume, setVolume] = useState(100);
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
@@ -38,121 +41,174 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
const playerRef = useRef<ReactPlayer | null>(null);
|
const playerRef = useRef<ReactPlayer | null>(null);
|
||||||
const { objectId: youtubeObjectId, videoId: roomVideoId, currentVideoState, hasControl } = useFurnitureYoutubeWidget();
|
const { objectId: youtubeObjectId, videoId: roomVideoId, currentVideoState, hasControl } = useFurnitureYoutubeWidget();
|
||||||
const [spectators, setSpectators] = useState< { id: number; name: string; look: string }[] >([]);
|
const [spectators, setSpectators] = useState< { id: number; name: string; look: string }[] >([]);
|
||||||
const [broadcastVideo, setBroadcastVideo] = useState("");
|
const [broadcastVideo, setBroadcastVideo] = useState('');
|
||||||
const [broadcastSender, setBroadcastSender] = useState("");
|
const [broadcastSender, setBroadcastSender] = useState('');
|
||||||
const [broadcastPlaylist, setBroadcastPlaylist] = useState<string[]>([]);
|
const [broadcastPlaylist, setBroadcastPlaylist] = useState<string[]>([]);
|
||||||
const [watcherIds, setWatcherIds] = useState<Set<number>>(new Set());
|
const [watcherIds, setWatcherIds] = useState<Set<number>>(new Set());
|
||||||
const [youtubeEnabled, setYoutubeEnabled] = useState(getYoutubeRoomEnabled());
|
const [youtubeEnabled, setYoutubeEnabled] = useState(getYoutubeRoomEnabled());
|
||||||
|
|
||||||
useMessageEvent<YouTubeRoomSettingsEvent>(YouTubeRoomSettingsEvent, event => {
|
useMessageEvent<YouTubeRoomSettingsEvent>(YouTubeRoomSettingsEvent, event =>
|
||||||
|
{
|
||||||
setYoutubeEnabled(event.getParser().youtubeEnabled);
|
setYoutubeEnabled(event.getParser().youtubeEnabled);
|
||||||
});
|
});
|
||||||
useMessageEvent<YouTubeRoomBroadcastEvent>(YouTubeRoomBroadcastEvent, event => {
|
useMessageEvent<YouTubeRoomBroadcastEvent>(YouTubeRoomBroadcastEvent, event =>
|
||||||
|
{
|
||||||
const parser = event.getParser();
|
const parser = event.getParser();
|
||||||
setBroadcastVideo(parser.videoId);
|
setBroadcastVideo(parser.videoId);
|
||||||
setBroadcastSender(parser.senderName);
|
setBroadcastSender(parser.senderName);
|
||||||
setBroadcastPlaylist(parser.playlist);
|
setBroadcastPlaylist(parser.playlist);
|
||||||
if (parser.videoId) {
|
if (parser.videoId)
|
||||||
|
{
|
||||||
setInputValue(parser.videoId);
|
setInputValue(parser.videoId);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
setTab("player");
|
setTab('player');
|
||||||
} else {
|
}
|
||||||
setInputValue("");
|
else
|
||||||
setBroadcastVideo("");
|
{
|
||||||
setBroadcastSender("");
|
setInputValue('');
|
||||||
|
setBroadcastVideo('');
|
||||||
|
setBroadcastSender('');
|
||||||
setBroadcastPlaylist([]);
|
setBroadcastPlaylist([]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useMessageEvent<YouTubeRoomWatchersEvent>(YouTubeRoomWatchersEvent, event => { setWatcherIds(new Set(event.getParser().watcherIds)); loadRoomUsers(); });
|
useMessageEvent<YouTubeRoomWatchersEvent>(YouTubeRoomWatchersEvent, event =>
|
||||||
|
{
|
||||||
|
setWatcherIds(new Set(event.getParser().watcherIds)); loadRoomUsers();
|
||||||
|
});
|
||||||
|
|
||||||
const sentWatchingRef = useRef(false);
|
const sentWatchingRef = useRef(false);
|
||||||
const hasVideo = !!(inputValue && extractVideoId(inputValue));
|
const hasVideo = !!(inputValue && extractVideoId(inputValue));
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
if (isOpen && hasVideo && !sentWatchingRef.current) {
|
{
|
||||||
try { SendMessageComposer(new YouTubeRoomWatchingComposer(true)); } catch(e) {}
|
if (isOpen && hasVideo && !sentWatchingRef.current)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SendMessageComposer(new YouTubeRoomWatchingComposer(true));
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{}
|
||||||
sentWatchingRef.current = true;
|
sentWatchingRef.current = true;
|
||||||
} else if ((!isOpen || !hasVideo) && sentWatchingRef.current) {
|
}
|
||||||
try { SendMessageComposer(new YouTubeRoomWatchingComposer(false)); } catch(e) {}
|
else if ((!isOpen || !hasVideo) && sentWatchingRef.current)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SendMessageComposer(new YouTubeRoomWatchingComposer(false));
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{}
|
||||||
sentWatchingRef.current = false;
|
sentWatchingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [isOpen, hasVideo]);
|
}, [isOpen, hasVideo]);
|
||||||
|
|
||||||
const loadRoomUsers = () => {
|
const loadRoomUsers = () =>
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
const roomSession = GetRoomSession();
|
const roomSession = GetRoomSession();
|
||||||
if (!roomSession) { setSpectators([]); return; }
|
if (!roomSession)
|
||||||
|
{
|
||||||
|
setSpectators([]); return;
|
||||||
|
}
|
||||||
const users: { id: number; name: string; look: string }[] = [];
|
const users: { id: number; name: string; look: string }[] = [];
|
||||||
const seen = new Set<number>();
|
const seen = new Set<number>();
|
||||||
for (let i = 0; i < 500; i++) {
|
for (let i = 0; i < 500; i++)
|
||||||
|
{
|
||||||
const userData = roomSession.userDataManager.getUserDataByIndex(i);
|
const userData = roomSession.userDataManager.getUserDataByIndex(i);
|
||||||
if (userData && userData.name && userData.type === 1 && !seen.has(userData.userId)) {
|
if (userData && userData.name && userData.type === 1 && !seen.has(userData.userId))
|
||||||
|
{
|
||||||
seen.add(userData.userId);
|
seen.add(userData.userId);
|
||||||
users.push({ id: userData.userId, name: userData.name, look: userData.figure });
|
users.push({ id: userData.userId, name: userData.name, look: userData.figure });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSpectators(users);
|
setSpectators(users);
|
||||||
} catch (e) {
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
setSpectators([]);
|
setSpectators([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
if (isOpen) loadRoomUsers();
|
if (isOpen) loadRoomUsers();
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
if (youtubeObjectId && youtubeObjectId !== -1) {
|
{
|
||||||
|
if (youtubeObjectId && youtubeObjectId !== -1)
|
||||||
|
{
|
||||||
setIsRoomMode(true);
|
setIsRoomMode(true);
|
||||||
if (roomVideoId) {
|
if (roomVideoId)
|
||||||
|
{
|
||||||
setInputValue(roomVideoId);
|
setInputValue(roomVideoId);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
setIsRoomMode(false);
|
setIsRoomMode(false);
|
||||||
}
|
}
|
||||||
}, [youtubeObjectId, roomVideoId]);
|
}, [youtubeObjectId, roomVideoId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
const handler = () => setIsOpen((p) => !p);
|
const handler = () => setIsOpen((p) => !p);
|
||||||
window.addEventListener("youtube:toggle", handler);
|
window.addEventListener('youtube:toggle', handler);
|
||||||
return () => window.removeEventListener("youtube:toggle", handler);
|
return () => window.removeEventListener('youtube:toggle', handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
const savedHistory = localStorage.getItem("youtube_history");
|
{
|
||||||
if (savedHistory) {
|
const savedHistory = localStorage.getItem('youtube_history');
|
||||||
try {
|
if (savedHistory)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
const parsed = JSON.parse(savedHistory);
|
const parsed = JSON.parse(savedHistory);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed))
|
||||||
setHistory(parsed.map((entry: any) => typeof entry === "string" ? entry : entry?.id).filter(Boolean));
|
{
|
||||||
|
setHistory(parsed.map((entry: any) => typeof entry === 'string' ? entry : entry?.id).filter(Boolean));
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
}
|
||||||
|
catch (e)
|
||||||
|
{}
|
||||||
}
|
}
|
||||||
const savedPlaylist = localStorage.getItem("youtube_playlist");
|
const savedPlaylist = localStorage.getItem('youtube_playlist');
|
||||||
if (savedPlaylist) {
|
if (savedPlaylist)
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
const parsed = JSON.parse(savedPlaylist);
|
const parsed = JSON.parse(savedPlaylist);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed))
|
||||||
setPlaylist(parsed.map((entry: any) => typeof entry === "string" ? entry : entry?.id).filter(Boolean));
|
{
|
||||||
|
setPlaylist(parsed.map((entry: any) => typeof entry === 'string' ? entry : entry?.id).filter(Boolean));
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
}
|
||||||
|
catch (e)
|
||||||
|
{}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
|
{
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"youtube_history",
|
'youtube_history',
|
||||||
JSON.stringify(history.slice(0, 50)),
|
JSON.stringify(history.slice(0, 50)),
|
||||||
);
|
);
|
||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
localStorage.setItem("youtube_playlist", JSON.stringify(playlist));
|
{
|
||||||
|
localStorage.setItem('youtube_playlist', JSON.stringify(playlist));
|
||||||
}, [playlist]);
|
}, [playlist]);
|
||||||
|
|
||||||
const addToHistory = (id: string) => {
|
const addToHistory = (id: string) =>
|
||||||
|
{
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
setHistory((prev) => {
|
setHistory((prev) =>
|
||||||
|
{
|
||||||
const filtered = prev.filter((v) => v !== id);
|
const filtered = prev.filter((v) => v !== id);
|
||||||
return [id, ...filtered].slice(0, 50);
|
return [id, ...filtered].slice(0, 50);
|
||||||
});
|
});
|
||||||
@@ -199,9 +255,11 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const addToPlaylist = () => {
|
const addToPlaylist = () =>
|
||||||
|
{
|
||||||
const id = extractVideoId(inputValue);
|
const id = extractVideoId(inputValue);
|
||||||
if (id && !playlist.includes(id)) {
|
if (id && !playlist.includes(id))
|
||||||
|
{
|
||||||
setPlaylist((p) => [...p, id]);
|
setPlaylist((p) => [...p, id]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -222,11 +280,12 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
label: string;
|
label: string;
|
||||||
}) => (
|
}) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
|
{
|
||||||
setVolume(value);
|
setVolume(value);
|
||||||
setVolumePreset(value);
|
setVolumePreset(value);
|
||||||
}}
|
}}
|
||||||
className={`px-2 py-1 rounded text-xs ${volumePreset === value ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`}
|
className={`px-2 py-1 rounded text-xs ${volumePreset === value ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
@@ -234,55 +293,58 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NitroCardView
|
<NitroCardView
|
||||||
className={`youtube-player-modal ${isFullscreen ? "!fixed inset-0 w-full h-full z-[9999] rounded-none" : "w-[550px]"}`}
|
className={`youtube-player-modal ${isFullscreen ? '!fixed inset-0 w-full h-full z-[9999] rounded-none' : 'w-[550px]'}`}
|
||||||
>
|
>
|
||||||
<NitroCardHeaderView
|
<NitroCardHeaderView
|
||||||
headerText={isRoomMode ? "📺 YouTube TV" : "▶ YouTube"}
|
headerText={isRoomMode ? '📺 YouTube TV' : '▶ YouTube'}
|
||||||
onCloseClick={() => setIsOpen(false)}
|
onCloseClick={() => setIsOpen(false)}
|
||||||
/>
|
/>
|
||||||
<NitroCardContentView>
|
<NitroCardContentView>
|
||||||
<div className="flex gap-1 mb-3 border-b border-gray-700 pb-2 flex-wrap">
|
<div className="flex gap-1 mb-3 border-b border-gray-700 pb-2 flex-wrap">
|
||||||
<button
|
<button
|
||||||
onClick={() => setTab("player")}
|
onClick={() => setTab('player')}
|
||||||
className={`px-3 py-1 rounded text-sm ${tab === "player" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`}
|
className={`px-3 py-1 rounded text-sm ${tab === 'player' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||||
>
|
>
|
||||||
▶
|
▶
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setTab("playlist")}
|
onClick={() => setTab('playlist')}
|
||||||
className={`px-3 py-1 rounded text-sm ${tab === "playlist" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`}
|
className={`px-3 py-1 rounded text-sm ${tab === 'playlist' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||||
>
|
>
|
||||||
📋 {playlist.length}
|
📋 {playlist.length}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setTab("history")}
|
onClick={() => setTab('history')}
|
||||||
className={`px-3 py-1 rounded text-sm ${tab === "history" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`}
|
className={`px-3 py-1 rounded text-sm ${tab === 'history' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||||
>
|
>
|
||||||
🕐 {history.length}
|
🕐 {history.length}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setTab("share")}
|
onClick={() => setTab('share')}
|
||||||
className={`px-3 py-1 rounded text-sm ${tab === "share" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`}
|
className={`px-3 py-1 rounded text-sm ${tab === 'share' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||||
>
|
>
|
||||||
📤
|
📤
|
||||||
</button>
|
</button>
|
||||||
{watcherIds.size > 0 && (
|
{watcherIds.size > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setTab("spectators"); loadRoomUsers(); }}
|
onClick={() =>
|
||||||
className={`px-3 py-1 rounded text-sm ${tab === "spectators" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`}
|
{
|
||||||
|
setTab('spectators'); loadRoomUsers();
|
||||||
|
}}
|
||||||
|
className={`px-3 py-1 rounded text-sm ${tab === 'spectators' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||||
>
|
>
|
||||||
📺 {watcherIds.size}
|
📺 {watcherIds.size}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setTab("settings")}
|
onClick={() => setTab('settings')}
|
||||||
className={`px-3 py-1 rounded text-sm ${tab === "settings" ? "bg-amber-600 text-white" : "bg-gray-700 text-gray-300"}`}
|
className={`px-3 py-1 rounded text-sm ${tab === 'settings' ? 'bg-amber-600 text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||||
>
|
>
|
||||||
⚙️
|
⚙️
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tab === "player" && (
|
{tab === 'player' && (
|
||||||
<>
|
<>
|
||||||
{isRoomMode && (
|
{isRoomMode && (
|
||||||
<div className="mb-2 p-2 bg-blue-900/50 rounded flex justify-between text-sm">
|
<div className="mb-2 p-2 bg-blue-900/50 rounded flex justify-between text-sm">
|
||||||
@@ -311,10 +373,13 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
|
|
||||||
{videoId ? (
|
{videoId ? (
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
ref={ref => { playerRef.current = ref; }}
|
ref={ref =>
|
||||||
|
{
|
||||||
|
playerRef.current = ref;
|
||||||
|
}}
|
||||||
url={`https://www.youtube.com/watch?v=${videoId}`}
|
url={`https://www.youtube.com/watch?v=${videoId}`}
|
||||||
width="100%"
|
width="100%"
|
||||||
height={isFullscreen ? "100%" : 280}
|
height={isFullscreen ? '100%' : 280}
|
||||||
playing
|
playing
|
||||||
muted={isMuted}
|
muted={isMuted}
|
||||||
loop={isLooping}
|
loop={isLooping}
|
||||||
@@ -347,7 +412,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
}
|
}
|
||||||
className="px-4 py-1 bg-amber-600 rounded text-white font-bold text-sm"
|
className="px-4 py-1 bg-amber-600 rounded text-white font-bold text-sm"
|
||||||
>
|
>
|
||||||
{isPlaying ? "⏸" : "▶"}
|
{isPlaying ? '⏸' : '▶'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
@@ -363,12 +428,16 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
<span className="text-purple-300">📡 {broadcastSender} broadcasting</span>
|
<span className="text-purple-300">📡 {broadcastSender} broadcasting</span>
|
||||||
{isMyRoom && (
|
{isMyRoom && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
try {
|
{
|
||||||
SendMessageComposer(new YouTubeRoomPlayComposer("", []));
|
try
|
||||||
} catch(e) {}
|
{
|
||||||
setBroadcastVideo("");
|
SendMessageComposer(new YouTubeRoomPlayComposer('', []));
|
||||||
setBroadcastSender("");
|
}
|
||||||
|
catch(e)
|
||||||
|
{}
|
||||||
|
setBroadcastVideo('');
|
||||||
|
setBroadcastSender('');
|
||||||
setBroadcastPlaylist([]);
|
setBroadcastPlaylist([]);
|
||||||
}}
|
}}
|
||||||
className="px-2 py-0.5 bg-red-700 hover:bg-red-600 rounded text-white text-xs"
|
className="px-2 py-0.5 bg-red-700 hover:bg-red-600 rounded text-white text-xs"
|
||||||
@@ -385,15 +454,19 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
disabled={!!broadcastVideo && !isMyRoom}
|
disabled={!!broadcastVideo && !isMyRoom}
|
||||||
className={`flex-1 p-2 rounded text-white text-sm ${(!!broadcastVideo && !isMyRoom) ? "bg-gray-800" : "bg-gray-700"}`}
|
className={`flex-1 p-2 rounded text-white text-sm ${(!!broadcastVideo && !isMyRoom) ? 'bg-gray-800' : 'bg-gray-700'}`}
|
||||||
placeholder="YouTube URL / video ID"
|
placeholder="YouTube URL / video ID"
|
||||||
/>
|
/>
|
||||||
{isMyRoom && youtubeEnabled && videoId && (
|
{isMyRoom && youtubeEnabled && videoId && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
SendMessageComposer(new YouTubeRoomPlayComposer(videoId, playlist));
|
SendMessageComposer(new YouTubeRoomPlayComposer(videoId, playlist));
|
||||||
} catch(e) {}
|
}
|
||||||
|
catch(e)
|
||||||
|
{}
|
||||||
}}
|
}}
|
||||||
className="px-3 bg-purple-600 rounded text-white text-sm whitespace-nowrap"
|
className="px-3 bg-purple-600 rounded text-white text-sm whitespace-nowrap"
|
||||||
title="Speel deze video voor iedereen in de kamer"
|
title="Speel deze video voor iedereen in de kamer"
|
||||||
@@ -405,7 +478,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "playlist" && (
|
{tab === 'playlist' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -415,7 +488,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
placeholder="Add video URL..."
|
placeholder="Add video URL..."
|
||||||
className="flex-1 p-2 bg-gray-700 text-white rounded text-sm"
|
className="flex-1 p-2 bg-gray-700 text-white rounded text-sm"
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
e.key === "Enter" && addToPlaylist()
|
e.key === 'Enter' && addToPlaylist()
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -427,7 +500,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setInputValue("")}
|
onClick={() => setInputValue('')}
|
||||||
className="flex-1 px-3 py-2 bg-gray-700 rounded text-white text-sm"
|
className="flex-1 px-3 py-2 bg-gray-700 rounded text-white text-sm"
|
||||||
>
|
>
|
||||||
🔄 New video
|
🔄 New video
|
||||||
@@ -449,9 +522,10 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex items-center gap-2 p-2 bg-gray-800 rounded hover:bg-gray-700 cursor-pointer"
|
className="flex items-center gap-2 p-2 bg-gray-800 rounded hover:bg-gray-700 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
|
{
|
||||||
setInputValue(id);
|
setInputValue(id);
|
||||||
setTab("player");
|
setTab('player');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-amber-500 text-sm w-6">
|
<span className="text-amber-500 text-sm w-6">
|
||||||
@@ -461,7 +535,8 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
{id}
|
{id}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) =>
|
||||||
|
{
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setPlaylist((p) =>
|
setPlaylist((p) =>
|
||||||
p.filter((x) => x !== id),
|
p.filter((x) => x !== id),
|
||||||
@@ -478,7 +553,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "history" && (
|
{tab === 'history' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-gray-400 text-sm">
|
<div className="text-gray-400 text-sm">
|
||||||
@@ -501,9 +576,10 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex items-center gap-2 p-2 bg-gray-800 rounded hover:bg-gray-700 cursor-pointer"
|
className="flex items-center gap-2 p-2 bg-gray-800 rounded hover:bg-gray-700 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
|
{
|
||||||
setInputValue(id);
|
setInputValue(id);
|
||||||
setTab("player");
|
setTab('player');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0 text-white text-sm truncate font-mono">
|
<div className="flex-1 min-w-0 text-white text-sm truncate font-mono">
|
||||||
@@ -516,7 +592,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "share" && (
|
{tab === 'share' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
<div className="text-gray-400 text-sm mb-2">
|
<div className="text-gray-400 text-sm mb-2">
|
||||||
@@ -532,7 +608,8 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
className="flex-1 p-2 bg-gray-700 text-white rounded text-sm"
|
className="flex-1 p-2 bg-gray-700 text-white rounded text-sm"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
|
{
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
`https://youtube.com/watch?v=${videoId}`,
|
`https://youtube.com/watch?v=${videoId}`,
|
||||||
);
|
);
|
||||||
@@ -555,12 +632,14 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
if (videoId) {
|
{
|
||||||
|
if (videoId)
|
||||||
|
{
|
||||||
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
||||||
'Now watching: https://youtube.com/watch?v=${videoId}',
|
'Now watching: https://youtube.com/watch?v=${videoId}',
|
||||||
)}`;
|
)}`;
|
||||||
window.open(url, "_blank");
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!videoId}
|
disabled={!videoId}
|
||||||
@@ -573,57 +652,61 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "spectators" && (() => {
|
{tab === 'spectators' && (() =>
|
||||||
|
{
|
||||||
const watchers: { id: number; name: string; look: string }[] = [];
|
const watchers: { id: number; name: string; look: string }[] = [];
|
||||||
const rs = GetRoomSession();
|
const rs = GetRoomSession();
|
||||||
if (rs) {
|
if (rs)
|
||||||
for (const uid of watcherIds) {
|
{
|
||||||
|
for (const uid of watcherIds)
|
||||||
|
{
|
||||||
const ud = rs.userDataManager.getUserData(uid);
|
const ud = rs.userDataManager.getUserData(uid);
|
||||||
if (ud && ud.name) {
|
if (ud && ud.name)
|
||||||
|
{
|
||||||
watchers.push({ id: ud.userId, name: ud.name, look: ud.figure });
|
watchers.push({ id: ud.userId, name: ud.name, look: ud.figure });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<div className="text-gray-400 text-sm">
|
<div className="text-gray-400 text-sm">
|
||||||
📺 {watchers.length} watching
|
📺 {watchers.length} watching
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={loadRoomUsers}
|
onClick={loadRoomUsers}
|
||||||
className="text-gray-400 hover:text-white text-xs"
|
className="text-gray-400 hover:text-white text-xs"
|
||||||
>
|
>
|
||||||
🔄
|
🔄
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{watchers.length === 0 ? (
|
{watchers.length === 0 ? (
|
||||||
<div className="text-gray-500 text-sm text-center py-4">
|
<div className="text-gray-500 text-sm text-center py-4">
|
||||||
No one is watching
|
No one is watching
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[200px] overflow-y-auto space-y-1">
|
<div className="max-h-[200px] overflow-y-auto space-y-1">
|
||||||
{watchers.map((user) => (
|
{watchers.map((user) => (
|
||||||
<div
|
<div
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="flex items-center gap-2 p-2 bg-gray-700 rounded"
|
className="flex items-center gap-2 p-2 bg-gray-700 rounded"
|
||||||
>
|
>
|
||||||
<div className="shrink-0 overflow-hidden">
|
<div className="shrink-0 overflow-hidden">
|
||||||
<LayoutAvatarImageView figure={user.look} headOnly direction={2} scale={1} className="!w-[45px] !h-[65px] -mt-[5px] -ml-[5px]" />
|
<LayoutAvatarImageView figure={user.look} headOnly direction={2} scale={1} className="!w-[45px] !h-[65px] -mt-[5px] -ml-[5px]" />
|
||||||
|
</div>
|
||||||
|
<span className="text-white text-sm flex-1">
|
||||||
|
{user.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-amber-400 text-xs">📺</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white text-sm flex-1">
|
))}
|
||||||
{user.name}
|
</div>
|
||||||
</span>
|
)}
|
||||||
<span className="text-amber-400 text-xs">📺</span>
|
</div>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{tab === "settings" && (
|
{tab === 'settings' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
@@ -636,7 +719,7 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
}
|
}
|
||||||
className="text-gray-400 text-xs"
|
className="text-gray-400 text-xs"
|
||||||
>
|
>
|
||||||
{showVolumeSlider ? "▼" : "▲"}
|
{showVolumeSlider ? '▼' : '▲'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{showVolumeSlider && (
|
{showVolumeSlider && (
|
||||||
@@ -645,7 +728,8 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
value={volume}
|
value={volume}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
|
{
|
||||||
setVolume(parseInt(e.target.value));
|
setVolume(parseInt(e.target.value));
|
||||||
setVolumePreset(
|
setVolumePreset(
|
||||||
parseInt(e.target.value),
|
parseInt(e.target.value),
|
||||||
@@ -702,19 +786,19 @@ export const YouTubePlayerView: FC<{}> = () => {
|
|||||||
<div className="p-2 bg-gray-800 rounded text-xs text-gray-400">
|
<div className="p-2 bg-gray-800 rounded text-xs text-gray-400">
|
||||||
<div className="font-bold mb-1">ℹ️ Info</div>
|
<div className="font-bold mb-1">ℹ️ Info</div>
|
||||||
<div>
|
<div>
|
||||||
📡 Broadcast:{" "}
|
📡 Broadcast:{' '}
|
||||||
{broadcastVideo
|
{broadcastVideo
|
||||||
? <span className="text-green-400">✓ Active ({broadcastSender} playing)</span>
|
? <span className="text-green-400">✓ Active ({broadcastSender} playing)</span>
|
||||||
: <span className="text-gray-500">✕ No video</span>}
|
: <span className="text-gray-500">✕ No video</span>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
🎮 Controle:{" "}
|
🎮 Controle:{' '}
|
||||||
{isMyRoom
|
{isMyRoom
|
||||||
? <span className="text-green-400">✓ You are the owner</span>
|
? <span className="text-green-400">✓ You are the owner</span>
|
||||||
: <span className="text-gray-500">✕ Viewing only</span>}
|
: <span className="text-gray-500">✕ Viewing only</span>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
👁️ Viewers:{" "}
|
👁️ Viewers:{' '}
|
||||||
<span className="text-amber-400">{watcherIds.size}</span>
|
<span className="text-amber-400">{watcherIds.size}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -341,20 +341,20 @@ const VARIABLES_ELEMENTS: VariablesElementButton[] = [
|
|||||||
const EDITABLE_FURNI_VARIABLES: string[] = [ '@position_x', '@position_y', '@rotation', '@altitude', '@state', '@wallitem_offset' ];
|
const EDITABLE_FURNI_VARIABLES: string[] = [ '@position_x', '@position_y', '@rotation', '@altitude', '@state', '@wallitem_offset' ];
|
||||||
const EDITABLE_USER_VARIABLES: string[] = [ '@position_x', '@position_y', '@direction' ];
|
const EDITABLE_USER_VARIABLES: string[] = [ '@position_x', '@position_y', '@direction' ];
|
||||||
const createVariableDefinition = (key: string, target: 'Furni' | 'User' | 'Global' | 'Context', availability: string = 'Always', canWriteTo = false): VariableDefinition =>
|
const createVariableDefinition = (key: string, target: 'Furni' | 'User' | 'Global' | 'Context', availability: string = 'Always', canWriteTo = false): VariableDefinition =>
|
||||||
({
|
({
|
||||||
key,
|
key,
|
||||||
target,
|
target,
|
||||||
type: 'Internal',
|
type: 'Internal',
|
||||||
hasValue: true,
|
hasValue: true,
|
||||||
availability,
|
availability,
|
||||||
canWriteTo,
|
canWriteTo,
|
||||||
canCreateDelete: false,
|
canCreateDelete: false,
|
||||||
canIntercept: false,
|
canIntercept: false,
|
||||||
hasCreationTime: false,
|
hasCreationTime: false,
|
||||||
hasUpdateTime: false,
|
hasUpdateTime: false,
|
||||||
isTextConnected: false,
|
isTextConnected: false,
|
||||||
isAlwaysAvailable: (availability === 'Always')
|
isAlwaysAvailable: (availability === 'Always')
|
||||||
});
|
});
|
||||||
const VARIABLE_DEFINITIONS: Record<VariablesElementType, VariableDefinition[]> = {
|
const VARIABLE_DEFINITIONS: Record<VariablesElementType, VariableDefinition[]> = {
|
||||||
furni: [
|
furni: [
|
||||||
createVariableDefinition('~teleport.target_id', 'Furni', 'Conditional'),
|
createVariableDefinition('~teleport.target_id', 'Furni', 'Conditional'),
|
||||||
@@ -461,27 +461,27 @@ const DIRECTION_NAMES: string[] = [ 'North', 'North-East', 'East', 'South-East',
|
|||||||
const HOTEL_TIME_FORMATTERS: Map<string, Intl.DateTimeFormat> = new Map();
|
const HOTEL_TIME_FORMATTERS: Map<string, Intl.DateTimeFormat> = new Map();
|
||||||
|
|
||||||
const createEmptyMonitorSnapshot = (): MonitorSnapshot =>
|
const createEmptyMonitorSnapshot = (): MonitorSnapshot =>
|
||||||
({
|
({
|
||||||
usageCurrentWindow: 0,
|
usageCurrentWindow: 0,
|
||||||
usageLimitPerWindow: 0,
|
usageLimitPerWindow: 0,
|
||||||
isHeavy: false,
|
isHeavy: false,
|
||||||
delayedEventsPending: 0,
|
delayedEventsPending: 0,
|
||||||
delayedEventsLimit: 0,
|
delayedEventsLimit: 0,
|
||||||
averageExecutionMs: 0,
|
averageExecutionMs: 0,
|
||||||
peakExecutionMs: 0,
|
peakExecutionMs: 0,
|
||||||
recursionDepthCurrent: 0,
|
recursionDepthCurrent: 0,
|
||||||
recursionDepthLimit: 0,
|
recursionDepthLimit: 0,
|
||||||
killedRemainingSeconds: 0,
|
killedRemainingSeconds: 0,
|
||||||
usageWindowMs: 0,
|
usageWindowMs: 0,
|
||||||
overloadAverageThresholdMs: 0,
|
overloadAverageThresholdMs: 0,
|
||||||
overloadPeakThresholdMs: 0,
|
overloadPeakThresholdMs: 0,
|
||||||
heavyUsageThresholdPercent: 0,
|
heavyUsageThresholdPercent: 0,
|
||||||
heavyConsecutiveWindowsThreshold: 0,
|
heavyConsecutiveWindowsThreshold: 0,
|
||||||
overloadConsecutiveWindowsThreshold: 0,
|
overloadConsecutiveWindowsThreshold: 0,
|
||||||
heavyDelayedThresholdPercent: 0,
|
heavyDelayedThresholdPercent: 0,
|
||||||
logs: [],
|
logs: [],
|
||||||
history: []
|
history: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const getHotelTimeFormatter = (timeZone: string): Intl.DateTimeFormat =>
|
const getHotelTimeFormatter = (timeZone: string): Intl.DateTimeFormat =>
|
||||||
{
|
{
|
||||||
@@ -3245,7 +3245,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
|
|
||||||
switch(editingVariable)
|
switch(editingVariable)
|
||||||
{
|
{
|
||||||
case '@position_x': {
|
case '@position_x': {
|
||||||
const parsed = parseInt(editingValue.trim(), 10);
|
const parsed = parseInt(editingValue.trim(), 10);
|
||||||
|
|
||||||
if(Number.isNaN(parsed))
|
if(Number.isNaN(parsed))
|
||||||
@@ -3257,7 +3257,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
nextX = parsed;
|
nextX = parsed;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case '@position_y': {
|
case '@position_y': {
|
||||||
const parsed = parseInt(editingValue.trim(), 10);
|
const parsed = parseInt(editingValue.trim(), 10);
|
||||||
|
|
||||||
if(Number.isNaN(parsed))
|
if(Number.isNaN(parsed))
|
||||||
@@ -3632,7 +3632,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ isVariableHighlightActive && !!variableHighlightOverlays.length &&
|
{ isVariableHighlightActive && !!variableHighlightOverlays.length &&
|
||||||
<div className="pointer-events-none absolute left-0 top-0 z-30">
|
<div className="pointer-events-none absolute left-0 top-0 z-30">
|
||||||
{ variableHighlightOverlays.map(overlay => (
|
{ variableHighlightOverlays.map(overlay => (
|
||||||
<div
|
<div
|
||||||
@@ -3653,16 +3653,16 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
</div>
|
</div>
|
||||||
)) }
|
)) }
|
||||||
</div> }
|
</div> }
|
||||||
<NitroCardView className="min-w-[520px] max-w-[520px]" theme="primary-slim" uniqueKey="wired-creator-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
<NitroCardView className="min-w-[520px] max-w-[520px]" theme="primary-slim" uniqueKey="wired-creator-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
|
||||||
<NitroCardHeaderView headerText="Wired Creator Tools (:wired)" onCloseClick={ () => setIsVisible(false) } />
|
<NitroCardHeaderView headerText="Wired Creator Tools (:wired)" onCloseClick={ () => setIsVisible(false) } />
|
||||||
<NitroCardTabsView justifyContent="start">
|
<NitroCardTabsView justifyContent="start">
|
||||||
{ TABS.map(tab => (
|
{ TABS.map(tab => (
|
||||||
<NitroCardTabsItemView key={ tab.key } isActive={ (activeTab === tab.key) } onClick={ () => setActiveTab(tab.key) }>
|
<NitroCardTabsItemView key={ tab.key } isActive={ (activeTab === tab.key) } onClick={ () => setActiveTab(tab.key) }>
|
||||||
<Text>{ tab.label }</Text>
|
<Text>{ tab.label }</Text>
|
||||||
</NitroCardTabsItemView>
|
</NitroCardTabsItemView>
|
||||||
)) }
|
)) }
|
||||||
</NitroCardTabsView>
|
</NitroCardTabsView>
|
||||||
<NitroCardContentView className="text-black bg-[#e9e6d9]" gap={ 3 }>
|
<NitroCardContentView className="text-black bg-[#e9e6d9]" gap={ 3 }>
|
||||||
{ (activeTab === 'monitor') &&
|
{ (activeTab === 'monitor') &&
|
||||||
<div className="p-3 flex flex-col gap-3 relative">
|
<div className="p-3 flex flex-col gap-3 relative">
|
||||||
<div className="grid grid-cols-[190px_1fr] gap-3">
|
<div className="grid grid-cols-[190px_1fr] gap-3">
|
||||||
@@ -3682,7 +3682,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
<img alt="Monitor preview" className="max-w-full max-h-[180px] object-contain" src={ wiredMonitorImage } />
|
<img alt="Monitor preview" className="max-w-full max-h-[180px] object-contain" src={ wiredMonitorImage } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded border border-[#b9b3a5] p-2 flex flex-col gap-2">
|
<div className="bg-white rounded border border-[#b9b3a5] p-2 flex flex-col gap-2">
|
||||||
<Text bold>Logs:</Text>
|
<Text bold>Logs:</Text>
|
||||||
<div className="max-h-[180px] overflow-y-auto border border-[#d1ccbf] rounded">
|
<div className="max-h-[180px] overflow-y-auto border border-[#d1ccbf] rounded">
|
||||||
<table className="w-full text-[12px]">
|
<table className="w-full text-[12px]">
|
||||||
@@ -3925,7 +3925,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
onChange={ event => setEditingValue(event.target.value) }
|
onChange={ event => setEditingValue(event.target.value) }
|
||||||
onKeyDownCapture={ onVariableInputKeyDown } /> }
|
onKeyDownCapture={ onVariableInputKeyDown } /> }
|
||||||
{ (editingVariable !== variable.key) && !variable.editable && <span className={ variable.valueClassName }>{ variable.value }</span> }
|
{ (editingVariable !== variable.key) && !variable.editable && <span className={ variable.valueClassName }>{ variable.value }</span> }
|
||||||
{ (editingVariable !== variable.key) && variable.editable &&
|
{ (editingVariable !== variable.key) && variable.editable &&
|
||||||
<button
|
<button
|
||||||
className={ `w-full cursor-pointer rounded px-1 text-right text-[#1b57b2] hover:underline ${ variable.valueClassName ?? '' }` }
|
className={ `w-full cursor-pointer rounded px-1 text-right text-[#1b57b2] hover:underline ${ variable.valueClassName ?? '' }` }
|
||||||
type="button"
|
type="button"
|
||||||
@@ -4105,9 +4105,9 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> }
|
</div> }
|
||||||
</NitroCardContentView>
|
</NitroCardContentView>
|
||||||
</NitroCardView>
|
</NitroCardView>
|
||||||
{ isMonitorHistoryOpen &&
|
{ isMonitorHistoryOpen &&
|
||||||
<NitroCardView className="min-w-[760px] max-w-[760px] max-h-[520px]" theme="primary-slim" uniqueKey="wired-monitor-history" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 560 } offsetTop={ 40 }>
|
<NitroCardView className="min-w-[760px] max-w-[760px] max-h-[520px]" theme="primary-slim" uniqueKey="wired-monitor-history" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 560 } offsetTop={ 40 }>
|
||||||
<NitroCardHeaderView headerText="Wired Monitor Logs" onCloseClick={ () => setIsMonitorHistoryOpen(false) } />
|
<NitroCardHeaderView headerText="Wired Monitor Logs" onCloseClick={ () => setIsMonitorHistoryOpen(false) } />
|
||||||
<NitroCardContentView className="text-black bg-[#f4efe3] p-3 flex flex-col gap-3" overflow="hidden">
|
<NitroCardContentView className="text-black bg-[#f4efe3] p-3 flex flex-col gap-3" overflow="hidden">
|
||||||
@@ -4173,7 +4173,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
</div>
|
</div>
|
||||||
</NitroCardContentView>
|
</NitroCardContentView>
|
||||||
</NitroCardView> }
|
</NitroCardView> }
|
||||||
{ isMonitorInfoOpen &&
|
{ isMonitorInfoOpen &&
|
||||||
<NitroCardView className="min-w-[560px] max-w-[560px] max-h-[520px]" theme="primary-slim" uniqueKey="wired-monitor-info" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 610 } offsetTop={ 80 }>
|
<NitroCardView className="min-w-[560px] max-w-[560px] max-h-[520px]" theme="primary-slim" uniqueKey="wired-monitor-info" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 610 } offsetTop={ 80 }>
|
||||||
<NitroCardHeaderView headerText="Wired Monitor Information" onCloseClick={ () => setIsMonitorInfoOpen(false) } />
|
<NitroCardHeaderView headerText="Wired Monitor Information" onCloseClick={ () => setIsMonitorInfoOpen(false) } />
|
||||||
<NitroCardContentView className="text-black bg-[#f4efe3] p-4 flex flex-col gap-4 overflow-y-auto">
|
<NitroCardContentView className="text-black bg-[#f4efe3] p-4 flex flex-col gap-4 overflow-y-auto">
|
||||||
@@ -4187,7 +4187,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
)) }
|
)) }
|
||||||
</NitroCardContentView>
|
</NitroCardContentView>
|
||||||
</NitroCardView> }
|
</NitroCardView> }
|
||||||
{ isVariableManageOpen && !!selectedVariableDefinition &&
|
{ isVariableManageOpen && !!selectedVariableDefinition &&
|
||||||
<NitroCardView className="min-w-[860px] max-w-[860px] max-h-[620px]" theme="primary-slim" uniqueKey="wired-variable-management" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 540 } offsetTop={ 60 }>
|
<NitroCardView className="min-w-[860px] max-w-[860px] max-h-[620px]" theme="primary-slim" uniqueKey="wired-variable-management" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 540 } offsetTop={ 60 }>
|
||||||
<NitroCardHeaderView headerText="Variable Management" onCloseClick={ () => setIsVariableManageOpen(false) } />
|
<NitroCardHeaderView headerText="Variable Management" onCloseClick={ () => setIsVariableManageOpen(false) } />
|
||||||
<NitroCardContentView className="text-black bg-[#f4efe3] p-3 flex flex-col gap-3" overflow="hidden">
|
<NitroCardContentView className="text-black bg-[#f4efe3] p-3 flex flex-col gap-3" overflow="hidden">
|
||||||
@@ -4322,10 +4322,10 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
»
|
»
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NitroCardContentView>
|
</NitroCardContentView>
|
||||||
</NitroCardView> }
|
</NitroCardView> }
|
||||||
{ !!selectedManagedVariableEntry && !!selectedVariableDefinition &&
|
{ !!selectedManagedVariableEntry && !!selectedVariableDefinition &&
|
||||||
<NitroCardView className="min-w-[430px] max-w-[430px] max-h-[620px]" theme="primary-slim" uniqueKey="wired-variable-management-entry" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 890 } offsetTop={ 110 }>
|
<NitroCardView className="min-w-[430px] max-w-[430px] max-h-[620px]" theme="primary-slim" uniqueKey="wired-variable-management-entry" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 890 } offsetTop={ 110 }>
|
||||||
<NitroCardHeaderView headerText={ managedHolderPanelTitle } onCloseClick={ () => setSelectedManagedVariableEntry(null) } />
|
<NitroCardHeaderView headerText={ managedHolderPanelTitle } onCloseClick={ () => setSelectedManagedVariableEntry(null) } />
|
||||||
<NitroCardContentView className="text-black bg-[#f4efe3] p-3 flex flex-col gap-3 relative" overflow="hidden">
|
<NitroCardContentView className="text-black bg-[#f4efe3] p-3 flex flex-col gap-3 relative" overflow="hidden">
|
||||||
@@ -4459,7 +4459,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
|
|||||||
</div>
|
</div>
|
||||||
</NitroCardContentView>
|
</NitroCardContentView>
|
||||||
</NitroCardView> }
|
</NitroCardView> }
|
||||||
{ !!selectedMonitorErrorInfo &&
|
{ !!selectedMonitorErrorInfo &&
|
||||||
<NitroCardView className="min-w-[470px] max-w-[470px] max-h-[500px]" theme="primary-slim" uniqueKey="wired-monitor-error-info" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 660 } offsetTop={ 120 }>
|
<NitroCardView className="min-w-[470px] max-w-[470px] max-h-[500px]" theme="primary-slim" uniqueKey="wired-monitor-error-info" windowPosition={ DraggableWindowPosition.TOP_LEFT } offsetLeft={ 660 } offsetTop={ 120 }>
|
||||||
<NitroCardHeaderView
|
<NitroCardHeaderView
|
||||||
headerText="Wired Error Information"
|
headerText="Wired Error Information"
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ const INTERNAL_TOKEN_PREFIX = 'internal:';
|
|||||||
const GROUP_TOKEN_PREFIX = 'group:';
|
const GROUP_TOKEN_PREFIX = 'group:';
|
||||||
|
|
||||||
const createInternalMeta = (key: string, canUseAsDestination = false, canUseAsReference = false): IInternalVariableMeta =>
|
const createInternalMeta = (key: string, canUseAsDestination = false, canUseAsReference = false): IInternalVariableMeta =>
|
||||||
({
|
({
|
||||||
key,
|
key,
|
||||||
canUseAsDestination,
|
canUseAsDestination,
|
||||||
canUseAsReference
|
canUseAsReference
|
||||||
});
|
});
|
||||||
|
|
||||||
export const normalizeInternalVariableKey = (key: string) =>
|
export const normalizeInternalVariableKey = (key: string) =>
|
||||||
{
|
{
|
||||||
@@ -216,30 +216,30 @@ const getRootKey = (key: string) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createInternalEntry = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, meta: IInternalVariableMeta): IWiredVariablePickerEntry =>
|
const createInternalEntry = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, meta: IInternalVariableMeta): IWiredVariablePickerEntry =>
|
||||||
({
|
({
|
||||||
id: `${ INTERNAL_TOKEN_PREFIX }${ meta.key }`,
|
id: `${ INTERNAL_TOKEN_PREFIX }${ meta.key }`,
|
||||||
token: `${ INTERNAL_TOKEN_PREFIX }${ meta.key }`,
|
token: `${ INTERNAL_TOKEN_PREFIX }${ meta.key }`,
|
||||||
label: meta.key,
|
label: meta.key,
|
||||||
displayLabel: meta.key,
|
displayLabel: meta.key,
|
||||||
searchableText: meta.key,
|
searchableText: meta.key,
|
||||||
selectable: getInternalSelectable(usage, meta),
|
selectable: getInternalSelectable(usage, meta),
|
||||||
hasValue: meta.canUseAsReference,
|
hasValue: meta.canUseAsReference,
|
||||||
kind: 'internal',
|
kind: 'internal',
|
||||||
target
|
target
|
||||||
});
|
});
|
||||||
|
|
||||||
const createCustomEntry = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, definition: IWiredVariableDefinitionLike): IWiredVariablePickerEntry =>
|
const createCustomEntry = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, definition: IWiredVariableDefinitionLike): IWiredVariablePickerEntry =>
|
||||||
({
|
({
|
||||||
id: `${ CUSTOM_TOKEN_PREFIX }${ definition.itemId }`,
|
id: `${ CUSTOM_TOKEN_PREFIX }${ definition.itemId }`,
|
||||||
token: `${ CUSTOM_TOKEN_PREFIX }${ definition.itemId }`,
|
token: `${ CUSTOM_TOKEN_PREFIX }${ definition.itemId }`,
|
||||||
label: definition.name,
|
label: definition.name,
|
||||||
displayLabel: definition.name,
|
displayLabel: definition.name,
|
||||||
searchableText: definition.name,
|
searchableText: definition.name,
|
||||||
selectable: getCustomSelectable(usage, definition),
|
selectable: getCustomSelectable(usage, definition),
|
||||||
hasValue: !!definition.hasValue,
|
hasValue: !!definition.hasValue,
|
||||||
kind: 'custom',
|
kind: 'custom',
|
||||||
target
|
target
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupEntries = (entries: IWiredVariablePickerEntry[]) =>
|
const groupEntries = (entries: IWiredVariablePickerEntry[]) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -211,6 +211,6 @@ export const WiredActionFurniToFurniView: FC<{}> = () =>
|
|||||||
onSelectionActivate={ () => switchSelection('target') } />
|
onSelectionActivate={ () => switchSelection('target') } />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ const REWARD_TYPES: { value: RewardType, label: string }[] = [
|
|||||||
const SELECTABLE_REWARD_TYPES = REWARD_TYPES.filter(entry => (entry.value !== 'respect'));
|
const SELECTABLE_REWARD_TYPES = REWARD_TYPES.filter(entry => (entry.value !== 'respect'));
|
||||||
|
|
||||||
const createReward = (): RewardEntry =>
|
const createReward = (): RewardEntry =>
|
||||||
({
|
({
|
||||||
rewardType: 'furni',
|
rewardType: 'furni',
|
||||||
rewardValue: '',
|
rewardValue: '',
|
||||||
probability: DEFAULT_PROBABILITY,
|
probability: DEFAULT_PROBABILITY,
|
||||||
pointsType: DEFAULT_POINTS_TYPE
|
pointsType: DEFAULT_POINTS_TYPE
|
||||||
});
|
});
|
||||||
|
|
||||||
const getRewardValuePlaceholder = (rewardType: RewardType) =>
|
const getRewardValuePlaceholder = (rewardType: RewardType) =>
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user