diff --git a/components/home/TagInput.tsx b/components/home/TagInput.tsx new file mode 100644 index 0000000..50fa542 --- /dev/null +++ b/components/home/TagInput.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Icons } from '@/components/ui/Icon'; + +interface TagInputProps { + newTagInput: string; + onNewTagInputChange: (value: string) => void; + onAddTag: () => void; +} + +export function TagInput({ + newTagInput, + onNewTagInputChange, + onAddTag, +}: TagInputProps) { + return ( +
+ onNewTagInputChange(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && onAddTag()} + placeholder="添加自定义标签..." + className="flex-1 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] text-[var(--text-color)] px-4 py-2 focus:outline-none focus:border-[var(--accent-color)] transition-colors rounded-[var(--radius-2xl)]" + /> + +
+ ); +} diff --git a/components/home/TagList.tsx b/components/home/TagList.tsx new file mode 100644 index 0000000..0209dca --- /dev/null +++ b/components/home/TagList.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, + DragStartEvent, + DragOverlay, +} from '@dnd-kit/core'; +import { + SortableContext, + sortableKeyboardCoordinates, + horizontalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { SortableTag, Tag } from './SortableTag'; +import { useState, useRef, useEffect } from 'react'; + +interface TagListProps { + tags: Tag[]; + selectedTag: string; + showTagManager: boolean; + justAddedTag: boolean; + onTagSelect: (tagId: string) => void; + onTagDelete: (tagId: string) => void; + onDragEnd: (event: DragEndEvent) => void; + onJustAddedTagHandled: () => void; +} + +export function TagList({ + tags, + selectedTag, + showTagManager, + justAddedTag, + onTagSelect, + onTagDelete, + onDragEnd, + onJustAddedTagHandled, +}: TagListProps) { + const scrollContainerRef = useRef(null); + const [activeId, setActiveId] = useState(null); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + // Auto-scroll to end when new tag is added + useEffect(() => { + if (justAddedTag && scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + left: scrollContainerRef.current.scrollWidth, + behavior: 'smooth', + }); + onJustAddedTagHandled(); + } + }, [justAddedTag, onJustAddedTagHandled]); + + const handleDragStart = (event: DragStartEvent) => { + setActiveId(event.active.id as string); + }; + + const handleDragEnd = (event: DragEndEvent) => { + setActiveId(null); + onDragEnd(event); + }; + + const activeTag = tags.find((t) => t.id === activeId); + + return ( + +
+ t.id)} + strategy={horizontalListSortingStrategy} + > + {tags.map((tag) => ( + + ))} + +
+ + + {activeId && activeTag ? ( +
+ +
+ ) : null} +
+
+ ); +} diff --git a/components/home/TagManager.tsx b/components/home/TagManager.tsx index 4b90220..27eabee 100644 --- a/components/home/TagManager.tsx +++ b/components/home/TagManager.tsx @@ -1,24 +1,8 @@ -'use client'; - -import { useEffect, useRef, useState } from 'react'; import { Icons } from '@/components/ui/Icon'; -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - DragEndEvent, - DragStartEvent, - DragOverlay, -} from '@dnd-kit/core'; -import { - SortableContext, - sortableKeyboardCoordinates, - horizontalListSortingStrategy, -} from '@dnd-kit/sortable'; -import { SortableTag, Tag } from './SortableTag'; +import { DragEndEvent } from '@dnd-kit/core'; +import { TagInput } from './TagInput'; +import { TagList } from './TagList'; +import { Tag } from './SortableTag'; interface TagManagerProps { tags: Tag[]; @@ -51,42 +35,6 @@ export function TagManager({ onDragEnd, onJustAddedTagHandled, }: TagManagerProps) { - const scrollContainerRef = useRef(null); - const [activeId, setActiveId] = useState(null); - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, - }, - }), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ); - - // Auto-scroll to end when new tag is added - useEffect(() => { - if (justAddedTag && scrollContainerRef.current) { - scrollContainerRef.current.scrollTo({ - left: scrollContainerRef.current.scrollWidth, - behavior: 'smooth', - }); - onJustAddedTagHandled(); - } - }, [justAddedTag, onJustAddedTagHandled]); - - const handleDragStart = (event: DragStartEvent) => { - setActiveId(event.active.id as string); - }; - - const handleDragEnd = (event: DragEndEvent) => { - setActiveId(null); - onDragEnd(event); - }; - - const activeTag = tags.find((t) => t.id === activeId); - return ( <> {/* Management Controls */} @@ -111,62 +59,24 @@ export function TagManager({ {/* Add Custom Tag */} {showTagManager && ( -
- onNewTagInputChange(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && onAddTag()} - placeholder="添加自定义标签..." - className="flex-1 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] text-[var(--text-color)] px-4 py-2 focus:outline-none focus:border-[var(--accent-color)] transition-colors rounded-[var(--radius-2xl)]" - /> - -
+ )} {/* Tag Filter */} - -
- t.id)} - strategy={horizontalListSortingStrategy} - > - {tags.map((tag) => ( - - ))} - -
- - - {activeId && activeTag ? ( -
- -
- ) : null} -
-
+ ); } diff --git a/components/player/VideoPlayer.tsx b/components/player/VideoPlayer.tsx index 6db60f0..e5ae6e9 100644 --- a/components/player/VideoPlayer.tsx +++ b/components/player/VideoPlayer.tsx @@ -3,10 +3,10 @@ import { useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { Card } from '@/components/ui/Card'; -import { Button } from '@/components/ui/Button'; -import { Icons } from '@/components/ui/Icon'; import { useHistoryStore } from '@/lib/store/history-store'; import { CustomVideoPlayer } from './CustomVideoPlayer'; +import { VideoPlayerError } from './VideoPlayerError'; +import { VideoPlayerEmpty } from './VideoPlayerEmpty'; interface VideoPlayerProps { playUrl: string; @@ -111,53 +111,19 @@ export function VideoPlayer({ playUrl, videoId, currentEpisode, onBack }: VideoP : playUrl; if (!playUrl) { - return ( - -
-
- -

暂无播放源

-
-
-
- ); + return ; } return ( {videoError ? ( -
-
- -

播放失败

-

{videoError}

-
- - {retryCount < MAX_MANUAL_RETRIES && ( - - )} -
-
-
+ ) : ( +
+
+ +

暂无播放源

+
+
+
+ ); +} diff --git a/components/player/VideoPlayerError.tsx b/components/player/VideoPlayerError.tsx new file mode 100644 index 0000000..430d9ee --- /dev/null +++ b/components/player/VideoPlayerError.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { Button } from '@/components/ui/Button'; +import { Icons } from '@/components/ui/Icon'; + +interface VideoPlayerErrorProps { + error: string; + onBack: () => void; + onRetry: () => void; + retryCount: number; + maxRetries: number; +} + +export function VideoPlayerError({ + error, + onBack, + onRetry, + retryCount, + maxRetries, +}: VideoPlayerErrorProps) { + return ( +
+
+ +

播放失败

+

{error}

+
+ + {retryCount < maxRetries && ( + + )} +
+
+
+ ); +} diff --git a/components/player/hooks/useDesktopPlayerLogic.ts b/components/player/hooks/useDesktopPlayerLogic.ts index 3d256d9..94f15cd 100644 --- a/components/player/hooks/useDesktopPlayerLogic.ts +++ b/components/player/hooks/useDesktopPlayerLogic.ts @@ -7,6 +7,7 @@ import { useControlsVisibility } from './desktop/useControlsVisibility'; import { useUtilities } from './desktop/useUtilities'; import { useDesktopShortcuts } from './desktop/useDesktopShortcuts'; import { useDesktopPlayerState } from './useDesktopPlayerState'; +import { getCopyUrl } from '../utils/urlUtils'; type DesktopPlayerState = ReturnType; @@ -132,27 +133,7 @@ export function useDesktopPlayerLogic({ skipBackward: skipControls.skipBackward, changePlaybackSpeed: playbackControls.changePlaybackSpeed, handleCopyLink: (type: 'original' | 'proxy' = 'original') => { - let urlToCopy = src; - - // If user wants original link, strip proxy prefix if present - if (type === 'original') { - if (urlToCopy.includes('/api/proxy?url=')) { - const match = urlToCopy.match(/url=([^&]*)/); - if (match && match[1]) { - urlToCopy = decodeURIComponent(match[1]); - } - } - } - // If user wants proxy link, ensure it has proxy prefix - else if (type === 'proxy') { - if (!urlToCopy.includes('/api/proxy?url=')) { - urlToCopy = `${window.location.origin}/api/proxy?url=${encodeURIComponent(urlToCopy)}`; - } else if (urlToCopy.startsWith('/')) { - // Ensure absolute URL for copy - urlToCopy = `${window.location.origin}${urlToCopy}`; - } - } - + const urlToCopy = getCopyUrl(src, type); utilities.handleCopyLink(urlToCopy); }, startSpeedMenuTimeout: controlsVisibility.startSpeedMenuTimeout, diff --git a/components/player/utils/urlUtils.ts b/components/player/utils/urlUtils.ts new file mode 100644 index 0000000..c40b256 --- /dev/null +++ b/components/player/utils/urlUtils.ts @@ -0,0 +1,24 @@ +export function getCopyUrl(src: string, type: 'original' | 'proxy' = 'original'): string { + let urlToCopy = src; + + // If user wants original link, strip proxy prefix if present + if (type === 'original') { + if (urlToCopy.includes('/api/proxy?url=')) { + const match = urlToCopy.match(/url=([^&]*)/); + if (match && match[1]) { + urlToCopy = decodeURIComponent(match[1]); + } + } + } + // If user wants proxy link, ensure it has proxy prefix + else if (type === 'proxy') { + if (!urlToCopy.includes('/api/proxy?url=')) { + urlToCopy = `${window.location.origin}/api/proxy?url=${encodeURIComponent(urlToCopy)}`; + } else if (urlToCopy.startsWith('/')) { + // Ensure absolute URL for copy + urlToCopy = `${window.location.origin}${urlToCopy}`; + } + } + + return urlToCopy; +}