仅netdisk显示

This commit is contained in:
mtvpls
2026-07-23 15:42:13 +08:00
parent 5d90eb8ce7
commit 19f4957160
2 changed files with 78 additions and 43 deletions
+13 -18
View File
@@ -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;
} }
/** /**
+65 -25
View File
@@ -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}
/> />