Documentation Index
Fetch the complete documentation index at: https://mintlify.com/SlasshyOverhere/StreamVault/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The ResumeDialog component prompts users to resume playback from their last position or start over. It displays progress information with a visual timeline and supports keyboard shortcuts.
Component Interface
interface ResumeDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
title: string
mediaType: 'movie' | 'tvshow' | 'tvepisode'
seasonEpisode?: string // e.g., "S02E05"
currentPosition: number // in seconds
duration: number // in seconds
posterUrl?: string
onResume: () => void
onStartOver: () => void
isStreaming?: boolean // Shows different UI for streaming
}
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:14-26
Features
- Progress Visualization: Animated progress bar with percentage and time remaining
- Keyboard Shortcuts:
Enter/R to resume, S to start over
- Streaming Mode: Simplified UI when progress is in browser localStorage
- Time Formatting: Human-readable timestamps (HH:MM:SS)
- Poster Background: Blurred poster image backdrop
Usage
Basic Resume Dialog
import { ResumeDialog } from '@/components/ResumeDialog'
import { useState } from 'react'
function MediaCard({ media }) {
const [showResume, setShowResume] = useState(false)
const hasProgress = media.current_position > 0
const handlePlay = () => {
if (hasProgress) {
setShowResume(true)
} else {
startPlayback(0)
}
}
const handleResume = () => {
startPlayback(media.current_position)
}
const handleStartOver = () => {
startPlayback(0)
}
return (
<>
<button onClick={handlePlay}>Play</button>
<ResumeDialog
open={showResume}
onOpenChange={setShowResume}
title={media.title}
mediaType="movie"
currentPosition={media.current_position}
duration={media.duration}
posterUrl={media.poster_url}
onResume={handleResume}
onStartOver={handleStartOver}
/>
</>
)
}
TV Show Episode
<ResumeDialog
open={showResume}
onOpenChange={setShowResume}
title="Breaking Bad"
mediaType="tvepisode"
seasonEpisode="S02E05"
currentPosition={1245} // 20:45
duration={2700} // 45:00
posterUrl="https://image.tmdb.org/..."
onResume={handleResume}
onStartOver={handleStartOver}
/>
Streaming Mode
For cloud streaming where progress is in browser:
<ResumeDialog
open={showResume}
onOpenChange={setShowResume}
title="Movie Title"
mediaType="movie"
currentPosition={0} // Not used in streaming mode
duration={0} // Not used in streaming mode
isStreaming={true} // Simplifies UI
onResume={() => {
// Player resumes from browser localStorage
openVideasyPlayer()
}}
onStartOver={() => {
// Clear browser localStorage and start fresh
clearStreamingProgress()
openVideasyPlayer()
}}
/>
const formatTime = (seconds: number): string => {
const hrs = Math.floor(seconds / 3600)
const mins = Math.floor((seconds % 3600) / 60)
const secs = Math.floor(seconds % 60)
if (hrs > 0) {
return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
return `${mins}:${secs.toString().padStart(2, '0')}`
}
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:28-38
Examples:
formatTime(125) → "2:05"
formatTime(3665) → "1:01:05"
Format Time Remaining
const formatTimeRemaining = (current: number, total: number): string => {
const remaining = total - current
const mins = Math.floor(remaining / 60)
if (mins < 1) return 'less than a minute'
if (mins === 1) return '1 minute'
if (mins < 60) return `${mins} minutes`
const hrs = Math.floor(mins / 60)
const remMins = mins % 60
if (hrs === 1) {
return remMins > 0 ? `1 hour ${remMins} mins` : '1 hour'
}
return remMins > 0 ? `${hrs} hours ${remMins} mins` : `${hrs} hours`
}
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:41-55
Examples:
formatTimeRemaining(1800, 3600) → "30 minutes"
formatTimeRemaining(1800, 7200) → "1 hour 30 mins"
Progress Visualization
Progress Calculation
const progressPercent = duration > 0
? (currentPosition / duration) * 100
: 0
const timeRemaining = formatTimeRemaining(currentPosition, duration)
const hasProgressData = duration > 0 && currentPosition > 0
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:70-72
Animated Progress Bar
<motion.div
initial={{ width: 0 }}
animate={{ width: `${Math.min(progressPercent, 100)}%` }}
transition={{ duration: 0.8, ease: [0.4, 0, 0.2, 1] }}
className="absolute inset-y-0 left-0 bg-gradient-to-r from-gray-500 via-gray-400 to-gray-300 rounded-full"
>
{/* Glow effect */}
<div className="absolute inset-0 bg-gradient-to-r from-gray-500 via-gray-400 to-gray-300 blur-sm opacity-60" />
{/* Shine animation */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer" />
</motion.div>
{/* Progress dot */}
<motion.div
initial={{ left: 0 }}
animate={{ left: `${Math.min(progressPercent, 100)}%` }}
transition={{ duration: 0.8, ease: [0.4, 0, 0.2, 1] }}
className="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-4 h-4 rounded-full bg-white shadow-lg shadow-gray-500/50"
/>
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:167-184
Keyboard Shortcuts
| Key | Action |
|---|
Enter / R | Resume playback |
S | Start over from beginning |
Esc | Close dialog (via Dialog component) |
Implementation
useEffect(() => {
if (!open) return
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === 'r' || e.key === 'R') {
e.preventDefault()
handleResume()
} else if (e.key === 's' || e.key === 'S') {
e.preventDefault()
handleStartOver()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [open])
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:85-100
UI Modes
With Progress Data
Shows detailed progress visualization:
- Current time / Total duration
- Animated progress bar
- Percentage watched
- Time remaining
{hasProgressData && !isStreaming ? (
<motion.div className="...">
{/* Time display */}
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-white">
{formatTime(currentPosition)}
</span>
<span className="text-white/40">/</span>
<span className="text-lg text-white/50">
{formatTime(duration)}
</span>
</div>
{/* Progress bar */}
<div className="relative h-3 bg-white/10 rounded-full">
{/* ... animated bar ... */}
</div>
{/* Stats */}
<div className="flex justify-between text-sm">
<span>{progressPercent.toFixed(0)}% watched</span>
<span>{timeRemaining} remaining</span>
</div>
</motion.div>
) : (
// Streaming or no progress - simple UI
)}
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:134-198
Streaming Mode
Simplified UI for cloud streaming:
- Play icon instead of progress bar
- Generic “You’ve watched this before” message
- Note about browser localStorage
<motion.div className="...">
<div className="flex flex-col items-center gap-4">
<div className="p-4 rounded-2xl bg-gradient-to-br from-gray-500/20 to-gray-500/20">
<Play className="h-8 w-8 text-gray-400 fill-gray-400" />
</div>
<p className="text-white font-medium text-lg">
You've watched this before
</p>
{isStreaming && (
<p className="text-xs text-white/40">
Your progress is saved in your browser
</p>
)}
</div>
</motion.div>
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:201-226
<Button onClick={handleResume}>
<Play className="h-4 w-4 fill-current" />
{hasProgressData && !isStreaming
? `Resume at ${formatTime(currentPosition)}`
: 'Continue Watching'
}
<kbd>R</kbd>
</Button>
Shows:
- With progress: “Resume at 12:34”
- Streaming: “Continue Watching”
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:249-258
<Button variant="outline" onClick={handleStartOver}>
<RotateCcw className="h-4 w-4" />
Start Over
<kbd>S</kbd>
</Button>
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:238-248
Integration Example
Complete Flow
import { useState } from 'react'
import { invoke } from '@tauri-apps/api/tauri'
import { ResumeDialog } from '@/components/ResumeDialog'
import { VideoPlayer } from '@/components/VideoPlayer'
function MoviePlayer({ media }) {
const [showResume, setShowResume] = useState(false)
const [showPlayer, setShowPlayer] = useState(false)
const [resumeFrom, setResumeFrom] = useState(0)
const checkAndPlay = () => {
// Check if media has progress
if (media.current_position > 0 && media.current_position < media.duration * 0.95) {
setShowResume(true)
} else {
startPlayback(0)
}
}
const startPlayback = (position: number) => {
setResumeFrom(position)
setShowPlayer(true)
}
const handleProgress = async (currentTime: number, duration: number) => {
// Save progress to database
await invoke('update_progress', {
mediaId: media.id,
currentTime,
duration
})
}
return (
<>
<button onClick={checkAndPlay}>
Play Movie
</button>
<ResumeDialog
open={showResume}
onOpenChange={setShowResume}
title={media.title}
mediaType="movie"
currentPosition={media.current_position}
duration={media.duration}
posterUrl={media.poster_url}
onResume={() => startPlayback(media.current_position)}
onStartOver={() => startPlayback(0)}
/>
{showPlayer && (
<VideoPlayer
src={media.file_path}
title={media.title}
initialTime={resumeFrom}
onProgress={handleProgress}
onClose={() => setShowPlayer(false)}
/>
)}
</>
)
}
Styling
The dialog uses Tailwind CSS with glassmorphism effects:
<DialogContent className="
sm:max-w-lg
bg-[#0c0a1a]/95
backdrop-blur-2xl
border border-white/10
shadow-[0_0_80px_rgba(255,255,255,0.1)]
rounded-2xl
">
Source: /home/daytona/workspace/source/src/components/ResumeDialog.tsx:104