mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 21:36:56 -04:00
feat: Add daemon startup overlay and status hook
Co-authored-by: ijamespine <ijamespine@me.com>
This commit is contained in:
@@ -49,6 +49,8 @@ import { useState } from "react";
|
||||
import type { File } from "@sd/ts-client";
|
||||
import { File as FileComponent } from "./components/Explorer/File";
|
||||
import { DaemonDisconnectedOverlay } from "./components/DaemonDisconnectedOverlay";
|
||||
import { DaemonStartupOverlay } from "./components/DaemonStartupOverlay";
|
||||
import { useDaemonStatus } from "./hooks/useDaemonStatus";
|
||||
import { useFileOperationDialog } from "./components/FileOperationModal";
|
||||
import { House, Clock, Heart, Folders } from "@phosphor-icons/react";
|
||||
|
||||
@@ -828,6 +830,28 @@ export function ExplorerLayout() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* DaemonOverlays - Manages which daemon overlay to show
|
||||
*
|
||||
* Shows startup overlay during initial app launch while waiting for daemon.
|
||||
* Shows disconnected overlay if daemon disconnects after initial connection.
|
||||
*/
|
||||
function DaemonOverlays() {
|
||||
const { isConnected, isStarting } = useDaemonStatus();
|
||||
|
||||
// Show startup overlay during initial launch (isStarting=true, not connected)
|
||||
// Show disconnected overlay if daemon disconnects after we were connected (isStarting=false, not connected)
|
||||
const showStartup = isStarting && !isConnected;
|
||||
const showDisconnected = !isStarting && !isConnected;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DaemonStartupOverlay show={showStartup} />
|
||||
{showDisconnected && <DaemonDisconnectedOverlay />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Explorer({ client }: AppProps) {
|
||||
const router = createExplorerRouter();
|
||||
|
||||
@@ -837,7 +861,7 @@ export function Explorer({ client }: AppProps) {
|
||||
<DndWrapper>
|
||||
<RouterProvider router={router} />
|
||||
</DndWrapper>
|
||||
<DaemonDisconnectedOverlay />
|
||||
<DaemonOverlays />
|
||||
<Dialogs />
|
||||
<ReactQueryDevtools
|
||||
initialIsOpen={false}
|
||||
|
||||
70
packages/interface/src/components/DaemonStartupOverlay.tsx
Normal file
70
packages/interface/src/components/DaemonStartupOverlay.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { AppLogo } from "@sd/assets/images";
|
||||
|
||||
export function DaemonStartupOverlay({ show }: { show: boolean }) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{show && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-app"
|
||||
>
|
||||
{/* Logo with subtle pulse animation */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="relative"
|
||||
>
|
||||
<img
|
||||
src={AppLogo}
|
||||
alt="Spacedrive"
|
||||
className="size-24 select-none"
|
||||
draggable={false}
|
||||
/>
|
||||
|
||||
{/* Subtle glow effect behind logo */}
|
||||
<div className="absolute inset-0 -z-10 blur-3xl">
|
||||
<div className="size-full rounded-full bg-accent/20" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Loading indicator */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="mt-8 flex flex-col items-center gap-4"
|
||||
>
|
||||
{/* Animated dots loader */}
|
||||
<div className="flex gap-1.5">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="size-2 rounded-full bg-accent"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.5, 1, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
delay: i * 0.15,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-ink-dull">
|
||||
Starting Spacedrive...
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,27 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { usePlatform } from '../platform';
|
||||
|
||||
export interface DaemonStatus {
|
||||
isConnected: boolean;
|
||||
isChecking: boolean;
|
||||
isInstalled: boolean;
|
||||
/** True during initial app startup while waiting for daemon. Once connected, stays false. */
|
||||
isStarting: boolean;
|
||||
}
|
||||
|
||||
export function useDaemonStatus() {
|
||||
const platform = usePlatform();
|
||||
// For Tauri, start in "starting" state. For web, assume connected.
|
||||
const isTauri = platform.platform === 'tauri';
|
||||
const [status, setStatus] = useState<DaemonStatus>({
|
||||
isConnected: true,
|
||||
isConnected: !isTauri, // Web is always "connected", Tauri starts disconnected
|
||||
isChecking: false,
|
||||
isInstalled: false,
|
||||
isStarting: isTauri, // Only Tauri starts in "starting" state
|
||||
});
|
||||
|
||||
// Track if we've ever been connected - once connected, isStarting stays false
|
||||
const hasEverConnected = useRef(!isTauri);
|
||||
|
||||
useEffect(() => {
|
||||
if (platform.platform !== 'tauri') {
|
||||
@@ -31,11 +39,18 @@ export function useDaemonStatus() {
|
||||
const daemonStatus = await platform.getDaemonStatus?.();
|
||||
if (mounted) {
|
||||
const isRunning = daemonStatus?.is_running ?? false;
|
||||
|
||||
if (isRunning) {
|
||||
hasEverConnected.current = true;
|
||||
}
|
||||
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
isConnected: isRunning,
|
||||
// Only clear isChecking if we're connected (daemon started successfully)
|
||||
isChecking: isRunning ? false : prev.isChecking,
|
||||
// Clear isStarting once we're connected
|
||||
isStarting: isRunning ? false : prev.isStarting,
|
||||
}));
|
||||
|
||||
// Clear polling if daemon is back online
|
||||
@@ -59,10 +74,12 @@ export function useDaemonStatus() {
|
||||
const unlistenConnected = await platform.onDaemonConnected?.(() => {
|
||||
console.log('[useDaemonStatus] daemon-connected event received');
|
||||
if (mounted) {
|
||||
hasEverConnected.current = true;
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
isConnected: true,
|
||||
isChecking: false,
|
||||
isStarting: false, // No longer starting once connected
|
||||
}));
|
||||
|
||||
// Stop polling when connected
|
||||
@@ -80,6 +97,9 @@ export function useDaemonStatus() {
|
||||
...prev,
|
||||
isConnected: false,
|
||||
isChecking: false,
|
||||
// If we were ever connected before, this is a disconnection, not startup
|
||||
// Keep isStarting as is - only clear it on connect
|
||||
isStarting: hasEverConnected.current ? false : prev.isStarting,
|
||||
}));
|
||||
|
||||
// Start polling when disconnected
|
||||
@@ -95,6 +115,9 @@ export function useDaemonStatus() {
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
isChecking: true,
|
||||
// If daemon is starting (e.g., user clicked restart), show startup state
|
||||
// But only if we haven't connected yet in this session
|
||||
isStarting: !hasEverConnected.current,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user