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';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
|
@ -11,8 +11,9 @@ import {
|
||||||
PlayCircle,
|
PlayCircle,
|
||||||
Monitor,
|
Monitor,
|
||||||
Settings,
|
Settings,
|
||||||
HelpCircle,
|
X,
|
||||||
X
|
Bookmark,
|
||||||
|
Star
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { VideoFormat, VideoFile } from '@/lib/video-format-detector';
|
import { VideoFormat, VideoFile } from '@/lib/video-format-detector';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
@ -22,6 +23,7 @@ import {
|
||||||
shouldAutoLaunch,
|
shouldAutoLaunch,
|
||||||
PlayerPreferences
|
PlayerPreferences
|
||||||
} from '@/lib/player-preferences';
|
} from '@/lib/player-preferences';
|
||||||
|
import { StarRating } from '@/components/star-rating';
|
||||||
|
|
||||||
interface LocalPlayerLauncherProps {
|
interface LocalPlayerLauncherProps {
|
||||||
video: VideoFile;
|
video: VideoFile;
|
||||||
|
|
@ -30,6 +32,11 @@ interface LocalPlayerLauncherProps {
|
||||||
onPlayerSelect?: (player: string) => void;
|
onPlayerSelect?: (player: string) => void;
|
||||||
formatFileSize?: (bytes: number) => string;
|
formatFileSize?: (bytes: number) => string;
|
||||||
className?: 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 {
|
interface PlayerInfo {
|
||||||
|
|
@ -54,15 +61,6 @@ const PLAYER_INFO: Record<string, PlayerInfo> = {
|
||||||
protocolUrl: 'vlc://',
|
protocolUrl: 'vlc://',
|
||||||
commandLine: '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: {
|
potplayer: {
|
||||||
id: 'potplayer',
|
id: 'potplayer',
|
||||||
name: 'PotPlayer',
|
name: 'PotPlayer',
|
||||||
|
|
@ -146,53 +144,115 @@ export default function LocalPlayerLauncher({
|
||||||
onClose,
|
onClose,
|
||||||
onPlayerSelect,
|
onPlayerSelect,
|
||||||
formatFileSize,
|
formatFileSize,
|
||||||
className
|
className,
|
||||||
|
onBookmark,
|
||||||
|
onUnbookmark,
|
||||||
|
onRate,
|
||||||
|
showBookmarks = true,
|
||||||
|
showRatings = true
|
||||||
}: LocalPlayerLauncherProps) {
|
}: LocalPlayerLauncherProps) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [detectedPlayers, setDetectedPlayers] = useState<string[]>([]);
|
const [detectedPlayers, setDetectedPlayers] = useState<string[]>([]);
|
||||||
const [isDetecting, setIsDetecting] = useState(true);
|
const [isDetecting, setIsDetecting] = useState(true);
|
||||||
const [launchStatus, setLaunchStatus] = useState<'idle' | 'launching' | 'success' | 'error'>('idle');
|
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 [preferences, setPreferences] = useState<PlayerPreferences | null>(null);
|
||||||
const [showingAutoLaunchConfirm, setShowingAutoLaunchConfirm] = useState(false);
|
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 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(() => {
|
useEffect(() => {
|
||||||
const prefs = loadPlayerPreferences();
|
if (video.id) {
|
||||||
setPreferences(prefs);
|
checkBookmarkStatus();
|
||||||
|
checkRatingStatus();
|
||||||
|
}
|
||||||
|
}, [video.id]);
|
||||||
|
|
||||||
const autoLaunchCheck = shouldAutoLaunch();
|
// Check bookmark status
|
||||||
|
const checkBookmarkStatus = useCallback(async () => {
|
||||||
|
if (!video.id || !showBookmarks) return;
|
||||||
|
|
||||||
if (autoLaunchCheck.autoLaunch && autoLaunchCheck.playerId) {
|
try {
|
||||||
if (autoLaunchCheck.needsConfirmation) {
|
const response = await fetch(`/api/bookmarks/${video.id}`);
|
||||||
setShowingAutoLaunchConfirm(true);
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setIsBookmarked(data.isBookmarked || false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking bookmark status:', error);
|
||||||
|
}
|
||||||
|
}, [video.id, showBookmarks]);
|
||||||
|
|
||||||
|
// 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 {
|
} else {
|
||||||
// Auto-launch immediately
|
await onBookmark?.(video.id);
|
||||||
handlePlayerLaunch(autoLaunchCheck.playerId, true);
|
setIsBookmarked(true);
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
// Detect available players normally
|
console.error('Error toggling bookmark:', error);
|
||||||
detectAvailablePlayers();
|
} finally {
|
||||||
|
setBookmarkLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [video.id, isBookmarked, bookmarkLoading, onBookmark, onUnbookmark]);
|
||||||
|
|
||||||
// Handle auto-launch confirmation
|
// Handle rating change
|
||||||
const handleConfirmAutoLaunch = () => {
|
const handleRatingChange = useCallback(async (rating: number) => {
|
||||||
const autoLaunchCheck = shouldAutoLaunch();
|
if (!video.id || ratingLoading) return;
|
||||||
if (autoLaunchCheck.playerId) {
|
|
||||||
setShowingAutoLaunchConfirm(false);
|
setRatingLoading(true);
|
||||||
handlePlayerLaunch(autoLaunchCheck.playerId, 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 = () => {
|
const detectAvailablePlayers = useCallback(async () => {
|
||||||
setShowingAutoLaunchConfirm(false);
|
|
||||||
detectAvailablePlayers();
|
|
||||||
};
|
|
||||||
|
|
||||||
const detectAvailablePlayers = async () => {
|
|
||||||
setIsDetecting(true);
|
setIsDetecting(true);
|
||||||
try {
|
try {
|
||||||
// In a real implementation, this would test protocol handlers
|
// In a real implementation, this would test protocol handlers
|
||||||
|
|
@ -215,17 +275,99 @@ export default function LocalPlayerLauncher({
|
||||||
} finally {
|
} finally {
|
||||||
setIsDetecting(false);
|
setIsDetecting(false);
|
||||||
}
|
}
|
||||||
};
|
}, [recommendedPlayers]);
|
||||||
|
|
||||||
const getPlatform = (): string => {
|
const handlePlayerLaunch = useCallback(async (playerId: string, isAutoLaunch: boolean = false) => {
|
||||||
if (typeof window === 'undefined') return 'Unknown';
|
const player = PLAYER_INFO[playerId];
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
setLaunchStatus('launching');
|
||||||
if (userAgent.includes('mac')) return 'macOS';
|
setLaunchedPlayerId(playerId);
|
||||||
if (userAgent.includes('win')) return 'Windows';
|
|
||||||
if (userAgent.includes('linux')) return 'Linux';
|
try {
|
||||||
return 'Unknown';
|
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 () => {
|
const handleCopyUrl = async () => {
|
||||||
try {
|
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 = () => {
|
const handleManualOpen = () => {
|
||||||
// Open the stream URL in a new tab for manual copy/paste
|
// Open the stream URL in a new tab for manual copy/paste
|
||||||
|
|
@ -306,8 +400,8 @@ export default function LocalPlayerLauncher({
|
||||||
if (!player) return null;
|
if (!player) return null;
|
||||||
|
|
||||||
const isAvailable = detectedPlayers.includes(playerId);
|
const isAvailable = detectedPlayers.includes(playerId);
|
||||||
const isLaunching = launchStatus === 'launching';
|
const isLaunching = launchStatus === 'launching' && launchedPlayerId === playerId;
|
||||||
const isSuccess = launchStatus === 'success';
|
const isSuccess = launchStatus === 'success' && launchedPlayerId === playerId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -425,10 +519,10 @@ export default function LocalPlayerLauncher({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4", className)}>
|
<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">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Monitor className="h-5 w-5" />
|
<Monitor className="h-5 w-5" />
|
||||||
Local Video Player Required
|
Local Video Player Required
|
||||||
|
|
@ -441,7 +535,7 @@ export default function LocalPlayerLauncher({
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8 flex-shrink-0 -mt-1"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -449,12 +543,64 @@ export default function LocalPlayerLauncher({
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Video Info */}
|
{/* Video Info with Bookmark & Rating */}
|
||||||
<div className="bg-muted rounded-lg p-3">
|
<div className="bg-gradient-to-br from-muted/50 to-muted rounded-lg p-4 border border-border">
|
||||||
<div className="font-medium text-sm">{video.title || 'Untitled Video'}</div>
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="flex-1 min-w-0">
|
||||||
Format: {format.streamInfo?.contentType || 'Unknown'} •
|
<div className="flex items-center gap-2 mb-2">
|
||||||
Size: {formatFileSize ? formatFileSize(video.size) : `${(video.size / 1024 / 1024).toFixed(1)} MB`}
|
<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>
|
||||||
</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" />
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current" />
|
||||||
Detecting available players...
|
Detecting available players...
|
||||||
</div>
|
</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>
|
<Alert>
|
||||||
<Monitor className="h-4 w-4" />
|
<Monitor className="h-4 w-4" />
|
||||||
|
|
@ -535,40 +688,6 @@ export default function LocalPlayerLauncher({
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,11 @@ export default function UnifiedVideoPlayer({
|
||||||
console.log(`Selected player: ${playerId}`);
|
console.log(`Selected player: ${playerId}`);
|
||||||
}}
|
}}
|
||||||
formatFileSize={formatFileSize}
|
formatFileSize={formatFileSize}
|
||||||
|
onBookmark={handleBookmarkToggle}
|
||||||
|
onUnbookmark={handleUnbookmark}
|
||||||
|
onRate={handleRatingUpdate}
|
||||||
|
showBookmarks={showBookmarks}
|
||||||
|
showRatings={showRatings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue