修复VideoCard来源数量浮层左上角被裁切,并阻止长按同时弹出操作菜单
This commit is contained in:
+136
-111
@@ -986,9 +986,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
|||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 海报容器 */}
|
{/* 海报容器:不在外层 overflow-hidden,避免裁切来源数量浮层 */}
|
||||||
<div
|
<div
|
||||||
className={`relative overflow-hidden rounded-lg ${
|
className={`relative rounded-lg ${
|
||||||
origin === 'live'
|
origin === 'live'
|
||||||
? 'ring-1 ring-gray-300/80 dark:ring-gray-600/80'
|
? 'ring-1 ring-gray-300/80 dark:ring-gray-600/80'
|
||||||
: ''
|
: ''
|
||||||
@@ -1007,119 +1007,124 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
|||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 骨架屏 */}
|
{/* 仅图片层做圆角裁剪 */}
|
||||||
{!isLoading && !isDirectPlaySource && (
|
<div className='absolute inset-0 overflow-hidden rounded-lg'>
|
||||||
<ImagePlaceholder
|
{/* 骨架屏 */}
|
||||||
aspectRatio={
|
{!isLoading && !isDirectPlaySource && (
|
||||||
orientation === 'horizontal' ? 'aspect-[3/2]' : 'aspect-[2/3]'
|
<ImagePlaceholder
|
||||||
}
|
aspectRatio={
|
||||||
/>
|
orientation === 'horizontal'
|
||||||
)}
|
? 'aspect-[3/2]'
|
||||||
{isDirectPlaySource ? (
|
: 'aspect-[2/3]'
|
||||||
<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}
|
|
||||||
alt={actualTitle}
|
|
||||||
fill
|
|
||||||
className={
|
|
||||||
origin === 'live'
|
|
||||||
? 'object-contain'
|
|
||||||
: orientation === 'horizontal'
|
|
||||||
? 'object-cover object-center'
|
|
||||||
: 'object-cover'
|
|
||||||
}
|
|
||||||
referrerPolicy='no-referrer'
|
|
||||||
loading='lazy'
|
|
||||||
onLoadingComplete={() => {
|
|
||||||
setIsLoading(true);
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowImageViewer(true);
|
|
||||||
}}
|
|
||||||
onError={(e) => {
|
|
||||||
const img = e.currentTarget as HTMLImageElement;
|
|
||||||
const doubanFallbackPoster =
|
|
||||||
getDoubanImageFallbackUrl(actualPoster);
|
|
||||||
if (
|
|
||||||
doubanFallbackPoster &&
|
|
||||||
tryApplyDoubanImageFallback(img, actualPoster)
|
|
||||||
) {
|
|
||||||
setDisplayPoster(doubanFallbackPoster);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
/>
|
||||||
const bangumiFallbackPoster =
|
)}
|
||||||
getBangumiImageFallbackUrl(actualPoster);
|
{isDirectPlaySource ? (
|
||||||
if (
|
<div className='absolute inset-0 flex items-center justify-center bg-gray-200/80 dark:bg-gray-700/80'>
|
||||||
bangumiFallbackPoster &&
|
<Link className='w-8 h-8 text-blue-500' />
|
||||||
tryApplyBangumiImageFallback(img, actualPoster)
|
</div>
|
||||||
) {
|
) : isNetdiskSource(actualSource) &&
|
||||||
setDisplayPoster(bangumiFallbackPoster);
|
!actualPoster &&
|
||||||
return;
|
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}
|
||||||
|
alt={actualTitle}
|
||||||
|
fill
|
||||||
|
className={
|
||||||
|
origin === 'live'
|
||||||
|
? 'object-contain'
|
||||||
|
: orientation === 'horizontal'
|
||||||
|
? 'object-cover object-center'
|
||||||
|
: 'object-cover'
|
||||||
}
|
}
|
||||||
|
referrerPolicy='no-referrer'
|
||||||
|
loading='lazy'
|
||||||
|
onLoadingComplete={() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowImageViewer(true);
|
||||||
|
}}
|
||||||
|
onError={(e) => {
|
||||||
|
const img = e.currentTarget as HTMLImageElement;
|
||||||
|
const doubanFallbackPoster =
|
||||||
|
getDoubanImageFallbackUrl(actualPoster);
|
||||||
|
if (
|
||||||
|
doubanFallbackPoster &&
|
||||||
|
tryApplyDoubanImageFallback(img, actualPoster)
|
||||||
|
) {
|
||||||
|
setDisplayPoster(doubanFallbackPoster);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
const bangumiFallbackPoster =
|
||||||
clearBangumiImageFallbackCacheIfFailed(img, actualPoster)
|
getBangumiImageFallbackUrl(actualPoster);
|
||||||
) {
|
if (
|
||||||
setDisplayPoster(processedPoster);
|
bangumiFallbackPoster &&
|
||||||
return;
|
tryApplyBangumiImageFallback(img, actualPoster)
|
||||||
}
|
) {
|
||||||
|
setDisplayPoster(bangumiFallbackPoster);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 图片加载失败时的重试机制
|
if (
|
||||||
if (!img.dataset.retried) {
|
clearBangumiImageFallbackCacheIfFailed(img, actualPoster)
|
||||||
img.dataset.retried = 'true';
|
) {
|
||||||
setTimeout(() => {
|
|
||||||
setDisplayPoster(processedPoster);
|
setDisplayPoster(processedPoster);
|
||||||
img.src = processedPoster;
|
return;
|
||||||
}, 2000);
|
}
|
||||||
|
|
||||||
|
// 图片加载失败时的重试机制
|
||||||
|
if (!img.dataset.retried) {
|
||||||
|
img.dataset.retried = 'true';
|
||||||
|
setTimeout(() => {
|
||||||
|
setDisplayPoster(processedPoster);
|
||||||
|
img.src = processedPoster;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
// 禁用图片的默认长按效果
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitTouchCallout: 'none',
|
||||||
|
pointerEvents: 'auto', // 改为auto以响应点击事件
|
||||||
|
cursor: 'pointer', // 添加指针样式
|
||||||
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
}}
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
onDragStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 悬浮遮罩 */}
|
||||||
|
<div
|
||||||
|
className='absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100'
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
// 禁用图片的默认长按效果
|
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
pointerEvents: 'auto', // 改为auto以响应点击事件
|
|
||||||
cursor: 'pointer', // 添加指针样式
|
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
onDragStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* 悬浮遮罩 */}
|
|
||||||
<div
|
|
||||||
className='absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100'
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
WebkitUserSelect: 'none',
|
|
||||||
userSelect: 'none',
|
|
||||||
WebkitTouchCallout: 'none',
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
onContextMenu={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 播放按钮或上映倒计时 */}
|
{/* 播放按钮或上映倒计时 */}
|
||||||
{isUpcoming && daysUntilRelease !== null ? (
|
{isUpcoming && daysUntilRelease !== null ? (
|
||||||
@@ -1511,7 +1516,8 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-1 right-1 sm:bottom-2 sm:right-2 transition-all duration-300 ease-in-out delay-75 ${
|
data-button='true'
|
||||||
|
className={`absolute bottom-1 right-1 z-[55] sm:bottom-2 sm:right-2 transition-all duration-300 ease-in-out delay-75 ${
|
||||||
from === 'search'
|
from === 'search'
|
||||||
? 'opacity-100'
|
? 'opacity-100'
|
||||||
: 'opacity-0 sm:group-hover:opacity-100'
|
: 'opacity-0 sm:group-hover:opacity-100'
|
||||||
@@ -1523,9 +1529,32 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
|||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
|
// 阻止冒泡到卡片:避免长按/右键同时弹出操作菜单
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onTouchMove={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onTouchEnd={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
// 右键/中键按下时也不要触发卡片手势
|
||||||
|
if (e.button !== 0) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1547,10 +1576,6 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
|||||||
WebkitTouchCallout: 'none',
|
WebkitTouchCallout: 'none',
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
onContextMenu={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{sourceCount}
|
{sourceCount}
|
||||||
</div>
|
</div>
|
||||||
@@ -1590,7 +1615,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
|||||||
|
|
||||||
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 right-0 z-[60] mb-2 -translate-x-0 opacity-0 invisible pointer-events-none transition-all duration-200 ease-out delay-100 group-hover/sources:opacity-100 group-hover/sources:visible sm:right-0 sm:translate-x-0'
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
@@ -1604,7 +1629,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='bg-gray-800/90 backdrop-blur-sm text-white text-xs sm:text-xs rounded-lg shadow-xl border border-white/10 p-1.5 sm:p-2 min-w-[100px] sm:min-w-[120px] max-w-[140px] sm:max-w-[200px] overflow-hidden'
|
className='relative rounded-lg border border-white/10 bg-gray-800/95 p-1.5 text-xs text-white shadow-xl backdrop-blur-sm sm:p-2 sm:text-xs min-w-[100px] max-w-[140px] sm:min-w-[120px] sm:max-w-[200px]'
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
@@ -1624,9 +1649,9 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
|||||||
key={index}
|
key={index}
|
||||||
className='flex items-center gap-1 sm:gap-1.5'
|
className='flex items-center gap-1 sm:gap-1.5'
|
||||||
>
|
>
|
||||||
<div className='w-0.5 h-0.5 sm:w-1 sm:h-1 bg-blue-400 rounded-full flex-shrink-0'></div>
|
<div className='h-0.5 w-0.5 flex-shrink-0 rounded-full bg-blue-400 sm:h-1 sm:w-1'></div>
|
||||||
<span
|
<span
|
||||||
className='truncate text-[10px] sm:text-xs leading-tight'
|
className='truncate text-[10px] leading-tight sm:text-xs'
|
||||||
title={sourceName}
|
title={sourceName}
|
||||||
>
|
>
|
||||||
{sourceName}
|
{sourceName}
|
||||||
@@ -1637,17 +1662,17 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(
|
|||||||
|
|
||||||
{/* 显示更多提示 */}
|
{/* 显示更多提示 */}
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
<div className='mt-1 sm:mt-2 pt-1 sm:pt-1.5 border-t border-gray-700/50'>
|
<div className='mt-1 border-t border-gray-700/50 pt-1 sm:mt-2 sm:pt-1.5'>
|
||||||
<div className='flex items-center justify-center text-gray-400'>
|
<div className='flex items-center justify-center text-gray-400'>
|
||||||
<span className='text-[10px] sm:text-xs font-medium'>
|
<span className='text-[10px] font-medium sm:text-xs'>
|
||||||
+{remainingCount} 播放源
|
+{remainingCount} 播放源
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 小箭头 */}
|
{/* 小箭头:放在内容盒外侧,避免被裁切 */}
|
||||||
<div className='absolute top-full right-2 sm:right-3 w-0 h-0 border-l-[4px] border-r-[4px] border-t-[4px] sm:border-l-[6px] sm:border-r-[6px] sm:border-t-[6px] border-transparent border-t-gray-800/90'></div>
|
<div className='absolute top-full right-2 h-0 w-0 border-l-[4px] border-r-[4px] border-t-[4px] border-transparent border-t-gray-800/95 sm:right-3 sm:border-l-[6px] sm:border-r-[6px] sm:border-t-[6px]'></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -103,17 +103,15 @@ export const useLongPress = ({
|
|||||||
// 触摸事件处理器
|
// 触摸事件处理器
|
||||||
const onTouchStart = useCallback(
|
const onTouchStart = useCallback(
|
||||||
(e: React.TouchEvent) => {
|
(e: React.TouchEvent) => {
|
||||||
// 检查是否触摸的是按钮或其他交互元素
|
// 触摸到标记为 data-button 的交互区域(或其子元素)时:
|
||||||
|
// 不启动卡片长按,避免与控件自身手势(如来源数量浮层)冲突
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
const buttonElement = target.closest('[data-button]');
|
if (target.closest('[data-button]')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 更精确的按钮检测:只有当触摸目标直接是按钮元素或其直接子元素时才认为是按钮
|
|
||||||
const isDirectButton = target.hasAttribute('data-button');
|
|
||||||
const isButton = !!buttonElement && isDirectButton;
|
|
||||||
|
|
||||||
// 阻止默认的长按行为,但不阻止触摸开始事件
|
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
handleStart(touch.clientX, touch.clientY, !!isButton);
|
handleStart(touch.clientX, touch.clientY, false);
|
||||||
},
|
},
|
||||||
[handleStart]
|
[handleStart]
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user