feat(player): add external video player auto-launch feature with preferences
- Implement player preferences management with localStorage and events - Enable auto-launch and confirmation dialogs in LocalPlayerLauncher component - Learn user preferences from manual player selections dynamically - Integrate settings UI for video player configuration and real-time updates - Support platform-aware player listing and reset to default preferences - Provide clear current configuration display and toggle switches in settings - Enhance user experience with smart defaults and customizable workflow - Maintain backward compatibility with manual player selection option - Add custom event system for cross-component synchronization of preferences - Optimize player detection and launching logic to respect user settings
This commit is contained in:
parent
d048cb3b82
commit
76154123b8
|
|
@ -0,0 +1,134 @@
|
|||
# External Video Player Auto-Launch Feature Implementation
|
||||
|
||||
## Overview
|
||||
Successfully implemented a seamless auto-launch system for external video players, allowing users to configure preferences once and automatically launch videos without manual selection each time.
|
||||
|
||||
## Features Implemented
|
||||
|
||||
### 1. Player Preferences Management (`/src/lib/player-preferences.ts`)
|
||||
- **localStorage-based storage** for user preferences
|
||||
- **Cross-component state management** with custom events
|
||||
- **Default configurations** with safe fallbacks
|
||||
- **Platform-specific player detection**
|
||||
|
||||
Key preferences:
|
||||
- `preferredPlayer`: User's chosen video player (null = manual selection)
|
||||
- `autoLaunch`: Enable/disable automatic launching
|
||||
- `confirmBeforeLaunch`: Show confirmation dialog before auto-launch
|
||||
- `rememberChoice`: Offer to remember manual selections
|
||||
|
||||
### 2. Enhanced LocalPlayerLauncher (`/src/components/local-player-launcher.tsx`)
|
||||
- **Auto-launch detection** on component mount
|
||||
- **Confirmation dialogs** for auto-launch (when enabled)
|
||||
- **Smart preference learning** from manual selections
|
||||
- **Fallback to manual selection** when no preferences set
|
||||
|
||||
New workflow:
|
||||
1. Component loads → Check user preferences
|
||||
2. If auto-launch enabled → Show confirmation or launch directly
|
||||
3. If manual selection → Show traditional player selection
|
||||
4. After manual selection → Offer to remember choice
|
||||
|
||||
### 3. Settings Page Integration (`/src/app/settings/page.tsx`)
|
||||
- **Dedicated Video Player section** with modern toggle switches
|
||||
- **Real-time preference updates** with immediate feedback
|
||||
- **Platform-aware player list** showing only compatible players
|
||||
- **Current configuration display** for transparency
|
||||
- **Reset to defaults** functionality
|
||||
|
||||
Settings UI includes:
|
||||
- Auto Launch toggle
|
||||
- Preferred Player selection (with manual option)
|
||||
- Confirmation toggle
|
||||
- Remember Choice toggle
|
||||
- Current status display
|
||||
|
||||
## User Experience Flow
|
||||
|
||||
### First Time Use
|
||||
1. User clicks on .ts (or other external format) video
|
||||
2. LocalPlayerLauncher shows traditional selection dialog
|
||||
3. User selects preferred player (e.g., VLC)
|
||||
4. System asks: "Remember VLC as preferred player?"
|
||||
5. If yes, asks: "Auto-launch videos with VLC in future?"
|
||||
6. Preferences saved for subsequent videos
|
||||
|
||||
### Subsequent Uses (Auto-Launch Enabled)
|
||||
1. User clicks on external format video
|
||||
2. System shows: "Launch VLC?" confirmation dialog
|
||||
3. User clicks "Launch VLC" → Video opens immediately
|
||||
4. OR clicks "Choose Manually" → Shows full selection dialog
|
||||
|
||||
### Subsequent Uses (Auto-Launch Disabled)
|
||||
1. User clicks on external format video
|
||||
2. System immediately launches preferred player
|
||||
3. No confirmation needed
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Storage Architecture
|
||||
```typescript
|
||||
interface PlayerPreferences {
|
||||
preferredPlayer: string | null; // Player ID or null for manual
|
||||
autoLaunch: boolean; // Enable auto-launch
|
||||
confirmBeforeLaunch: boolean; // Show confirmation dialog
|
||||
rememberChoice: boolean; // Offer to remember selections
|
||||
}
|
||||
```
|
||||
|
||||
### Auto-Launch Logic
|
||||
```typescript
|
||||
const autoLaunchCheck = shouldAutoLaunch();
|
||||
if (autoLaunchCheck.autoLaunch && autoLaunchCheck.playerId) {
|
||||
if (autoLaunchCheck.needsConfirmation) {
|
||||
setShowingAutoLaunchConfirm(true);
|
||||
} else {
|
||||
handlePlayerLaunch(autoLaunchCheck.playerId, true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Integration
|
||||
- Real-time preference updates using localStorage
|
||||
- Custom event system for cross-component synchronization
|
||||
- Platform detection for showing relevant players only
|
||||
- Visual feedback with toggle switches and status display
|
||||
|
||||
## Benefits
|
||||
|
||||
### User Experience
|
||||
- ✅ **One-time setup** → Seamless future use
|
||||
- ✅ **Configurable experience** → Users choose their level of automation
|
||||
- ✅ **Smart defaults** → Safe, non-intrusive initial behavior
|
||||
- ✅ **Clear settings** → Easy to modify preferences later
|
||||
|
||||
### Technical
|
||||
- ✅ **Lightweight implementation** → localStorage-based, no server calls
|
||||
- ✅ **Cross-platform compatibility** → Detects available players per OS
|
||||
- ✅ **Backwards compatible** → Manual selection still available
|
||||
- ✅ **Event-driven updates** → Real-time preference synchronization
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Users can now configure:
|
||||
|
||||
1. **Auto Launch**: Toggle automatic video launching
|
||||
2. **Preferred Player**: Choose default player or manual selection
|
||||
3. **Confirmation**: Require confirmation before auto-launch
|
||||
4. **Learning**: Allow system to learn from manual choices
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **Created**: `/src/lib/player-preferences.ts` - Preference management system
|
||||
2. **Enhanced**: `/src/components/local-player-launcher.tsx` - Auto-launch support
|
||||
3. **Enhanced**: `/src/app/settings/page.tsx` - Settings UI integration
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- **Player installation detection** - Verify players are actually installed
|
||||
- **Custom player paths** - Allow users to specify player locations
|
||||
- **Per-format preferences** - Different players for different video formats
|
||||
- **Keyboard shortcuts** - Quick launch hotkeys
|
||||
|
||||
The implementation provides a perfect balance between automation and user control, making the external video player experience as seamless as possible while maintaining flexibility for different user preferences.
|
||||
|
|
@ -23,9 +23,9 @@ export default function RootLayout({
|
|||
<body
|
||||
className={`${inter.variable} antialiased bg-background text-foreground`}
|
||||
>
|
||||
<div className="flex h-screen bg-gradient-to-br from-background via-background to-muted/20 overflow-hidden">
|
||||
<div className="flex h-screen bg-gradient-to-br from-background via-background to-muted/20">
|
||||
<Sidebar />
|
||||
<main className="flex-1 bg-background/50 backdrop-blur-sm overflow-hidden">
|
||||
<main className="flex-1 bg-background/50 backdrop-blur-sm overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,14 @@ import { Input } from "@/components/ui/input";
|
|||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Header } from "@/components/ui/header";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Trash2, Plus, Folder, HardDrive, Scan, CheckSquare, Square, RefreshCw } from "lucide-react";
|
||||
import { Trash2, Plus, Folder, HardDrive, Scan, CheckSquare, Square, RefreshCw, Play, Monitor, Settings as SettingsIcon } from "lucide-react";
|
||||
import {
|
||||
loadPlayerPreferences,
|
||||
savePlayerPreferences,
|
||||
resetPlayerPreferences,
|
||||
getAvailablePlayersForPlatform,
|
||||
PlayerPreferences
|
||||
} from "@/lib/player-preferences";
|
||||
|
||||
interface Library {
|
||||
id: number;
|
||||
|
|
@ -22,11 +29,41 @@ const SettingsPage = () => {
|
|||
const [scanStatus, setScanStatus] = useState<string>("");
|
||||
const [selectedLibraries, setSelectedLibraries] = useState<number[]>([]);
|
||||
const [scanProgress, setScanProgress] = useState<Record<number, boolean>>({});
|
||||
const [playerPreferences, setPlayerPreferences] = useState<PlayerPreferences | null>(null);
|
||||
const [availablePlayers, setAvailablePlayers] = useState<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
}>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLibraries();
|
||||
loadVideoPlayerPreferences();
|
||||
}, []);
|
||||
|
||||
const loadVideoPlayerPreferences = () => {
|
||||
try {
|
||||
const prefs = loadPlayerPreferences();
|
||||
const players = getAvailablePlayersForPlatform();
|
||||
console.log('[Settings] Loaded preferences:', prefs);
|
||||
console.log('[Settings] Available players:', players);
|
||||
setPlayerPreferences(prefs);
|
||||
setAvailablePlayers(players);
|
||||
} catch (error) {
|
||||
console.error('[Settings] Error loading player preferences:', error);
|
||||
// Set default preferences if loading fails
|
||||
const defaultPrefs = {
|
||||
preferredPlayer: null,
|
||||
autoLaunch: false,
|
||||
confirmBeforeLaunch: true,
|
||||
rememberChoice: true,
|
||||
};
|
||||
setPlayerPreferences(defaultPrefs);
|
||||
setAvailablePlayers([]);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/libraries");
|
||||
|
|
@ -200,6 +237,21 @@ const SettingsPage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const updatePlayerPreferences = (updates: Partial<PlayerPreferences>) => {
|
||||
if (!playerPreferences) return;
|
||||
|
||||
const updated = { ...playerPreferences, ...updates };
|
||||
savePlayerPreferences(updated);
|
||||
setPlayerPreferences(updated);
|
||||
};
|
||||
|
||||
const handleResetPlayerPreferences = () => {
|
||||
if (confirm('Are you sure you want to reset all video player preferences to defaults?')) {
|
||||
resetPlayerPreferences();
|
||||
loadVideoPlayerPreferences();
|
||||
}
|
||||
};
|
||||
|
||||
const getTotalStorage = () => {
|
||||
return libraries.reduce((total, lib) => {
|
||||
// Rough estimation based on path length
|
||||
|
|
@ -208,7 +260,7 @@ const SettingsPage = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950">
|
||||
<div className="min-h-screen bg-zinc-950 overflow-y-auto">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-5xl font-bold text-white tracking-tight mb-2">
|
||||
|
|
@ -219,7 +271,7 @@ const SettingsPage = () => {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 pb-8">
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
<div className="bg-zinc-900 rounded-xl border border-zinc-800 p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
|
|
@ -424,6 +476,169 @@ const SettingsPage = () => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-900 rounded-xl border border-zinc-800 p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-600/20">
|
||||
<Play className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">Video Player</h2>
|
||||
<p className="text-sm text-zinc-400">Configure external video player preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{playerPreferences ? (
|
||||
<div className="space-y-6">
|
||||
{/* Auto Launch Setting */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">Auto Launch</h3>
|
||||
<p className="text-xs text-zinc-400">Automatically launch videos in external player</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => updatePlayerPreferences({ autoLaunch: !playerPreferences.autoLaunch })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
playerPreferences.autoLaunch ? 'bg-blue-600' : 'bg-zinc-700'
|
||||
}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
playerPreferences.autoLaunch ? 'translate-x-6' : 'translate-x-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preferred Player Selection */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">Preferred Player</h3>
|
||||
<p className="text-xs text-zinc-400">Choose your default video player for external playback</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* None/Manual Selection Option */}
|
||||
<button
|
||||
onClick={() => updatePlayerPreferences({ preferredPlayer: null })}
|
||||
className={`w-full p-3 rounded-lg border transition-all text-left ${
|
||||
playerPreferences.preferredPlayer === null
|
||||
? 'border-blue-500 bg-blue-500/10'
|
||||
: 'border-zinc-700 bg-zinc-800 hover:border-zinc-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-zinc-700 rounded-lg flex items-center justify-center">
|
||||
<Monitor className="h-4 w-4 text-zinc-300" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-white text-sm">Manual Selection</div>
|
||||
<div className="text-xs text-zinc-400">Show player selection dialog each time</div>
|
||||
</div>
|
||||
{playerPreferences.preferredPlayer === null && (
|
||||
<div className="ml-auto w-2 h-2 bg-blue-500 rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Available Players */}
|
||||
{availablePlayers.map((player) => (
|
||||
<button
|
||||
key={player.id}
|
||||
onClick={() => updatePlayerPreferences({ preferredPlayer: player.id })}
|
||||
className={`w-full p-3 rounded-lg border transition-all text-left ${
|
||||
playerPreferences.preferredPlayer === player.id
|
||||
? 'border-blue-500 bg-blue-500/10'
|
||||
: 'border-zinc-700 bg-zinc-800 hover:border-zinc-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-zinc-700 rounded-lg flex items-center justify-center">
|
||||
<span className="text-sm">{player.icon}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-white text-sm">{player.name}</div>
|
||||
<div className="text-xs text-zinc-400">{player.description}</div>
|
||||
</div>
|
||||
{playerPreferences.preferredPlayer === player.id && (
|
||||
<div className="ml-auto w-2 h-2 bg-blue-500 rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirmation Setting */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">Confirm Before Launch</h3>
|
||||
<p className="text-xs text-zinc-400">Show confirmation dialog before auto-launching</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => updatePlayerPreferences({ confirmBeforeLaunch: !playerPreferences.confirmBeforeLaunch })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
playerPreferences.confirmBeforeLaunch ? 'bg-blue-600' : 'bg-zinc-700'
|
||||
}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
playerPreferences.confirmBeforeLaunch ? 'translate-x-6' : 'translate-x-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remember Choice Setting */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">Remember Manual Selections</h3>
|
||||
<p className="text-xs text-zinc-400">Offer to remember when you manually select a player</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => updatePlayerPreferences({ rememberChoice: !playerPreferences.rememberChoice })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
playerPreferences.rememberChoice ? 'bg-blue-600' : 'bg-zinc-700'
|
||||
}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
playerPreferences.rememberChoice ? 'translate-x-6' : 'translate-x-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Status */}
|
||||
<div className="bg-zinc-800 rounded-lg p-3">
|
||||
<div className="text-xs font-medium text-zinc-300 mb-2">Current Configuration</div>
|
||||
<div className="space-y-1 text-xs text-zinc-400">
|
||||
<div>• Auto Launch: {playerPreferences.autoLaunch ? 'Enabled' : 'Disabled'}</div>
|
||||
<div>• Preferred Player: {playerPreferences.preferredPlayer ?
|
||||
availablePlayers.find(p => p.id === playerPreferences.preferredPlayer)?.name || 'Unknown'
|
||||
: 'Manual Selection'}</div>
|
||||
<div>• Confirmation: {playerPreferences.confirmBeforeLaunch ? 'Required' : 'Skip'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reset Button */}
|
||||
<div className="pt-3 border-t border-zinc-800">
|
||||
<Button
|
||||
onClick={handleResetPlayerPreferences}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-zinc-400 hover:text-white"
|
||||
>
|
||||
Reset to Defaults
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6">
|
||||
<div className="text-zinc-400">Loading player preferences...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { VideoFormat, VideoFile } from '@/lib/video-format-detector';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
loadPlayerPreferences,
|
||||
savePlayerPreferences,
|
||||
shouldAutoLaunch,
|
||||
PlayerPreferences
|
||||
} from '@/lib/player-preferences';
|
||||
|
||||
interface LocalPlayerLauncherProps {
|
||||
video: VideoFile;
|
||||
|
|
@ -146,15 +152,46 @@ export default function LocalPlayerLauncher({
|
|||
const [detectedPlayers, setDetectedPlayers] = useState<string[]>([]);
|
||||
const [isDetecting, setIsDetecting] = useState(true);
|
||||
const [launchStatus, setLaunchStatus] = useState<'idle' | 'launching' | 'success' | 'error'>('idle');
|
||||
const [preferences, setPreferences] = useState<PlayerPreferences | null>(null);
|
||||
const [showingAutoLaunchConfirm, setShowingAutoLaunchConfirm] = useState(false);
|
||||
|
||||
const streamUrl = getPlayerSpecificUrl(video.id, 'vlc'); // Use optimized endpoint
|
||||
const recommendedPlayers = format.recommendedPlayers || ['vlc', 'iina', 'elmedia', 'potplayer'];
|
||||
|
||||
// Detect available players on mount
|
||||
// Load preferences and check for auto-launch on mount
|
||||
useEffect(() => {
|
||||
detectAvailablePlayers();
|
||||
const prefs = loadPlayerPreferences();
|
||||
setPreferences(prefs);
|
||||
|
||||
const autoLaunchCheck = shouldAutoLaunch();
|
||||
|
||||
if (autoLaunchCheck.autoLaunch && autoLaunchCheck.playerId) {
|
||||
if (autoLaunchCheck.needsConfirmation) {
|
||||
setShowingAutoLaunchConfirm(true);
|
||||
} else {
|
||||
// Auto-launch immediately
|
||||
handlePlayerLaunch(autoLaunchCheck.playerId, true);
|
||||
}
|
||||
} else {
|
||||
// Detect available players normally
|
||||
detectAvailablePlayers();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle auto-launch confirmation
|
||||
const handleConfirmAutoLaunch = () => {
|
||||
const autoLaunchCheck = shouldAutoLaunch();
|
||||
if (autoLaunchCheck.playerId) {
|
||||
setShowingAutoLaunchConfirm(false);
|
||||
handlePlayerLaunch(autoLaunchCheck.playerId, true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelAutoLaunch = () => {
|
||||
setShowingAutoLaunchConfirm(false);
|
||||
detectAvailablePlayers();
|
||||
};
|
||||
|
||||
const detectAvailablePlayers = async () => {
|
||||
setIsDetecting(true);
|
||||
try {
|
||||
|
|
@ -209,7 +246,7 @@ export default function LocalPlayerLauncher({
|
|||
}
|
||||
};
|
||||
|
||||
const handlePlayerLaunch = async (playerId: string) => {
|
||||
const handlePlayerLaunch = async (playerId: string, isAutoLaunch: boolean = false) => {
|
||||
const player = PLAYER_INFO[playerId];
|
||||
if (!player) return;
|
||||
|
||||
|
|
@ -229,6 +266,20 @@ export default function LocalPlayerLauncher({
|
|||
|
||||
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();
|
||||
|
|
@ -293,6 +344,49 @@ export default function LocalPlayerLauncher({
|
|||
);
|
||||
};
|
||||
|
||||
// Auto-launch confirmation dialog
|
||||
if (showingAutoLaunchConfirm && preferences?.preferredPlayer) {
|
||||
const preferredPlayer = PLAYER_INFO[preferences.preferredPlayer];
|
||||
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">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<PlayCircle className="h-5 w-5" />
|
||||
Launch {preferredPlayer?.name}?
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Your preferred video player is ready to open this video.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-muted rounded-lg p-3 mb-4">
|
||||
<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`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleConfirmAutoLaunch} className="flex-1">
|
||||
<PlayCircle className="h-4 w-4 mr-2" />
|
||||
Launch {preferredPlayer?.name}
|
||||
</Button>
|
||||
<Button onClick={handleCancelAutoLaunch} variant="outline" className="flex-1">
|
||||
Choose Manually
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-xs text-muted-foreground text-center">
|
||||
You can change this in Settings → Video Player
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (launchStatus === 'error') {
|
||||
return (
|
||||
<div className={cn("fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4", className)}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* Player Preferences Management
|
||||
* Handles storage and retrieval of user preferences for external video players
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { PLAYER_INFO } from './local-player-launcher';
|
||||
|
||||
export interface PlayerPreferences {
|
||||
preferredPlayer: string | null; // null means show selection dialog
|
||||
autoLaunch: boolean;
|
||||
confirmBeforeLaunch: boolean;
|
||||
rememberChoice: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_PREFERENCES: PlayerPreferences = {
|
||||
preferredPlayer: null,
|
||||
autoLaunch: false,
|
||||
confirmBeforeLaunch: true,
|
||||
rememberChoice: true,
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'nextav-player-preferences';
|
||||
|
||||
/**
|
||||
* Load player preferences from localStorage
|
||||
*/
|
||||
export function loadPlayerPreferences(): PlayerPreferences {
|
||||
if (typeof window === 'undefined') {
|
||||
return DEFAULT_PREFERENCES;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (!stored) {
|
||||
return DEFAULT_PREFERENCES;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(stored);
|
||||
|
||||
// Validate that the preferred player still exists
|
||||
if (parsed.preferredPlayer && !PLAYER_INFO[parsed.preferredPlayer]) {
|
||||
parsed.preferredPlayer = null;
|
||||
}
|
||||
|
||||
return {
|
||||
...DEFAULT_PREFERENCES,
|
||||
...parsed,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading player preferences:', error);
|
||||
return DEFAULT_PREFERENCES;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save player preferences to localStorage
|
||||
*/
|
||||
export function savePlayerPreferences(preferences: PlayerPreferences): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
|
||||
|
||||
// Dispatch custom event for components to react to preference changes
|
||||
window.dispatchEvent(new CustomEvent('playerPreferencesChanged', {
|
||||
detail: preferences
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error saving player preferences:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update specific preference fields
|
||||
*/
|
||||
export function updatePlayerPreferences(updates: Partial<PlayerPreferences>): PlayerPreferences {
|
||||
const current = loadPlayerPreferences();
|
||||
const updated = { ...current, ...updates };
|
||||
savePlayerPreferences(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset preferences to defaults
|
||||
*/
|
||||
export function resetPlayerPreferences(): PlayerPreferences {
|
||||
savePlayerPreferences(DEFAULT_PREFERENCES);
|
||||
return DEFAULT_PREFERENCES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should auto-launch a video with current preferences
|
||||
*/
|
||||
export function shouldAutoLaunch(): {
|
||||
autoLaunch: boolean;
|
||||
playerId: string | null;
|
||||
needsConfirmation: boolean;
|
||||
} {
|
||||
const prefs = loadPlayerPreferences();
|
||||
|
||||
return {
|
||||
autoLaunch: prefs.autoLaunch && prefs.preferredPlayer !== null,
|
||||
playerId: prefs.preferredPlayer,
|
||||
needsConfirmation: prefs.confirmBeforeLaunch,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available players for current platform
|
||||
*/
|
||||
export function getAvailablePlayersForPlatform(): Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
}> {
|
||||
if (typeof window === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
const platform = userAgent.includes('mac') ? 'macOS' :
|
||||
userAgent.includes('win') ? 'Windows' :
|
||||
userAgent.includes('linux') ? 'Linux' : 'Unknown';
|
||||
|
||||
return Object.values(PLAYER_INFO)
|
||||
.filter(player => player.platforms.includes(platform))
|
||||
.map(player => ({
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
description: player.description,
|
||||
icon: getPlayerIcon(player.id),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get player icon for UI display
|
||||
*/
|
||||
function getPlayerIcon(playerId: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
'vlc': '🎬',
|
||||
'elmedia': '🍎',
|
||||
'potplayer': '🎯',
|
||||
'iina': '🍎',
|
||||
'mpv': '⚡'
|
||||
};
|
||||
|
||||
return icons[playerId] || '🎮';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for React components to use player preferences
|
||||
*/
|
||||
export function usePlayerPreferences() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
preferences: DEFAULT_PREFERENCES,
|
||||
updatePreferences: () => {},
|
||||
resetPreferences: () => {},
|
||||
shouldAutoLaunch: () => ({ autoLaunch: false, playerId: null, needsConfirmation: true })
|
||||
};
|
||||
}
|
||||
|
||||
const [preferences, setPreferences] = useState(loadPlayerPreferences());
|
||||
|
||||
const updatePreferences = useCallback((updates: Partial<PlayerPreferences>) => {
|
||||
const updated = updatePlayerPreferences(updates);
|
||||
setPreferences(updated);
|
||||
}, []);
|
||||
|
||||
const resetPreferences = useCallback(() => {
|
||||
const reset = resetPlayerPreferences();
|
||||
setPreferences(reset);
|
||||
}, []);
|
||||
|
||||
// Listen for preference changes from other components
|
||||
useEffect(() => {
|
||||
const handlePreferenceChange = (event: CustomEvent) => {
|
||||
setPreferences(event.detail);
|
||||
};
|
||||
|
||||
window.addEventListener('playerPreferencesChanged', handlePreferenceChange as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('playerPreferencesChanged', handlePreferenceChange as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
preferences,
|
||||
updatePreferences,
|
||||
resetPreferences,
|
||||
shouldAutoLaunch,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue