mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
Merge remote-tracking branch 'duckie/main' into merge-duckie-main-2026-05-06
# Conflicts: # index.html # public/UITexts.example # public/renderer-config.example # src/App.tsx # src/components/login/LoginView.tsx # src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx # src/components/toolbar/ToolbarView.tsx # src/components/user-profile/UserContainerView.tsx
This commit is contained in:
+127
-14
@@ -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<HTMLDivElement>, 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<SliderProps> = 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 <Flex fullWidth gap={ 1 } classNames={ [ 'nitro-slider-wrapper' ] }>
|
||||
{ !disabledButton && <Button classNames={ [ 'nitro-slider-button', 'nitro-slider-button-left' ] } disabled={ minimum >= currentValue } onClick={ () => onChange(roundToStep(minimum < currentValue ? currentValue - buttonStep : minimum), 0) }><FaAngleLeft /></Button> }
|
||||
<ReactSlider className={ 'nitro-slider' } max={ max } min={ min } step={ step } value={ value } onChange={ onChange } { ...rest } />
|
||||
{ !disabledButton && <Button classNames={ [ 'nitro-slider-button', 'nitro-slider-button-right' ] } disabled={ maximum <= currentValue } onClick={ () => onChange(roundToStep(maximum > currentValue ? currentValue + buttonStep : maximum), 0) }><FaAngleRight /></Button> }
|
||||
</Flex>;
|
||||
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<HTMLDivElement> = {
|
||||
key: i,
|
||||
className: cn('thumb', `thumb-${ i }`, thumbClassName)
|
||||
};
|
||||
|
||||
const state: SliderThumbState = {
|
||||
index: i,
|
||||
value: isRange ? valueArr : currentValue,
|
||||
valueNow: valueArr[i] ?? 0
|
||||
};
|
||||
|
||||
return (
|
||||
<RadixSlider.Thumb key={ i } asChild>
|
||||
{ renderThumb ? renderThumb(baseProps, state) : <div { ...baseProps } /> }
|
||||
</RadixSlider.Thumb>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex fullWidth gap={ 1 } classNames={ [ 'nitro-slider-wrapper' ] }>
|
||||
{ !disabledButton && (
|
||||
<Button classNames={ [ 'nitro-slider-button', 'nitro-slider-button-left' ] } disabled={ disabled || (minimum >= currentValue) } onClick={ stepDown }>
|
||||
<FaAngleLeft />
|
||||
</Button>
|
||||
) }
|
||||
<RadixSlider.Root
|
||||
inverted={ invert }
|
||||
disabled={ disabled }
|
||||
className={ cn('nitro-slider', 'relative', 'min-w-0', 'grow', className) }
|
||||
style={ style }
|
||||
max={ max }
|
||||
min={ min }
|
||||
step={ step }
|
||||
value={ value !== undefined ? valueArr : undefined }
|
||||
defaultValue={ defaultValue !== undefined ? toArray(defaultValue) : undefined }
|
||||
onValueChange={ emit }>
|
||||
<RadixSlider.Track className={ cn('track', 'track-1', 'grow', trackClassName) }>
|
||||
<RadixSlider.Range className={ cn('track', 'track-0', trackClassName) } />
|
||||
</RadixSlider.Track>
|
||||
{ valueArr.map((_, i) => renderThumbElement(i)) }
|
||||
</RadixSlider.Root>
|
||||
{ !disabledButton && (
|
||||
<Button classNames={ [ 'nitro-slider-button', 'nitro-slider-button-right' ] } disabled={ disabled || (maximum <= currentValue) } onClick={ stepUp }>
|
||||
<FaAngleRight />
|
||||
</Button>
|
||||
) }
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<LayoutBadgeImageViewProps> = props =>
|
||||
{
|
||||
const { badgeCode = null, isGroup = false, showInfo = false, customTitle = null, isGrayscale = false, scale = 1, classNames = [], style = {}, children = null, ...rest } = props;
|
||||
const [ imageElement, setImageElement ] = useState<HTMLImageElement>(null);
|
||||
const [ tooltipPosition, setTooltipPosition ] = useState<{ top: number; left: number } | null>(null);
|
||||
const badgeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const tooltipsEnabled = showInfo && GetConfigurationValue<boolean>('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<LayoutBadgeImageViewProps> = props =>
|
||||
}, [ badgeCode, isGroup ]);
|
||||
|
||||
return (
|
||||
<Base className="group" classNames={ getClassNames } style={ getStyle } { ...rest }>
|
||||
{ (showInfo && GetConfigurationValue<boolean>('badge.descriptions.enabled', true)) &&
|
||||
<Base className="hidden group-hover:block before:absolute before:content-['_'] before:w-0 before:h-0 before:border-l-10! before:border-b-10! before:border-t-10! before:top-[10px] before:-right-[10px] before:border-l-[white] before:border-t-transparent before:border-b-transparent z-50 absolute pointer-events-none select-none w-[210px] rounded-[.25rem] bg-[#fff] -left-[220px] text-black py-1 px-2 small">
|
||||
<Base
|
||||
innerRef={ badgeRef }
|
||||
classNames={ getClassNames }
|
||||
style={ getStyle }
|
||||
onMouseEnter={ tooltipsEnabled ? showTooltip : undefined }
|
||||
onMouseLeave={ tooltipsEnabled ? hideTooltip : undefined }
|
||||
{ ...rest }>
|
||||
{ tooltipsEnabled && tooltipPosition && createPortal(
|
||||
<div
|
||||
className="fixed z-[9999] pointer-events-none select-none w-[210px] rounded-[.25rem] bg-[#fff] text-black py-1 px-2 small"
|
||||
style={ { top: tooltipPosition.top, left: tooltipPosition.left } }>
|
||||
<div className="font-bold mb-1">{ isGroup ? customTitle : LocalizeBadgeName(badgeCode) }</div>
|
||||
<div>{ isGroup ? LocalizeText('group.badgepopup.body') : LocalizeBadgeDescription(badgeCode) }</div>
|
||||
</Base> }
|
||||
</div>,
|
||||
document.body
|
||||
) }
|
||||
{ children }
|
||||
</Base>
|
||||
);
|
||||
|
||||
@@ -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<TransitionAnimationProps> = props =>
|
||||
{
|
||||
const { type = null, inProp = false, timeout = 300, className = null, children = null } = props;
|
||||
|
||||
const [ isChildrenVisible, setChildrenVisible ] = useState(false);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
let timeoutData: ReturnType<typeof setTimeout> = 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 (
|
||||
<Transition in={ inProp } timeout={ timeout }>
|
||||
{ state => (
|
||||
<div className={ (className ?? '') + ' animate__animated' } style={ { ...getTransitionAnimationStyle(type, state, timeout) } }>
|
||||
{ isChildrenVisible && children }
|
||||
</div>
|
||||
<AnimatePresence initial={ false }>
|
||||
{ inProp && (
|
||||
<motion.div
|
||||
className={ className ?? '' }
|
||||
variants={ variants }
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
transition={ { duration } }>
|
||||
{ children }
|
||||
</motion.div>
|
||||
) }
|
||||
</Transition>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<CSSProperties>
|
||||
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: {}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user