feat(local-player-launcher): add bookmarking and rating functionality
- Add bookmark toggle button with loading state and server API integration - Implement star rating component with interactive rating updates - Fetch and display current bookmark and rating status on component mount - Improve player launch logic to track which player was launched - Add auto-launch confirmation dialog handling and prevent repeated auto-launch - Enhance UI to display bookmark, rating controls, and video info with styling - Remove Elmedia Player from recommended players list - Refactor player detection and launch functions with useCallback for optimization - Update UnifiedVideoPlayer to pass bookmark and rating handlers and flags to local player launcher component
This commit is contained in:
parent
ac06835850
commit
fd07b25abf
BIN
data/media.db
BIN
data/media.db
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
|
@ -11,8 +11,9 @@ import {
|
|||
PlayCircle,
|
||||
Monitor,
|
||||
Settings,
|
||||
HelpCircle,
|
||||
X
|
||||
X,
|
||||
Bookmark,
|
||||
Star
|
||||
} from 'lucide-react';
|
||||
import { VideoFormat, VideoFile } from '@/lib/video-format-detector';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
@ -22,6 +23,7 @@ import {
|
|||
shouldAutoLaunch,
|
||||
PlayerPreferences
|
||||
} from '@/lib/player-preferences';
|
||||
import { StarRating } from '@/components/star-rating';
|
||||
|
||||
interface LocalPlayerLauncherProps {
|
||||
video: VideoFile;
|
||||
|
|
@ -30,6 +32,11 @@ interface LocalPlayerLauncherProps {
|
|||
onPlayerSelect?: (player: string) => void;
|
||||
formatFileSize?: (bytes: number) => string;
|
||||
className?: string;
|
||||
onBookmark?: (id: number) => Promise<void>;
|
||||
onUnbookmark?: (id: number) => Promise<void>;
|
||||
onRate?: (id: number, rating: number) => Promise<void>;
|
||||
showBookmarks?: boolean;
|
||||
showRatings?: boolean;
|
||||
}
|
||||
|
||||
interface PlayerInfo {
|
||||
|
|
@ -54,15 +61,6 @@ const PLAYER_INFO: Record<string, PlayerInfo> = {
|
|||
protocolUrl: 'vlc://',
|
||||
commandLine: 'vlc'
|
||||
},
|
||||
elmedia: {
|
||||
id: 'elmedia',
|
||||
name: 'Elmedia Player',
|
||||
icon: '🍎',
|
||||
description: 'Advanced media player for macOS with streaming capabilities',
|
||||
platforms: ['macOS'],
|
||||
downloadUrl: 'https://www.elmedia-video-player.com/',
|
||||
commandLine: 'open -a "Elmedia Player"'
|
||||
},
|
||||
potplayer: {
|
||||
id: 'potplayer',
|
||||
name: 'PotPlayer',
|
||||
|
|
@ -146,53 +144,115 @@ export default function LocalPlayerLauncher({
|
|||
onClose,
|
||||
onPlayerSelect,
|
||||
formatFileSize,
|
||||
className
|
||||
className,
|
||||
onBookmark,
|
||||
onUnbookmark,
|
||||
onRate,
|
||||
showBookmarks = true,
|
||||
showRatings = true
|
||||
}: LocalPlayerLauncherProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [detectedPlayers, setDetectedPlayers] = useState<string[]>([]);
|
||||
const [isDetecting, setIsDetecting] = useState(true);
|
||||
const [launchStatus, setLaunchStatus] = useState<'idle' | 'launching' | 'success' | 'error'>('idle');
|
||||
const [launchedPlayerId, setLaunchedPlayerId] = useState<string | null>(null); // Track which player was launched
|
||||
const [preferences, setPreferences] = useState<PlayerPreferences | null>(null);
|
||||
const [showingAutoLaunchConfirm, setShowingAutoLaunchConfirm] = useState(false);
|
||||
const [hasAutoLaunched, setHasAutoLaunched] = useState(false); // Prevent repeated auto-launch
|
||||
|
||||
// Bookmark and rating states
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
const [bookmarkLoading, setBookmarkLoading] = useState(false);
|
||||
const [currentRating, setCurrentRating] = useState(0);
|
||||
const [ratingLoading, setRatingLoading] = useState(false);
|
||||
|
||||
const streamUrl = getPlayerSpecificUrl(video.id, 'vlc'); // Use optimized endpoint
|
||||
const recommendedPlayers = format.recommendedPlayers || ['vlc', 'iina', 'elmedia', 'potplayer'];
|
||||
const recommendedPlayers = format.recommendedPlayers || ['vlc', 'iina', 'potplayer'];
|
||||
|
||||
// Load preferences and check for auto-launch on mount
|
||||
// Check bookmark and rating status on mount
|
||||
useEffect(() => {
|
||||
const prefs = loadPlayerPreferences();
|
||||
setPreferences(prefs);
|
||||
if (video.id) {
|
||||
checkBookmarkStatus();
|
||||
checkRatingStatus();
|
||||
}
|
||||
}, [video.id]);
|
||||
|
||||
// Check bookmark status
|
||||
const checkBookmarkStatus = useCallback(async () => {
|
||||
if (!video.id || !showBookmarks) return;
|
||||
|
||||
const autoLaunchCheck = shouldAutoLaunch();
|
||||
|
||||
if (autoLaunchCheck.autoLaunch && autoLaunchCheck.playerId) {
|
||||
if (autoLaunchCheck.needsConfirmation) {
|
||||
setShowingAutoLaunchConfirm(true);
|
||||
} else {
|
||||
// Auto-launch immediately
|
||||
handlePlayerLaunch(autoLaunchCheck.playerId, true);
|
||||
try {
|
||||
const response = await fetch(`/api/bookmarks/${video.id}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setIsBookmarked(data.isBookmarked || false);
|
||||
}
|
||||
} else {
|
||||
// Detect available players normally
|
||||
detectAvailablePlayers();
|
||||
} catch (error) {
|
||||
console.error('Error checking bookmark status:', error);
|
||||
}
|
||||
}, []);
|
||||
}, [video.id, showBookmarks]);
|
||||
|
||||
// Handle auto-launch confirmation
|
||||
const handleConfirmAutoLaunch = () => {
|
||||
const autoLaunchCheck = shouldAutoLaunch();
|
||||
if (autoLaunchCheck.playerId) {
|
||||
setShowingAutoLaunchConfirm(false);
|
||||
handlePlayerLaunch(autoLaunchCheck.playerId, true);
|
||||
// Check rating status
|
||||
const checkRatingStatus = useCallback(async () => {
|
||||
if (!video.id || !showRatings) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/stars/${video.id}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCurrentRating(data.rating || 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking rating status:', error);
|
||||
}
|
||||
}, [video.id, showRatings]);
|
||||
|
||||
// Handle bookmark toggle
|
||||
const handleBookmarkToggle = useCallback(async () => {
|
||||
if (!video.id || bookmarkLoading) return;
|
||||
|
||||
setBookmarkLoading(true);
|
||||
try {
|
||||
if (isBookmarked) {
|
||||
await onUnbookmark?.(video.id);
|
||||
setIsBookmarked(false);
|
||||
} else {
|
||||
await onBookmark?.(video.id);
|
||||
setIsBookmarked(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling bookmark:', error);
|
||||
} finally {
|
||||
setBookmarkLoading(false);
|
||||
}
|
||||
}, [video.id, isBookmarked, bookmarkLoading, onBookmark, onUnbookmark]);
|
||||
|
||||
// Handle rating change
|
||||
const handleRatingChange = useCallback(async (rating: number) => {
|
||||
if (!video.id || ratingLoading) return;
|
||||
|
||||
setRatingLoading(true);
|
||||
try {
|
||||
await onRate?.(video.id, rating);
|
||||
setCurrentRating(rating);
|
||||
} catch (error) {
|
||||
console.error('Error updating rating:', error);
|
||||
} finally {
|
||||
setRatingLoading(false);
|
||||
}
|
||||
}, [video.id, ratingLoading, onRate]);
|
||||
|
||||
const getPlatform = (): string => {
|
||||
if (typeof window === 'undefined') return 'Unknown';
|
||||
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
if (userAgent.includes('mac')) return 'macOS';
|
||||
if (userAgent.includes('win')) return 'Windows';
|
||||
if (userAgent.includes('linux')) return 'Linux';
|
||||
return 'Unknown';
|
||||
};
|
||||
|
||||
const handleCancelAutoLaunch = () => {
|
||||
setShowingAutoLaunchConfirm(false);
|
||||
detectAvailablePlayers();
|
||||
};
|
||||
|
||||
const detectAvailablePlayers = async () => {
|
||||
const detectAvailablePlayers = useCallback(async () => {
|
||||
setIsDetecting(true);
|
||||
try {
|
||||
// In a real implementation, this would test protocol handlers
|
||||
|
|
@ -215,17 +275,99 @@ export default function LocalPlayerLauncher({
|
|||
} finally {
|
||||
setIsDetecting(false);
|
||||
}
|
||||
};
|
||||
}, [recommendedPlayers]);
|
||||
|
||||
const getPlatform = (): string => {
|
||||
if (typeof window === 'undefined') return 'Unknown';
|
||||
const handlePlayerLaunch = useCallback(async (playerId: string, isAutoLaunch: boolean = false) => {
|
||||
const player = PLAYER_INFO[playerId];
|
||||
if (!player) return;
|
||||
|
||||
setLaunchStatus('launching');
|
||||
setLaunchedPlayerId(playerId);
|
||||
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
if (userAgent.includes('mac')) return 'macOS';
|
||||
if (userAgent.includes('win')) return 'Windows';
|
||||
if (userAgent.includes('linux')) return 'Linux';
|
||||
return 'Unknown';
|
||||
};
|
||||
try {
|
||||
if (player.protocolUrl) {
|
||||
const protocolUrl = player.protocolUrl + encodeURIComponent(streamUrl);
|
||||
window.location.href = protocolUrl;
|
||||
setLaunchStatus('success');
|
||||
} else {
|
||||
console.log(`Would launch ${player.name} with: ${streamUrl}`);
|
||||
setLaunchStatus('success');
|
||||
}
|
||||
|
||||
onPlayerSelect?.(playerId);
|
||||
|
||||
if (!isAutoLaunch && preferences && !preferences.preferredPlayer && preferences.rememberChoice) {
|
||||
const shouldRemember = confirm(`Would you like to remember ${player.name} as your preferred video player? You can change this later in Settings.`);
|
||||
if (shouldRemember) {
|
||||
const updatedPrefs = {
|
||||
...preferences,
|
||||
preferredPlayer: playerId,
|
||||
autoLaunch: confirm('Would you like to automatically launch videos with this player in the future?')
|
||||
};
|
||||
savePlayerPreferences(updatedPrefs);
|
||||
setPreferences(updatedPrefs);
|
||||
}
|
||||
}
|
||||
|
||||
const hasInteractiveFeatures = showBookmarks || showRatings;
|
||||
if (!hasInteractiveFeatures) {
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to launch player:', error);
|
||||
setLaunchStatus('error');
|
||||
setLaunchedPlayerId(null);
|
||||
|
||||
setTimeout(() => {
|
||||
setLaunchStatus('idle');
|
||||
}, 3000);
|
||||
}
|
||||
}, [streamUrl, onPlayerSelect, preferences, showBookmarks, showRatings, onClose]);
|
||||
|
||||
// Load preferences and check for auto-launch on mount
|
||||
useEffect(() => {
|
||||
const prefs = loadPlayerPreferences();
|
||||
setPreferences(prefs);
|
||||
|
||||
const autoLaunchCheck = shouldAutoLaunch();
|
||||
|
||||
// Only auto-launch once per dialog session
|
||||
if (!hasAutoLaunched && autoLaunchCheck.autoLaunch && autoLaunchCheck.playerId) {
|
||||
if (autoLaunchCheck.needsConfirmation) {
|
||||
setShowingAutoLaunchConfirm(true);
|
||||
} else {
|
||||
// Auto-launch immediately, but still show player detection
|
||||
setHasAutoLaunched(true); // Mark as launched to prevent repeats
|
||||
detectAvailablePlayers().then(() => {
|
||||
handlePlayerLaunch(autoLaunchCheck.playerId!, true);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Detect available players normally
|
||||
detectAvailablePlayers();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Run only once on mount
|
||||
|
||||
// Handle auto-launch confirmation
|
||||
const handleConfirmAutoLaunch = useCallback(() => {
|
||||
const autoLaunchCheck = shouldAutoLaunch();
|
||||
if (autoLaunchCheck.playerId) {
|
||||
setShowingAutoLaunchConfirm(false);
|
||||
setHasAutoLaunched(true); // Mark as launched
|
||||
handlePlayerLaunch(autoLaunchCheck.playerId, true);
|
||||
}
|
||||
}, [handlePlayerLaunch]);
|
||||
|
||||
const handleCancelAutoLaunch = useCallback(() => {
|
||||
setShowingAutoLaunchConfirm(false);
|
||||
detectAvailablePlayers();
|
||||
}, [detectAvailablePlayers]);
|
||||
|
||||
|
||||
|
||||
const handleCopyUrl = async () => {
|
||||
try {
|
||||
|
|
@ -246,55 +388,7 @@ export default function LocalPlayerLauncher({
|
|||
}
|
||||
};
|
||||
|
||||
const handlePlayerLaunch = async (playerId: string, isAutoLaunch: boolean = false) => {
|
||||
const player = PLAYER_INFO[playerId];
|
||||
if (!player) return;
|
||||
|
||||
setLaunchStatus('launching');
|
||||
|
||||
try {
|
||||
// Try protocol handler first (requires user gesture)
|
||||
if (player.protocolUrl) {
|
||||
const protocolUrl = player.protocolUrl + encodeURIComponent(streamUrl);
|
||||
window.location.href = protocolUrl;
|
||||
setLaunchStatus('success');
|
||||
} else {
|
||||
// Fallback to command line approach (would need server-side support)
|
||||
console.log(`Would launch ${player.name} with: ${streamUrl}`);
|
||||
setLaunchStatus('success');
|
||||
}
|
||||
|
||||
onPlayerSelect?.(playerId);
|
||||
|
||||
// If this was manual selection and user hasn't set preferences, offer to remember
|
||||
if (!isAutoLaunch && preferences && !preferences.preferredPlayer && preferences.rememberChoice) {
|
||||
const shouldRemember = confirm(`Would you like to remember ${player.name} as your preferred video player? You can change this later in Settings.`);
|
||||
if (shouldRemember) {
|
||||
const updatedPrefs = {
|
||||
...preferences,
|
||||
preferredPlayer: playerId,
|
||||
autoLaunch: confirm('Would you like to automatically launch videos with this player in the future?')
|
||||
};
|
||||
savePlayerPreferences(updatedPrefs);
|
||||
setPreferences(updatedPrefs);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-close after successful launch
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to launch player:', error);
|
||||
setLaunchStatus('error');
|
||||
|
||||
// Reset status after showing error
|
||||
setTimeout(() => {
|
||||
setLaunchStatus('idle');
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualOpen = () => {
|
||||
// Open the stream URL in a new tab for manual copy/paste
|
||||
|
|
@ -306,8 +400,8 @@ export default function LocalPlayerLauncher({
|
|||
if (!player) return null;
|
||||
|
||||
const isAvailable = detectedPlayers.includes(playerId);
|
||||
const isLaunching = launchStatus === 'launching';
|
||||
const isSuccess = launchStatus === 'success';
|
||||
const isLaunching = launchStatus === 'launching' && launchedPlayerId === playerId;
|
||||
const isSuccess = launchStatus === 'success' && launchedPlayerId === playerId;
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -425,10 +519,10 @@ export default function LocalPlayerLauncher({
|
|||
|
||||
return (
|
||||
<div className={cn("fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4", className)}>
|
||||
<Card className="w-full max-w-md">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Monitor className="h-5 w-5" />
|
||||
Local Video Player Required
|
||||
|
|
@ -441,7 +535,7 @@ export default function LocalPlayerLauncher({
|
|||
onClick={onClose}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
className="h-8 w-8 flex-shrink-0 -mt-1"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
@ -449,12 +543,64 @@ export default function LocalPlayerLauncher({
|
|||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{/* Video Info */}
|
||||
<div className="bg-muted rounded-lg p-3">
|
||||
<div className="font-medium text-sm">{video.title || 'Untitled Video'}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Format: {format.streamInfo?.contentType || 'Unknown'} •
|
||||
Size: {formatFileSize ? formatFileSize(video.size) : `${(video.size / 1024 / 1024).toFixed(1)} MB`}
|
||||
{/* Video Info with Bookmark & Rating */}
|
||||
<div className="bg-gradient-to-br from-muted/50 to-muted rounded-lg p-4 border border-border">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="font-semibold text-base line-clamp-1">{video.title || 'Untitled Video'}</h3>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Format: {format.streamInfo?.contentType || 'Unknown'}</span>
|
||||
<span>•</span>
|
||||
<span>Size: {formatFileSize ? formatFileSize(video.size) : `${(video.size / 1024 / 1024).toFixed(1)} MB`}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground/70 line-clamp-1" title={video.path}>
|
||||
{video.path}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bookmark & Rating Controls */}
|
||||
<div className="flex flex-col gap-2 items-end flex-shrink-0">
|
||||
{showBookmarks && (
|
||||
<Button
|
||||
onClick={handleBookmarkToggle}
|
||||
disabled={bookmarkLoading}
|
||||
variant={isBookmarked ? "default" : "outline"}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-9 px-3",
|
||||
isBookmarked && "bg-yellow-500 hover:bg-yellow-600 border-yellow-500"
|
||||
)}
|
||||
title={isBookmarked ? "Remove bookmark" : "Add bookmark"}
|
||||
>
|
||||
<Bookmark className={cn("h-4 w-4", isBookmarked && "fill-current")} />
|
||||
<span className="ml-2 text-sm">
|
||||
{bookmarkLoading ? '...' : isBookmarked ? 'Bookmarked' : 'Bookmark'}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showRatings && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StarRating
|
||||
rating={currentRating}
|
||||
count={0}
|
||||
size="sm"
|
||||
showCount={false}
|
||||
interactive={true}
|
||||
onRate={handleRatingChange}
|
||||
/>
|
||||
{currentRating > 0 && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
{currentRating}/5
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -464,6 +610,13 @@ export default function LocalPlayerLauncher({
|
|||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current" />
|
||||
Detecting available players...
|
||||
</div>
|
||||
) : launchStatus === 'success' ? (
|
||||
<Alert className="bg-green-500/10 border-green-500/20">
|
||||
<Check className="h-4 w-4 text-green-400" />
|
||||
<AlertDescription className="text-green-400">
|
||||
Player launched successfully! You can now bookmark or rate this video before closing.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert>
|
||||
<Monitor className="h-4 w-4" />
|
||||
|
|
@ -535,40 +688,6 @@ export default function LocalPlayerLauncher({
|
|||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Help Section */}
|
||||
<div className="bg-muted rounded-lg p-3 text-xs">
|
||||
<div className="flex items-center gap-2 font-medium mb-1">
|
||||
<HelpCircle className="h-3 w-3" />
|
||||
Better Streaming Solution
|
||||
</div>
|
||||
<div className="space-y-2 text-muted-foreground">
|
||||
<div className="bg-blue-500/10 border border-blue-500/20 rounded p-2">
|
||||
<div className="font-medium text-blue-400 mb-1">💡 Recommended Solution</div>
|
||||
<div className="text-xs">
|
||||
Use our <strong>External Streaming API</strong> for better compatibility:
|
||||
<br />
|
||||
<code className="bg-black/20 px-1 rounded text-xs mt-1 inline-block">
|
||||
{getPlayerSpecificUrl(video.id, 'vlc')}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
<li>• ✅ Proper HTTP range support for seeking</li>
|
||||
<li>• ✅ Optimized chunked streaming</li>
|
||||
<li>• ✅ Works with VLC, MPV, PotPlayer, etc.</li>
|
||||
<li>• ✅ No transcoding needed</li>
|
||||
</ul>
|
||||
<div className="mt-2 p-2 bg-green-500/10 border border-green-500/20 rounded">
|
||||
<div className="font-medium text-green-400">Quick Setup:</div>
|
||||
<div className="text-xs mt-1">
|
||||
1. Copy URL above<br />
|
||||
2. Open VLC → Media → Open Network Stream<br />
|
||||
3. Paste URL and click Play
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -175,6 +175,11 @@ export default function UnifiedVideoPlayer({
|
|||
console.log(`Selected player: ${playerId}`);
|
||||
}}
|
||||
formatFileSize={formatFileSize}
|
||||
onBookmark={handleBookmarkToggle}
|
||||
onUnbookmark={handleUnbookmark}
|
||||
onRate={handleRatingUpdate}
|
||||
showBookmarks={showBookmarks}
|
||||
showRatings={showRatings}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue