mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 12:50:42 -07:00
- Added a loading message feature that cycles through various messages while the server is starting in Tauri. - Improved the loading screen UI with a new layout and animations for the voicebox logo and loading text. - Refactored the App component to include necessary imports and state management for loading messages. - Updated styles for better visual appeal and user experience during the loading phase.
135 lines
3.9 KiB
TypeScript
135 lines
3.9 KiB
TypeScript
import { motion, useAnimationFrame, useMotionValue, useTransform } from 'motion/react';
|
|
import type React from 'react';
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
interface ShinyTextProps {
|
|
text: string;
|
|
disabled?: boolean;
|
|
speed?: number;
|
|
className?: string;
|
|
color?: string;
|
|
shineColor?: string;
|
|
spread?: number;
|
|
yoyo?: boolean;
|
|
pauseOnHover?: boolean;
|
|
direction?: 'left' | 'right';
|
|
delay?: number;
|
|
}
|
|
|
|
const ShinyText: React.FC<ShinyTextProps> = ({
|
|
text,
|
|
disabled = false,
|
|
speed = 2,
|
|
className = '',
|
|
color = '#b5b5b5',
|
|
shineColor = '#ffffff',
|
|
spread = 120,
|
|
yoyo = false,
|
|
pauseOnHover = false,
|
|
direction = 'left',
|
|
delay = 0,
|
|
}) => {
|
|
const [isPaused, setIsPaused] = useState(false);
|
|
const progress = useMotionValue(0);
|
|
const elapsedRef = useRef(0);
|
|
const lastTimeRef = useRef<number | null>(null);
|
|
const directionRef = useRef(direction === 'left' ? 1 : -1);
|
|
|
|
const animationDuration = speed * 1000;
|
|
const delayDuration = delay * 1000;
|
|
|
|
useAnimationFrame((time) => {
|
|
if (disabled || isPaused) {
|
|
lastTimeRef.current = null;
|
|
return;
|
|
}
|
|
|
|
if (lastTimeRef.current === null) {
|
|
lastTimeRef.current = time;
|
|
return;
|
|
}
|
|
|
|
const deltaTime = time - lastTimeRef.current;
|
|
lastTimeRef.current = time;
|
|
|
|
elapsedRef.current += deltaTime;
|
|
|
|
// Animation goes from 0 to 100
|
|
if (yoyo) {
|
|
const cycleDuration = animationDuration + delayDuration;
|
|
const fullCycle = cycleDuration * 2;
|
|
const cycleTime = elapsedRef.current % fullCycle;
|
|
|
|
if (cycleTime < animationDuration) {
|
|
// Forward animation: 0 -> 100
|
|
const p = (cycleTime / animationDuration) * 100;
|
|
progress.set(directionRef.current === 1 ? p : 100 - p);
|
|
} else if (cycleTime < cycleDuration) {
|
|
// Delay at end
|
|
progress.set(directionRef.current === 1 ? 100 : 0);
|
|
} else if (cycleTime < cycleDuration + animationDuration) {
|
|
// Reverse animation: 100 -> 0
|
|
const reverseTime = cycleTime - cycleDuration;
|
|
const p = 100 - (reverseTime / animationDuration) * 100;
|
|
progress.set(directionRef.current === 1 ? p : 100 - p);
|
|
} else {
|
|
// Delay at start
|
|
progress.set(directionRef.current === 1 ? 0 : 100);
|
|
}
|
|
} else {
|
|
const cycleDuration = animationDuration + delayDuration;
|
|
const cycleTime = elapsedRef.current % cycleDuration;
|
|
|
|
if (cycleTime < animationDuration) {
|
|
// Animation phase: 0 -> 100
|
|
const p = (cycleTime / animationDuration) * 100;
|
|
progress.set(directionRef.current === 1 ? p : 100 - p);
|
|
} else {
|
|
// Delay phase - hold at end (shine off-screen)
|
|
progress.set(directionRef.current === 1 ? 100 : 0);
|
|
}
|
|
}
|
|
});
|
|
|
|
useEffect(() => {
|
|
directionRef.current = direction === 'left' ? 1 : -1;
|
|
elapsedRef.current = 0;
|
|
progress.set(0);
|
|
// eslint-d, progress.setisable-next-line react-hooks/exhaustive-deps
|
|
}, [direction]);
|
|
|
|
// Transform: p=0 -> 150% (shine off right), p=100 -> -50% (shine off left)
|
|
const backgroundPosition = useTransform(progress, (p) => `${150 - p * 2}% center`);
|
|
|
|
const handleMouseEnter = useCallback(() => {
|
|
if (pauseOnHover) setIsPaused(true);
|
|
}, [pauseOnHover]);
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
if (pauseOnHover) setIsPaused(false);
|
|
}, [pauseOnHover]);
|
|
|
|
const gradientStyle: React.CSSProperties = {
|
|
backgroundImage: `linear-gradient(${spread}deg, ${color} 0%, ${color} 35%, ${shineColor} 50%, ${color} 65%, ${color} 100%)`,
|
|
backgroundSize: '200% auto',
|
|
WebkitBackgroundClip: 'text',
|
|
backgroundClip: 'text',
|
|
WebkitTextFillColor: 'transparent',
|
|
};
|
|
|
|
return (
|
|
<motion.span
|
|
className={`inline-block ${className}`}
|
|
style={{ ...gradientStyle, backgroundPosition }}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
{text}
|
|
</motion.span>
|
|
);
|
|
};
|
|
|
|
export default ShinyText;
|
|
// plugins: [],
|
|
// };
|