mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 22:30:40 -07:00
Enhance App component with loading messages and UI improvements
- 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.
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"framer-motion": "^12.29.0",
|
||||
"lucide-react": "^0.454.0",
|
||||
"motion": "^12.29.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-hook-form": "^7.53.0",
|
||||
|
||||
+58
-11
@@ -1,24 +1,49 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
||||
import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
|
||||
import { GenerationForm } from '@/components/Generation/GenerationForm';
|
||||
import { HistoryTable } from '@/components/History/HistoryTable';
|
||||
import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
|
||||
import { ModelManagement } from '@/components/ServerSettings/ModelManagement';
|
||||
import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
|
||||
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
|
||||
import { ModelManagement } from '@/components/ServerSettings/ModelManagement';
|
||||
import ShinyText from '@/components/ShinyText';
|
||||
import { Sidebar } from '@/components/Sidebar';
|
||||
import { UpdateNotification } from '@/components/UpdateNotification';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
|
||||
import { Sidebar } from '@/components/Sidebar';
|
||||
import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
|
||||
import { UpdateNotification } from '@/components/UpdateNotification';
|
||||
import { isTauri, startServer, setupWindowCloseHandler } from '@/lib/tauri';
|
||||
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
||||
import { isTauri, setupWindowCloseHandler, startServer } from '@/lib/tauri';
|
||||
|
||||
// Track if server is starting to prevent duplicate starts
|
||||
let serverStarting = false;
|
||||
|
||||
const LOADING_MESSAGES = [
|
||||
'Warming up tensors...',
|
||||
'Calibrating synthesizer engine...',
|
||||
'Initializing voice models...',
|
||||
'Loading neural networks...',
|
||||
'Preparing audio pipelines...',
|
||||
'Optimizing waveform generators...',
|
||||
'Tuning frequency analyzers...',
|
||||
'Building voice embeddings...',
|
||||
'Configuring text-to-speech cores...',
|
||||
'Syncing audio buffers...',
|
||||
'Establishing model connections...',
|
||||
'Preprocessing training data...',
|
||||
'Validating voice samples...',
|
||||
'Compiling inference engines...',
|
||||
'Mapping phoneme sequences...',
|
||||
'Aligning prosody parameters...',
|
||||
'Activating speech synthesis...',
|
||||
'Fine-tuning acoustic models...',
|
||||
'Preparing voice cloning matrices...',
|
||||
'Initializing Qwen TTS framework...',
|
||||
];
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState('main');
|
||||
const [serverReady, setServerReady] = useState(false);
|
||||
const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
|
||||
|
||||
// Setup window close handler and auto-start server when running in Tauri (production only)
|
||||
useEffect(() => {
|
||||
@@ -74,21 +99,43 @@ function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Cycle through loading messages every 3 seconds
|
||||
useEffect(() => {
|
||||
if (!isTauri() || serverReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setLoadingMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length);
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [serverReady]);
|
||||
|
||||
// Show loading screen while server is starting in Tauri
|
||||
if (isTauri() && !serverReady) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center space-y-6">
|
||||
<div className="flex justify-center">
|
||||
<div className="flex justify-center relative">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-48 h-48 rounded-full bg-accent/20 blur-3xl" />
|
||||
</div>
|
||||
<img
|
||||
src={voiceboxLogo}
|
||||
alt="Voicebox"
|
||||
className="w-16 h-16 object-contain animate-fade-in-scale"
|
||||
className="w-48 h-48 object-contain animate-fade-in-scale relative z-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="animate-fade-in-delayed">
|
||||
<ShinyText
|
||||
text={LOADING_MESSAGES[loadingMessageIndex]}
|
||||
className="text-lg font-medium text-muted-foreground"
|
||||
speed={2}
|
||||
color="hsl(var(--muted-foreground))"
|
||||
shineColor="hsl(var(--foreground))"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-lg animate-fade-in-delayed">
|
||||
Starting voicebox...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
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: [],
|
||||
// };
|
||||
@@ -42,6 +42,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"framer-motion": "^12.29.0",
|
||||
"lucide-react": "^0.454.0",
|
||||
"motion": "^12.29.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-hook-form": "^7.53.0",
|
||||
@@ -859,6 +860,8 @@
|
||||
|
||||
"minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"motion": ["[email protected]", "", { "dependencies": { "framer-motion": "^12.29.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rjB5CP2N9S2ESAyEFnAFMgTec6X8yvfxLNcz8n12gPq3M48R7ZbBeVYkDOTj8SPMwfvGIFI801SiPSr1+HCr9g=="],
|
||||
|
||||
"motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="],
|
||||
|
||||
"motion-utils": ["[email protected]", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="],
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user