移动云盘在线播放

This commit is contained in:
mtvpls
2026-04-26 17:41:56 +08:00
parent d8d7e57eab
commit c877397682
16 changed files with 984 additions and 104 deletions
+2 -1
View File
@@ -14,6 +14,7 @@ import type { DanmakuComment,DanmakuSelection } from '@/lib/danmaku/types';
import { generateStorageKey, getCachedPlayRecordsSnapshot } from '@/lib/db.client';
import { isEpisodeHiddenByFilter } from '@/lib/episode-filter';
import { loadAllLocalEpisodeProgressRecords } from '@/lib/episode-progress';
import { isNetdiskSource } from '@/lib/netdisk/source';
import { EpisodeFilterConfig,SearchResult } from '@/lib/types';
import { getVideoResolutionFromM3u8 } from '@/lib/utils';
@@ -1001,7 +1002,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
{/* 源名称和集数信息 - 垂直居中 */}
<div className='flex items-center justify-between'>
<span className={`text-xs px-2 py-1 border rounded text-gray-700 dark:text-gray-300 ${
source.source === 'xiaoya' ? 'border-blue-500' : source.source === 'quark-temp' ? 'border-purple-500' : source.source === 'openlist' || source.source === 'emby' || source.source?.startsWith('emby_')
source.source === 'xiaoya' ? 'border-blue-500' : isNetdiskSource(source.source) ? 'border-purple-500' : source.source === 'openlist' || source.source === 'emby' || source.source?.startsWith('emby_')
? 'border-yellow-500'
: 'border-gray-500/60'
}`}>
+18 -15
View File
@@ -5,9 +5,10 @@ import { AlertCircle, Copy, ExternalLink, Loader2, RefreshCw } from 'lucide-reac
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import Toast, { ToastProps } from '@/components/Toast';
import { PansouLink, PansouSearchResult } from '@/lib/pansou.client';
import Toast, { ToastProps } from '@/components/Toast';
interface PansouSearchProps {
keyword: string;
triggerSearch?: boolean; // 触发搜索的标志
@@ -108,7 +109,7 @@ export default function PansouSearch({
}
searchPansou();
}, [triggerSearch, searchPansou]); // 依赖 triggerSearch 和 searchPansou
}, [triggerSearch]); // 只在触发标志变化时搜索,避免 keyword 变化自动搜索
const handleCopy = async (text: string, url: string) => {
try {
@@ -159,10 +160,10 @@ export default function PansouSearch({
}
};
const handleQuarkInstantPlay = async (link: PansouLink) => {
const handleNetdiskInstantPlay = async (cloudType: string, link: PansouLink) => {
try {
setPlayingUrl(link.url);
const response = await fetch('/api/netdisk/quark/instant-play', {
const response = await fetch(cloudType === 'mobile' ? '/api/netdisk/mobile/instant-play' : '/api/netdisk/quark/instant-play', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -180,7 +181,7 @@ export default function PansouSearch({
}
router.push(
`/play?source=quark-temp&id=${encodeURIComponent(data.id)}&title=${encodeURIComponent(data.title || keyword)}`
`/play?source=${encodeURIComponent(data.source || (cloudType === 'mobile' ? 'netdisk-mobile' : 'netdisk-quark'))}&id=${encodeURIComponent(data.id)}&title=${encodeURIComponent(data.title || keyword)}`
);
} catch (err: any) {
setToast({
@@ -338,24 +339,26 @@ export default function PansouSearch({
{/* 操作按钮 */}
<div className='flex items-center gap-1 flex-shrink-0'>
{cloudType === 'quark' && (
{(cloudType === 'quark' || cloudType === 'mobile') && (
<>
<button
onClick={() => handleQuarkInstantPlay(link)}
onClick={() => handleNetdiskInstantPlay(cloudType, link)}
disabled={playingUrl === link.url}
className='px-2 py-1 rounded-md bg-green-600 hover:bg-green-700 text-white text-xs transition-colors disabled:opacity-60'
title='立即播放'
>
{playingUrl === link.url ? '处理中...' : '立即播放'}
</button>
<button
onClick={() => handleQuarkTransfer(link)}
disabled={transferingUrl === link.url}
className='px-2 py-1 rounded-md bg-purple-600 hover:bg-purple-700 text-white text-xs transition-colors disabled:opacity-60'
title='转存到配置目录'
>
{transferingUrl === link.url ? '转存中...' : '转存'}
</button>
{cloudType === 'quark' && (
<button
onClick={() => handleQuarkTransfer(link)}
disabled={transferingUrl === link.url}
className='px-2 py-1 rounded-md bg-purple-600 hover:bg-purple-700 text-white text-xs transition-colors disabled:opacity-60'
title='转存到配置目录'
>
{transferingUrl === link.url ? '转存中...' : '转存'}
</button>
)}
</>
)}
<button
+29 -6
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any,react-hooks/exhaustive-deps,@typescript-eslint/no-empty-function */
import { ExternalLink, Heart, Info, Link, PlayCircleIcon, Radio, Sparkles, Trash2 } from 'lucide-react';
import { Cloud, ExternalLink, Heart, Info, Link, PlayCircleIcon, Radio, Sparkles, Trash2 } from 'lucide-react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import React, {
@@ -21,11 +21,12 @@ import {
saveFavorite,
subscribeToDataUpdates,
} from '@/lib/db.client';
import { isNetdiskSource } from '@/lib/netdisk/source';
import {
processImageUrl,
base58Decode,
tryApplyDoubanImageFallback,
getDoubanImageFallbackUrl,
processImageUrl,
tryApplyDoubanImageFallback,
} from '@/lib/utils';
import { useLongPress } from '@/hooks/useLongPress';
@@ -112,7 +113,25 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
const router = useRouter();
const actualTitle = title;
const actualPoster = poster;
const processedPoster = useMemo(() => processImageUrl(actualPoster), [actualPoster]);
const netdiskPosterPlaceholder = useMemo(() => {
return `data:image/svg+xml;utf8,${encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 600">
<rect width="400" height="600" fill="#f3f4f6"/>
<g fill="none" stroke="#9ca3af" stroke-width="16" stroke-linecap="round" stroke-linejoin="round">
<path d="M118 332c-30.9 0-56-25.1-56-56 0-28.5 21.3-52 48.9-55.4C120.6 184.7 154.8 160 195 160c51.1 0 92.9 39.2 97.1 89.2 27.3 4.2 47.9 27.7 47.9 56.8 0 32-26 58-58 58H118z"/>
</g>
</svg>
`)}`;
}, []);
const processedPoster = useMemo(
() =>
actualPoster
? processImageUrl(actualPoster)
: isNetdiskSource(source)
? netdiskPosterPlaceholder
: '',
[actualPoster, source, netdiskPosterPlaceholder]
);
const [favorited, setFavorited] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [showMobileActions, setShowMobileActions] = useState(false);
@@ -747,6 +766,10 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
<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>
) : (isNetdiskSource(actualSource) && !actualPoster && displayPoster === netdiskPosterPlaceholder) ? (
<div className='absolute inset-0 flex flex-col items-center justify-center bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400'>
<Cloud className='w-10 h-10 opacity-80' />
</div>
) : (
<Image
src={displayPoster}
@@ -1061,7 +1084,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
>
<span
className={`inline-block border rounded px-1 py-0.5 text-[8px] text-white/90 bg-black/60 ${
actualSource === 'xiaoya' ? 'border-blue-500' : actualSource === 'quark-temp' ? 'border-purple-500' : actualSource === 'openlist' || actualSource === 'emby' || actualSource?.startsWith('emby_') ? 'border-yellow-500' : origin === 'live' ? 'border-red-500' : 'border-white/60'
actualSource === 'xiaoya' ? 'border-blue-500' : isNetdiskSource(actualSource) ? 'border-purple-500' : actualSource === 'openlist' || actualSource === 'emby' || actualSource?.startsWith('emby_') ? 'border-yellow-500' : origin === 'live' ? 'border-red-500' : 'border-white/60'
}`}
style={{
WebkitUserSelect: 'none',
@@ -1385,7 +1408,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
{config.showSourceName && source_name && !cmsData && (
<span
className={`inline-block border rounded px-1 py-0.5 text-[8px] text-white/90 bg-black/30 backdrop-blur-sm ${
actualSource === 'xiaoya' ? 'border-blue-500' : actualSource === 'quark-temp' ? 'border-purple-500' : actualSource === 'openlist' || actualSource === 'emby' || actualSource?.startsWith('emby_') ? 'border-yellow-500' : 'border-white/60'
actualSource === 'xiaoya' ? 'border-blue-500' : isNetdiskSource(actualSource) ? 'border-purple-500' : actualSource === 'openlist' || actualSource === 'emby' || actualSource?.startsWith('emby_') ? 'border-yellow-500' : 'border-white/60'
}`}
style={{
WebkitUserSelect: 'none',