feat: Add new player utilities and update video player components
This commit is contained in:
@@ -24,6 +24,7 @@ import { useCloudStorageUrl } from '../hooks/useCloudStorageUrl';
|
||||
import { useShareVideo } from '../hooks/useShareVideo'; // Added
|
||||
import { Collection, Video } from '../types';
|
||||
import { formatDuration, parseDuration } from '../utils/formatUtils';
|
||||
import { getAvailablePlayers, getPlayerUrl } from '../utils/playerUtils'; // Added
|
||||
import CollectionModal from './CollectionModal';
|
||||
import ConfirmationModal from './ConfirmationModal';
|
||||
import VideoKebabMenuButtons from './VideoPlayer/VideoInfo/VideoKebabMenuButtons'; // Added
|
||||
@@ -236,68 +237,55 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
try {
|
||||
let playerUrl = '';
|
||||
|
||||
switch (player) {
|
||||
case 'vlc':
|
||||
playerUrl = `vlc://${resolvedVideoUrl}`;
|
||||
break;
|
||||
case 'iina':
|
||||
playerUrl = `iina://weblink?url=${encodeURIComponent(resolvedVideoUrl)}`;
|
||||
break;
|
||||
case 'mpv':
|
||||
playerUrl = `mpv://${resolvedVideoUrl}`;
|
||||
break;
|
||||
case 'potplayer':
|
||||
playerUrl = `potplayer://${resolvedVideoUrl}`;
|
||||
break;
|
||||
case 'infuse':
|
||||
playerUrl = `infuse://x-callback-url/play?url=${encodeURIComponent(resolvedVideoUrl)}`;
|
||||
break;
|
||||
case 'copy':
|
||||
// Copy URL to clipboard
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(resolvedVideoUrl).then(() => {
|
||||
if (player === 'copy') {
|
||||
// Copy URL to clipboard
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(resolvedVideoUrl).then(() => {
|
||||
showSnackbar(t('linkCopied'), 'success');
|
||||
}).catch(() => {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
});
|
||||
} else {
|
||||
// Fallback
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = resolvedVideoUrl;
|
||||
textArea.style.position = "fixed";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showSnackbar(t('linkCopied'), 'success');
|
||||
}).catch(() => {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
});
|
||||
} else {
|
||||
// Fallback
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = resolvedVideoUrl;
|
||||
textArea.style.position = "fixed";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showSnackbar(t('linkCopied'), 'success');
|
||||
} else {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
} else {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
} catch (err) {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
}
|
||||
handlePlayerMenuClose();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
handlePlayerMenuClose();
|
||||
return;
|
||||
} else {
|
||||
playerUrl = getPlayerUrl(player, resolvedVideoUrl);
|
||||
}
|
||||
|
||||
// Try to open the player URL using a hidden anchor element
|
||||
const link = document.createElement('a');
|
||||
link.href = playerUrl;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
// This prevents navigation away from the page
|
||||
if (playerUrl) {
|
||||
const link = document.createElement('a');
|
||||
link.href = playerUrl;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// Show a message after a short delay
|
||||
setTimeout(() => {
|
||||
showSnackbar(t('openInExternalPlayer'), 'info');
|
||||
}, 500);
|
||||
// Show a message after a short delay
|
||||
setTimeout(() => {
|
||||
showSnackbar(t('openInExternalPlayer'), 'info');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error opening player:', error);
|
||||
@@ -612,11 +600,11 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={() => handlePlayerSelect('vlc')}>VLC</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('iina')}>IINA</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('mpv')}>mpv</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('potplayer')}>PotPlayer</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('infuse')}>Infuse</MenuItem>
|
||||
{getAvailablePlayers().map((player) => (
|
||||
<MenuItem key={player.id} onClick={() => handlePlayerSelect(player.id)}>
|
||||
{player.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem onClick={() => handlePlayerSelect('copy')}>{t('copyUrl')}</MenuItem>
|
||||
</Menu>
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useVideo } from '../../../contexts/VideoContext';
|
||||
import { useVisitorMode } from '../../../contexts/VisitorModeContext';
|
||||
import { useCloudStorageUrl } from '../../../hooks/useCloudStorageUrl';
|
||||
import { useShareVideo } from '../../../hooks/useShareVideo';
|
||||
import { Video } from '../../../types';
|
||||
import { Video } from '../../../types'; // Add imports
|
||||
import { getAvailablePlayers, getPlayerUrl } from '../../../utils/playerUtils'; // Import new utils
|
||||
import VideoKebabMenuButtons from './VideoKebabMenuButtons';
|
||||
|
||||
interface VideoActionButtonsProps {
|
||||
@@ -90,54 +91,42 @@ const VideoActionButtons: React.FC<VideoActionButtonsProps> = ({
|
||||
try {
|
||||
let playerUrl = '';
|
||||
|
||||
switch (player) {
|
||||
case 'vlc':
|
||||
playerUrl = `vlc://${resolvedVideoUrl}`;
|
||||
break;
|
||||
case 'iina':
|
||||
playerUrl = `iina://weblink?url=${encodeURIComponent(resolvedVideoUrl)}`;
|
||||
break;
|
||||
case 'mpv':
|
||||
playerUrl = `mpv://${resolvedVideoUrl}`;
|
||||
break;
|
||||
case 'potplayer':
|
||||
playerUrl = `potplayer://${resolvedVideoUrl}`;
|
||||
break;
|
||||
case 'infuse':
|
||||
playerUrl = `infuse://x-callback-url/play?url=${encodeURIComponent(resolvedVideoUrl)}`;
|
||||
break;
|
||||
case 'copy':
|
||||
// Copy URL to clipboard
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(resolvedVideoUrl).then(() => {
|
||||
if (player === 'copy') {
|
||||
// Copy logic will be handled below but we need valid logic structure
|
||||
} else {
|
||||
playerUrl = getPlayerUrl(player, resolvedVideoUrl);
|
||||
}
|
||||
|
||||
if (player === 'copy') {
|
||||
// Copy URL to clipboard
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(resolvedVideoUrl).then(() => {
|
||||
showSnackbar(t('linkCopied'), 'success');
|
||||
}).catch(() => {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
});
|
||||
} else {
|
||||
// Fallback
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = resolvedVideoUrl;
|
||||
textArea.style.position = "fixed";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showSnackbar(t('linkCopied'), 'success');
|
||||
}).catch(() => {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
});
|
||||
} else {
|
||||
// Fallback
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = resolvedVideoUrl;
|
||||
textArea.style.position = "fixed";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showSnackbar(t('linkCopied'), 'success');
|
||||
} else {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
} else {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
} catch (err) {
|
||||
showSnackbar(t('copyFailed'), 'error');
|
||||
}
|
||||
handlePlayerMenuClose();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
handlePlayerMenuClose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to open the player URL using a hidden anchor element
|
||||
@@ -187,11 +176,11 @@ const VideoActionButtons: React.FC<VideoActionButtonsProps> = ({
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={() => handlePlayerSelect('vlc')}>VLC</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('iina')}>IINA</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('mpv')}>mpv</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('potplayer')}>PotPlayer</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('infuse')}>Infuse</MenuItem>
|
||||
{getAvailablePlayers().map((player) => (
|
||||
<MenuItem key={player.id} onClick={() => handlePlayerSelect(player.id)}>
|
||||
{player.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem onClick={() => handlePlayerSelect('copy')}>{t('copyUrl')}</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -270,11 +259,11 @@ const VideoActionButtons: React.FC<VideoActionButtonsProps> = ({
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={() => handlePlayerSelect('vlc')}>VLC</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('iina')}>IINA</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('mpv')}>mpv</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('potplayer')}>PotPlayer</MenuItem>
|
||||
<MenuItem onClick={() => handlePlayerSelect('infuse')}>Infuse</MenuItem>
|
||||
{getAvailablePlayers().map((player) => (
|
||||
<MenuItem key={player.id} onClick={() => handlePlayerSelect(player.id)}>
|
||||
{player.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem onClick={() => handlePlayerSelect('copy')}>{t('copyUrl')}</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
|
||||
161
frontend/src/utils/playerUtils.ts
Normal file
161
frontend/src/utils/playerUtils.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
export interface PlayerOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Platform detection functions
|
||||
// Using navigator.userAgent instead of deprecated navigator.platform
|
||||
// Note: isMac() excludes iOS devices to avoid overlap with isIOS()
|
||||
export const isMac = () => {
|
||||
const userAgent = navigator.userAgent;
|
||||
// Exclude iOS devices (iPod, iPhone, iPad) from Mac detection
|
||||
if (/iPod|iPhone|iPad/.test(userAgent)) {
|
||||
return false;
|
||||
}
|
||||
// Check for Mac (Macintosh, MacIntel, MacPPC, Mac68K)
|
||||
return /Macintosh|MacIntel|MacPPC|Mac68K/.test(userAgent);
|
||||
};
|
||||
|
||||
export const isWindows = () => {
|
||||
const userAgent = navigator.userAgent;
|
||||
return /Win32|Win64|Windows|WinCE/.test(userAgent);
|
||||
};
|
||||
|
||||
export const isIOS = () => {
|
||||
const userAgent = navigator.userAgent;
|
||||
// Check for iOS devices (iPod, iPhone, iPad)
|
||||
// Exclude IE/Edge legacy (MSStream check)
|
||||
const isNotLegacyIE = !(window as unknown as { MSStream?: unknown }).MSStream;
|
||||
if (/iPod|iPhone|iPad/.test(userAgent) && isNotLegacyIE) {
|
||||
return true;
|
||||
}
|
||||
// Check for iPadOS (iPad running iPadOS may report as Mac with touch support)
|
||||
// This is a fallback for cases where userAgent doesn't explicitly mention iPad
|
||||
return (
|
||||
/Macintosh/.test(userAgent) && navigator.maxTouchPoints > 1 && isNotLegacyIE
|
||||
);
|
||||
};
|
||||
|
||||
export const isAndroid = () => /Android/.test(navigator.userAgent);
|
||||
|
||||
export const isLinux = () => {
|
||||
const userAgent = navigator.userAgent;
|
||||
return /Linux/.test(userAgent) && !isAndroid() && !/Android/.test(userAgent);
|
||||
};
|
||||
|
||||
// Player definitions
|
||||
const PLAYERS = {
|
||||
VLC: { id: "vlc", name: "VLC" },
|
||||
IINA: { id: "iina", name: "IINA" },
|
||||
INFUSE: { id: "infuse", name: "Infuse" },
|
||||
MPV: { id: "mpv", name: "mpv" },
|
||||
POTPLAYER: { id: "potplayer", name: "PotPlayer" },
|
||||
MXPLAYER: { id: "mxplayer", name: "MX Player" },
|
||||
KMPLAYER: { id: "kmplayer", name: "KMPlayer" },
|
||||
GOMPLAYER: { id: "gomplayer", name: "GOM Player" },
|
||||
NPLAYER: { id: "nplayer", name: "nPlayer" },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get available players based on the current platform
|
||||
* @returns Array of available player options
|
||||
*/
|
||||
export const getAvailablePlayers = (): PlayerOption[] => {
|
||||
const players: PlayerOption[] = [];
|
||||
const isDesktop = isMac() || isWindows() || isLinux();
|
||||
const isMobile = isIOS() || isAndroid();
|
||||
|
||||
// Desktop players
|
||||
if (isDesktop) {
|
||||
players.push(PLAYERS.VLC);
|
||||
players.push(PLAYERS.MPV);
|
||||
|
||||
// Mac-specific players
|
||||
if (isMac()) {
|
||||
players.push(PLAYERS.IINA);
|
||||
players.push(PLAYERS.INFUSE);
|
||||
}
|
||||
|
||||
// Windows-specific players
|
||||
if (isWindows()) {
|
||||
players.push(PLAYERS.POTPLAYER);
|
||||
players.push(PLAYERS.KMPLAYER);
|
||||
players.push(PLAYERS.GOMPLAYER);
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile players
|
||||
if (isMobile) {
|
||||
players.push(PLAYERS.VLC);
|
||||
|
||||
// iOS-specific players
|
||||
if (isIOS()) {
|
||||
players.push(PLAYERS.INFUSE);
|
||||
players.push(PLAYERS.NPLAYER);
|
||||
}
|
||||
|
||||
// Android-specific players
|
||||
if (isAndroid()) {
|
||||
players.push(PLAYERS.MXPLAYER);
|
||||
players.push(PLAYERS.KMPLAYER);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no platform detected, at least offer VLC
|
||||
if (players.length === 0) {
|
||||
players.push(PLAYERS.VLC);
|
||||
}
|
||||
|
||||
// Remove duplicates by converting to Map and back to array
|
||||
const playerMap = new Map<string, PlayerOption>();
|
||||
for (const player of players) {
|
||||
if (!playerMap.has(player.id)) {
|
||||
playerMap.set(player.id, player);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(playerMap.values());
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a player URL scheme for the given player ID and video URL
|
||||
* @param playerId - The ID of the player (e.g., 'vlc', 'iina', 'mpv')
|
||||
* @param videoUrl - The URL of the video to play
|
||||
* @returns The player URL scheme, or empty string if playerId is invalid
|
||||
*/
|
||||
export const getPlayerUrl = (playerId: string, videoUrl: string): string => {
|
||||
// Validate inputs
|
||||
if (!playerId || !videoUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Ensure videoUrl is a valid string
|
||||
const encodedUrl = encodeURIComponent(videoUrl);
|
||||
|
||||
switch (playerId) {
|
||||
case "vlc":
|
||||
return `vlc://${videoUrl}`;
|
||||
case "iina":
|
||||
return `iina://weblink?url=${encodedUrl}`;
|
||||
case "mpv":
|
||||
return `mpv://${videoUrl}`;
|
||||
case "potplayer":
|
||||
return `potplayer://${videoUrl}`;
|
||||
case "infuse":
|
||||
return `infuse://x-callback-url/play?url=${encodedUrl}`;
|
||||
case "mxplayer":
|
||||
// MX Player Android intent URL
|
||||
return `intent:${videoUrl}#Intent;package=com.mxtech.videoplayer.ad;action=android.intent.action.VIEW;type=video/*;end`;
|
||||
case "kmplayer":
|
||||
// KMPlayer URL scheme (may vary by platform)
|
||||
return `kmplayer://${videoUrl}`;
|
||||
case "gomplayer":
|
||||
// GOM Player URL scheme
|
||||
return `gomplayer://${videoUrl}`;
|
||||
case "nplayer":
|
||||
// nPlayer iOS URL scheme
|
||||
return `nplayer://${encodedUrl}`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user