直链播放
This commit is contained in:
+85
-2
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Bot, ChevronRight, ListVideo } from 'lucide-react';
|
import { Bot, ChevronRight, Link as LinkIcon, ListVideo } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { Suspense, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
import { getDoubanCategories } from '@/lib/douban.client';
|
import { getDoubanCategories } from '@/lib/douban.client';
|
||||||
import { getTMDBImageUrl, TMDBItem } from '@/lib/tmdb.client';
|
import { getTMDBImageUrl, TMDBItem } from '@/lib/tmdb.client';
|
||||||
import { DoubanItem } from '@/lib/types';
|
import { DoubanItem } from '@/lib/types';
|
||||||
import { processImageUrl } from '@/lib/utils';
|
import { base58Encode, processImageUrl } from '@/lib/utils';
|
||||||
|
|
||||||
import AIChatPanel from '@/components/AIChatPanel';
|
import AIChatPanel from '@/components/AIChatPanel';
|
||||||
import BannerCarousel from '@/components/BannerCarousel';
|
import BannerCarousel from '@/components/BannerCarousel';
|
||||||
@@ -45,6 +46,7 @@ function HomeClient() {
|
|||||||
>([]);
|
>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { announcement } = useSite();
|
const { announcement } = useSite();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// 首页模块配置状态
|
// 首页模块配置状态
|
||||||
const [homeModules, setHomeModules] = useState<HomeModule[]>([
|
const [homeModules, setHomeModules] = useState<HomeModule[]>([
|
||||||
@@ -64,6 +66,23 @@ function HomeClient() {
|
|||||||
const [aiEnabled, setAiEnabled] = useState(false);
|
const [aiEnabled, setAiEnabled] = useState(false);
|
||||||
const [aiDefaultMessageNoVideo, setAiDefaultMessageNoVideo] = useState('你好!我是MoonTVPlus的AI影视助手。想看什么电影或剧集?需要推荐吗?');
|
const [aiDefaultMessageNoVideo, setAiDefaultMessageNoVideo] = useState('你好!我是MoonTVPlus的AI影视助手。想看什么电影或剧集?需要推荐吗?');
|
||||||
const [sourceSearchEnabled, setSourceSearchEnabled] = useState(true);
|
const [sourceSearchEnabled, setSourceSearchEnabled] = useState(true);
|
||||||
|
const [showDirectPlayDialog, setShowDirectPlayDialog] = useState(false);
|
||||||
|
const [directPlayUrl, setDirectPlayUrl] = useState('');
|
||||||
|
|
||||||
|
const handleDirectPlay = () => {
|
||||||
|
setDirectPlayUrl('');
|
||||||
|
setShowDirectPlayDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitDirectPlay = () => {
|
||||||
|
const trimmed = directPlayUrl.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
const encoded = base58Encode(trimmed);
|
||||||
|
if (!encoded) return;
|
||||||
|
setShowDirectPlayDialog(false);
|
||||||
|
setDirectPlayUrl('');
|
||||||
|
router.push(`/play?source=directplay&id=${encodeURIComponent(encoded)}`);
|
||||||
|
};
|
||||||
|
|
||||||
const loadHomeLayoutSettings = () => {
|
const loadHomeLayoutSettings = () => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
@@ -566,6 +585,14 @@ function HomeClient() {
|
|||||||
<>
|
<>
|
||||||
{/* 源站寻片和AI问片入口 */}
|
{/* 源站寻片和AI问片入口 */}
|
||||||
<div className={`flex items-center justify-end gap-2 mb-4 ${homeBannerEnabled ? '' : 'mt-[30px]'}`}>
|
<div className={`flex items-center justify-end gap-2 mb-4 ${homeBannerEnabled ? '' : 'mt-[30px]'}`}>
|
||||||
|
<button
|
||||||
|
onClick={handleDirectPlay}
|
||||||
|
className='p-1.5 rounded-lg text-blue-500 hover:text-blue-600 transition-colors'
|
||||||
|
title='直链播放'
|
||||||
|
>
|
||||||
|
<LinkIcon size={18} />
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* 源站寻片入口 */}
|
{/* 源站寻片入口 */}
|
||||||
{sourceSearchEnabled && (
|
{sourceSearchEnabled && (
|
||||||
<Link href='/source-search'>
|
<Link href='/source-search'>
|
||||||
@@ -635,6 +662,62 @@ function HomeClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showDirectPlayDialog && (
|
||||||
|
<div
|
||||||
|
className='fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4'
|
||||||
|
onClick={() => setShowDirectPlayDialog(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='bg-white dark:bg-gray-900 rounded-lg shadow-xl w-full max-w-lg'
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700'>
|
||||||
|
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
|
直链播放
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDirectPlayDialog(false)}
|
||||||
|
className='p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors'
|
||||||
|
aria-label='关闭'
|
||||||
|
>
|
||||||
|
<span className='text-gray-600 dark:text-gray-400'>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='p-4 space-y-4'>
|
||||||
|
<div className='text-sm text-gray-600 dark:text-gray-300'>
|
||||||
|
请输入可直接播放的视频链接。
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
value={directPlayUrl}
|
||||||
|
onChange={(event) => setDirectPlayUrl(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
submitDirectPlay();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder='https://example.com/video.m3u8'
|
||||||
|
className='w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||||||
|
/>
|
||||||
|
<div className='flex justify-end gap-2'>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDirectPlayDialog(false)}
|
||||||
|
className='px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={submitDirectPlay}
|
||||||
|
disabled={!directPlayUrl.trim()}
|
||||||
|
className='px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
|
||||||
|
>
|
||||||
|
开始播放
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-12
@@ -48,7 +48,7 @@ import {
|
|||||||
import { getDoubanDetail } from '@/lib/douban.client';
|
import { getDoubanDetail } from '@/lib/douban.client';
|
||||||
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
||||||
import { DanmakuFilterConfig, EpisodeFilterConfig,SearchResult } from '@/lib/types';
|
import { DanmakuFilterConfig, EpisodeFilterConfig,SearchResult } from '@/lib/types';
|
||||||
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
import { base58Decode, getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
||||||
import { useEnableComments } from '@/hooks/useEnableComments';
|
import { useEnableComments } from '@/hooks/useEnableComments';
|
||||||
import { usePlaySync } from '@/hooks/usePlaySync';
|
import { usePlaySync } from '@/hooks/usePlaySync';
|
||||||
|
|
||||||
@@ -488,6 +488,7 @@ function PlayPageClient() {
|
|||||||
const [currentSource, setCurrentSource] = useState(searchParams.get('source') || '');
|
const [currentSource, setCurrentSource] = useState(searchParams.get('source') || '');
|
||||||
const [currentId, setCurrentId] = useState(searchParams.get('id') || '');
|
const [currentId, setCurrentId] = useState(searchParams.get('id') || '');
|
||||||
const [fileName] = useState(searchParams.get('fileName') || ''); // 小雅源:用户点击的文件名
|
const [fileName] = useState(searchParams.get('fileName') || ''); // 小雅源:用户点击的文件名
|
||||||
|
const isDirectPlay = currentSource === 'directplay';
|
||||||
|
|
||||||
// 解析 source 参数以获取 embyKey(仅用于 API 调用)
|
// 解析 source 参数以获取 embyKey(仅用于 API 调用)
|
||||||
const parseSourceForApi = (source: string): { source: string; embyKey?: string } => {
|
const parseSourceForApi = (source: string): { source: string; embyKey?: string } => {
|
||||||
@@ -601,6 +602,10 @@ function PlayPageClient() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDirectPlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否禁用了自动装填弹幕
|
// 检查是否禁用了自动装填弹幕
|
||||||
const disableAutoLoad = localStorage.getItem('disableAutoLoadDanmaku') === 'true';
|
const disableAutoLoad = localStorage.getItem('disableAutoLoadDanmaku') === 'true';
|
||||||
if (disableAutoLoad) {
|
if (disableAutoLoad) {
|
||||||
@@ -938,11 +943,19 @@ function PlayPageClient() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadDanmakuForCurrentEpisode();
|
loadDanmakuForCurrentEpisode();
|
||||||
}, [currentEpisodeIndex, videoTitle, loading]);
|
}, [currentEpisodeIndex, videoTitle, loading, isDirectPlay]);
|
||||||
|
|
||||||
// 获取豆瓣评分数据
|
// 获取豆瓣评分数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDoubanRating = async () => {
|
const fetchDoubanRating = async () => {
|
||||||
|
if (isDirectPlay) {
|
||||||
|
setDoubanRating(null);
|
||||||
|
setDoubanCardSubtitle('');
|
||||||
|
setDoubanAka([]);
|
||||||
|
setDoubanYear('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!videoDoubanId || videoDoubanId === 0) {
|
if (!videoDoubanId || videoDoubanId === 0) {
|
||||||
setDoubanRating(null);
|
setDoubanRating(null);
|
||||||
setDoubanCardSubtitle('');
|
setDoubanCardSubtitle('');
|
||||||
@@ -994,11 +1007,16 @@ function PlayPageClient() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchDoubanRating();
|
fetchDoubanRating();
|
||||||
}, [videoDoubanId]);
|
}, [videoDoubanId, isDirectPlay]);
|
||||||
|
|
||||||
// 获取TMDB背景图
|
// 获取TMDB背景图
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTMDBBackdrop = async () => {
|
const fetchTMDBBackdrop = async () => {
|
||||||
|
if (isDirectPlay) {
|
||||||
|
setTmdbBackdrop(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否禁用背景图
|
// 检查是否禁用背景图
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const disabled = localStorage.getItem('tmdb_backdrop_disabled');
|
const disabled = localStorage.getItem('tmdb_backdrop_disabled');
|
||||||
@@ -1146,7 +1164,7 @@ function PlayPageClient() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchTMDBBackdrop();
|
fetchTMDBBackdrop();
|
||||||
}, [videoTitle, videoDoubanId]);
|
}, [videoTitle, videoDoubanId, isDirectPlay]);
|
||||||
|
|
||||||
|
|
||||||
// 视频播放地址
|
// 视频播放地址
|
||||||
@@ -1168,6 +1186,14 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
// 总集数
|
// 总集数
|
||||||
const totalEpisodes = detail?.episodes?.length || 0;
|
const totalEpisodes = detail?.episodes?.length || 0;
|
||||||
|
const directEpisodeLabel = detail?.episodes_titles?.[currentEpisodeIndex] || '直链';
|
||||||
|
const shouldShowEpisodeLabel = totalEpisodes > 1 || isDirectPlay;
|
||||||
|
const episodeLabel = isDirectPlay
|
||||||
|
? directEpisodeLabel
|
||||||
|
: detail?.episodes_titles?.[currentEpisodeIndex] || `第 ${currentEpisodeIndex + 1} 集`;
|
||||||
|
const playerEpisodeLabel = isDirectPlay
|
||||||
|
? directEpisodeLabel
|
||||||
|
: `第${currentEpisodeIndex + 1}集`;
|
||||||
|
|
||||||
// 用于记录是否需要在播放器 ready 后跳转到指定进度
|
// 用于记录是否需要在播放器 ready 后跳转到指定进度
|
||||||
const resumeTimeRef = useRef<number | null>(null);
|
const resumeTimeRef = useRef<number | null>(null);
|
||||||
@@ -2808,6 +2834,73 @@ function PlayPageClient() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initAll = async () => {
|
const initAll = async () => {
|
||||||
|
if (currentSource === 'directplay') {
|
||||||
|
if (!currentId) {
|
||||||
|
setError('缺少直链地址');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setLoadingStage('fetching');
|
||||||
|
setLoadingMessage('🎬 正在准备直链播放...');
|
||||||
|
|
||||||
|
let directUrl = '';
|
||||||
|
try {
|
||||||
|
directUrl = base58Decode(currentId);
|
||||||
|
} catch (decodeError) {
|
||||||
|
console.error('直链地址解析失败:', decodeError);
|
||||||
|
setError('直链地址解析失败');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directDetail: SearchResult = {
|
||||||
|
id: currentId,
|
||||||
|
title: '直链播放',
|
||||||
|
poster: '',
|
||||||
|
episodes: [directUrl],
|
||||||
|
episodes_titles: ['直链'],
|
||||||
|
source: 'directplay',
|
||||||
|
source_name: '直链',
|
||||||
|
class: '',
|
||||||
|
year: '',
|
||||||
|
desc: '',
|
||||||
|
type_name: '',
|
||||||
|
douban_id: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
setNeedPrefer(false);
|
||||||
|
setCurrentSource('directplay');
|
||||||
|
setCurrentId(currentId);
|
||||||
|
setVideoTitle('直链播放');
|
||||||
|
setVideoYear('');
|
||||||
|
setVideoCover('');
|
||||||
|
setVideoDoubanId(0);
|
||||||
|
setCorrectedDesc('');
|
||||||
|
setDetail(directDetail);
|
||||||
|
setSourceProxyMode(false);
|
||||||
|
setAvailableSources([directDetail]);
|
||||||
|
setCurrentEpisodeIndex(0);
|
||||||
|
setSourceSearchError(null);
|
||||||
|
setSourceSearchLoading(false);
|
||||||
|
setBackgroundSourcesLoading(false);
|
||||||
|
|
||||||
|
const newUrl = new URL(window.location.href);
|
||||||
|
newUrl.searchParams.set('source', 'directplay');
|
||||||
|
newUrl.searchParams.set('id', currentId);
|
||||||
|
newUrl.searchParams.delete('prefer');
|
||||||
|
newUrl.searchParams.delete('fileName');
|
||||||
|
window.history.replaceState({}, '', newUrl.toString());
|
||||||
|
|
||||||
|
setLoadingStage('ready');
|
||||||
|
setLoadingMessage('✨ 准备就绪,即将开始播放...');
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentSource && !currentId && !videoTitle && !searchTitle) {
|
if (!currentSource && !currentId && !videoTitle && !searchTitle) {
|
||||||
setError('缺少必要参数');
|
setError('缺少必要参数');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -3698,6 +3791,7 @@ function PlayPageClient() {
|
|||||||
// 预加载下一集弹幕(完全复制 loadDanmakuForCurrentEpisode 的逻辑)
|
// 预加载下一集弹幕(完全复制 loadDanmakuForCurrentEpisode 的逻辑)
|
||||||
const preloadNextEpisodeDanmaku = async () => {
|
const preloadNextEpisodeDanmaku = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (isDirectPlay) return;
|
||||||
const disableAutoLoad = localStorage.getItem('disableAutoLoadDanmaku') === 'true';
|
const disableAutoLoad = localStorage.getItem('disableAutoLoadDanmaku') === 'true';
|
||||||
if (disableAutoLoad) return;
|
if (disableAutoLoad) return;
|
||||||
|
|
||||||
@@ -4043,6 +4137,7 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
// 自动搜索并加载弹幕
|
// 自动搜索并加载弹幕
|
||||||
const autoSearchDanmaku = async () => {
|
const autoSearchDanmaku = async () => {
|
||||||
|
if (isDirectPlay) return;
|
||||||
const disableAutoLoad = localStorage.getItem('disableAutoLoadDanmaku') === 'true';
|
const disableAutoLoad = localStorage.getItem('disableAutoLoadDanmaku') === 'true';
|
||||||
if (disableAutoLoad) return;
|
if (disableAutoLoad) return;
|
||||||
|
|
||||||
@@ -4616,9 +4711,7 @@ function PlayPageClient() {
|
|||||||
// 非WebKit浏览器且播放器已存在,使用switch方法切换
|
// 非WebKit浏览器且播放器已存在,使用switch方法切换
|
||||||
if (!isWebkit && artPlayerRef.current) {
|
if (!isWebkit && artPlayerRef.current) {
|
||||||
artPlayerRef.current.switch = videoUrl;
|
artPlayerRef.current.switch = videoUrl;
|
||||||
artPlayerRef.current.title = `${videoTitle} - 第${
|
artPlayerRef.current.title = `${videoTitle} - ${playerEpisodeLabel}`;
|
||||||
currentEpisodeIndex + 1
|
|
||||||
}集`;
|
|
||||||
artPlayerRef.current.poster = videoCover;
|
artPlayerRef.current.poster = videoCover;
|
||||||
if (artPlayerRef.current?.video) {
|
if (artPlayerRef.current?.video) {
|
||||||
ensureVideoSource(
|
ensureVideoSource(
|
||||||
@@ -6993,12 +7086,9 @@ function PlayPageClient() {
|
|||||||
<h1 className={`text-xl font-semibold flex items-center gap-2 flex-wrap ${tmdbBackdrop ? 'text-white' : 'text-gray-900 dark:text-gray-100'}`}>
|
<h1 className={`text-xl font-semibold flex items-center gap-2 flex-wrap ${tmdbBackdrop ? 'text-white' : 'text-gray-900 dark:text-gray-100'}`}>
|
||||||
<span>
|
<span>
|
||||||
{videoTitle || '影片标题'}
|
{videoTitle || '影片标题'}
|
||||||
{totalEpisodes > 1 && (
|
{shouldShowEpisodeLabel && (
|
||||||
<span className={tmdbBackdrop ? 'text-white opacity-80' : 'text-gray-500 dark:text-gray-400'}>
|
<span className={tmdbBackdrop ? 'text-white opacity-80' : 'text-gray-500 dark:text-gray-400'}>
|
||||||
{` > ${
|
{` > ${episodeLabel}`}
|
||||||
detail?.episodes_titles?.[currentEpisodeIndex] ||
|
|
||||||
`第 ${currentEpisodeIndex + 1} 集`
|
|
||||||
}`}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -7568,6 +7658,8 @@ function PlayPageClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isDirectPlay && (
|
||||||
|
<>
|
||||||
{/* 详情展示 */}
|
{/* 详情展示 */}
|
||||||
<div className='grid grid-cols-1 md:grid-cols-5 lg:grid-cols-6 gap-4'>
|
<div className='grid grid-cols-1 md:grid-cols-5 lg:grid-cols-6 gap-4'>
|
||||||
{/* 文字区 */}
|
{/* 文字区 */}
|
||||||
@@ -7817,6 +7909,8 @@ function PlayPageClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toast通知 */}
|
{/* Toast通知 */}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
|
||||||
import { Settings } from 'lucide-react';
|
import { Link as LinkIcon, Settings } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -811,8 +811,10 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
}`.trim()}
|
}`.trim()}
|
||||||
>
|
>
|
||||||
{/* 封面 */}
|
{/* 封面 */}
|
||||||
<div className='flex-shrink-0 w-12 h-20 bg-gray-300 dark:bg-gray-600 rounded overflow-hidden'>
|
<div className='flex-shrink-0 w-12 h-20 bg-gray-300 dark:bg-gray-600 rounded overflow-hidden flex items-center justify-center'>
|
||||||
{source.poster && (
|
{source.source === 'directplay' ? (
|
||||||
|
<LinkIcon className='w-6 h-6 text-blue-500' />
|
||||||
|
) : source.poster ? (
|
||||||
<img
|
<img
|
||||||
src={processImageUrl(source.poster)}
|
src={processImageUrl(source.poster)}
|
||||||
alt={source.title}
|
alt={source.title}
|
||||||
@@ -822,7 +824,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
target.style.display = 'none';
|
target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 信息区域 */}
|
{/* 信息区域 */}
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
|||||||
const actualYear = year;
|
const actualYear = year;
|
||||||
const actualQuery = query || '';
|
const actualQuery = query || '';
|
||||||
const actualSearchType = type;
|
const actualSearchType = type;
|
||||||
|
const isDirectPlaySource = actualSource === 'directplay';
|
||||||
const displayYear = useMemo(() => {
|
const displayYear = useMemo(() => {
|
||||||
if (!actualYear) return '';
|
if (!actualYear) return '';
|
||||||
const normalized = actualYear.trim();
|
const normalized = actualYear.trim();
|
||||||
@@ -719,8 +720,12 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 骨架屏 */}
|
{/* 骨架屏 */}
|
||||||
{!isLoading && <ImagePlaceholder aspectRatio={orientation === 'horizontal' ? 'aspect-[3/2]' : 'aspect-[2/3]'} />}
|
{!isLoading && !isDirectPlaySource && <ImagePlaceholder aspectRatio={orientation === 'horizontal' ? 'aspect-[3/2]' : 'aspect-[2/3]'} />}
|
||||||
{/* 图片 */}
|
{isDirectPlaySource ? (
|
||||||
|
<div className='absolute inset-0 flex items-center justify-center bg-gray-200/80 dark:bg-gray-700/80'>
|
||||||
|
<Link className='w-8 h-8 text-blue-500' />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Image
|
<Image
|
||||||
src={processImageUrl(actualPoster)}
|
src={processImageUrl(actualPoster)}
|
||||||
alt={actualTitle}
|
alt={actualTitle}
|
||||||
@@ -755,6 +760,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
|||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 悬浮遮罩 */}
|
{/* 悬浮遮罩 */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user