Skip to main content

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()
  }}
/>

Time Formatting

Format Seconds to Timestamp

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

KeyAction
Enter / RResume playback
SStart over from beginning
EscClose 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 Labels

Resume Button

<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

Start Over Button

<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

Build docs developers (and LLMs) love