mirror of https://github.com/djteang/OrangeTV.git
Tune home page layout
This commit is contained in:
parent
9389be8b97
commit
f8eb7cea4c
|
|
@ -199,10 +199,7 @@ html {
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background:
|
background: rgb(var(--color-background));
|
||||||
radial-gradient(circle at 18% -12%, rgb(var(--color-accent) / 0.1), transparent 34rem),
|
|
||||||
linear-gradient(180deg, rgb(var(--color-background)) 0%, rgb(var(--color-surface-secondary)) 100%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
color: rgb(var(--color-foreground));
|
color: rgb(var(--color-foreground));
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
font-feature-settings: "tnum" 1, "cv02" 1, "cv03" 1, "cv04" 1;
|
font-feature-settings: "tnum" 1, "cv02" 1, "cv03" 1, "cv04" 1;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button, Card, EmptyState, Link as HeroLink, Skeleton } from '@heroui/react';
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
EmptyState,
|
||||||
|
Link as HeroLink,
|
||||||
|
Skeleton,
|
||||||
|
} from '@heroui/react';
|
||||||
import { Suspense, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -175,9 +181,10 @@ function HomeClient() {
|
||||||
<CapsuleSwitch
|
<CapsuleSwitch
|
||||||
options={[
|
options={[
|
||||||
{ label: '首页', value: 'home' },
|
{ label: '首页', value: 'home' },
|
||||||
{ label: '收藏夹', value: 'favorites' },
|
{ label: '收藏', value: 'favorites' },
|
||||||
]}
|
]}
|
||||||
active={activeTab}
|
active={activeTab}
|
||||||
|
compact
|
||||||
onChange={(value) => setActiveTab(value as 'home' | 'favorites')}
|
onChange={(value) => setActiveTab(value as 'home' | 'favorites')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -203,7 +210,7 @@ function HomeClient() {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<div className='grid justify-start grid-cols-3 gap-x-3 gap-y-14 px-0 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:gap-y-20'>
|
<div className='grid grid-cols-[repeat(auto-fill,_minmax(96px,_96px))] justify-start gap-x-3 gap-y-10 px-0 pt-2 pb-5 sm:grid-cols-[repeat(auto-fill,_minmax(180px,_180px))] sm:gap-x-5 sm:gap-y-12 sm:pb-3'>
|
||||||
{favoriteItems.map((item) => (
|
{favoriteItems.map((item) => (
|
||||||
<div key={item.id + item.source} className='w-full'>
|
<div key={item.id + item.source} className='w-full'>
|
||||||
<VideoCard
|
<VideoCard
|
||||||
|
|
@ -234,11 +241,7 @@ function HomeClient() {
|
||||||
<Card.Description>精选推荐</Card.Description>
|
<Card.Description>精选推荐</Card.Description>
|
||||||
<Card.Title>热门电影</Card.Title>
|
<Card.Title>热门电影</Card.Title>
|
||||||
</div>
|
</div>
|
||||||
<HeroLink
|
<HeroLink href='/douban?type=movie'>查看更多</HeroLink>
|
||||||
href='/douban?type=movie'
|
|
||||||
>
|
|
||||||
查看更多
|
|
||||||
</HeroLink>
|
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<ScrollableRow>
|
<ScrollableRow>
|
||||||
{loading
|
{loading
|
||||||
|
|
@ -279,9 +282,7 @@ function HomeClient() {
|
||||||
<Card.Description>Series</Card.Description>
|
<Card.Description>Series</Card.Description>
|
||||||
<Card.Title>热门剧集</Card.Title>
|
<Card.Title>热门剧集</Card.Title>
|
||||||
</div>
|
</div>
|
||||||
<HeroLink href='/douban?type=tv'>
|
<HeroLink href='/douban?type=tv'>查看更多</HeroLink>
|
||||||
查看更多
|
|
||||||
</HeroLink>
|
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<ScrollableRow>
|
<ScrollableRow>
|
||||||
{loading
|
{loading
|
||||||
|
|
@ -321,11 +322,7 @@ function HomeClient() {
|
||||||
<Card.Description>Bangumi</Card.Description>
|
<Card.Description>Bangumi</Card.Description>
|
||||||
<Card.Title>新番放送</Card.Title>
|
<Card.Title>新番放送</Card.Title>
|
||||||
</div>
|
</div>
|
||||||
<HeroLink
|
<HeroLink href='/douban?type=anime'>查看更多</HeroLink>
|
||||||
href='/douban?type=anime'
|
|
||||||
>
|
|
||||||
查看更多
|
|
||||||
</HeroLink>
|
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<ScrollableRow>
|
<ScrollableRow>
|
||||||
{loading
|
{loading
|
||||||
|
|
@ -394,11 +391,7 @@ function HomeClient() {
|
||||||
<Card.Description>Shows</Card.Description>
|
<Card.Description>Shows</Card.Description>
|
||||||
<Card.Title>热门综艺</Card.Title>
|
<Card.Title>热门综艺</Card.Title>
|
||||||
</div>
|
</div>
|
||||||
<HeroLink
|
<HeroLink href='/douban?type=show'>查看更多</HeroLink>
|
||||||
href='/douban?type=show'
|
|
||||||
>
|
|
||||||
查看更多
|
|
||||||
</HeroLink>
|
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<ScrollableRow>
|
<ScrollableRow>
|
||||||
{loading
|
{loading
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ interface CapsuleSwitchProps {
|
||||||
active: string;
|
active: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
compact?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CapsuleSwitch: React.FC<CapsuleSwitchProps> = ({
|
const CapsuleSwitch: React.FC<CapsuleSwitchProps> = ({
|
||||||
|
|
@ -14,13 +15,20 @@ const CapsuleSwitch: React.FC<CapsuleSwitchProps> = ({
|
||||||
active,
|
active,
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
|
compact = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const compactClasses =
|
||||||
|
'mx-auto w-fit [&_.tabs__list]:w-fit [&_.tabs__tab]:h-9 [&_.tabs__tab]:w-auto [&_.tabs__tab]:min-w-16 [&_.tabs__tab]:px-4 [&_.tabs__tab]:text-sm';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppFilterTabs
|
<AppFilterTabs
|
||||||
ariaLabel='内容切换'
|
ariaLabel='内容切换'
|
||||||
className={className}
|
className={[compact ? compactClasses : '', className]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
items={options.map((opt) => ({ key: opt.value, label: opt.label }))}
|
items={options.map((opt) => ({ key: opt.value, label: opt.label }))}
|
||||||
selectedKey={active}
|
selectedKey={active}
|
||||||
|
variant={compact ? 'primary' : 'secondary'}
|
||||||
onSelectionChange={onChange}
|
onSelectionChange={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export default function ScrollableRow({
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className='scrollbar-hide flex space-x-4 overflow-x-auto px-1 py-2 pb-12 sm:space-x-5 sm:pb-14'
|
className='scrollbar-hide flex space-x-4 overflow-x-auto px-1 pt-2 pb-5 sm:space-x-5 sm:pb-3'
|
||||||
onScroll={checkScroll}
|
onScroll={checkScroll}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,22 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any,react-hooks/exhaustive-deps,@typescript-eslint/no-empty-function */
|
/* eslint-disable @typescript-eslint/no-explicit-any,react-hooks/exhaustive-deps,@typescript-eslint/no-empty-function */
|
||||||
|
|
||||||
import { ExternalLink, Heart, Link as LinkIcon, PlayCircleIcon, Radio, Trash2 } from 'lucide-react';
|
import {
|
||||||
import { Badge, Button, Card, Chip, Link as HeroLink, ProgressBar, Tooltip } from '@heroui/react';
|
ExternalLink,
|
||||||
|
Heart,
|
||||||
|
Link as LinkIcon,
|
||||||
|
PlayCircleIcon,
|
||||||
|
Radio,
|
||||||
|
Trash2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Chip,
|
||||||
|
Link as HeroLink,
|
||||||
|
ProgressBar,
|
||||||
|
Tooltip,
|
||||||
|
} from '@heroui/react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React, {
|
import React, {
|
||||||
|
|
@ -59,7 +74,8 @@ export type VideoCardHandle = {
|
||||||
setDoubanId: (id?: number) => void;
|
setDoubanId: (id?: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard(
|
const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
||||||
|
function VideoCard(
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
title = '',
|
title = '',
|
||||||
|
|
@ -89,15 +105,17 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
const [favorited, setFavorited] = useState(false);
|
const [favorited, setFavorited] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [showMobileActions, setShowMobileActions] = useState(false);
|
const [showMobileActions, setShowMobileActions] = useState(false);
|
||||||
const [searchFavorited, setSearchFavorited] = useState<boolean | null>(null); // 搜索结果的收藏状态
|
const [searchFavorited, setSearchFavorited] = useState<boolean | null>(
|
||||||
|
null
|
||||||
|
); // 搜索结果的收藏状态
|
||||||
|
|
||||||
// 可外部修改的可控字段
|
// 可外部修改的可控字段
|
||||||
const [dynamicEpisodes, setDynamicEpisodes] = useState<number | undefined>(
|
const [dynamicEpisodes, setDynamicEpisodes] = useState<number | undefined>(
|
||||||
episodes
|
episodes
|
||||||
);
|
);
|
||||||
const [dynamicSourceNames, setDynamicSourceNames] = useState<string[] | undefined>(
|
const [dynamicSourceNames, setDynamicSourceNames] = useState<
|
||||||
source_names
|
string[] | undefined
|
||||||
);
|
>(source_names);
|
||||||
const [dynamicDoubanId, setDynamicDoubanId] = useState<number | undefined>(
|
const [dynamicDoubanId, setDynamicDoubanId] = useState<number | undefined>(
|
||||||
douban_id
|
douban_id
|
||||||
);
|
);
|
||||||
|
|
@ -129,12 +147,21 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
const actualYear = year;
|
const actualYear = year;
|
||||||
const actualQuery = query || '';
|
const actualQuery = query || '';
|
||||||
const actualSearchType = isAggregate
|
const actualSearchType = isAggregate
|
||||||
? (actualEpisodes && actualEpisodes === 1 ? 'movie' : 'tv')
|
? actualEpisodes && actualEpisodes === 1
|
||||||
|
? 'movie'
|
||||||
|
: 'tv'
|
||||||
: type;
|
: type;
|
||||||
|
|
||||||
// 获取收藏状态(搜索结果、豆瓣和短剧页面不检查)
|
// 获取收藏状态(搜索结果、豆瓣和短剧页面不检查)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (from === 'douban' || from === 'search' || from === 'shortdrama' || !actualSource || !actualId) return;
|
if (
|
||||||
|
from === 'douban' ||
|
||||||
|
from === 'search' ||
|
||||||
|
from === 'shortdrama' ||
|
||||||
|
!actualSource ||
|
||||||
|
!actualId
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
const fetchFavoriteStatus = async () => {
|
const fetchFavoriteStatus = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -165,11 +192,18 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
async (e: React.MouseEvent) => {
|
async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (from === 'douban' || from === 'shortdrama' || !actualSource || !actualId) return;
|
if (
|
||||||
|
from === 'douban' ||
|
||||||
|
from === 'shortdrama' ||
|
||||||
|
!actualSource ||
|
||||||
|
!actualId
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 确定当前收藏状态
|
// 确定当前收藏状态
|
||||||
const currentFavorited = from === 'search' ? searchFavorited : favorited;
|
const currentFavorited =
|
||||||
|
from === 'search' ? searchFavorited : favorited;
|
||||||
|
|
||||||
if (currentFavorited) {
|
if (currentFavorited) {
|
||||||
// 如果已收藏,删除收藏
|
// 如果已收藏,删除收藏
|
||||||
|
|
@ -236,7 +270,10 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
|
|
||||||
if (origin === 'live' && actualSource && actualId) {
|
if (origin === 'live' && actualSource && actualId) {
|
||||||
// 直播内容跳转到直播页面
|
// 直播内容跳转到直播页面
|
||||||
const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`;
|
const url = `/live?source=${actualSource.replace(
|
||||||
|
'live_',
|
||||||
|
''
|
||||||
|
)}&id=${actualId.replace('live_', '')}`;
|
||||||
router.push(url);
|
router.push(url);
|
||||||
} else if (from === 'shortdrama' && actualId) {
|
} else if (from === 'shortdrama' && actualId) {
|
||||||
// 短剧内容跳转到播放页面,传递剧集ID用于调用获取全集地址的接口
|
// 短剧内容跳转到播放页面,传递剧集ID用于调用获取全集地址的接口
|
||||||
|
|
@ -249,15 +286,25 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
|
|
||||||
const url = `/play?${urlParams.toString()}`;
|
const url = `/play?${urlParams.toString()}`;
|
||||||
router.push(url);
|
router.push(url);
|
||||||
} else if (from === 'douban' || (isAggregate && !actualSource && !actualId)) {
|
} else if (
|
||||||
const url = `/play?title=${encodeURIComponent(actualTitle.trim())}${actualYear ? `&year=${actualYear}` : ''
|
from === 'douban' ||
|
||||||
}${actualSearchType ? `&stype=${actualSearchType}` : ''}${isAggregate ? '&prefer=true' : ''}${actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''}`;
|
(isAggregate && !actualSource && !actualId)
|
||||||
|
) {
|
||||||
|
const url = `/play?title=${encodeURIComponent(actualTitle.trim())}${
|
||||||
|
actualYear ? `&year=${actualYear}` : ''
|
||||||
|
}${actualSearchType ? `&stype=${actualSearchType}` : ''}${
|
||||||
|
isAggregate ? '&prefer=true' : ''
|
||||||
|
}${
|
||||||
|
actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''
|
||||||
|
}`;
|
||||||
router.push(url);
|
router.push(url);
|
||||||
} else if (actualSource && actualId) {
|
} else if (actualSource && actualId) {
|
||||||
const url = `/play?source=${actualSource}&id=${actualId}&title=${encodeURIComponent(
|
const url = `/play?source=${actualSource}&id=${actualId}&title=${encodeURIComponent(
|
||||||
actualTitle
|
actualTitle
|
||||||
)}${actualYear ? `&year=${actualYear}` : ''}${isAggregate ? '&prefer=true' : ''
|
)}${actualYear ? `&year=${actualYear}` : ''}${
|
||||||
}${actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''
|
isAggregate ? '&prefer=true' : ''
|
||||||
|
}${
|
||||||
|
actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''
|
||||||
}${actualSearchType ? `&stype=${actualSearchType}` : ''}`;
|
}${actualSearchType ? `&stype=${actualSearchType}` : ''}`;
|
||||||
router.push(url);
|
router.push(url);
|
||||||
}
|
}
|
||||||
|
|
@ -283,16 +330,30 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
|
|
||||||
if (origin === 'live' && actualSource && actualId) {
|
if (origin === 'live' && actualSource && actualId) {
|
||||||
// 直播内容跳转到直播页面
|
// 直播内容跳转到直播页面
|
||||||
const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`;
|
const url = `/live?source=${actualSource.replace(
|
||||||
|
'live_',
|
||||||
|
''
|
||||||
|
)}&id=${actualId.replace('live_', '')}`;
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
} else if (from === 'douban' || (isAggregate && !actualSource && !actualId)) {
|
} else if (
|
||||||
const url = `/play?title=${encodeURIComponent(actualTitle.trim())}${actualYear ? `&year=${actualYear}` : ''}${actualSearchType ? `&stype=${actualSearchType}` : ''}${isAggregate ? '&prefer=true' : ''}${actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''}`;
|
from === 'douban' ||
|
||||||
|
(isAggregate && !actualSource && !actualId)
|
||||||
|
) {
|
||||||
|
const url = `/play?title=${encodeURIComponent(actualTitle.trim())}${
|
||||||
|
actualYear ? `&year=${actualYear}` : ''
|
||||||
|
}${actualSearchType ? `&stype=${actualSearchType}` : ''}${
|
||||||
|
isAggregate ? '&prefer=true' : ''
|
||||||
|
}${
|
||||||
|
actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''
|
||||||
|
}`;
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
} else if (actualSource && actualId) {
|
} else if (actualSource && actualId) {
|
||||||
const url = `/play?source=${actualSource}&id=${actualId}&title=${encodeURIComponent(
|
const url = `/play?source=${actualSource}&id=${actualId}&title=${encodeURIComponent(
|
||||||
actualTitle
|
actualTitle
|
||||||
)}${actualYear ? `&year=${actualYear}` : ''}${isAggregate ? '&prefer=true' : ''
|
)}${actualYear ? `&year=${actualYear}` : ''}${
|
||||||
}${actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''
|
isAggregate ? '&prefer=true' : ''
|
||||||
|
}${
|
||||||
|
actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''
|
||||||
}${actualSearchType ? `&stype=${actualSearchType}` : ''}`;
|
}${actualSearchType ? `&stype=${actualSearchType}` : ''}`;
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +371,13 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
|
|
||||||
// 检查搜索结果的收藏状态
|
// 检查搜索结果的收藏状态
|
||||||
const checkSearchFavoriteStatus = useCallback(async () => {
|
const checkSearchFavoriteStatus = useCallback(async () => {
|
||||||
if (from === 'search' && !isAggregate && actualSource && actualId && searchFavorited === null) {
|
if (
|
||||||
|
from === 'search' &&
|
||||||
|
!isAggregate &&
|
||||||
|
actualSource &&
|
||||||
|
actualId &&
|
||||||
|
searchFavorited === null
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const fav = await isFavorited(actualSource, actualId);
|
const fav = await isFavorited(actualSource, actualId);
|
||||||
setSearchFavorited(fav);
|
setSearchFavorited(fav);
|
||||||
|
|
@ -322,16 +389,31 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
|
|
||||||
// 长按操作
|
// 长按操作
|
||||||
const handleLongPress = useCallback(() => {
|
const handleLongPress = useCallback(() => {
|
||||||
if (!showMobileActions) { // 防止重复触发
|
if (!showMobileActions) {
|
||||||
|
// 防止重复触发
|
||||||
// 立即显示菜单,避免等待数据加载导致动画卡顿
|
// 立即显示菜单,避免等待数据加载导致动画卡顿
|
||||||
setShowMobileActions(true);
|
setShowMobileActions(true);
|
||||||
|
|
||||||
// 异步检查收藏状态,不阻塞菜单显示
|
// 异步检查收藏状态,不阻塞菜单显示
|
||||||
if (from === 'search' && !isAggregate && actualSource && actualId && searchFavorited === null) {
|
if (
|
||||||
|
from === 'search' &&
|
||||||
|
!isAggregate &&
|
||||||
|
actualSource &&
|
||||||
|
actualId &&
|
||||||
|
searchFavorited === null
|
||||||
|
) {
|
||||||
checkSearchFavoriteStatus();
|
checkSearchFavoriteStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [showMobileActions, from, isAggregate, actualSource, actualId, searchFavorited, checkSearchFavoriteStatus]);
|
}, [
|
||||||
|
showMobileActions,
|
||||||
|
from,
|
||||||
|
isAggregate,
|
||||||
|
actualSource,
|
||||||
|
actualId,
|
||||||
|
searchFavorited,
|
||||||
|
checkSearchFavoriteStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
// 长按手势hook
|
// 长按手势hook
|
||||||
const longPressProps = useLongPress({
|
const longPressProps = useLongPress({
|
||||||
|
|
@ -423,8 +505,15 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
// 聚合源信息 - 直接在菜单中展示,不需要单独的操作项
|
// 聚合源信息 - 直接在菜单中展示,不需要单独的操作项
|
||||||
|
|
||||||
// 收藏/取消收藏操作
|
// 收藏/取消收藏操作
|
||||||
if (config.showHeart && from !== 'douban' && from !== 'shortdrama' && actualSource && actualId) {
|
if (
|
||||||
const currentFavorited = from === 'search' ? searchFavorited : favorited;
|
config.showHeart &&
|
||||||
|
from !== 'douban' &&
|
||||||
|
from !== 'shortdrama' &&
|
||||||
|
actualSource &&
|
||||||
|
actualId
|
||||||
|
) {
|
||||||
|
const currentFavorited =
|
||||||
|
from === 'search' ? searchFavorited : favorited;
|
||||||
|
|
||||||
if (from === 'search') {
|
if (from === 'search') {
|
||||||
// 搜索结果:根据加载状态显示不同的选项
|
// 搜索结果:根据加载状态显示不同的选项
|
||||||
|
|
@ -434,9 +523,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
id: 'favorite',
|
id: 'favorite',
|
||||||
label: currentFavorited ? '取消收藏' : '添加收藏',
|
label: currentFavorited ? '取消收藏' : '添加收藏',
|
||||||
icon: currentFavorited ? (
|
icon: currentFavorited ? (
|
||||||
<Heart size={20} className="fill-red-600 stroke-red-600" />
|
<Heart size={20} className='fill-red-600 stroke-red-600' />
|
||||||
) : (
|
) : (
|
||||||
<Heart size={20} className="fill-transparent stroke-red-500" />
|
<Heart size={20} className='fill-transparent stroke-red-500' />
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const mockEvent = {
|
const mockEvent = {
|
||||||
|
|
@ -445,7 +534,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
} as React.MouseEvent;
|
} as React.MouseEvent;
|
||||||
handleToggleFavorite(mockEvent);
|
handleToggleFavorite(mockEvent);
|
||||||
},
|
},
|
||||||
color: currentFavorited ? ('danger' as const) : ('default' as const),
|
color: currentFavorited
|
||||||
|
? ('danger' as const)
|
||||||
|
: ('default' as const),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 正在加载中,显示占位项
|
// 正在加载中,显示占位项
|
||||||
|
|
@ -463,9 +554,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
id: 'favorite',
|
id: 'favorite',
|
||||||
label: currentFavorited ? '取消收藏' : '添加收藏',
|
label: currentFavorited ? '取消收藏' : '添加收藏',
|
||||||
icon: currentFavorited ? (
|
icon: currentFavorited ? (
|
||||||
<Heart size={20} className="fill-red-600 stroke-red-600" />
|
<Heart size={20} className='fill-red-600 stroke-red-600' />
|
||||||
) : (
|
) : (
|
||||||
<Heart size={20} className="fill-transparent stroke-red-500" />
|
<Heart size={20} className='fill-transparent stroke-red-500' />
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const mockEvent = {
|
const mockEvent = {
|
||||||
|
|
@ -474,13 +565,20 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
} as React.MouseEvent;
|
} as React.MouseEvent;
|
||||||
handleToggleFavorite(mockEvent);
|
handleToggleFavorite(mockEvent);
|
||||||
},
|
},
|
||||||
color: currentFavorited ? ('danger' as const) : ('default' as const),
|
color: currentFavorited
|
||||||
|
? ('danger' as const)
|
||||||
|
: ('default' as const),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除播放记录操作
|
// 删除播放记录操作
|
||||||
if (config.showCheckCircle && from === 'playrecord' && actualSource && actualId) {
|
if (
|
||||||
|
config.showCheckCircle &&
|
||||||
|
from === 'playrecord' &&
|
||||||
|
actualSource &&
|
||||||
|
actualId
|
||||||
|
) {
|
||||||
actions.push({
|
actions.push({
|
||||||
id: 'delete',
|
id: 'delete',
|
||||||
label: '删除记录',
|
label: '删除记录',
|
||||||
|
|
@ -536,7 +634,8 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
className='group relative z-0 w-full cursor-pointer overflow-visible rounded-none p-0'
|
className='group relative z-0 w-full cursor-pointer overflow-visible rounded-none p-0'
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...longPressProps}
|
{...longPressProps}
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
// 禁用所有默认的长按和选择效果
|
// 禁用所有默认的长按和选择效果
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
|
@ -545,7 +644,8 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
touchAction: 'manipulation',
|
touchAction: 'manipulation',
|
||||||
// 禁用右键菜单和长按菜单
|
// 禁用右键菜单和长按菜单
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
// 阻止默认右键菜单
|
// 阻止默认右键菜单
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -555,13 +655,18 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
setShowMobileActions(true);
|
setShowMobileActions(true);
|
||||||
|
|
||||||
// 异步检查收藏状态,不阻塞菜单显示
|
// 异步检查收藏状态,不阻塞菜单显示
|
||||||
if (from === 'search' && !isAggregate && actualSource && actualId && searchFavorited === null) {
|
if (
|
||||||
|
from === 'search' &&
|
||||||
|
!isAggregate &&
|
||||||
|
actualSource &&
|
||||||
|
actualId &&
|
||||||
|
searchFavorited === null
|
||||||
|
) {
|
||||||
checkSearchFavoriteStatus();
|
checkSearchFavoriteStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
// 阻止拖拽
|
// 阻止拖拽
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -572,11 +677,13 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
<Card
|
<Card
|
||||||
variant='default'
|
variant='default'
|
||||||
className='relative aspect-[2/3] overflow-hidden rounded-lg p-0'
|
className='relative aspect-[2/3] overflow-hidden rounded-lg p-0'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -590,7 +697,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
src={processImageUrl(actualPoster)}
|
src={processImageUrl(actualPoster)}
|
||||||
alt={actualTitle}
|
alt={actualTitle}
|
||||||
fill
|
fill
|
||||||
className={origin === 'live' ? 'object-contain' : 'object-cover'}
|
className={
|
||||||
|
origin === 'live' ? 'object-contain' : 'object-cover'
|
||||||
|
}
|
||||||
referrerPolicy='no-referrer'
|
referrerPolicy='no-referrer'
|
||||||
loading='lazy'
|
loading='lazy'
|
||||||
onLoadingComplete={() => setIsLoading(true)}
|
onLoadingComplete={() => setIsLoading(true)}
|
||||||
|
|
@ -606,24 +715,28 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
// 禁用图片的默认长按效果
|
// 禁用图片的默认长按效果
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
pointerEvents: 'none', // 图片不响应任何指针事件
|
pointerEvents: 'none', // 图片不响应任何指针事件
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 悬浮遮罩 */}
|
{/* 悬浮遮罩 */}
|
||||||
<div
|
<div
|
||||||
className='absolute inset-0 bg-black/35 opacity-0 transition-opacity duration-300 ease-in-out group-hover:opacity-100'
|
className='absolute inset-0 bg-black/35 opacity-0 transition-opacity duration-300 ease-in-out group-hover:opacity-100'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -633,13 +746,15 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
{/* 播放按钮 */}
|
{/* 播放按钮 */}
|
||||||
{config.showPlayButton && (
|
{config.showPlayButton && (
|
||||||
<div
|
<div
|
||||||
data-button="true"
|
data-button='true'
|
||||||
className='absolute inset-0 flex items-center justify-center opacity-0 transition-all duration-300 ease-in-out delay-75 group-hover:opacity-100 group-hover:scale-100'
|
className='absolute inset-0 flex items-center justify-center opacity-0 transition-all duration-300 ease-in-out delay-75 group-hover:opacity-100 group-hover:scale-100'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -649,11 +764,13 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
size={50}
|
size={50}
|
||||||
strokeWidth={0.8}
|
strokeWidth={0.8}
|
||||||
className='fill-background text-accent'
|
className='fill-background text-accent'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -665,13 +782,15 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
{(config.showHeart || config.showCheckCircle) && (
|
{(config.showHeart || config.showCheckCircle) && (
|
||||||
<div
|
<div
|
||||||
data-button="true"
|
data-button='true'
|
||||||
className='absolute bottom-3 right-3 flex gap-3 opacity-0 translate-y-2 transition-all duration-300 ease-in-out sm:group-hover:opacity-100 sm:group-hover:translate-y-0'
|
className='absolute bottom-3 right-3 flex gap-3 opacity-0 translate-y-2 transition-all duration-300 ease-in-out sm:group-hover:opacity-100 sm:group-hover:translate-y-0'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -692,7 +811,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{config.showHeart && from !== 'search' && from !== 'shortdrama' && (
|
{config.showHeart &&
|
||||||
|
from !== 'search' &&
|
||||||
|
from !== 'shortdrama' && (
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
size='sm'
|
size='sm'
|
||||||
|
|
@ -704,23 +825,31 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
} as React.MouseEvent)
|
} as React.MouseEvent)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Heart size={16} className={favorited ? 'fill-current' : ''} />
|
<Heart
|
||||||
|
size={16}
|
||||||
|
className={favorited ? 'fill-current' : ''}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 年份徽章 */}
|
{/* 年份徽章 */}
|
||||||
{config.showYear && actualYear && actualYear !== 'unknown' && actualYear.trim() !== '' && (
|
{config.showYear &&
|
||||||
|
actualYear &&
|
||||||
|
actualYear !== 'unknown' &&
|
||||||
|
actualYear.trim() !== '' && (
|
||||||
<Badge
|
<Badge
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
className='absolute left-2 top-2'
|
className='absolute left-2 top-2'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -737,11 +866,13 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
color='accent'
|
color='accent'
|
||||||
variant='primary'
|
variant='primary'
|
||||||
className='absolute right-2 top-2'
|
className='absolute right-2 top-2'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -752,28 +883,34 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{actualEpisodes && actualEpisodes > 1 && (
|
{actualEpisodes && actualEpisodes > 1 && (
|
||||||
<Badge
|
<Chip
|
||||||
size='sm'
|
size='md'
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
className='absolute right-2 top-2'
|
className='absolute right-3 top-3 min-w-12 justify-center'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Badge.Label>{currentEpisode
|
<Chip.Label>
|
||||||
|
{currentEpisode
|
||||||
? `${currentEpisode}/${actualEpisodes}`
|
? `${currentEpisode}/${actualEpisodes}`
|
||||||
: actualEpisodes}</Badge.Label>
|
: actualEpisodes}
|
||||||
</Badge>
|
</Chip.Label>
|
||||||
|
</Chip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 豆瓣链接 */}
|
{/* 豆瓣链接 */}
|
||||||
{config.showDoubanLink && actualDoubanId && actualDoubanId !== 0 && (
|
{config.showDoubanLink &&
|
||||||
|
actualDoubanId &&
|
||||||
|
actualDoubanId !== 0 && (
|
||||||
<HeroLink
|
<HeroLink
|
||||||
href={
|
href={
|
||||||
isBangumi
|
isBangumi
|
||||||
|
|
@ -784,11 +921,13 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className='absolute top-2 left-2 opacity-0 -translate-x-2 transition-all duration-300 ease-in-out delay-100 sm:group-hover:opacity-100 sm:group-hover:translate-x-0'
|
className='absolute top-2 left-2 opacity-0 -translate-x-2 transition-all duration-300 ease-in-out delay-100 sm:group-hover:opacity-100 sm:group-hover:translate-x-0'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -796,29 +935,36 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
>
|
>
|
||||||
<LinkIcon
|
<LinkIcon
|
||||||
size={18}
|
size={18}
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</HeroLink>
|
</HeroLink>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 聚合播放源指示器 */}
|
{/* 聚合播放源指示器 */}
|
||||||
{isAggregate && dynamicSourceNames && dynamicSourceNames.length > 0 && (() => {
|
{isAggregate &&
|
||||||
|
dynamicSourceNames &&
|
||||||
|
dynamicSourceNames.length > 0 &&
|
||||||
|
(() => {
|
||||||
const uniqueSources = Array.from(new Set(dynamicSourceNames));
|
const uniqueSources = Array.from(new Set(dynamicSourceNames));
|
||||||
const sourceCount = uniqueSources.length;
|
const sourceCount = uniqueSources.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='absolute bottom-2 right-2 opacity-0 transition-all duration-300 ease-in-out delay-75 sm:group-hover:opacity-100'
|
className='absolute bottom-2 right-2 opacity-0 transition-all duration-300 ease-in-out delay-75 sm:group-hover:opacity-100'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -826,21 +972,25 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='relative group/sources'
|
className='relative group/sources'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
size='sm'
|
size='sm'
|
||||||
color='accent'
|
color='accent'
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -852,31 +1002,46 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
{/* 播放源详情悬浮框 */}
|
{/* 播放源详情悬浮框 */}
|
||||||
{(() => {
|
{(() => {
|
||||||
// 优先显示的播放源(常见的主流平台)
|
// 优先显示的播放源(常见的主流平台)
|
||||||
const prioritySources = ['爱奇艺', '腾讯视频', '优酷', '芒果TV', '哔哩哔哩', 'Netflix', 'Disney+'];
|
const prioritySources = [
|
||||||
|
'爱奇艺',
|
||||||
|
'腾讯视频',
|
||||||
|
'优酷',
|
||||||
|
'芒果TV',
|
||||||
|
'哔哩哔哩',
|
||||||
|
'Netflix',
|
||||||
|
'Disney+',
|
||||||
|
];
|
||||||
|
|
||||||
// 按优先级排序播放源
|
// 按优先级排序播放源
|
||||||
const sortedSources = uniqueSources.sort((a, b) => {
|
const sortedSources = uniqueSources.sort((a, b) => {
|
||||||
const aIndex = prioritySources.indexOf(a);
|
const aIndex = prioritySources.indexOf(a);
|
||||||
const bIndex = prioritySources.indexOf(b);
|
const bIndex = prioritySources.indexOf(b);
|
||||||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
if (aIndex !== -1 && bIndex !== -1)
|
||||||
|
return aIndex - bIndex;
|
||||||
if (aIndex !== -1) return -1;
|
if (aIndex !== -1) return -1;
|
||||||
if (bIndex !== -1) return 1;
|
if (bIndex !== -1) return 1;
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxDisplayCount = 6; // 最多显示6个
|
const maxDisplayCount = 6; // 最多显示6个
|
||||||
const displaySources = sortedSources.slice(0, maxDisplayCount);
|
const displaySources = sortedSources.slice(
|
||||||
|
0,
|
||||||
|
maxDisplayCount
|
||||||
|
);
|
||||||
const hasMore = sortedSources.length > maxDisplayCount;
|
const hasMore = sortedSources.length > maxDisplayCount;
|
||||||
const remainingCount = sortedSources.length - maxDisplayCount;
|
const remainingCount =
|
||||||
|
sortedSources.length - maxDisplayCount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='absolute bottom-full mb-2 opacity-0 invisible group-hover/sources:opacity-100 group-hover/sources:visible transition-all duration-200 ease-out delay-100 pointer-events-none z-50 right-0 sm:right-0 -translate-x-0 sm:translate-x-0'
|
className='absolute bottom-full mb-2 opacity-0 invisible group-hover/sources:opacity-100 group-hover/sources:visible transition-all duration-200 ease-out delay-100 pointer-events-none z-50 right-0 sm:right-0 -translate-x-0 sm:translate-x-0'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -884,11 +1049,13 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='min-w-[100px] max-w-[140px] overflow-hidden rounded-xl border border-border/70 bg-overlay/95 p-1.5 text-xs text-foreground shadow-xl backdrop-blur sm:min-w-[120px] sm:max-w-[200px] sm:p-2'
|
className='min-w-[100px] max-w-[140px] overflow-hidden rounded-xl border border-border/70 bg-overlay/95 p-1.5 text-xs text-foreground shadow-xl backdrop-blur sm:min-w-[120px] sm:max-w-[200px] sm:p-2'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -897,9 +1064,15 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
{/* 单列布局 */}
|
{/* 单列布局 */}
|
||||||
<div className='space-y-0.5 sm:space-y-1'>
|
<div className='space-y-0.5 sm:space-y-1'>
|
||||||
{displaySources.map((sourceName, index) => (
|
{displaySources.map((sourceName, index) => (
|
||||||
<div key={index} className='flex items-center gap-1 sm:gap-1.5'>
|
<div
|
||||||
|
key={index}
|
||||||
|
className='flex items-center gap-1 sm:gap-1.5'
|
||||||
|
>
|
||||||
<div className='h-3 w-1 rounded-full flex-shrink-0 bg-accent/70'></div>
|
<div className='h-3 w-1 rounded-full flex-shrink-0 bg-accent/70'></div>
|
||||||
<span className='truncate text-[10px] sm:text-xs leading-tight' title={sourceName}>
|
<span
|
||||||
|
className='truncate text-[10px] sm:text-xs leading-tight'
|
||||||
|
title={sourceName}
|
||||||
|
>
|
||||||
{sourceName}
|
{sourceName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -910,7 +1083,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
<div className='mt-1 border-t border-border/70 pt-1 sm:mt-2 sm:pt-1.5'>
|
<div className='mt-1 border-t border-border/70 pt-1 sm:mt-2 sm:pt-1.5'>
|
||||||
<div className='flex items-center justify-center text-muted'>
|
<div className='flex items-center justify-center text-muted'>
|
||||||
<span className='text-[10px] sm:text-xs font-medium'>+{remainingCount} 播放源</span>
|
<span className='text-[10px] sm:text-xs font-medium'>
|
||||||
|
+{remainingCount} 播放源
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -935,11 +1110,13 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
className='mt-2'
|
className='mt-2'
|
||||||
size='sm'
|
size='sm'
|
||||||
color='accent'
|
color='accent'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -954,33 +1131,40 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
{/* 标题与来源 */}
|
{/* 标题与来源 */}
|
||||||
<div
|
<div
|
||||||
className='mt-3 text-left'
|
className='mt-3 text-left'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className='flex min-w-0 items-center gap-2'>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Tooltip.Trigger>
|
<Tooltip.Trigger>
|
||||||
<div
|
<div
|
||||||
className='relative'
|
className='relative min-w-0 flex-1'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className='block truncate text-sm font-semibold'
|
className='block truncate text-sm font-semibold'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -990,33 +1174,34 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content placement='top'>
|
<Tooltip.Content placement='top'>{actualTitle}</Tooltip.Content>
|
||||||
{actualTitle}
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{config.showSourceName && source_name && (
|
{config.showSourceName && source_name && (
|
||||||
<Chip
|
<Chip
|
||||||
size='sm'
|
size='sm'
|
||||||
color='accent'
|
color='accent'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='mt-1'
|
className='shrink-0'
|
||||||
style={{
|
style={
|
||||||
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{origin === 'live' && (
|
{origin === 'live' && (
|
||||||
<Radio size={12} className="inline-block mr-1 text-muted" />
|
<Radio size={12} className='inline-block mr-1 text-muted' />
|
||||||
)}
|
)}
|
||||||
<Chip.Label>{source_name}</Chip.Label>
|
<Chip.Label>{source_name}</Chip.Label>
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 操作菜单 - 支持右键和长按触发 */}
|
{/* 操作菜单 - 支持右键和长按触发 */}
|
||||||
|
|
@ -1026,7 +1211,11 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
title={actualTitle}
|
title={actualTitle}
|
||||||
poster={processImageUrl(actualPoster)}
|
poster={processImageUrl(actualPoster)}
|
||||||
actions={mobileActions}
|
actions={mobileActions}
|
||||||
sources={isAggregate && dynamicSourceNames ? Array.from(new Set(dynamicSourceNames)) : undefined}
|
sources={
|
||||||
|
isAggregate && dynamicSourceNames
|
||||||
|
? Array.from(new Set(dynamicSourceNames))
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
isAggregate={isAggregate}
|
isAggregate={isAggregate}
|
||||||
sourceName={source_name}
|
sourceName={source_name}
|
||||||
currentEpisode={currentEpisode}
|
currentEpisode={currentEpisode}
|
||||||
|
|
@ -1036,7 +1225,6 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default memo(VideoCard);
|
export default memo(VideoCard);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue