feat: Add daemon startup overlay and status hook

Co-authored-by: ijamespine <ijamespine@me.com>
This commit is contained in:
Cursor Agent
2025-12-24 16:58:56 +00:00
parent 9888a4cbb3
commit 940bd90a8f
4 changed files with 120 additions and 3 deletions

BIN
bun.lockb
View File

Binary file not shown.

View File

@@ -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}

View 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>
);
}

View File

@@ -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,
}));
}
});