feat: Add external player integration for video playback

This commit is contained in:
Peifan Li
2025-12-10 23:15:41 -05:00
parent e1bc7c464e
commit 694886d71c
11 changed files with 100 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ import {
Folder,
Link as LinkIcon,
LocalOffer,
PlayArrow,
Share,
VideoLibrary
} from '@mui/icons-material';
@@ -22,6 +23,9 @@ import {
Button,
Chip,
Divider,
ListItemText,
Menu,
MenuItem,
Rating,
Stack,
TextField,
@@ -77,8 +81,11 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
const [showDescriptionExpandButton, setShowDescriptionExpandButton] = useState(false);
const descriptionRef = useRef<HTMLParagraphElement>(null);
const [playerMenuAnchor, setPlayerMenuAnchor] = useState<null | HTMLElement>(null);
useEffect(() => {
const checkOverflow = () => {
const element = titleRef.current;
@@ -187,6 +194,42 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
return `${year}-${month}-${day}`;
};
const handleOpenPlayerMenu = (event: React.MouseEvent<HTMLElement>) => {
setPlayerMenuAnchor(event.currentTarget);
};
const handleClosePlayerMenu = () => {
setPlayerMenuAnchor(null);
};
const handlePlayInPlayer = (scheme: string) => {
const videoUrl = `${BACKEND_URL}${video.videoPath || video.sourceUrl}`;
let url = '';
switch (scheme) {
case 'iina':
url = `iina://weblink?url=${videoUrl}`;
break;
case 'vlc':
url = `vlc://${videoUrl}`;
break;
case 'potplayer':
url = `potplayer://${videoUrl}`;
break;
case 'mpv':
url = `mpv://${videoUrl}`;
break;
case 'infuse':
url = `infuse://x-callback-url/play?url=${videoUrl}`;
break;
}
if (url) {
window.location.href = url;
}
handleClosePlayerMenu();
};
return (
<Box sx={{ mt: 2 }}>
{isEditingTitle ? (
@@ -356,6 +399,43 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
</Box>
<Stack direction="row" spacing={1}>
<Tooltip title={t('openInExternalPlayer')}>
<Button
variant="outlined"
color="inherit"
onClick={handleOpenPlayerMenu}
sx={{ minWidth: 'auto', p: 1, color: 'text.secondary', borderColor: 'text.secondary', '&:hover': { color: 'primary.main', borderColor: 'primary.main' } }}
>
<PlayArrow />
</Button>
</Tooltip>
<Menu
anchorEl={playerMenuAnchor}
open={Boolean(playerMenuAnchor)}
onClose={handleClosePlayerMenu}
>
<MenuItem disabled>
<Typography variant="caption" color="text.secondary">
{t('playWith')}
</Typography>
</MenuItem>
<Divider />
<MenuItem onClick={() => handlePlayInPlayer('iina')}>
<ListItemText>IINA</ListItemText>
</MenuItem>
<MenuItem onClick={() => handlePlayInPlayer('vlc')}>
<ListItemText>VLC</ListItemText>
</MenuItem>
<MenuItem onClick={() => handlePlayInPlayer('potplayer')}>
<ListItemText>PotPlayer</ListItemText>
</MenuItem>
<MenuItem onClick={() => handlePlayInPlayer('mpv')}>
<ListItemText>MPV</ListItemText>
</MenuItem>
<MenuItem onClick={() => handlePlayInPlayer('infuse')}>
<ListItemText>Infuse</ListItemText>
</MenuItem>
</Menu>
<Tooltip title={t('share')}>
<Button
variant="outlined"

View File

@@ -217,6 +217,8 @@ export const ar = {
pleaseEnterSearchTerm: "الرجاء إدخال مصطلح البحث",
failedToSearch: "فشل البحث. يرجى المحاولة مرة أخرى.",
searchCancelled: "تم إلغاء البحث",
openInExternalPlayer: "فتح في مشغل خارجي",
playWith: "تشغيل بواسطة...",
// Login
signIn: "تسجيل الدخول",

View File

@@ -211,6 +211,8 @@ export const de = {
pleaseEnterSearchTerm: "Bitte geben Sie einen Suchbegriff ein",
failedToSearch: "Suche fehlgeschlagen. Bitte versuchen Sie es erneut.",
searchCancelled: "Suche abgebrochen",
openInExternalPlayer: "In externem Player öffnen",
playWith: "Abspielen mit...",
signIn: "Anmelden",
verifying: "Überprüfen...",
incorrectPassword: "Falsches Passwort",

View File

@@ -216,6 +216,8 @@ export const en = {
pleaseEnterSearchTerm: "Please enter a search term",
failedToSearch: "Failed to search. Please try again.",
searchCancelled: "Search was cancelled",
openInExternalPlayer: "Open in external player",
playWith: "Play with...",
// Login
signIn: "Sign in",

View File

@@ -226,6 +226,8 @@ export const es = {
pleaseEnterSearchTerm: "Por favor, introduzca un término de búsqueda",
failedToSearch: "Error en la búsqueda. Por favor, inténtelo de nuevo.",
searchCancelled: "Búsqueda cancelada",
openInExternalPlayer: "Abrir en reproductor externo",
playWith: "Reproducir con...",
signIn: "Iniciar Sesión",
verifying: "Verificando...",
incorrectPassword: "Contraseña incorrecta",

View File

@@ -236,6 +236,8 @@ export const fr = {
pleaseEnterSearchTerm: "Veuillez entrer un terme de recherche",
failedToSearch: "Échec de la recherche. Veuillez réessayer.",
searchCancelled: "Recherche annulée",
openInExternalPlayer: "Ouvrir dans un lecteur externe",
playWith: "Lire avec...",
// Login
signIn: "Se connecter",

View File

@@ -222,6 +222,8 @@ export const ja = {
pleaseEnterSearchTerm: "検索語を入力してください",
failedToSearch: "検索に失敗しました。もう一度お試しください。",
searchCancelled: "検索がキャンセルされました",
openInExternalPlayer: "外部プレーヤーで開く",
playWith: "で再生...",
// Login
signIn: "サインイン",

View File

@@ -219,6 +219,8 @@ export const ko = {
pleaseEnterSearchTerm: "검색어를 입력해주세요",
failedToSearch: "검색 실패. 다시 시도해주세요.",
searchCancelled: "검색이 취소되었습니다",
openInExternalPlayer: "외부 플레이어에서 열기",
playWith: "다음으로 재생...",
// Login
signIn: "로그인",

View File

@@ -231,6 +231,8 @@ export const pt = {
pleaseEnterSearchTerm: "Por favor, insira um termo de pesquisa",
failedToSearch: "Falha na pesquisa. Por favor, tente novamente.",
searchCancelled: "Pesquisa cancelada",
openInExternalPlayer: "Abrir no player externo",
playWith: "Reproduzir com...",
// Login
signIn: "Entrar",

View File

@@ -223,6 +223,8 @@ export const ru = {
pleaseEnterSearchTerm: "Пожалуйста, введите поисковый запрос",
failedToSearch: "Поиск не удался. Пожалуйста, попробуйте снова.",
searchCancelled: "Поиск отменен",
openInExternalPlayer: "Открыть во внешнем плеере",
playWith: "Воспроизвести с помощью...",
// Login
signIn: "Войти",

View File

@@ -211,6 +211,8 @@ export const zh = {
pleaseEnterSearchTerm: "请输入搜索词",
failedToSearch: "搜索失败。请稍后再试。",
searchCancelled: "搜索已取消",
openInExternalPlayer: "在外部播放器中打开",
playWith: "使用此应用播放...",
// Login
signIn: "登录",