Skip to main content

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