mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
Merge remote-tracking branch 'duckie/main' into merge-duckie-main-2026-05-06
# Conflicts: # index.html # public/UITexts.example # public/renderer-config.example # src/App.tsx # src/components/login/LoginView.tsx # src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx # src/components/toolbar/ToolbarView.tsx # src/components/user-profile/UserContainerView.tsx
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
const STORAGE_KEY = 'nitro.access.token';
|
||||
const EXPIRES_KEY = 'nitro.access.token.exp';
|
||||
|
||||
export const setAccessToken = (token: string | null | undefined, expiresAt?: number | null): void =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if(token && typeof token === 'string')
|
||||
{
|
||||
window.localStorage.setItem(STORAGE_KEY, token);
|
||||
if(typeof expiresAt === 'number' && expiresAt > 0) window.localStorage.setItem(EXPIRES_KEY, String(expiresAt));
|
||||
else window.localStorage.removeItem(EXPIRES_KEY);
|
||||
}
|
||||
else
|
||||
{
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
window.localStorage.removeItem(EXPIRES_KEY);
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
};
|
||||
|
||||
export const getAccessToken = (): string =>
|
||||
{
|
||||
try { return window.localStorage.getItem(STORAGE_KEY) ?? ''; }
|
||||
catch { return ''; }
|
||||
};
|
||||
|
||||
export const getAccessTokenExpiresAt = (): number =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const raw = window.localStorage.getItem(EXPIRES_KEY);
|
||||
if(!raw) return 0;
|
||||
const value = parseInt(raw, 10);
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
catch { return 0; }
|
||||
};
|
||||
|
||||
export const clearAccessToken = (): void =>
|
||||
{
|
||||
setAccessToken(null);
|
||||
};
|
||||
|
||||
export const persistAccessTokenFromPayload = (payload: Record<string, unknown> | null | undefined): void =>
|
||||
{
|
||||
if(!payload) return;
|
||||
const token = typeof payload.accessToken === 'string' ? payload.accessToken : '';
|
||||
const expiresAt = typeof payload.accessTokenExpiresAt === 'number' ? payload.accessTokenExpiresAt : null;
|
||||
if(token) setAccessToken(token, expiresAt);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './accessToken';
|
||||
@@ -224,12 +224,11 @@ export class AvatarEditorThumbnailsHelper
|
||||
|
||||
const texture = avatarImage.processAsTexture(AvatarSetType.HEAD, false);
|
||||
const sprite = new NitroSprite(texture);
|
||||
|
||||
if(isDisabled) sprite.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ];
|
||||
|
||||
const frame = AvatarEditorThumbnailsHelper.findOpaqueBoundsFrame(sprite, texture.width, texture.height);
|
||||
const imageUrl = await TextureUtils.generateImageUrl({
|
||||
target: sprite,
|
||||
frame: new NitroRectangle(0, 0, texture.width, texture.height)
|
||||
frame
|
||||
});
|
||||
|
||||
sprite.destroy();
|
||||
@@ -244,6 +243,49 @@ export class AvatarEditorThumbnailsHelper
|
||||
});
|
||||
}
|
||||
|
||||
private static findOpaqueBoundsFrame(sprite: NitroSprite, fallbackWidth: number, fallbackHeight: number): NitroRectangle
|
||||
{
|
||||
try
|
||||
{
|
||||
const data = TextureUtils.getPixels(sprite);
|
||||
if(!data) return new NitroRectangle(0, 0, fallbackWidth, fallbackHeight);
|
||||
|
||||
const pixels = data.pixels as Uint8ClampedArray | Uint8Array;
|
||||
const width = data.width;
|
||||
const height = data.height;
|
||||
if(!pixels || width <= 0 || height <= 0) return new NitroRectangle(0, 0, fallbackWidth, fallbackHeight);
|
||||
const ALPHA_THRESHOLD = 8;
|
||||
|
||||
let minX = width;
|
||||
let minY = height;
|
||||
let maxX = -1;
|
||||
let maxY = -1;
|
||||
|
||||
for(let y = 0; y < height; y++)
|
||||
{
|
||||
const rowStart = y * width * 4;
|
||||
for(let x = 0; x < width; x++)
|
||||
{
|
||||
if(pixels[rowStart + (x * 4) + 3] > ALPHA_THRESHOLD)
|
||||
{
|
||||
if(x < minX) minX = x;
|
||||
if(x > maxX) maxX = x;
|
||||
if(y < minY) minY = y;
|
||||
if(y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(maxX < minX || maxY < minY) return new NitroRectangle(0, 0, fallbackWidth, fallbackHeight);
|
||||
|
||||
return new NitroRectangle(minX, minY, (maxX - minX) + 1, (maxY - minY) + 1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new NitroRectangle(0, 0, fallbackWidth, fallbackHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private static sortByDrawOrder(a: IFigurePart, b: IFigurePart): number
|
||||
{
|
||||
const indexA = AvatarEditorThumbnailsHelper.DRAW_ORDER.indexOf(a.type);
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { GetConfiguration, GetLocalizationManager } from '@nitrots/nitro-renderer';
|
||||
import { getAccessToken } from '../auth';
|
||||
|
||||
export interface CustomBadgeRecord
|
||||
{
|
||||
badgeId: string;
|
||||
badgeCode: string;
|
||||
name: string;
|
||||
description: string;
|
||||
dateCreated: number;
|
||||
dateEdit: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface CustomBadgeListResponse
|
||||
{
|
||||
badges: CustomBadgeRecord[];
|
||||
max: number;
|
||||
badgeWidth: number;
|
||||
badgeHeight: number;
|
||||
maxBadgeSizeBytes: number;
|
||||
priceBadge?: number;
|
||||
currencyType?: number;
|
||||
}
|
||||
|
||||
export interface CustomBadgeError
|
||||
{
|
||||
error: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
const interpolate = (value: string): string =>
|
||||
{
|
||||
try { return GetConfiguration().interpolate(value); }
|
||||
catch { return value; }
|
||||
};
|
||||
|
||||
const getConfigUrl = (key: string, fallback: string): string =>
|
||||
interpolate(GetConfiguration().getValue<string>(key, fallback));
|
||||
|
||||
const buildUrl = (key: string, fallback: string, badgeId?: string): string =>
|
||||
{
|
||||
const template = getConfigUrl(key, fallback);
|
||||
if(!badgeId) return template;
|
||||
if(template.includes('%badgeId%')) return template.replace(/%badgeId%/g, encodeURIComponent(badgeId));
|
||||
return template + (template.endsWith('/') ? '' : '/') + encodeURIComponent(badgeId);
|
||||
};
|
||||
|
||||
const authHeaders = (): Record<string, string> =>
|
||||
{
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'NitroCustomBadges'
|
||||
};
|
||||
const token = getAccessToken();
|
||||
if(token) headers['Authorization'] = `Bearer ${ token }`;
|
||||
return headers;
|
||||
};
|
||||
|
||||
const parseJson = async <T>(response: Response): Promise<T> =>
|
||||
{
|
||||
const text = await response.text();
|
||||
if(!text) return {} as T;
|
||||
try { return JSON.parse(text) as T; }
|
||||
catch { throw new Error('Invalid response from server.'); }
|
||||
};
|
||||
|
||||
const throwOnError = async (response: Response): Promise<void> =>
|
||||
{
|
||||
if(response.ok) return;
|
||||
const payload = await parseJson<CustomBadgeError>(response);
|
||||
const message = payload?.error || `Request failed (${ response.status }).`;
|
||||
const err = new Error(message) as Error & { status: number; code?: string };
|
||||
err.status = response.status;
|
||||
if(payload?.code) err.code = payload.code;
|
||||
throw err;
|
||||
};
|
||||
|
||||
export const fetchCustomBadges = async (): Promise<CustomBadgeListResponse> =>
|
||||
{
|
||||
const url = buildUrl('badges.custom.list.endpoint', '/api/badges/custom');
|
||||
const response = await fetch(url, { method: 'GET', credentials: 'include', headers: authHeaders() });
|
||||
await throwOnError(response);
|
||||
return parseJson<CustomBadgeListResponse>(response);
|
||||
};
|
||||
|
||||
export const createCustomBadge = async (body: { name: string; description: string; image: string }): Promise<CustomBadgeRecord> =>
|
||||
{
|
||||
const url = buildUrl('badges.custom.create.endpoint', '/api/badges/custom');
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
await throwOnError(response);
|
||||
return parseJson<CustomBadgeRecord>(response);
|
||||
};
|
||||
|
||||
export const updateCustomBadge = async (badgeId: string, body: { name: string; description: string; image: string }): Promise<CustomBadgeRecord> =>
|
||||
{
|
||||
const url = buildUrl('badges.custom.update.endpoint', '/api/badges/custom/%badgeId%', badgeId);
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
await throwOnError(response);
|
||||
return parseJson<CustomBadgeRecord>(response);
|
||||
};
|
||||
|
||||
export const deleteCustomBadge = async (badgeId: string): Promise<void> =>
|
||||
{
|
||||
const url = buildUrl('badges.custom.delete.endpoint', '/api/badges/custom/%badgeId%', badgeId);
|
||||
const response = await fetch(url, { method: 'DELETE', credentials: 'include', headers: authHeaders() });
|
||||
await throwOnError(response);
|
||||
};
|
||||
|
||||
export const isCustomBadgeCode = (code: string | null | undefined): boolean =>
|
||||
{
|
||||
if(!code) return false;
|
||||
return /^CUST[A-Z0-9]{5}-\d+$/.test(code);
|
||||
};
|
||||
|
||||
let customBadgeTextsLoadPromise: Promise<void> | null = null;
|
||||
|
||||
const injectTextsIntoLocalization = (texts: Record<string, string> | null | undefined): void =>
|
||||
{
|
||||
if(!texts) return;
|
||||
let manager: ReturnType<typeof GetLocalizationManager> | null = null;
|
||||
try { manager = GetLocalizationManager(); }
|
||||
catch { return; }
|
||||
if(!manager || typeof manager.setValue !== 'function') return;
|
||||
for(const key of Object.keys(texts))
|
||||
{
|
||||
const value = texts[key];
|
||||
if(typeof value === 'string') manager.setValue(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
export const ensureCustomBadgeTexts = (): Promise<void> =>
|
||||
{
|
||||
if(customBadgeTextsLoadPromise) return customBadgeTextsLoadPromise;
|
||||
customBadgeTextsLoadPromise = (async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const url = buildUrl('badges.custom.texts.endpoint', '/api/badges/custom/texts');
|
||||
const response = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Accept': 'application/json' } });
|
||||
if(!response.ok) return;
|
||||
const payload = await parseJson<{ texts: Record<string, string> }>(response);
|
||||
injectTextsIntoLocalization(payload.texts);
|
||||
}
|
||||
catch {}
|
||||
})();
|
||||
return customBadgeTextsLoadPromise;
|
||||
};
|
||||
|
||||
export const refreshCustomBadgeTexts = (): Promise<void> =>
|
||||
{
|
||||
customBadgeTextsLoadPromise = null;
|
||||
return ensureCustomBadgeTexts();
|
||||
};
|
||||
|
||||
export const setCustomBadgeText = (badgeId: string, name: string, description: string): void =>
|
||||
{
|
||||
injectTextsIntoLocalization({
|
||||
[`badge_name_${ badgeId }`]: name || badgeId,
|
||||
[`badge_desc_${ badgeId }`]: description || ''
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './CustomBadgeApi';
|
||||
@@ -1,7 +1,9 @@
|
||||
export * from './GetRendererVersion';
|
||||
export * from './GetUIVersion';
|
||||
export * from './achievements';
|
||||
export * from './auth';
|
||||
export * from './avatar';
|
||||
export * from './badges';
|
||||
export * from './camera';
|
||||
export * from './campaign';
|
||||
export * from './catalog';
|
||||
|
||||
@@ -23,6 +23,7 @@ export class AvatarInfoUser implements IAvatarInfo
|
||||
public backgroundId: number = 0;
|
||||
public standId: number = 0;
|
||||
public overlayId: number = 0;
|
||||
public cardBackgroundId: number = 0;
|
||||
public webID: number = 0;
|
||||
public xp: number = 0;
|
||||
public userType: number = -1;
|
||||
|
||||
@@ -191,6 +191,7 @@ export class AvatarInfoUtilities
|
||||
userInfo.backgroundId = userData.background;
|
||||
userInfo.standId = userData.stand;
|
||||
userInfo.overlayId = userData.overlay;
|
||||
userInfo.cardBackgroundId = userData.cardBackground ?? 0;
|
||||
userInfo.achievementScore = userData.activityPoints;
|
||||
userInfo.webID = userData.webID;
|
||||
userInfo.roomIndex = userData.roomIndex;
|
||||
|
||||
Reference in New Issue
Block a user