仅netdisk显示
This commit is contained in:
@@ -142,30 +142,25 @@ function applyClientAdProxyToEpisodes(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘集标题:保留完整文件名(去掉常见视频扩展名),
|
||||||
|
* 供前端选集按钮长按/右键查看全名;按钮短标签仍由前端从文件名提取集数。
|
||||||
|
* `parsed` 仅用于排序,不再覆盖为「第N集」。
|
||||||
|
*/
|
||||||
function formatNetdiskEpisodeTitle(
|
function formatNetdiskEpisodeTitle(
|
||||||
parsed: {
|
_parsed: {
|
||||||
season?: number;
|
season?: number;
|
||||||
episode?: number;
|
episode?: number;
|
||||||
},
|
},
|
||||||
fallback: string
|
fallback: string
|
||||||
) {
|
) {
|
||||||
if (parsed.season && parsed.episode) {
|
const name = (fallback || '').trim();
|
||||||
const season = String(Math.trunc(parsed.season)).padStart(2, '0');
|
if (!name) return fallback;
|
||||||
const episodeValue = parsed.episode;
|
// 去掉末尾视频扩展名,保留完整可读文件名
|
||||||
const episode = Number.isInteger(episodeValue)
|
return name.replace(
|
||||||
? String(Math.trunc(episodeValue)).padStart(2, '0')
|
/\.(mp4|mkv|ts|m2ts|avi|mov|wmv|flv|webm|m4v|rmvb|iso|mpg|mpeg|m3u8)$/i,
|
||||||
: String(episodeValue);
|
''
|
||||||
return `S${season}E${episode}`;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (parsed.episode) {
|
|
||||||
const episodeValue = parsed.episode;
|
|
||||||
return Number.isInteger(episodeValue)
|
|
||||||
? `第${Math.trunc(episodeValue)}集`
|
|
||||||
: `第${episodeValue}集`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import EpisodeFilterSettings from '@/components/EpisodeFilterSettings';
|
|||||||
import ProxyImage from '@/components/ProxyImage';
|
import ProxyImage from '@/components/ProxyImage';
|
||||||
import { useLongPress } from '@/hooks/useLongPress';
|
import { useLongPress } from '@/hooks/useLongPress';
|
||||||
|
|
||||||
/** 选集按钮上显示的短标签(数字等) */
|
/** 选集按钮上显示的短标签(数字等);全名仍保留在 originalTitle 供长按查看 */
|
||||||
function getEpisodeDisplayLabel(
|
function getEpisodeDisplayLabel(
|
||||||
title: string | undefined,
|
title: string | undefined,
|
||||||
episodeNumber: number
|
episodeNumber: number
|
||||||
@@ -31,19 +31,44 @@ function getEpisodeDisplayLabel(
|
|||||||
if (!title) {
|
if (!title) {
|
||||||
return String(episodeNumber);
|
return String(episodeNumber);
|
||||||
}
|
}
|
||||||
// 如果是 OVA 格式,直接返回完整标题
|
// OVA 单独展示
|
||||||
if (title.match(/^OVA\s+\d+/i)) {
|
const ovaMatch = title.match(/OVA\s*(\d+(?:\.\d+)?)/i);
|
||||||
return title;
|
if (ovaMatch) {
|
||||||
|
return `OVA ${ovaMatch[1]}`;
|
||||||
}
|
}
|
||||||
// 如果匹配 S01E01 格式,只显示集数部分(去掉 SxxE)
|
// S01E05 / s01e05 → 5
|
||||||
const sxxexxMatch = title.match(/[Ss]\d+[Ee](\d{1,4}(?:\.\d+)?)/);
|
const sxxexxMatch = title.match(/[Ss]\d+[Ee](\d{1,4}(?:\.\d+)?)/);
|
||||||
if (sxxexxMatch) {
|
if (sxxexxMatch) {
|
||||||
return sxxexxMatch[1];
|
return sxxexxMatch[1];
|
||||||
}
|
}
|
||||||
// 如果匹配"第X集"、"第X话"、"X集"、"X话"格式,提取中间的数字(支持小数)
|
// 第12集 / 12话 → 12
|
||||||
const match = title.match(/(?:第)?(\d+(?:\.\d+)?)(?:集|话)/);
|
const zhMatch = title.match(/(?:第)?(\d+(?:\.\d+)?)(?:集|话)/);
|
||||||
if (match) {
|
if (zhMatch) {
|
||||||
return match[1];
|
return zhMatch[1];
|
||||||
|
}
|
||||||
|
// [01] / (01) → 1(网盘常见)
|
||||||
|
const bracketMatch = title.match(/[[(【](\d+(?:\.\d+)?)[\])】]/);
|
||||||
|
if (bracketMatch) {
|
||||||
|
return bracketMatch[1];
|
||||||
|
}
|
||||||
|
// E01 / EP01 / ep.01 → 1
|
||||||
|
const epMatch = title.match(/(?:^|[^a-zA-Z])(?:EP|E|ep|e)[.\s_-]*(\d+(?:\.\d+)?)/);
|
||||||
|
if (epMatch) {
|
||||||
|
return epMatch[1];
|
||||||
|
}
|
||||||
|
// _01_ / -01- → 1
|
||||||
|
const sepMatch = title.match(/[_-](\d+(?:\.\d+)?)[_-]/);
|
||||||
|
if (sepMatch) {
|
||||||
|
return sepMatch[1];
|
||||||
|
}
|
||||||
|
// 纯数字开头:01.xxx / 01 xxx
|
||||||
|
const leadingNum = title.match(/^(\d+(?:\.\d+)?)[^\d.]/);
|
||||||
|
if (leadingNum) {
|
||||||
|
return leadingNum[1];
|
||||||
|
}
|
||||||
|
// 整串就是数字
|
||||||
|
if (/^\d+(?:\.\d+)?$/.test(title.trim())) {
|
||||||
|
return title.trim();
|
||||||
}
|
}
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
@@ -61,24 +86,30 @@ interface EpisodeButtonProps {
|
|||||||
isWatched: boolean;
|
isWatched: boolean;
|
||||||
originalTitle?: string;
|
originalTitle?: string;
|
||||||
inactiveEpisodeClass: string;
|
inactiveEpisodeClass: string;
|
||||||
|
/** 仅 netdisk 源启用长按/右键查看全名 */
|
||||||
|
enableOriginalNamePopup?: boolean;
|
||||||
onSelect: (zeroBasedIndex: number) => void;
|
onSelect: (zeroBasedIndex: number) => void;
|
||||||
onShowOriginalName: (title: string, rect: DOMRect) => void;
|
onShowOriginalName: (title: string, rect: DOMRect) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单集按钮:点击选集;移动端长按 / 桌面右键显示原集名 popup */
|
/** 单集按钮:点击选集;netdisk 时移动端长按 / 桌面右键显示原集名 popup */
|
||||||
const EpisodeButton: React.FC<EpisodeButtonProps> = ({
|
const EpisodeButton: React.FC<EpisodeButtonProps> = ({
|
||||||
episodeNumber,
|
episodeNumber,
|
||||||
isActive,
|
isActive,
|
||||||
isWatched,
|
isWatched,
|
||||||
originalTitle,
|
originalTitle,
|
||||||
inactiveEpisodeClass,
|
inactiveEpisodeClass,
|
||||||
|
enableOriginalNamePopup = false,
|
||||||
onSelect,
|
onSelect,
|
||||||
onShowOriginalName,
|
onShowOriginalName,
|
||||||
}) => {
|
}) => {
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const displayLabel = getEpisodeDisplayLabel(originalTitle, episodeNumber);
|
const displayLabel = getEpisodeDisplayLabel(originalTitle, episodeNumber);
|
||||||
const canShowOriginalName =
|
const canShowOriginalName =
|
||||||
!!originalTitle && originalTitle.trim() !== '' && originalTitle !== displayLabel;
|
enableOriginalNamePopup &&
|
||||||
|
!!originalTitle &&
|
||||||
|
originalTitle.trim() !== '' &&
|
||||||
|
originalTitle !== displayLabel;
|
||||||
|
|
||||||
const showOriginalName = useCallback(() => {
|
const showOriginalName = useCallback(() => {
|
||||||
if (!canShowOriginalName || !buttonRef.current) return;
|
if (!canShowOriginalName || !buttonRef.current) return;
|
||||||
@@ -99,7 +130,8 @@ const EpisodeButton: React.FC<EpisodeButtonProps> = ({
|
|||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
type='button'
|
type='button'
|
||||||
// 不用 disabled,否则当前集无法长按/右键查看原名
|
// netdisk 不用 disabled,否则当前集无法长按/右键查看原名
|
||||||
|
disabled={canShowOriginalName ? undefined : isActive || undefined}
|
||||||
aria-disabled={isActive || undefined}
|
aria-disabled={isActive || undefined}
|
||||||
aria-current={isActive ? 'true' : undefined}
|
aria-current={isActive ? 'true' : undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -107,14 +139,19 @@ const EpisodeButton: React.FC<EpisodeButtonProps> = ({
|
|||||||
onSelect(episodeNumber - 1);
|
onSelect(episodeNumber - 1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={
|
||||||
if (!canShowOriginalName) return;
|
canShowOriginalName
|
||||||
e.preventDefault();
|
? (e) => {
|
||||||
e.stopPropagation();
|
e.preventDefault();
|
||||||
showOriginalName();
|
e.stopPropagation();
|
||||||
}}
|
showOriginalName();
|
||||||
{...longPressProps}
|
}
|
||||||
className={`relative h-10 min-w-10 px-3 py-2 flex items-center justify-center text-sm font-medium rounded-md transition-all duration-200 whitespace-nowrap font-mono border select-none
|
: undefined
|
||||||
|
}
|
||||||
|
{...(canShowOriginalName ? longPressProps : {})}
|
||||||
|
className={`relative h-10 min-w-10 px-3 py-2 flex items-center justify-center text-sm font-medium rounded-md transition-all duration-200 whitespace-nowrap font-mono border ${
|
||||||
|
canShowOriginalName ? 'select-none' : ''
|
||||||
|
}
|
||||||
${isActive
|
${isActive
|
||||||
? 'bg-green-500 text-white border-green-400 shadow-lg shadow-green-500/25 dark:bg-green-600 cursor-default'
|
? 'bg-green-500 text-white border-green-400 shadow-lg shadow-green-500/25 dark:bg-green-600 cursor-default'
|
||||||
: isWatched
|
: isWatched
|
||||||
@@ -122,11 +159,13 @@ const EpisodeButton: React.FC<EpisodeButtonProps> = ({
|
|||||||
: inactiveEpisodeClass
|
: inactiveEpisodeClass
|
||||||
}`.trim()}
|
}`.trim()}
|
||||||
style={
|
style={
|
||||||
{
|
canShowOriginalName
|
||||||
WebkitUserSelect: 'none',
|
? ({
|
||||||
userSelect: 'none',
|
WebkitUserSelect: 'none',
|
||||||
WebkitTouchCallout: 'none',
|
userSelect: 'none',
|
||||||
} as React.CSSProperties
|
WebkitTouchCallout: 'none',
|
||||||
|
} as React.CSSProperties)
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
title={isWatched && !isActive ? '已观看过' : undefined}
|
title={isWatched && !isActive ? '已观看过' : undefined}
|
||||||
>
|
>
|
||||||
@@ -1031,6 +1070,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
|||||||
isWatched={watchedEpisodes.has(episodeNumber)}
|
isWatched={watchedEpisodes.has(episodeNumber)}
|
||||||
originalTitle={episodes_titles?.[episodeNumber - 1]}
|
originalTitle={episodes_titles?.[episodeNumber - 1]}
|
||||||
inactiveEpisodeClass={inactiveEpisodeClass}
|
inactiveEpisodeClass={inactiveEpisodeClass}
|
||||||
|
enableOriginalNamePopup={isNetdiskSource(currentSource)}
|
||||||
onSelect={handleEpisodeClick}
|
onSelect={handleEpisodeClick}
|
||||||
onShowOriginalName={showEpisodeNamePopup}
|
onShowOriginalName={showEpisodeNamePopup}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user