diff --git a/package.json b/package.json index f43f8b0..6619dea 100644 --- a/package.json +++ b/package.json @@ -13,19 +13,17 @@ "@babel/runtime": "^7.29.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-slider": "^1.2.4", "@tanstack/react-virtual": "3.13.24", - "@types/react-transition-group": "^4.4.12", "dompurify": "^3.4.1", "emoji-mart": "^5.6.0", "emoji-toolkit": "10.0.0", "framer-motion": "^12.38.0", "react": "^19.2.5", - "react-bootstrap": "^2.10.10", "react-dom": "^19.2.5", "react-icons": "^5.5.0", - "react-slider": "^2.0.6", - "react-tiny-popover": "^8.1.6", - "react-youtube": "^10.1.0", + "react-player": "^2.16.0", "use-between": "^1.4.0" }, "devDependencies": { @@ -34,7 +32,6 @@ "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@types/react-slider": "^1.3.6", "@typescript-eslint/eslint-plugin": "^8.59.1", "@typescript-eslint/parser": "^8.59.1", "@vitejs/plugin-react": "^6.0.1", diff --git a/public/renderer-config.example b/public/renderer-config.example index b45973e..eccba80 100644 --- a/public/renderer-config.example +++ b/public/renderer-config.example @@ -55,6 +55,13 @@ "login.remember.endpoint": "${api.url}/api/auth/remember", "login.server_key.endpoint": "${api.url}/api/auth/server-key", "login.news.endpoint": "${api.url}/api/auth/news", + "login.sso-token.endpoint": "${api.url}/api/auth/sso-token", + "login.refresh.endpoint": "${api.url}/api/auth/refresh", + "badges.custom.list.endpoint": "${api.url}/api/badges/custom", + "badges.custom.create.endpoint": "${api.url}/api/badges/custom", + "badges.custom.update.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.delete.endpoint": "${api.url}/api/badges/custom/%badgeId%", + "badges.custom.texts.endpoint": "${api.url}/api/badges/custom/texts", "login.turnstile.enabled": true, "login.turnstile.sitekey": "", "avatar.mandatory.libraries": [ diff --git a/public/ui-config.example b/public/ui-config.example index 946e5e0..996976c 100644 --- a/public/ui-config.example +++ b/public/ui-config.example @@ -1346,6 +1346,23 @@ "isAmbassadorOnly": false } ], + "cards.data": [ + { "backgroundId": 1, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 2, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 3, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 4, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 5, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 6, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 7, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 8, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 9, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 10, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 11, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 12, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 13, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 14, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false }, + { "backgroundId": 15, "minRank": 0, "isHcOnly": false, "isAmbassadorOnly": false } + ], "stands.data": [ { "standId": 0, diff --git a/src/App.tsx b/src/App.tsx index f08efdc..b59c167 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroEventType, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useState } from 'react'; -import { GetUIVersion } from './api'; +import { clearAccessToken, getAccessToken, getAccessTokenExpiresAt, GetUIVersion, persistAccessTokenFromPayload } from './api'; import { Base } from './common'; import { LoadingView } from './components/loading/LoadingView'; import { LoginView } from './components/login/LoginView'; @@ -106,11 +106,13 @@ export const App: FC<{}> = props => window.localStorage.setItem('nitro.remember.token', payload.rememberToken); } catch {} + persistAccessTokenFromPayload(payload); } } else if(response.status === 401) { try { window.localStorage.removeItem('nitro.remember.token'); } catch {} + clearAccessToken(); } } catch {} @@ -118,6 +120,38 @@ export const App: FC<{}> = props => } } + if(ssoTicket) + { + const expiresAt = getAccessTokenExpiresAt(); + const nowSec = Math.floor(Date.now() / 1000); + const accessNeedsRefresh = !getAccessToken() || (expiresAt > 0 && expiresAt - nowSec < 60); + + if(accessNeedsRefresh) + { + const ssoTokenUrlTemplate = GetConfiguration().getValue('login.sso-token.endpoint', '/api/auth/sso-token'); + const ssoTokenUrl = GetConfiguration().interpolate(ssoTokenUrlTemplate); + try + { + const response = await fetch(ssoTokenUrl, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Requested-With': 'NitroSsoExchange' + }, + body: JSON.stringify({ ssoTicket }) + }); + if(response.ok) + { + const payload = await response.json(); + persistAccessTokenFromPayload(payload); + } + } + catch {} + } + } + if(!ssoTicket || ssoTicket === '') { const rawLoginEnabled = GetConfiguration().getValue('login.screen.enabled', false); @@ -219,10 +253,12 @@ export const App: FC<{}> = props => { try { window.localStorage.setItem('nitro.remember.token', payload.rememberToken); } catch {} } + persistAccessTokenFromPayload(payload); } else if(resp.status === 401) { try { window.localStorage.removeItem('nitro.remember.token'); } catch {} + clearAccessToken(); } } catch {} diff --git a/src/api/auth/accessToken.ts b/src/api/auth/accessToken.ts new file mode 100644 index 0000000..1d53575 --- /dev/null +++ b/src/api/auth/accessToken.ts @@ -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 | 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); +}; diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts new file mode 100644 index 0000000..865e7c0 --- /dev/null +++ b/src/api/auth/index.ts @@ -0,0 +1 @@ +export * from './accessToken'; diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index 9cb58b6..5176436 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -224,11 +224,8 @@ 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 @@ -257,7 +254,6 @@ export class AvatarEditorThumbnailsHelper 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; diff --git a/src/api/badges/CustomBadgeApi.ts b/src/api/badges/CustomBadgeApi.ts new file mode 100644 index 0000000..9e6eeca --- /dev/null +++ b/src/api/badges/CustomBadgeApi.ts @@ -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(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 => +{ + const headers: Record = { + 'Accept': 'application/json', + 'X-Requested-With': 'NitroCustomBadges' + }; + const token = getAccessToken(); + if(token) headers['Authorization'] = `Bearer ${ token }`; + return headers; +}; + +const parseJson = async (response: Response): Promise => +{ + 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 => +{ + if(response.ok) return; + const payload = await parseJson(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 => +{ + 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(response); +}; + +export const createCustomBadge = async (body: { name: string; description: string; image: string }): Promise => +{ + 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(response); +}; + +export const updateCustomBadge = async (badgeId: string, body: { name: string; description: string; image: string }): Promise => +{ + 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(response); +}; + +export const deleteCustomBadge = async (badgeId: string): Promise => +{ + 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 | null = null; + +const injectTextsIntoLocalization = (texts: Record | null | undefined): void => +{ + if(!texts) return; + let manager: ReturnType | 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 => +{ + 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 }>(response); + injectTextsIntoLocalization(payload.texts); + } + catch {} + })(); + return customBadgeTextsLoadPromise; +}; + +export const refreshCustomBadgeTexts = (): Promise => +{ + customBadgeTextsLoadPromise = null; + return ensureCustomBadgeTexts(); +}; + +export const setCustomBadgeText = (badgeId: string, name: string, description: string): void => +{ + injectTextsIntoLocalization({ + [`badge_name_${ badgeId }`]: name || badgeId, + [`badge_desc_${ badgeId }`]: description || '' + }); +}; diff --git a/src/api/badges/index.ts b/src/api/badges/index.ts new file mode 100644 index 0000000..75e2cd1 --- /dev/null +++ b/src/api/badges/index.ts @@ -0,0 +1 @@ +export * from './CustomBadgeApi'; diff --git a/src/api/index.ts b/src/api/index.ts index 0f11ac4..6bb1536 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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'; diff --git a/src/api/room/widgets/AvatarInfoUser.ts b/src/api/room/widgets/AvatarInfoUser.ts index fa3fc1a..ad728f3 100644 --- a/src/api/room/widgets/AvatarInfoUser.ts +++ b/src/api/room/widgets/AvatarInfoUser.ts @@ -16,6 +16,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; diff --git a/src/api/room/widgets/AvatarInfoUtilities.ts b/src/api/room/widgets/AvatarInfoUtilities.ts index 0def77a..f9b29ac 100644 --- a/src/api/room/widgets/AvatarInfoUtilities.ts +++ b/src/api/room/widgets/AvatarInfoUtilities.ts @@ -186,6 +186,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; diff --git a/src/common/Slider.tsx b/src/common/Slider.tsx index 72fb74c..8cd7490 100644 --- a/src/common/Slider.tsx +++ b/src/common/Slider.tsx @@ -1,35 +1,148 @@ -import { FC } from 'react'; -import ReactSlider, { ReactSliderProps } from 'react-slider'; +import * as RadixSlider from '@radix-ui/react-slider'; +import { CSSProperties, FC, HTMLProps, ReactElement } from 'react'; +import { FaAngleLeft, FaAngleRight } from 'react-icons/fa'; import { Button } from './Button'; import { Flex } from './Flex'; -import { FaAngleLeft, FaAngleRight } from 'react-icons/fa'; -export interface SliderProps extends ReactSliderProps +export interface SliderThumbState { - disabledButton?: boolean; + index: number; + value: number | number[]; + valueNow: number; } +export interface SliderProps +{ + min?: number; + max?: number; + step?: number; + value?: number | number[]; + defaultValue?: number | number[]; + onChange?: (value: any, thumbIndex: number) => void; + disabled?: boolean; + disabledButton?: boolean; + invert?: boolean; + className?: string; + style?: CSSProperties; + trackClassName?: string; + thumbClassName?: string; + renderThumb?: (props: HTMLProps, state: SliderThumbState) => ReactElement; +} + +const toArray = (value: number | number[] | undefined): number[] => +{ + if(Array.isArray(value)) return value; + if(typeof value === 'number') return [ value ]; + + return [ 0 ]; +}; + +const cn = (...parts: (string | undefined | false)[]) => parts.filter(Boolean).join(' '); + export const Slider: FC = props => { - const { disabledButton, max, min, step, value, onChange, ...rest } = props; - const currentValue = Array.isArray(value) ? value[0] : ((typeof value === 'number') ? value : 0); + const { + disabledButton, + disabled, + max = 100, + min = 0, + step = 1, + value, + defaultValue, + onChange, + invert, + className, + style, + trackClassName, + thumbClassName, + renderThumb + } = props; + + const valueArr = toArray(value); + const currentValue = valueArr[0] ?? 0; const minimum = (typeof min === 'number') ? min : 0; const maximum = (typeof max === 'number') ? max : 0; const buttonStep = ((typeof step === 'number') && (step > 0)) ? step : 1; + const isRange = valueArr.length > 1; const roundToStep = (nextValue: number) => { - if(typeof buttonStep !== 'number') return nextValue; - const decimalStep = buttonStep.toString(); const precision = decimalStep.includes('.') ? (decimalStep.length - decimalStep.indexOf('.') - 1) : 0; return parseFloat(nextValue.toFixed(precision)); }; - return - { !disabledButton && } - - { !disabledButton && } - ; + const emit = (next: number[]) => + { + if(!onChange) return; + + if(isRange) onChange(next, 0); + else onChange(next[0], 0); + }; + + const stepDown = () => + { + const next = roundToStep(minimum < currentValue ? currentValue - buttonStep : minimum); + + emit([ next, ...valueArr.slice(1) ]); + }; + + const stepUp = () => + { + const next = roundToStep(maximum > currentValue ? currentValue + buttonStep : maximum); + + emit([ next, ...valueArr.slice(1) ]); + }; + + const renderThumbElement = (i: number) => + { + const baseProps: HTMLProps = { + key: i, + className: cn('thumb', `thumb-${ i }`, thumbClassName) + }; + + const state: SliderThumbState = { + index: i, + value: isRange ? valueArr : currentValue, + valueNow: valueArr[i] ?? 0 + }; + + return ( + + { renderThumb ? renderThumb(baseProps, state) :
} + + ); + }; + + return ( + + { !disabledButton && ( + + ) } + + + + + { valueArr.map((_, i) => renderThumbElement(i)) } + + { !disabledButton && ( + + ) } + + ); } diff --git a/src/common/layout/LayoutBadgeImageView.tsx b/src/common/layout/LayoutBadgeImageView.tsx index 75a6533..7162627 100644 --- a/src/common/layout/LayoutBadgeImageView.tsx +++ b/src/common/layout/LayoutBadgeImageView.tsx @@ -1,5 +1,6 @@ import { BadgeImageReadyEvent, GetEventDispatcher, GetSessionDataManager, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer'; -import { CSSProperties, FC, useEffect, useMemo, useState } from 'react'; +import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { GetConfigurationValue, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../api'; import { Base, BaseProps } from '../Base'; @@ -17,6 +18,26 @@ export const LayoutBadgeImageView: FC = props => { const { badgeCode = null, isGroup = false, showInfo = false, customTitle = null, isGrayscale = false, scale = 1, classNames = [], style = {}, children = null, ...rest } = props; const [ imageElement, setImageElement ] = useState(null); + const [ tooltipPosition, setTooltipPosition ] = useState<{ top: number; left: number } | null>(null); + const badgeRef = useRef(null); + + const tooltipsEnabled = showInfo && GetConfigurationValue('badge.descriptions.enabled', true); + + const showTooltip = () => + { + if(!tooltipsEnabled || !badgeRef.current) return; + + const rect = badgeRef.current.getBoundingClientRect(); + const tooltipWidth = 210; + const gap = 10; + let left = rect.left - tooltipWidth - gap; + + if(left < gap) left = rect.right + gap; + + setTooltipPosition({ top: rect.top, left }); + }; + + const hideTooltip = () => setTooltipPosition(null); const getClassNames = useMemo(() => { @@ -116,12 +137,22 @@ export const LayoutBadgeImageView: FC = props => }, [ badgeCode, isGroup ]); return ( - - { (showInfo && GetConfigurationValue('badge.descriptions.enabled', true)) && - + + { tooltipsEnabled && tooltipPosition && createPortal( +
{ isGroup ? customTitle : LocalizeBadgeName(badgeCode) }
{ isGroup ? LocalizeText('group.badgepopup.body') : LocalizeBadgeDescription(badgeCode) }
- } +
, + document.body + ) } { children } ); diff --git a/src/common/transitions/TransitionAnimation.tsx b/src/common/transitions/TransitionAnimation.tsx index 8e34849..db90938 100644 --- a/src/common/transitions/TransitionAnimation.tsx +++ b/src/common/transitions/TransitionAnimation.tsx @@ -1,6 +1,6 @@ -import { FC, ReactNode, useEffect, useState } from 'react'; -import { Transition } from 'react-transition-group'; -import { getTransitionAnimationStyle } from './TransitionAnimationStyles'; +import { AnimatePresence, motion, Variants } from 'framer-motion'; +import { FC, ReactNode } from 'react'; +import { getTransitionVariants } from './TransitionAnimationStyles'; interface TransitionAnimationProps { @@ -15,38 +15,22 @@ export const TransitionAnimation: FC = props => { const { type = null, inProp = false, timeout = 300, className = null, children = null } = props; - const [ isChildrenVisible, setChildrenVisible ] = useState(false); - - useEffect(() => - { - let timeoutData: ReturnType = null; - - if(inProp) - { - setChildrenVisible(true); - } - else - { - timeoutData = setTimeout(() => - { - setChildrenVisible(false); - clearTimeout(timeout); - }, timeout); - } - - return () => - { - if(timeoutData) clearTimeout(timeoutData); - }; - }, [ inProp, timeout ]); + const variants: Variants = getTransitionVariants(type); + const duration = timeout / 1000; return ( - - { state => ( -
- { isChildrenVisible && children } -
+ + { inProp && ( + + { children } + ) } -
+ ); }; diff --git a/src/common/transitions/TransitionAnimationStyles.ts b/src/common/transitions/TransitionAnimationStyles.ts index feebdcc..ab7315e 100644 --- a/src/common/transitions/TransitionAnimationStyles.ts +++ b/src/common/transitions/TransitionAnimationStyles.ts @@ -1,136 +1,66 @@ -import { CSSProperties } from 'react'; -import { TransitionStatus } from 'react-transition-group'; -import { ENTERING, EXITING } from 'react-transition-group/Transition'; +import { Variants } from 'framer-motion'; import { TransitionAnimationTypes } from './TransitionAnimationTypes'; -export function getTransitionAnimationStyle(type: string, transition: TransitionStatus, timeout: number = 300): Partial +export function getTransitionVariants(type: string): Variants { switch(type) { case TransitionAnimationTypes.BOUNCE: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'bounceIn', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'bounceOut', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, scale: 0.3 }, + visible: { opacity: 1, scale: 1, transition: { type: 'spring', stiffness: 260, damping: 12 } }, + exit: { opacity: 0, scale: 0.3 } + }; case TransitionAnimationTypes.SLIDE_LEFT: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'slideInLeft', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'slideOutLeft', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, x: '-100%' }, + visible: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: '-100%' } + }; case TransitionAnimationTypes.SLIDE_RIGHT: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'slideInRight', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'slideOutRight', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, x: '100%' }, + visible: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: '100%' } + }; case TransitionAnimationTypes.FLIP_X: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'flipInX', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'flipOutX', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, rotateX: 90 }, + visible: { opacity: 1, rotateX: 0 }, + exit: { opacity: 0, rotateX: 90 } + }; case TransitionAnimationTypes.FADE_UP: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'fadeInUp', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'fadeOutDown', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: 20 } + }; case TransitionAnimationTypes.FADE_IN: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'fadeIn', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'fadeOut', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + exit: { opacity: 0 } + }; case TransitionAnimationTypes.FADE_DOWN: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'fadeInDown', - animationDuration: `${ timeout }ms` - }; - case EXITING: - return { - animationName: 'fadeOutUp', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { opacity: 0, y: -20 }, + visible: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 } + }; case TransitionAnimationTypes.HEAD_SHAKE: - switch(transition) - { - default: - return {}; - case ENTERING: - return { - animationName: 'headShake', - animationDuration: `${ timeout }ms` - }; - } + return { + hidden: { x: 0 }, + visible: { + x: [ 0, -6, 5, -3, 2, 0 ], + transition: { duration: 0.5 } + }, + exit: { x: 0 } + }; } - return null; + return { + hidden: {}, + visible: {}, + exit: {} + }; } diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 70f2fd3..41a7322 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -4,6 +4,7 @@ import { FC, useEffect, useState } from 'react'; import { useNitroEvent } from '../hooks'; import { AchievementsView } from './achievements/AchievementsView'; import { AvatarEditorView } from './avatar-editor'; +import { BadgeCreatorView } from './badge-creator'; import { AvatarEffectsView } from './avatar-effects'; import { CameraWidgetView } from './camera/CameraWidgetView'; import { CampaignView } from './campaign/CampaignView'; @@ -106,6 +107,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/backgrounds/BackgroundsView.tsx b/src/components/backgrounds/BackgroundsView.tsx index 9782dd6..ecd08df 100644 --- a/src/components/backgrounds/BackgroundsView.tsx +++ b/src/components/backgrounds/BackgroundsView.tsx @@ -20,9 +20,11 @@ interface BackgroundsViewProps { setSelectedStand: Dispatch>; selectedOverlay: number; setSelectedOverlay: Dispatch>; + selectedCardBackground: number; + setSelectedCardBackground: Dispatch>; } -const TABS = ['backgrounds', 'stands', 'overlays'] as const; +const TABS = ['backgrounds', 'stands', 'overlays', 'cards'] as const; type TabType = typeof TABS[number]; export const BackgroundsView: FC = ({ @@ -32,7 +34,9 @@ export const BackgroundsView: FC = ({ selectedStand, setSelectedStand, selectedOverlay, - setSelectedOverlay + setSelectedOverlay, + selectedCardBackground, + setSelectedCardBackground }) => { const [activeTab, setActiveTab] = useState('backgrounds'); const { roomSession } = useRoom(); @@ -58,20 +62,21 @@ export const BackgroundsView: FC = ({ const allData = useMemo(() => ({ backgrounds: processData(GetConfigurationValue('backgrounds.data'), 'background'), stands: processData(GetConfigurationValue('stands.data'), 'stand'), - overlays: processData(GetConfigurationValue('overlays.data'), 'overlay') + overlays: processData(GetConfigurationValue('overlays.data'), 'overlay'), + cards: processData(GetConfigurationValue('cards.data') || GetConfigurationValue('backgrounds.data'), 'background') }), [processData]); const handleSelection = useCallback((id: number) => { if (!roomSession) return; - const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay }; - - const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay }; + const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay, cards: setSelectedCardBackground }; + + const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay, cards: selectedCardBackground }; setters[activeTab](id); const newValues = { ...currentValues, [activeTab]: id }; - roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays ); - }, [activeTab, roomSession, selectedBackground, selectedStand, selectedOverlay, setSelectedBackground, setSelectedStand, setSelectedOverlay]); + roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays, newValues.cards ); + }, [activeTab, roomSession, selectedBackground, selectedStand, selectedOverlay, selectedCardBackground, setSelectedBackground, setSelectedStand, setSelectedOverlay, setSelectedCardBackground]); const renderItem = useCallback((item: ItemData, type: string) => ( = ({ onClick={() => item.selectable && handleSelection(item.id)} className={item.selectable ? '' : 'non-selectable'} > - + {item.isHcOnly && } ), [handleSelection]); @@ -103,7 +111,7 @@ export const BackgroundsView: FC = ({ Select an Option - {allData[activeTab].map(item => renderItem(item, activeTab.slice(0, -1)))} + {allData[activeTab].map(item => renderItem(item, activeTab === 'cards' ? 'card-background' : activeTab.slice(0, -1)))} diff --git a/src/components/badge-creator/BadgeCreatorView.tsx b/src/components/badge-creator/BadgeCreatorView.tsx new file mode 100644 index 0000000..e8d9db1 --- /dev/null +++ b/src/components/badge-creator/BadgeCreatorView.tsx @@ -0,0 +1,629 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, MouseEvent as ReactMouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { LocalizeText } from '../../api'; +import { createCustomBadge, CustomBadgeRecord, deleteCustomBadge, ensureCustomBadgeTexts, fetchCustomBadges, refreshCustomBadgeTexts, setCustomBadgeText, updateCustomBadge } from '../../api/badges'; +import { Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { useNotification } from '../../hooks'; + +const t = (key: string, fallback: string, params?: string[], replacements?: string[]): string => +{ + try + { + const value = LocalizeText(key, params ?? null, replacements ?? null); + if(value && value !== key) return value; + } + catch {} + + if(!params || !replacements) return fallback; + let out = fallback; + for(let i = 0; i < params.length; i++) + { + if(replacements[i] !== undefined) out = out.replace('%' + params[i] + '%', replacements[i]); + } + return out; +}; + +const GRID_WIDTH = 40; +const GRID_HEIGHT = 40; +const PIXEL_DISPLAY_SIZE = 12; +const TRANSPARENT = 0; + +const PALETTE: number[] = [ + 0xFF000000, 0xFF4F4F4F, 0xFF808080, 0xFFB0B0B0, 0xFFD8D8D8, 0xFFFFFFFF, TRANSPARENT, 0xFF7B0000, + 0xFFBF0000, 0xFFFF0000, 0xFFFF7777, 0xFFFF7700, 0xFFFFAA00, 0xFFFFD700, 0xFFFFEB3B, 0xFF003E1F, + 0xFF006837, 0xFF00A653, 0xFF2BC93C, 0xFF00C8A0, 0xFF00BCFF, 0xFF2962FF, 0xFF1A237E, 0xFF4A0072, + 0xFF9C00B5, 0xFFE91E63, 0xFFFF80AB, 0xFF5D2E1A, 0xFF8B5A2B, 0xFFC28E5E, 0xFFF1D7B6, 0xFFE8C3A0 +]; + +const currencyName = (type: number): string => +{ + if(type === -1) return 'credits'; + if(type === 0) return 'duckets'; + if(type === 5) return 'diamonds'; + return `currency #${ type }`; +}; + +type Tool = 'paint' | 'erase' | 'picker' | 'fill'; + +const floodFill = (grid: Uint32Array, w: number, h: number, startX: number, startY: number, replacement: number): Uint32Array => +{ + if(startX < 0 || startY < 0 || startX >= w || startY >= h) return grid; + const startIdx = startY * w + startX; + const target = grid[startIdx]; + if(target === replacement) return grid; + + const next = new Uint32Array(grid.length); + next.set(grid); + + const stack: number[] = [ startIdx ]; + while(stack.length) + { + const idx = stack.pop() as number; + if(next[idx] !== target) continue; + next[idx] = replacement; + const x = idx % w; + const y = (idx - x) / w; + if(x > 0) stack.push(idx - 1); + if(x < w - 1) stack.push(idx + 1); + if(y > 0) stack.push(idx - w); + if(y < h - 1) stack.push(idx + w); + } + return next; +}; + +const argbToCss = (argb: number): string => +{ + if(argb === TRANSPARENT) return 'transparent'; + const a = ((argb >>> 24) & 0xff) / 255; + const r = (argb >>> 16) & 0xff; + const g = (argb >>> 8) & 0xff; + const b = argb & 0xff; + return `rgba(${ r }, ${ g }, ${ b }, ${ a })`; +}; + +const argbToHex = (argb: number): string => +{ + if(argb === TRANSPARENT) return '#000000'; + const r = (argb >>> 16) & 0xff; + const g = (argb >>> 8) & 0xff; + const b = argb & 0xff; + return '#' + [ r, g, b ].map(c => c.toString(16).padStart(2, '0')).join(''); +}; + +const hexToArgb = (hex: string): number => +{ + const match = /^#?([0-9a-f]{6})$/i.exec(hex || ''); + if(!match) return 0xFF000000; + return (0xFF000000 | parseInt(match[1], 16)) >>> 0; +}; + +const emptyGrid = (): Uint32Array => new Uint32Array(GRID_WIDTH * GRID_HEIGHT); + +const cloneGrid = (src: Uint32Array): Uint32Array => +{ + const copy = new Uint32Array(src.length); + copy.set(src); + return copy; +}; + +const gridToPngBase64 = async (grid: Uint32Array): Promise<{ b64: string; bytes: number }> => +{ + const canvas = document.createElement('canvas'); + canvas.width = GRID_WIDTH; + canvas.height = GRID_HEIGHT; + const ctx = canvas.getContext('2d'); + if(!ctx) throw new Error('Canvas not supported.'); + + const image = ctx.createImageData(GRID_WIDTH, GRID_HEIGHT); + for(let i = 0; i < grid.length; i++) + { + const argb = grid[i]; + const o = i * 4; + image.data[o] = (argb >>> 16) & 0xff; + image.data[o + 1] = (argb >>> 8) & 0xff; + image.data[o + 2] = argb & 0xff; + image.data[o + 3] = (argb >>> 24) & 0xff; + } + ctx.putImageData(image, 0, 0); + + const blob: Blob = await new Promise((resolve, reject) => canvas.toBlob(b => b ? resolve(b) : reject(new Error('PNG encode failed.')), 'image/png')); + const arrayBuffer = await blob.arrayBuffer(); + const bytes = arrayBuffer.byteLength; + let binary = ''; + const u8 = new Uint8Array(arrayBuffer); + for(let i = 0; i < u8.length; i++) binary += String.fromCharCode(u8[i]); + return { b64: window.btoa(binary), bytes }; +}; + +const loadGridFromUrl = (url: string): Promise => + new Promise((resolve, reject) => + { + const image = new Image(); + image.crossOrigin = 'anonymous'; + image.onload = () => + { + try + { + const canvas = document.createElement('canvas'); + canvas.width = GRID_WIDTH; + canvas.height = GRID_HEIGHT; + const ctx = canvas.getContext('2d'); + if(!ctx) return reject(new Error('Canvas not supported.')); + ctx.clearRect(0, 0, GRID_WIDTH, GRID_HEIGHT); + ctx.drawImage(image, 0, 0, GRID_WIDTH, GRID_HEIGHT); + const data = ctx.getImageData(0, 0, GRID_WIDTH, GRID_HEIGHT).data; + const grid = emptyGrid(); + for(let i = 0; i < grid.length; i++) + { + const o = i * 4; + const a = data[o + 3]; + 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); + } + resolve(grid); + } + catch(err) { reject(err); } + }; + image.onerror = () => reject(new Error('Could not load badge image (CORS?).')); + image.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now(); + }); + +export const BadgeCreatorView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ grid, setGrid ] = useState(() => emptyGrid()); + const [ selectedColor, setSelectedColor ] = useState(PALETTE[0]); + const [ tool, setTool ] = useState('paint'); + const [ showGrid, setShowGrid ] = useState(true); + const [ name, setName ] = useState(''); + const [ description, setDescription ] = useState(''); + const [ editingBadgeId, setEditingBadgeId ] = useState(null); + const [ badges, setBadges ] = useState(null); + const [ pendingEditBadgeId, setPendingEditBadgeId ] = useState(null); + const [ maxBadges, setMaxBadges ] = useState(5); + const [ maxBytes, setMaxBytes ] = useState(40960); + const [ priceBadge, setPriceBadge ] = useState(0); + const [ currencyType, setCurrencyType ] = useState(-1); + const [ submitting, setSubmitting ] = useState(false); + const [ error, setError ] = useState(null); + + const { showConfirm } = useNotification(); + + const refresh = useCallback(async () => + { + try + { + const data = await fetchCustomBadges(); + setBadges(data.badges ?? []); + if(typeof data.max === 'number') setMaxBadges(data.max); + if(typeof data.maxBadgeSizeBytes === 'number') setMaxBytes(data.maxBadgeSizeBytes); + if(typeof data.priceBadge === 'number') setPriceBadge(data.priceBadge); + if(typeof data.currencyType === 'number') setCurrencyType(data.currencyType); + } + catch(err) + { + setBadges([]); + setError((err as Error)?.message || 'Could not load badges.'); + } + }, []); + + useEffect(() => + { + const tracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + if(parts.length < 2) return; + switch(parts[1]) + { + case 'show': setIsVisible(true); return; + case 'hide': setIsVisible(false); return; + case 'toggle': setIsVisible(v => !v); return; + case 'edit': + if(!parts[2]) return; + setPendingEditBadgeId(parts[2]); + setIsVisible(true); + return; + } + }, + eventUrlPrefix: 'badge-creator/' + }; + AddLinkEventTracker(tracker); + return () => RemoveLinkEventTracker(tracker); + }, []); + + useEffect(() => { if(isVisible) { refresh(); ensureCustomBadgeTexts(); } }, [ isVisible, refresh ]); + + const resetEditor = useCallback(() => + { + setGrid(emptyGrid()); + setName(''); + setDescription(''); + setEditingBadgeId(null); + setError(null); + }, []); + + const startEdit = useCallback(async (badge: CustomBadgeRecord) => + { + setError(null); + setEditingBadgeId(badge.badgeId); + setName(badge.name || ''); + setDescription(badge.description || ''); + try + { + const loaded = await loadGridFromUrl(badge.url); + setGrid(loaded); + } + catch(err) + { + setError((err as Error)?.message || 'Could not load that badge.'); + setGrid(emptyGrid()); + } + }, []); + + useEffect(() => + { + if(!pendingEditBadgeId || !badges) return; + const target = badges.find(b => b.badgeId === pendingEditBadgeId); + if(!target) return; + setPendingEditBadgeId(null); + startEdit(target); + }, [ pendingEditBadgeId, badges, startEdit ]); + + const paintAt = useCallback((x: number, y: number, isClick: boolean) => + { + if(x < 0 || y < 0 || x >= GRID_WIDTH || y >= GRID_HEIGHT) return; + const idx = y * GRID_WIDTH + x; + + if(tool === 'picker') + { + const cell = grid[idx]; + if(cell !== TRANSPARENT) setSelectedColor(cell); + setTool('paint'); + return; + } + + if(tool === 'fill') + { + if(!isClick) return; + setGrid(floodFill(grid, GRID_WIDTH, GRID_HEIGHT, x, y, selectedColor)); + return; + } + + const value = (tool === 'erase') ? TRANSPARENT : selectedColor; + if(grid[idx] === value) return; + const next = cloneGrid(grid); + next[idx] = value; + setGrid(next); + }, [ grid, selectedColor, tool ]); + + const isDraggingRef = useRef(false); + const colorInputRef = useRef(null); + const mainCanvasRef = useRef(null); + const previewCanvasRef = useRef(null); + + useEffect(() => + { + const targets = [ mainCanvasRef.current, previewCanvasRef.current ]; + for(const canvas of targets) + { + if(!canvas) continue; + const ctx = canvas.getContext('2d'); + if(!ctx) continue; + const image = ctx.createImageData(GRID_WIDTH, GRID_HEIGHT); + const buffer = image.data; + for(let i = 0; i < grid.length; i++) + { + const v = grid[i]; + const o = i * 4; + buffer[o] = (v >>> 16) & 0xff; + buffer[o + 1] = (v >>> 8) & 0xff; + buffer[o + 2] = v & 0xff; + buffer[o + 3] = (v >>> 24) & 0xff; + } + ctx.putImageData(image, 0, 0); + } + }, [ grid, isVisible ]); + + const openColorPicker = useCallback(() => + { + const input = colorInputRef.current; + if(!input) return; + input.value = argbToHex(selectedColor); + input.click(); + }, [ selectedColor ]); + + const handleColorPicked = useCallback((event: React.ChangeEvent) => + { + setSelectedColor(hexToArgb(event.target.value)); + setTool('paint'); + }, []); + + const cellFromEvent = useCallback((event: ReactMouseEvent): { x: number; y: number } => + { + const rect = event.currentTarget.getBoundingClientRect(); + const x = Math.floor(((event.clientX - rect.left) / rect.width) * GRID_WIDTH); + const y = Math.floor(((event.clientY - rect.top) / rect.height) * GRID_HEIGHT); + return { x, y }; + }, []); + + const handleMouseDown = useCallback((event: ReactMouseEvent) => + { + if(event.button !== 0) return; + event.preventDefault(); + isDraggingRef.current = true; + const { x, y } = cellFromEvent(event); + paintAt(x, y, true); + }, [ cellFromEvent, paintAt ]); + + const handleMouseMove = useCallback((event: ReactMouseEvent) => + { + if(!isDraggingRef.current) return; + const { x, y } = cellFromEvent(event); + paintAt(x, y, false); + }, [ cellFromEvent, paintAt ]); + + useEffect(() => + { + const stopDrag = () => { isDraggingRef.current = false; }; + window.addEventListener('mouseup', stopDrag); + return () => window.removeEventListener('mouseup', stopDrag); + }, []); + + const clearCanvas = useCallback(() => setGrid(emptyGrid()), []); + + const copyColor = useCallback(() => setTool('picker'), []); + + const isEmpty = useMemo(() => + { + for(let i = 0; i < grid.length; i++) if(grid[i] !== 0) return false; + return true; + }, [ grid ]); + + const canCreateMore = (badges?.length ?? 0) < maxBadges; + + const handleSave = useCallback(async () => + { + if(submitting) return; + if(isEmpty) { setError(t('badgecreator.error.empty', 'Draw something first.')); return; } + if(!editingBadgeId && !canCreateMore) + { + setError(t('badgecreator.error.limit', 'You already have %max% custom badges.', [ 'max' ], [ String(maxBadges) ])); + return; + } + + setSubmitting(true); + setError(null); + try + { + const { b64, bytes } = await gridToPngBase64(grid); + if(bytes > maxBytes) + { + setError(t('badgecreator.error.too_large', `Image is too large (${ bytes } / %max% bytes).`, [ 'max' ], [ String(maxBytes) ])); + return; + } + const body = { name: name.trim(), description: description.trim(), image: b64 }; + const saved = editingBadgeId + ? await updateCustomBadge(editingBadgeId, body) + : await createCustomBadge(body); + if(saved && saved.badgeId) setCustomBadgeText(saved.badgeId, saved.name, saved.description); + await refresh(); + refreshCustomBadgeTexts(); + resetEditor(); + } + catch(err) + { + setError((err as Error)?.message || 'Could not save the badge.'); + } + finally + { + setSubmitting(false); + } + }, [ submitting, isEmpty, editingBadgeId, canCreateMore, maxBadges, grid, maxBytes, name, description, refresh, resetEditor ]); + + const handleDelete = useCallback((badge: CustomBadgeRecord) => + { + showConfirm( + t('badgecreator.delete.confirm', 'Delete "%name%"?', [ 'name' ], [ badge.name || badge.badgeId ]), + async () => + { + try + { + await deleteCustomBadge(badge.badgeId); + if(editingBadgeId === badge.badgeId) resetEditor(); + await refresh(); + refreshCustomBadgeTexts(); + } + catch(err) + { + setError((err as Error)?.message || 'Could not delete the badge.'); + } + }, + null, null, null, + t('badgecreator.delete.title', 'Delete badge') + ); + }, [ showConfirm, editingBadgeId, refresh, resetEditor ]); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + + + +
+ +
+ + + + + + + + +
+ +
+ { t('badgecreator.palette', 'Palette') } +
+ { PALETTE.map((color, idx) => + { + const isTransparent = color === TRANSPARENT; + const isSelected = color === selectedColor; + return ( +
+
+ +
+ { argbToHex(selectedColor).toUpperCase() } + +
+
+
+ { t('badgecreator.preview', 'Preview') } +
+ +
+
+
+ { t('badgecreator.name', 'Name') } + setName(e.target.value) } /> +
+
+ { t('badgecreator.description', 'Description') } + setDescription(e.target.value) } /> +
+ { error && { error } } + { !editingBadgeId && priceBadge > 0 && + + { t('badgecreator.price', 'Cost: %price% %currency%', [ 'price', 'currency' ], [ String(priceBadge), currencyName(currencyType) ]) } + } + + + { editingBadgeId && + } + + + + + + { t('badgecreator.list.title', 'Your custom badges (%count%/%max%)', [ 'count', 'max' ], [ String(badges?.length ?? 0), String(maxBadges) ]) } + + { badges === null && { t('badgecreator.list.loading', 'Loading…') } } + { badges !== null && !badges.length && { t('badgecreator.list.empty', 'You haven\'t made any badges yet.') } } + { badges !== null && badges.map(badge => ( + + { + + { badge.name || badge.badgeId } + { badge.description && { badge.description } } + + + + + )) } + + + + ); +}; diff --git a/src/components/badge-creator/index.ts b/src/components/badge-creator/index.ts new file mode 100644 index 0000000..d12515e --- /dev/null +++ b/src/components/badge-creator/index.ts @@ -0,0 +1 @@ +export * from './BadgeCreatorView'; diff --git a/src/components/inventory/views/badge/InventoryBadgeView.tsx b/src/components/inventory/views/badge/InventoryBadgeView.tsx index f1fad00..1d39b6d 100644 --- a/src/components/inventory/views/badge/InventoryBadgeView.tsx +++ b/src/components/inventory/views/badge/InventoryBadgeView.tsx @@ -1,7 +1,7 @@ -import { DeleteBadgeMessageComposer } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, DeleteBadgeMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { FaTrashAlt } from 'react-icons/fa'; -import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, SendMessageComposer, UnseenItemCategory } from '../../../../api'; +import { FaPaintBrush, FaPencilAlt, FaTrashAlt } from 'react-icons/fa'; +import { deleteCustomBadge, ensureCustomBadgeTexts, fetchCustomBadges, GetConfigurationValue, isCustomBadgeCode, LocalizeBadgeName, LocalizeText, refreshCustomBadgeTexts, SendMessageComposer, UnseenItemCategory } from '../../../../api'; import { LayoutBadgeImageView } from '../../../../common'; import { useInventoryBadges, useInventoryUnseenTracker, useNotification } from '../../../../hooks'; import { InfiniteGrid, NitroButton } from '../../../../layout'; @@ -90,7 +90,60 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = const [ isDraggingFromActive, setIsDraggingFromActive ] = useState(false); const maxSlots = useMemo(() => GetConfigurationValue('user.badges.max.slots', 5), []); - const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes); + + const [ ownCustomBadgeIds, setOwnCustomBadgeIds ] = useState>(() => new Set()); + const [ filter, setFilter ] = useState<'all' | 'custom'>('all'); + + const refreshOwnCustomBadges = useCallback(async () => + { + try + { + const data = await fetchCustomBadges(); + setOwnCustomBadgeIds(new Set((data.badges ?? []).map(b => b.badgeId))); + } + catch + { + setOwnCustomBadgeIds(new Set()); + } + }, []); + + useEffect(() => { refreshOwnCustomBadges(); }, [ refreshOwnCustomBadges ]); + useEffect(() => { ensureCustomBadgeTexts(); }, []); + + const baseCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes); + const customCount = useMemo(() => baseCodes.filter(c => isCustomBadgeCode(c)).length, [ baseCodes ]); + const displayCodes = useMemo(() => + filter === 'custom' ? baseCodes.filter(c => isCustomBadgeCode(c)) : baseCodes, + [ baseCodes, filter ]); + + const isOwnCustomBadge = (code: string | null) => !!code && isCustomBadgeCode(code) && ownCustomBadgeIds.has(code); + + const handleEditCustom = useCallback(() => + { + if(!selectedBadgeCode) return; + CreateLinkEvent(`badge-creator/edit/${ selectedBadgeCode }`); + }, [ selectedBadgeCode ]); + + const handleDeleteCustom = useCallback(() => + { + if(!selectedBadgeCode) return; + const target = selectedBadgeCode; + showConfirm( + LocalizeText('inventory.delete.confirm_delete.info', [ 'furniname', 'amount' ], [ LocalizeBadgeName(target), '1' ]), + async () => + { + try + { + await deleteCustomBadge(target); + await refreshOwnCustomBadges(); + refreshCustomBadgeTexts(); + } + catch { /* error already surfaced server-side */ } + }, + null, null, null, + LocalizeText('inventory.delete.confirm_delete.title') + ); + }, [ selectedBadgeCode, showConfirm, refreshOwnCustomBadges ]); const attemptDeleteBadge = () => { @@ -205,6 +258,28 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = { LocalizeText('inventory.badges.clearbadge') }
) } +
+ + + +
columnCount={ 5 } estimateSize={ 50 } @@ -242,8 +317,12 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = onClick={ event => toggleBadge(selectedBadgeCode) }> { LocalizeText(isWearingBadge(selectedBadgeCode) ? 'inventory.badges.clearbadge' : 'inventory.badges.wearbadge') } + { isOwnCustomBadge(selectedBadgeCode) && + + + } { !isWearingBadge(selectedBadgeCode) && - + }
diff --git a/src/components/login/LoginView.tsx b/src/components/login/LoginView.tsx index e4a8f61..83a8c31 100644 --- a/src/components/login/LoginView.tsx +++ b/src/components/login/LoginView.tsx @@ -1,5 +1,5 @@ import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { GetConfigurationValue } from '../../api'; +import { GetConfigurationValue, persistAccessTokenFromPayload } from '../../api'; import { ForgotDialog } from './components/ForgotDialog'; import { NewsWindow } from './components/NewsWindow'; import { RegisterDialog } from './components/RegisterDialog'; @@ -244,6 +244,7 @@ export const LoginView: FC = ({ onAuthenticated }) => const rememberToken = typeof payload.rememberToken === 'string' ? payload.rememberToken : ''; if(rememberMe && rememberToken) window.localStorage.setItem('nitro.remember.token', rememberToken); else window.localStorage.removeItem('nitro.remember.token'); + persistAccessTokenFromPayload(payload); } catch {} diff --git a/src/components/login/utils/news.ts b/src/components/login/utils/news.ts index 510655c..0fa0c82 100644 --- a/src/components/login/utils/news.ts +++ b/src/components/login/utils/news.ts @@ -9,8 +9,12 @@ export const resolveNewsImage = (raw: string | null | undefined): string => const value = (raw ?? '').trim(); if(!value) return ''; if(/^https?:\/\//i.test(value)) return value; - if(value.startsWith('//')) return value; - if(value.startsWith('/') && !value.startsWith('//')) return value; + if(value.startsWith('//')) return window.location.protocol + value; + if(value.startsWith('/')) + { + try { return new URL(value, window.location.origin).href; } + catch { return window.location.origin + value; } + } if(value.startsWith('data:')) { return /^data:image\/[a-z0-9.+-]+[,;]/i.test(value) ? value : ''; diff --git a/src/components/navigator/views/search/NavigatorSearchResultItemInfoView.tsx b/src/components/navigator/views/search/NavigatorSearchResultItemInfoView.tsx index 645e5c9..40cc451 100644 --- a/src/components/navigator/views/search/NavigatorSearchResultItemInfoView.tsx +++ b/src/components/navigator/views/search/NavigatorSearchResultItemInfoView.tsx @@ -1,7 +1,7 @@ import { RoomDataParser, RoomSettingsComposer, UpdateHomeRoomMessageComposer } from '@nitrots/nitro-renderer'; +import * as Popover from '@radix-ui/react-popover'; import React, { FC, useRef, useState } from 'react'; import { FaUser } from 'react-icons/fa'; -import { ArrowContainer, Popover } from 'react-tiny-popover'; import { GetGroupInformation, GetSessionDataManager, GetUserProfile, LocalizeText, ReportType, SendMessageComposer, ToggleFavoriteRoom } from '../../../../api'; import { Column, Flex, LayoutBadgeImageView, LayoutRoomThumbnailView, NitroCardContentView, Text, UserProfileIconView } from '../../../../common'; import { useHelp, useNavigator } from '../../../../hooks'; @@ -26,6 +26,12 @@ export const NavigatorSearchResultItemInfoView: FC + { + if(!isControlled) setInternalVisible(open); + if(!open && setIsPopoverActive) setIsPopoverActive(false); + }; + const getUserCounterColor = () => { const num: number = (100 * (roomData.userCount / roomData.maxUserCount)); @@ -88,17 +94,22 @@ export const NavigatorSearchResultItemInfoView: FC ( - + + +
{ if(!isControlled) setInternalVisible(true); } } + onMouseLeave={ () => { if(!isControlled) setInternalVisible(false); } } + /> + + + e.stopPropagation() }> @@ -173,24 +184,9 @@ export const NavigatorSearchResultItemInfoView: FC } - - ) } - isOpen={ popoverOpen } - onClickOutside={ () => - { - if(!isControlled) setInternalVisible(false); - if(setIsPopoverActive) setIsPopoverActive(false); - } } - padding={ 10 } - positions={ [ 'right', 'left', 'top', 'bottom' ] } - > -
{ if(!isControlled) setInternalVisible(true); } } - onMouseLeave={ () => { if(!isControlled) setInternalVisible(false); } } - /> - + + + + ); }; diff --git a/src/components/purse/views/CurrencyView.tsx b/src/components/purse/views/CurrencyView.tsx index 9f72158..47c07ab 100644 --- a/src/components/purse/views/CurrencyView.tsx +++ b/src/components/purse/views/CurrencyView.tsx @@ -1,5 +1,4 @@ import { FC, useMemo } from 'react'; -import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import { LocalizeFormattedNumber, LocalizeShortNumber } from '../../../api'; import { Flex, LayoutCurrencyIcon, Text } from '../../../common'; @@ -17,23 +16,22 @@ export const CurrencyView: FC = props => const element = useMemo(() => { return ( - + { short ? LocalizeShortNumber(amount) : LocalizeFormattedNumber(amount) } ); }, [ amount, short, type ]); if(!short) return element; - + return ( - - { LocalizeFormattedNumber(amount) } - - }> +
{ element } - +
+ { LocalizeFormattedNumber(amount) } +
+
); } diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index d415a1b..f396c28 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -1,4 +1,4 @@ -import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer'; +import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSessionDataManager, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FaCrosshairs, FaRulerVertical, FaTimes } from 'react-icons/fa'; import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr'; @@ -585,19 +585,20 @@ export const InfoStandWidgetFurniView: FC = props onClick={ () => setDropdownOpen(!dropdownOpen) }> { dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` } - + if(typeId) window.dispatchEvent(new CustomEvent('furni-editor:open', { detail: { spriteId: typeId } })); + } }> + Edit Furni + } { dropdownOpen &&
{ /* Left panel: position + rotation */ } diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 91ced43..518bf7a 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -24,12 +24,14 @@ export const InfoStandWidgetUserView: FC = props = const [backgroundId, setBackgroundId] = useState(null); const [standId, setStandId] = useState(null); const [overlayId, setOverlayId] = useState(null); + const [cardBackgroundId, setCardBackgroundId] = useState(null); const [isVisible, setIsVisible] = useState(false); const { roomSession = null } = useRoom(); const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`; const infostandStandClass = `stand-${standId ?? 'default'}`; const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`; + const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : ''; const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]); @@ -91,6 +93,7 @@ export const InfoStandWidgetUserView: FC = props = newValue.backgroundId = event.backgroundId; newValue.standId = event.standId; newValue.overlayId = event.overlayId; + newValue.cardBackgroundId = event.cardBackgroundId ?? 0; return newValue; }); }); @@ -125,16 +128,12 @@ export const InfoStandWidgetUserView: FC = props = setBackgroundId(avatarInfo.backgroundId); setStandId(avatarInfo.standId); setOverlayId(avatarInfo.overlayId); + setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0); SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID)); return () => { - setIsEditingMotto(false); - setMotto(null); setRelationships(null); - setBackgroundId(null); - setStandId(null); - setOverlayId(null); }; }, [avatarInfo]); @@ -142,7 +141,7 @@ export const InfoStandWidgetUserView: FC = props = return ( <> - +
@@ -277,6 +276,8 @@ export const InfoStandWidgetUserView: FC = props = setSelectedStand={setStandId} selectedOverlay={overlayId} setSelectedOverlay={setOverlayId} + selectedCardBackground={cardBackgroundId} + setSelectedCardBackground={setCardBackgroundId} />
)} diff --git a/src/components/room/widgets/chat-input/ChatInputEmojiSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputEmojiSelectorView.tsx index f547e46..bd2dcae 100644 --- a/src/components/room/widgets/chat-input/ChatInputEmojiSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputEmojiSelectorView.tsx @@ -1,7 +1,7 @@ import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; +import * as Popover from '@radix-ui/react-popover'; import { FC, useState } from 'react'; -import { Popover } from 'react-tiny-popover'; interface ChatInputEmojiSelectorViewProps { @@ -19,19 +19,16 @@ export const ChatInputEmojiSelectorView: FC = p setSelectorVisible(false); }; - const toggleSelector = () => setSelectorVisible(prev => !prev); - return ( -
- } - isOpen={ selectorVisible } - positions={ [ 'top' ] } - onClickOutside={ () => setSelectorVisible(false) } - > -
🙂
-
-
+ + +
🙂
+
+ + + + + +
); }; diff --git a/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx index 2a6d165..ec089c3 100644 --- a/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx @@ -1,5 +1,5 @@ +import * as Popover from '@radix-ui/react-popover'; import { FC, useState } from 'react'; -import { ArrowContainer, Popover } from 'react-tiny-popover'; import { Flex, Grid, NitroCardContentView } from '../../../../common'; interface ChatInputStyleSelectorViewProps @@ -21,20 +21,17 @@ export const ChatInputStyleSelectorView: FC = p }; return ( - ( - + +
+
+
+ + + @@ -47,15 +44,9 @@ export const ChatInputStyleSelectorView: FC = p ))} - - )} - > -
setSelectorVisible(v => !v)} - > -
-
- + + + + ); -}; \ No newline at end of file +}; diff --git a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx index f2375ca..82a4b8c 100644 --- a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx +++ b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx @@ -1,6 +1,5 @@ -import { FC, useEffect, useState } from 'react'; -import YouTube, { Options } from 'react-youtube'; -import { YouTubePlayer } from 'youtube-player/dist/types'; +import { FC, useRef } from 'react'; +import ReactPlayer from 'react-player/youtube'; import { LocalizeText, YoutubeVideoPlaybackStateEnum } from '../../../../api'; import { AutoGrid, AutoGridProps, LayoutGridItem, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useFurnitureYoutubeWidget } from '../../../../hooks'; @@ -12,71 +11,24 @@ interface FurnitureYoutubeDisplayViewProps extends AutoGridProps export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewProps => { - const [ player, setPlayer ] = useState(null); const { objectId = -1, videoId = null, videoStart = 0, videoEnd = 0, currentVideoState = null, selectedVideo = null, playlists = [], onClose = null, previous = null, next = null, pause = null, play = null, selectVideo = null } = useFurnitureYoutubeWidget(); + const playerRef = useRef(null); - const onStateChange = (event: { target: YouTubePlayer; data: number }) => + const handlePlay = () => { - try - { - setPlayer(event.target); - - if(objectId === -1) return; - - switch(event.target.getPlayerState()) - { - case -1: - case 1: - if(currentVideoState !== 1) play(); - return; - case 2: - if(currentVideoState !== 2) pause(); - } - } - catch(err) {} + if(objectId === -1) return; + if(currentVideoState !== YoutubeVideoPlaybackStateEnum.PLAYING) play(); }; - useEffect(() => + const handlePause = () => { - if((currentVideoState === null) || !player) return; - - try - { - if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING)) - { - player.playVideo(); - - return; - } - - if((currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PAUSED)) - { - player.pauseVideo(); - - return; - } - } - catch(err) - { - setPlayer(null); - } - }, [ currentVideoState, player ]); + if(objectId === -1) return; + if(currentVideoState !== YoutubeVideoPlaybackStateEnum.PAUSED) pause(); + }; if(objectId === -1) return null; - const youtubeOptions: Options = { - height: '375', - width: '500', - playerVars: { - autoplay: 1, - disablekb: 1, - controls: 0, - origin: window.origin, - modestbranding: 1, - start: videoStart, - end: videoEnd - } - }; + const playing = (currentVideoState === null) ? true : (currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING); return ( @@ -85,7 +37,26 @@ export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewPr
{ (videoId && videoId.length > 0) && - setPlayer(event.target) } onStateChange={ onStateChange } /> + } { (!videoId || videoId.length === 0) &&
{ LocalizeText('widget.furni.video_viewer.no_videos') }
diff --git a/src/components/toolbar/ToolbarMeView.tsx b/src/components/toolbar/ToolbarMeView.tsx index 642e519..79fa576 100644 --- a/src/components/toolbar/ToolbarMeView.tsx +++ b/src/components/toolbar/ToolbarMeView.tsx @@ -32,7 +32,7 @@ export const ToolbarMeView: FC + { (GetConfigurationValue('guides.enabled') && useGuideTool) &&
DispatchUiEvent(new GuideToolEvent(GuideToolEvent.TOGGLE_GUIDE_TOOL)) } /> }
CreateLinkEvent('achievements/toggle') }> @@ -42,6 +42,7 @@ export const ToolbarMeView: FC GetUserProfile(GetSessionDataManager().userId) } />
CreateLinkEvent('navigator/search/myworld_view') } />
CreateLinkEvent('avatar-editor/toggle') } /> +
CreateLinkEvent('badge-creator/toggle') } title={ LocalizeText('toolbar.icon.label.badge_creator') } />
CreateLinkEvent('user-settings/toggle') } />
CreateLinkEvent('groupforum/toggle') } title={ LocalizeText('toolbar.icon.label.forums') } /> { children } diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 38758cd..c919d7f 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -184,7 +184,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } { setMeExpanded(value => !value); event.stopPropagation(); } }> - + { (getTotalUnseen > 0) && } @@ -279,7 +279,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } { setMeExpanded(value => !value); event.stopPropagation(); } }> - + { (getTotalUnseen > 0) && } diff --git a/src/components/toolbar/YouTubePlayerView.tsx b/src/components/toolbar/YouTubePlayerView.tsx index 39e8e76..5f1c89c 100644 --- a/src/components/toolbar/YouTubePlayerView.tsx +++ b/src/components/toolbar/YouTubePlayerView.tsx @@ -1,6 +1,6 @@ import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomSettingsEvent, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from "@nitrots/nitro-renderer"; import { FC, useEffect, useRef, useState } from "react"; -import YouTube from "react-youtube"; +import ReactPlayer from "react-player/youtube"; import { GetRoomSession, getYoutubeRoomEnabled, GetSessionDataManager, LocalizeText, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from "../../api"; import { NitroCardContentView, NitroCardHeaderView, NitroCardView, LayoutAvatarImageView } from "../../common"; import { useFurnitureYoutubeWidget, useMessageEvent } from "../../hooks"; @@ -35,7 +35,7 @@ export const YouTubePlayerView: FC<{}> = () => { const [playlist, setPlaylist] = useState([]); const [history, setHistory] = useState([]); const [showVolumeSlider, setShowVolumeSlider] = useState(true); - const playerRef = useRef(null); + const playerRef = useRef(null); const { objectId: youtubeObjectId, videoId: roomVideoId, currentVideoState, hasControl } = useFurnitureYoutubeWidget(); const [spectators, setSpectators] = useState< { id: number; name: string; look: string }[] >([]); const [broadcastVideo, setBroadcastVideo] = useState(""); @@ -310,22 +310,22 @@ export const YouTubePlayerView: FC<{}> = () => { )} {videoId ? ( - { playerRef.current = ref; }} + url={`https://www.youtube.com/watch?v=${videoId}`} + width="100%" + height={isFullscreen ? "100%" : 280} + playing + muted={isMuted} + loop={isLooping} + volume={Math.max(0, Math.min(1, volume / 100))} + config={{ playerVars: { autoplay: 1, - volume: volume, - muted: isMuted ? 1 : 0, loop: isLooping ? 1 : 0, }, }} - onReady={(e) => { - playerRef.current = e.target; - addToHistory(videoId); - }} + onReady={() => addToHistory(videoId)} /> ) : (
diff --git a/src/components/user-profile/UserContainerView.tsx b/src/components/user-profile/UserContainerView.tsx index 88d3150..1262bd4 100644 --- a/src/components/user-profile/UserContainerView.tsx +++ b/src/components/user-profile/UserContainerView.tsx @@ -18,6 +18,7 @@ export const UserContainerView: FC<{ const infostandBackgroundClass = `background-${userProfile.backgroundId ?? 'default'}`; const infostandStandClass = `stand-${userProfile.standId ?? 'default'}`; const infostandOverlayClass = `overlay-${userProfile.overlayId ?? 'default'}`; + const profileCardBgClass = userProfile.cardBackgroundId ? `card-background-${userProfile.cardBackgroundId}` : ''; const addFriend = () => { @@ -32,7 +33,7 @@ export const UserContainerView: FC<{ }, [ userProfile ]); return ( -
+
diff --git a/src/css/backgrounds/BackgroundsView.css b/src/css/backgrounds/BackgroundsView.css index 5b67a48..1aa6114 100644 --- a/src/css/backgrounds/BackgroundsView.css +++ b/src/css/backgrounds/BackgroundsView.css @@ -78,6 +78,656 @@ background: none; } +.profile-card-background { + background-repeat: repeat; + background-position: top left; + background-size: auto; + &.card-background-0 { + background-image: url('@/assets/images/backgrounds/background/bg_0.png'); + } + &.card-background-1 { + background-image: url('@/assets/images/backgrounds/background/bg_1.png'); + } + &.card-background-2 { + background-image: url('@/assets/images/backgrounds/background/bg_2.png'); + } + &.card-background-3 { + background-image: url('@/assets/images/backgrounds/background/bg_3.png'); + } + &.card-background-4 { + background-image: url('@/assets/images/backgrounds/background/bg_4.png'); + } + &.card-background-5 { + background-image: url('@/assets/images/backgrounds/background/bg_5.png'); + } + &.card-background-6 { + background-image: url('@/assets/images/backgrounds/background/bg_6.png'); + } + &.card-background-7 { + background-image: url('@/assets/images/backgrounds/background/bg_7.png'); + } + &.card-background-8 { + background-image: url('@/assets/images/backgrounds/background/bg_8.png'); + } + &.card-background-9 { + background-image: url('@/assets/images/backgrounds/background/bg_9.png'); + } + &.card-background-10 { + background-image: url('@/assets/images/backgrounds/background/bg_10.png'); + } + &.card-background-11 { + background-image: url('@/assets/images/backgrounds/background/bg_11.png'); + } + &.card-background-12 { + background-image: url('@/assets/images/backgrounds/background/bg_12.png'); + } + &.card-background-13 { + background-image: url('@/assets/images/backgrounds/background/bg_13.png'); + } + &.card-background-14 { + background-image: url('@/assets/images/backgrounds/background/bg_14.png'); + } + &.card-background-15 { + background-image: url('@/assets/images/backgrounds/background/bg_15.png'); + } + &.card-background-16 { + background-image: url('@/assets/images/backgrounds/background/bg_16.png'); + } + &.card-background-17 { + background-image: url('@/assets/images/backgrounds/background/bg_17.png'); + } + &.card-background-18 { + background-image: url('@/assets/images/backgrounds/background/bg_18.png'); + } + &.card-background-19 { + background-image: url('@/assets/images/backgrounds/background/bg_19.png'); + } + &.card-background-20 { + background-image: url('@/assets/images/backgrounds/background/bg_20.png'); + } + &.card-background-21 { + background-image: url('@/assets/images/backgrounds/background/bg_21.png'); + } + &.card-background-22 { + background-image: url('@/assets/images/backgrounds/background/bg_22.png'); + } + &.card-background-23 { + background-image: url('@/assets/images/backgrounds/background/bg_23.png'); + } + &.card-background-24 { + background-image: url('@/assets/images/backgrounds/background/bg_24.png'); + } + &.card-background-25 { + background-image: url('@/assets/images/backgrounds/background/bg_25.png'); + } + &.card-background-26 { + background-image: url('@/assets/images/backgrounds/background/bg_26.png'); + } + &.card-background-27 { + background-image: url('@/assets/images/backgrounds/background/bg_27.png'); + } + &.card-background-28 { + background-image: url('@/assets/images/backgrounds/background/bg_28.png'); + } + &.card-background-29 { + background-image: url('@/assets/images/backgrounds/background/bg_29.png'); + } + &.card-background-30 { + background-image: url('@/assets/images/backgrounds/background/bg_30.png'); + } + &.card-background-31 { + background-image: url('@/assets/images/backgrounds/background/bg_31.png'); + } + &.card-background-32 { + background-image: url('@/assets/images/backgrounds/background/bg_32.png'); + } + &.card-background-33 { + background-image: url('@/assets/images/backgrounds/background/bg_33.png'); + } + &.card-background-34 { + background-image: url('@/assets/images/backgrounds/background/bg_34.png'); + } + &.card-background-35 { + background-image: url('@/assets/images/backgrounds/background/bg_35.png'); + } + &.card-background-36 { + background-image: url('@/assets/images/backgrounds/background/bg_36.gif'); + } + &.card-background-37 { + background-image: url('@/assets/images/backgrounds/background/bg_37.png'); + } + &.card-background-38 { + background-image: url('@/assets/images/backgrounds/background/bg_38.png'); + } + &.card-background-39 { + background-image: url('@/assets/images/backgrounds/background/bg_39.png'); + } + &.card-background-40 { + background-image: url('@/assets/images/backgrounds/background/bg_40.png'); + } + &.card-background-41 { + background-image: url('@/assets/images/backgrounds/background/bg_41.png'); + } + &.card-background-42 { + background-image: url('@/assets/images/backgrounds/background/bg_42.png'); + } + &.card-background-43 { + background-image: url('@/assets/images/backgrounds/background/bg_43.png'); + } + &.card-background-44 { + background-image: url('@/assets/images/backgrounds/background/bg_44.png'); + } + &.card-background-45 { + background-image: url('@/assets/images/backgrounds/background/bg_45.png'); + } + &.card-background-46 { + background-image: url('@/assets/images/backgrounds/background/bg_46.png'); + } + &.card-background-47 { + background-image: url('@/assets/images/backgrounds/background/bg_47.png'); + } + &.card-background-48 { + background-image: url('@/assets/images/backgrounds/background/bg_48.png'); + } + &.card-background-49 { + background-image: url('@/assets/images/backgrounds/background/bg_49.png'); + } + &.card-background-50 { + background-image: url('@/assets/images/backgrounds/background/bg_50.png'); + } + &.card-background-51 { + background-image: url('@/assets/images/backgrounds/background/bg_51.gif'); + } + &.card-background-52 { + background-image: url('@/assets/images/backgrounds/background/bg_52.gif'); + } + &.card-background-53 { + background-image: url('@/assets/images/backgrounds/background/bg_53.gif'); + } + &.card-background-54 { + background-image: url('@/assets/images/backgrounds/background/bg_54.gif'); + } + &.card-background-55 { + background-image: url('@/assets/images/backgrounds/background/bg_55.gif'); + } + &.card-background-56 { + background-image: url('@/assets/images/backgrounds/background/bg_56.gif'); + } + &.card-background-57 { + background-image: url('@/assets/images/backgrounds/background/bg_57.gif'); + } + &.card-background-58 { + background-image: url('@/assets/images/backgrounds/background/bg_58.gif'); + } + &.card-background-59 { + background-image: url('@/assets/images/backgrounds/background/bg_59.gif'); + } + &.card-background-60 { + background-image: url('@/assets/images/backgrounds/background/bg_60.gif'); + } + &.card-background-61 { + background-image: url('@/assets/images/backgrounds/background/bg_61.gif'); + } + &.card-background-62 { + background-image: url('@/assets/images/backgrounds/background/bg_62.gif'); + } + &.card-background-63 { + background-image: url('@/assets/images/backgrounds/background/bg_63.gif'); + } + &.card-background-64 { + background-image: url('@/assets/images/backgrounds/background/bg_64.gif'); + } + &.card-background-65 { + background-image: url('@/assets/images/backgrounds/background/bg_65.gif'); + } + &.card-background-66 { + background-image: url('@/assets/images/backgrounds/background/bg_66.gif'); + } + &.card-background-67 { + background-image: url('@/assets/images/backgrounds/background/bg_67.gif'); + } + &.card-background-68 { + background-image: url('@/assets/images/backgrounds/background/bg_68.gif'); + } + &.card-background-69 { + background-image: url('@/assets/images/backgrounds/background/bg_69.gif'); + } + &.card-background-70 { + background-image: url('@/assets/images/backgrounds/background/bg_70.gif'); + } + &.card-background-71 { + background-image: url('@/assets/images/backgrounds/background/bg_71.gif'); + } + &.card-background-72 { + background-image: url('@/assets/images/backgrounds/background/bg_72.gif'); + } + &.card-background-73 { + background-image: url('@/assets/images/backgrounds/background/bg_73.gif'); + } + &.card-background-74 { + background-image: url('@/assets/images/backgrounds/background/bg_74.gif'); + } + &.card-background-75 { + background-image: url('@/assets/images/backgrounds/background/bg_75.gif'); + } + &.card-background-76 { + background-image: url('@/assets/images/backgrounds/background/bg_76.gif'); + } + &.card-background-77 { + background-image: url('@/assets/images/backgrounds/background/bg_77.gif'); + } + &.card-background-78 { + background-image: url('@/assets/images/backgrounds/background/bg_78.gif'); + } + &.card-background-79 { + background-image: url('@/assets/images/backgrounds/background/bg_79.gif'); + } + &.card-background-80 { + background-image: url('@/assets/images/backgrounds/background/bg_80.gif'); + } + &.card-background-81 { + background-image: url('@/assets/images/backgrounds/background/bg_81.gif'); + } + &.card-background-82 { + background-image: url('@/assets/images/backgrounds/background/bg_82.gif'); + } + &.card-background-83 { + background-image: url('@/assets/images/backgrounds/background/bg_83.gif'); + } + &.card-background-84 { + background-image: url('@/assets/images/backgrounds/background/bg_84.gif'); + } + &.card-background-85 { + background-image: url('@/assets/images/backgrounds/background/bg_85.gif'); + } + &.card-background-86 { + background-image: url('@/assets/images/backgrounds/background/bg_86.png'); + } + &.card-background-87 { + background-image: url('@/assets/images/backgrounds/background/bg_87.gif'); + } + &.card-background-88 { + background-image: url('@/assets/images/backgrounds/background/bg_88.gif'); + } + &.card-background-89 { + background-image: url('@/assets/images/backgrounds/background/bg_89.gif'); + } + &.card-background-90 { + background-image: url('@/assets/images/backgrounds/background/bg_90.gif'); + } + &.card-background-91 { + background-image: url('@/assets/images/backgrounds/background/bg_91.gif'); + } + &.card-background-92 { + background-image: url('@/assets/images/backgrounds/background/bg_92.gif'); + } + &.card-background-93 { + background-image: url('@/assets/images/backgrounds/background/bg_93.gif'); + } + &.card-background-94 { + background-image: url('@/assets/images/backgrounds/background/bg_94.gif'); + } + &.card-background-95 { + background-image: url('@/assets/images/backgrounds/background/bg_95.gif'); + } + &.card-background-96 { + background-image: url('@/assets/images/backgrounds/background/bg_96.gif'); + } + &.card-background-97 { + background-image: url('@/assets/images/backgrounds/background/bg_97.gif'); + } + &.card-background-98 { + background-image: url('@/assets/images/backgrounds/background/bg_98.gif'); + } + &.card-background-99 { + background-image: url('@/assets/images/backgrounds/background/bg_99.gif'); + } + &.card-background-100 { + background-image: url('@/assets/images/backgrounds/background/bg_100.gif'); + } + &.card-background-101 { + background-image: url('@/assets/images/backgrounds/background/bg_101.png'); + } + &.card-background-102 { + background-image: url('@/assets/images/backgrounds/background/bg_102.gif'); + } + &.card-background-103 { + background-image: url('@/assets/images/backgrounds/background/bg_103.gif'); + } + &.card-background-104 { + background-image: url('@/assets/images/backgrounds/background/bg_104.gif'); + } + &.card-background-105 { + background-image: url('@/assets/images/backgrounds/background/bg_105.gif'); + } + &.card-background-106 { + background-image: url('@/assets/images/backgrounds/background/bg_106.gif'); + } + &.card-background-107 { + background-image: url('@/assets/images/backgrounds/background/bg_107.gif'); + } + &.card-background-108 { + background-image: url('@/assets/images/backgrounds/background/bg_108.gif'); + } + &.card-background-109 { + background-image: url('@/assets/images/backgrounds/background/bg_109.gif'); + } + &.card-background-110 { + background-image: url('@/assets/images/backgrounds/background/bg_110.gif'); + } + &.card-background-111 { + background-image: url('@/assets/images/backgrounds/background/bg_111.gif'); + } + &.card-background-112 { + background-image: url('@/assets/images/backgrounds/background/bg_112.gif'); + } + &.card-background-113 { + background-image: url('@/assets/images/backgrounds/background/bg_113.gif'); + } + &.card-background-114 { + background-image: url('@/assets/images/backgrounds/background/bg_114.gif'); + } + &.card-background-115 { + background-image: url('@/assets/images/backgrounds/background/bg_115.gif'); + } + &.card-background-116 { + background-image: url('@/assets/images/backgrounds/background/bg_116.gif'); + } + &.card-background-117 { + background-image: url('@/assets/images/backgrounds/background/bg_117.gif'); + } + &.card-background-118 { + background-image: url('@/assets/images/backgrounds/background/bg_118.gif'); + } + &.card-background-119 { + background-image: url('@/assets/images/backgrounds/background/bg_119.gif'); + } + &.card-background-120 { + background-image: url('@/assets/images/backgrounds/background/bg_120.gif'); + } + &.card-background-121 { + background-image: url('@/assets/images/backgrounds/background/bg_121.gif'); + } + &.card-background-122 { + background-image: url('@/assets/images/backgrounds/background/bg_122.gif'); + } + &.card-background-123 { + background-image: url('@/assets/images/backgrounds/background/bg_123.gif'); + } + &.card-background-124 { + background-image: url('@/assets/images/backgrounds/background/bg_124.gif'); + } + &.card-background-125 { + background-image: url('@/assets/images/backgrounds/background/bg_125.gif'); + } + &.card-background-126 { + background-image: url('@/assets/images/backgrounds/background/bg_126.gif'); + } + &.card-background-127 { + background-image: url('@/assets/images/backgrounds/background/bg_127.gif'); + } + &.card-background-128 { + background-image: url('@/assets/images/backgrounds/background/bg_128.gif'); + } + &.card-background-129 { + background-image: url('@/assets/images/backgrounds/background/bg_129.gif'); + } + &.card-background-130 { + background-image: url('@/assets/images/backgrounds/background/bg_130.gif'); + } + &.card-background-131 { + background-image: url('@/assets/images/backgrounds/background/bg_131.gif'); + } + &.card-background-132 { + background-image: url('@/assets/images/backgrounds/background/bg_132.gif'); + } + &.card-background-133 { + background-image: url('@/assets/images/backgrounds/background/bg_133.gif'); + } + &.card-background-134 { + background-image: url('@/assets/images/backgrounds/background/bg_134.gif'); + } + &.card-background-135 { + background-image: url('@/assets/images/backgrounds/background/bg_135.gif'); + } + &.card-background-136 { + background-image: url('@/assets/images/backgrounds/background/bg_136.gif'); + } + &.card-background-137 { + background-image: url('@/assets/images/backgrounds/background/bg_137.gif'); + } + &.card-background-138 { + background-image: url('@/assets/images/backgrounds/background/bg_138.gif'); + } + &.card-background-139 { + background-image: url('@/assets/images/backgrounds/background/bg_139.gif'); + } + &.card-background-140 { + background-image: url('@/assets/images/backgrounds/background/bg_140.gif'); + } + &.card-background-141 { + background-image: url('@/assets/images/backgrounds/background/bg_141.gif'); + } + &.card-background-142 { + background-image: url('@/assets/images/backgrounds/background/bg_142.gif'); + } + &.card-background-143 { + background-image: url('@/assets/images/backgrounds/background/bg_143.gif'); + } + &.card-background-144 { + background-image: url('@/assets/images/backgrounds/background/bg_144.gif'); + } + &.card-background-145 { + background-image: url('@/assets/images/backgrounds/background/bg_145.gif'); + } + &.card-background-146 { + background-image: url('@/assets/images/backgrounds/background/bg_146.gif'); + } + &.card-background-147 { + background-image: url('@/assets/images/backgrounds/background/bg_147.gif'); + } + &.card-background-148 { + background-image: url('@/assets/images/backgrounds/background/bg_148.gif'); + } + &.card-background-149 { + background-image: url('@/assets/images/backgrounds/background/bg_149.gif'); + } + &.card-background-150 { + background-image: url('@/assets/images/backgrounds/background/bg_150.gif'); + } + &.card-background-151 { + background-image: url('@/assets/images/backgrounds/background/bg_151.gif'); + } + &.card-background-152 { + background-image: url('@/assets/images/backgrounds/background/bg_152.gif'); + } + &.card-background-153 { + background-image: url('@/assets/images/backgrounds/background/bg_153.gif'); + } + &.card-background-154 { + background-image: url('@/assets/images/backgrounds/background/bg_154.gif'); + } + &.card-background-155 { + background-image: url('@/assets/images/backgrounds/background/bg_155.gif'); + } + &.card-background-156 { + background-image: url('@/assets/images/backgrounds/background/bg_156.gif'); + } + &.card-background-157 { + background-image: url('@/assets/images/backgrounds/background/bg_157.gif'); + } + &.card-background-158 { + background-image: url('@/assets/images/backgrounds/background/bg_158.gif'); + } + &.card-background-159 { + background-image: url('@/assets/images/backgrounds/background/bg_159.gif'); + } + &.card-background-160 { + background-image: url('@/assets/images/backgrounds/background/bg_160.gif'); + } + &.card-background-161 { + background-image: url('@/assets/images/backgrounds/background/bg_161.gif'); + } + &.card-background-162 { + background-image: url('@/assets/images/backgrounds/background/bg_162.gif'); + } + &.card-background-163 { + background-image: url('@/assets/images/backgrounds/background/bg_163.gif'); + } + &.card-background-164 { + background-image: url('@/assets/images/backgrounds/background/bg_164.gif'); + } + &.card-background-165 { + background-image: url('@/assets/images/backgrounds/background/bg_165.gif'); + } + &.card-background-166 { + background-image: url('@/assets/images/backgrounds/background/bg_166.gif'); + } + &.card-background-167 { + background-image: url('@/assets/images/backgrounds/background/bg_167.gif'); + } + &.card-background-168 { + background-image: url('@/assets/images/backgrounds/background/bg_168.gif'); + } + &.card-background-169 { + background-image: url('@/assets/images/backgrounds/background/bg_169.gif'); + } + &.card-background-170 { + background-image: url('@/assets/images/backgrounds/background/bg_170.png'); + } + &.card-background-171 { + background-image: url('@/assets/images/backgrounds/background/bg_171.png'); + } + &.card-background-172 { + background-image: url('@/assets/images/backgrounds/background/bg_172.png'); + } + &.card-background-173 { + background-image: url('@/assets/images/backgrounds/background/bg_173.png'); + } + &.card-background-174 { + background-image: url('@/assets/images/backgrounds/background/bg_174.png'); + } + &.card-background-175 { + background-image: url('@/assets/images/backgrounds/background/bg_175.png'); + } + &.card-background-176 { + background-image: url('@/assets/images/backgrounds/background/bg_176.png'); + } + &.card-background-177 { + background-image: url('@/assets/images/backgrounds/background/bg_177.gif'); + } + &.card-background-178 { + background-image: url('@/assets/images/backgrounds/background/bg_178.png'); + } + &.card-background-179 { + background-image: url('@/assets/images/backgrounds/background/bg_179.png'); + } + &.card-background-180 { + background-image: url('@/assets/images/backgrounds/background/bg_180.png'); + } + &.card-background-181 { + background-image: url('@/assets/images/backgrounds/background/bg_181.png'); + } + &.card-background-182 { + background-image: url('@/assets/images/backgrounds/background/bg_182.png'); + } + &.card-background-183 { + background-image: url('@/assets/images/backgrounds/background/bg_183.png'); + } + &.card-background-184 { + background-image: url('@/assets/images/backgrounds/background/bg_184.png'); + } + &.card-background-185 { + background-image: url('@/assets/images/backgrounds/background/bg_185.png'); + } + &.card-background-186 { + background-image: url('@/assets/images/backgrounds/background/bg_186.png'); + } + &.card-background-187 { + background-image: url('@/assets/images/backgrounds/background/bg_187.gif'); + } +} + +.profile-card-background.card-background-1 { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-2 { + background: linear-gradient(135deg, #4ecdc4 0%, #44a8a3 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-3 { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-4 { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-5 { + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-6 { + background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-7 { + background: linear-gradient(135deg, #5ee7df 0%, #b490ca 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-8 { + background: linear-gradient(135deg, #243949 0%, #517fa4 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-9 { + background-image: repeating-linear-gradient(45deg, #ff6b9d 0 10px, #c06c84 10px 20px); + background-color: #c06c84; + background-size: auto; +} +.profile-card-background.card-background-10 { + background-image: repeating-linear-gradient(90deg, #2b5876 0 8px, #4e4376 8px 16px); + background-color: #2b5876; + background-size: auto; +} +.profile-card-background.card-background-11 { + background-image: radial-gradient(circle, #ffd54f 1.5px, transparent 2px); + background-color: #2c3e50; + background-size: 12px 12px; + background-repeat: repeat; +} +.profile-card-background.card-background-12 { + background-image: linear-gradient(45deg, #1a1a2e 25%, transparent 25%), linear-gradient(-45deg, #1a1a2e 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #1a1a2e 75%), linear-gradient(-45deg, transparent 75%, #1a1a2e 75%); + background-color: #16213e; + background-size: 16px 16px; + background-position: 0 0, 0 8px, 8px -8px, -8px 0; + background-repeat: repeat; +} +.profile-card-background.card-background-13 { + background: linear-gradient(135deg, #232526 0%, #414345 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-14 { + background: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%); + background-repeat: no-repeat; + background-size: cover; +} +.profile-card-background.card-background-15 { + background-image: linear-gradient(0deg, transparent 49%, rgba(255,255,255,0.08) 49% 51%, transparent 51%), linear-gradient(90deg, transparent 49%, rgba(255,255,255,0.08) 49% 51%, transparent 51%); + background-color: #1a1a2e; + background-size: 24px 24px; + background-repeat: repeat; +} + .profile-background { background-repeat: no-repeat; background-position: center; diff --git a/src/css/icons/icons.css b/src/css/icons/icons.css index 62120ea..54cced7 100644 --- a/src/css/icons/icons.css +++ b/src/css/icons/icons.css @@ -4,10 +4,6 @@ background-position: center; background-repeat: no-repeat; outline: 0; - image-rendering: -webkit-optimize-contrast !important; - image-rendering: -moz-crisp-edges !important; - image-rendering: crisp-edges !important; - image-rendering: pixelated !important; } .nitro-icon:hover { @@ -147,6 +143,15 @@ height: 30px; } +.nitro-icon.icon-me-badge-creator { + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: center; + background-size: 30px 30px; + width: 32px; + height: 32px; +} + .nitro-icon.icon-me-settings { background-image: url("@/assets/images/toolbar/icons/me-menu/cog.png"); width: 28px;