diff --git a/server/handler.go b/server/handler.go index c6196d6..230ecb0 100644 --- a/server/handler.go +++ b/server/handler.go @@ -146,7 +146,7 @@ func MovieList(c *gin.Context) { sortBy := c.Query("sort") order := c.Query("order") if sortBy == "" { - sortBy = "created_time" // 默认按创建时间排序 + sortBy = "duration" // 默认按创建时间排序 } // 应用排序 diff --git a/server/movie.go b/server/movie.go index dfaf42e..0094974 100644 --- a/server/movie.go +++ b/server/movie.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" "sync" + "time" ) // Movie 电影信息结构 @@ -74,7 +75,7 @@ var Movies []*Movie // 存储所有电影信息的全局切 var MovieDict = make(map[string]*Movie) // 存储需要处理缩略图的视频字典 var MovieDictLock sync.Mutex // 保护MovieDict的并发访问 -var IsRemakePNG = false // 是否重新生成所有PNG缩略图 +var IsRemakePNG = true // 是否重新生成所有PNG缩略图 var Categories = []string{ // 分类 "15min", "30min", "60min", "大于60min", "最新添加"} @@ -223,38 +224,55 @@ func generateThumbnail(movie *Movie) { baseName := strings.TrimSuffix(movie.FileName, filepath.Ext(movie.FileName)) outputPath := filepath.Join("movie", baseName+".png") - // 根据时长选择不同的采样策略 - var filter string - switch { - case movie.Duration <= 2: - filter = "select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,5)',scale=320:180,tile=3x3" - case movie.Duration <= 5: - filter = "select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,10)',scale=320:180,tile=3x3" - case movie.Duration <= 30: - filter = "select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,20)',scale=320:180,tile=3x3" - case movie.Duration <= 60: - filter = "select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,35)',scale=320:180,tile=3x3" - default: - filter = "select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,60)',scale=320:180,tile=3x3" - } + // --- Start of improved logic --- - // 执行ffmpeg命令生成缩略图 + // Fixed skip for the beginning of the video, as per original code's ffmpeg args + const skipStartSeconds = 10.0 + // Number of frames we want in the tile (for a 3x3 grid) + const numFramesInTile = 9.0 // Use float for calculations + + durationAfterSkip := float64(movie.Duration) - skipStartSeconds + + var intervalSeconds float64 + + if durationAfterSkip <= 0.1 { // Use a small threshold like 0.1s + intervalSeconds = 0.05 // A very small interval to grab whatever is there. + } else { + if numFramesInTile > 1 { + intervalSeconds = durationAfterSkip / (numFramesInTile - 1.0) + } else { + intervalSeconds = durationAfterSkip + } + if intervalSeconds <= 0 { + intervalSeconds = 0.05 // Fallback to a tiny interval + } + } + filter := fmt.Sprintf("select='isnan(prev_selected_t)+gte(t-prev_selected_t\\,%.3f)',scale=320:180,tile=3x3", intervalSeconds) + + log.Printf("Movie: %s, OriginalDuration: %ds, SkipStart: %.1fs, DurationForSampling: %.2fs, CalculatedInterval: %.3fs", + movie.FileName, movie.Duration, skipStartSeconds, durationAfterSkip, intervalSeconds) + + // Execute ffmpeg command cmd := exec.Command("ffmpeg", + "-ss", fmt.Sprintf("%.0f", skipStartSeconds), // Using the defined skipStartSeconds "-i", movie.VideoPath, "-vf", filter, - "-frames:v", "1", - "-q:v", "3", - "-y", // 覆盖已存在文件 + "-frames:v", "1", // Crucial for outputting a single tiled image + "-q:v", "3", // Quality for the frames before tiling/PNG compression + "-y", // Overwrite output file if it exists outputPath, ) var stderr bytes.Buffer cmd.Stderr = &stderr + + startTime := time.Now() if err := cmd.Run(); err != nil { - log.Printf("生成缩略图失败: %s (%d min): %v\n错误输出: %s", + // Assuming movie.Duration is in seconds, adjusted log from (%d min) to (%d s) + log.Printf("生成缩略图失败: %s (%d s): %v\n错误输出: %s", movie.VideoPath, movie.Duration, err, stderr.String()) } else { - log.Printf("成功生成缩略图: %s", outputPath) + log.Printf("成功生成缩略图: %s (耗时: %v)", outputPath, time.Since(startTime)) } } diff --git a/src/Main.jsx b/src/Main.jsx index ddd1e56..7c5ff91 100644 --- a/src/Main.jsx +++ b/src/Main.jsx @@ -23,11 +23,11 @@ const categories = [ { label: '最新添加', value: '最新添加' }, ]; -// 新增搜索类别常量 const SEARCH_CATEGORY = 'search'; const LIMIT = 20; const DEFAULT_CATEGORY = categories[0].value; + const usePersistedState = (key, defaultValue) => { const [state, setState] = useState(() => { const stored = localStorage.getItem(key); @@ -46,281 +46,307 @@ const Main = () => { const [loading, setLoading] = useState(false); const isFirstLoad = useRef(true); const scrollListenerActive = useRef(false); + const skipScrollRestore = useRef(false); + const isPaginating = useRef(false); // New ref for pagination - - - - // 初始化状态 - 添加搜索相关状态 const [params, setParams] = usePersistedState('params', { lastCategory: DEFAULT_CATEGORY, history: Object.fromEntries([ - ...categories.map(cat => [ - cat.value, - { lastPage: 1, scrollPos: 0 } - ]), - // 为搜索添加单独的历史记录 - [SEARCH_CATEGORY, { lastPage: 1, scrollPos: 0 }] + ...categories.map(cat => [cat.value, { lastPage: 1, scrollPos: 0 }]), + [SEARCH_CATEGORY, { lastPage: 1, scrollPos: 0, searchQuery: '' }] // Ensure searchQuery is part of history for search ]), - // 新增搜索查询状态 - searchQuery: '' + searchQuery: '' // Global search query, might be redundant if using history.searchQuery }); const [movies, setMovies] = useState([]); const [pagination, setPagination] = useState({ page: 1, total: 0, pages: 1 }); - const currentCategory = params.lastCategory || DEFAULT_CATEGORY; - - // 新增搜索输入状态 const [searchInput, setSearchInput] = useState(params.searchQuery || ''); - // 滚动位置处理 - const saveScrollPosition = useCallback((category = currentCategory) => { - const currentScrollPos = window.scrollY; + const currentCategory = params.lastCategory || DEFAULT_CATEGORY; + const currentCategoryHistory = params.history[currentCategory] || { lastPage: 1, scrollPos: 0 }; - setParams(prev => { - const updatedHistory = { - ...prev.history, - [category]: { - ...(prev.history[category] || { lastPage: 1, scrollPos: 0 }), - scrollPos: currentScrollPos + + const handleScrollPosition = useCallback((action, category = currentCategory) => { + if (action === 'save') { + const currentScrollPos = window.scrollY; + setParams(prev => ({ + ...prev, + history: { + ...prev.history, + [category]: { + ...(prev.history[category] || { lastPage: 1 }), + scrollPos: currentScrollPos + } } - }; + })); + } else if (action === 'restore') { + const scrollPos = params.history?.[category]?.scrollPos || 0; + // console.log(`Attempting to restore scroll for ${category} to ${scrollPos}`); + requestAnimationFrame(() => { + // Increased timeout slightly for better rendering chance + setTimeout(() => { + window.scrollTo(0, scrollPos); + // console.log(`Scrolled to ${scrollPos} for ${category}`); + }, 150); + }); + } + }, [setParams, params.history, currentCategory]); - return { ...prev, history: updatedHistory }; - }); - }, [setParams, currentCategory]); - const setupScrollListener = useCallback(() => { - if (scrollListenerActive.current) return; - - const handleScroll = () => { + const manageScrollListener = useCallback((action) => { + const scrollHandler = () => { clearTimeout(window.scrollTimer); - window.scrollTimer = setTimeout(() => saveScrollPosition(), 100); + window.scrollTimer = setTimeout(() => handleScrollPosition('save'), 200); // Debounce save }; + window.scrollHandler = scrollHandler; // Store to remove the correct one - window.addEventListener('scroll', handleScroll); - scrollListenerActive.current = true; - - return () => { - window.removeEventListener('scroll', handleScroll); - clearTimeout(window.scrollTimer); - scrollListenerActive.current = false; - }; - }, [saveScrollPosition]); - - const removeScrollListener = useCallback(() => { - if (scrollListenerActive.current) { + if (action === 'setup' && !scrollListenerActive.current) { + window.addEventListener('scroll', window.scrollHandler); + scrollListenerActive.current = true; + } else if (action === 'remove' && scrollListenerActive.current) { window.removeEventListener('scroll', window.scrollHandler); scrollListenerActive.current = false; + clearTimeout(window.scrollTimer); + } + }, [handleScrollPosition]); + + + const fetchMovies = useCallback(async (category, page, search = '', options = {}) => { + if (options.skipRestore) { + skipScrollRestore.current = true; } - }, []); - // 修改fetchMovies以支持搜索 - const fetchMovies = useCallback(async (category, page, search = '') => { setLoading(true); - try { const isSearch = category === SEARCH_CATEGORY; const isLatest = category === '最新添加'; let apiUrl = `/movie/list?page=${page}&limit=${LIMIT}`; - - if (isSearch) { - // 搜索请求 - apiUrl += `&search=${encodeURIComponent(search)}`; - } else if (isLatest) { - // 最新添加请求 - apiUrl += `&sort=created_time&order=desc`; - } else { - // 分类请求 - apiUrl += `&category=${encodeURIComponent(category)}`; - } + if (isSearch) apiUrl += `&search=${encodeURIComponent(search)}`; + else if (isLatest) apiUrl += `&sort=created_time&order=desc`; + else apiUrl += `&category=${encodeURIComponent(category)}`; const response = await axios.get(apiUrl); - if (response.status !== 200) return; + if (response.status !== 200) { + console.error("API Error:", response); + setMovies([]); + setPagination({ page, total: 0, pages: 1 }); + return; + } const { items, total } = response.data.data; const totalPages = Math.ceil(total / LIMIT); - if (items.length === 0 && page > 1) { + if (items.length === 0 && page > 1 && !isSearch) { // Don't auto-decrement page for search setParams(prev => ({ ...prev, history: { ...prev.history, - [category]: { ...prev.history[category], lastPage: page - 1 } + [category]: { ...prev.history[category], lastPage: Math.max(1, page - 1) } } })); + // Optionally, fetch page - 1 here, or let user do it. + // For now, just update the state and show no movies. + setMovies([]); + setPagination({ page: Math.max(1, page - 1), total, pages: totalPages }); return; } setMovies(items); setPagination({ page, total, pages: totalPages }); + } catch (error) { console.error('Error fetching movies:', error); + setMovies([]); + setPagination({ page, total: 0, pages: 1 }); } finally { - setLoading(false); + setLoading(false); // This will trigger the useEffect[loading] } - }, [setParams]); + }, [setParams]); // Removed currentCategory from deps as fetchMovies doesn't directly use it for scroll - // 恢复滚动位置 - const restoreScrollPosition = useCallback((category) => { - const scrollPos = params.history?.[category]?.scrollPos || 0; - requestAnimationFrame(() => { - setTimeout(() => window.scrollTo(0, scrollPos), 100); - }); - }, [params.history]); + const handleTransition = useCallback((actionCallback) => { + // console.log(`Transition: Saving scroll for ${currentCategory}`); + manageScrollListener('remove'); + handleScrollPosition('save'); // Save scroll for the category we are leaving + actionCallback(); + // manageScrollListener will be set up again in useEffect[loading] + }, [manageScrollListener, handleScrollPosition, currentCategory]); - // 初始加载 + + // Initial Load useEffect(() => { if (!isFirstLoad.current) return; - const category = currentCategory; - const categoryHistory = params.history[category] || { lastPage: 1 }; + const categoryToLoad = params.lastCategory || DEFAULT_CATEGORY; + const historyForCategory = params.history[categoryToLoad] || { lastPage: 1, scrollPos: 0 }; + const pageToLoad = historyForCategory.lastPage; + let searchQueryToLoad = ''; + if (categoryToLoad === SEARCH_CATEGORY) { + searchQueryToLoad = params.searchQuery || (historyForCategory.searchQuery || ''); + setSearchInput(searchQueryToLoad); // Sync search input + } + // console.log(`Initial Load: Fetching ${categoryToLoad}, page ${pageToLoad}, search: "${searchQueryToLoad}"`); + fetchMovies(categoryToLoad, pageToLoad, searchQueryToLoad); + isFirstLoad.current = false; + }, []); // Runs only once on mount - // 如果是搜索类别,传递搜索查询 - if (category === SEARCH_CATEGORY) { - fetchMovies(category, categoryHistory.lastPage, params.searchQuery); + + // Scroll Management after data loading + useEffect(() => { + if (loading || isFirstLoad.current) return; // Don't run if loading or still initial phase + + manageScrollListener('remove'); // Ensure no listener interference + + if (isPaginating.current) { + // console.log("Pagination: Scrolling to top"); + window.scrollTo(0, 0); + isPaginating.current = false; + } else if (skipScrollRestore.current) { + // console.log(`Skipping scroll restore for ${currentCategory} (e.g., after rename)`); + skipScrollRestore.current = false; } else { - fetchMovies(category, categoryHistory.lastPage); + // console.log(`Restoring scroll for ${currentCategory} (normal flow)`); + handleScrollPosition('restore', currentCategory); } - isFirstLoad.current = false; - }, [fetchMovies, currentCategory, params.history, params.searchQuery]); + manageScrollListener('setup'); // Setup listener after scroll decision + + }, [loading, currentCategory, handleScrollPosition, manageScrollListener]); - // 数据加载完成后处理 - useEffect(() => { - if (loading || isFirstLoad.current) return; - restoreScrollPosition(currentCategory); - setupScrollListener(); - }, [loading, restoreScrollPosition, currentCategory, setupScrollListener]); - // 类别切换处理 - 修改以支持搜索类别 const handleCategoryChange = useCallback((newCategory) => { - removeScrollListener(); - saveScrollPosition(); - - setParams(prev => { - const categoryHistory = prev.history[newCategory] || { lastPage: 1 }; - - // 如果是搜索类别,使用保存的搜索查询 + handleTransition(() => { + const newCategoryHistory = params.history[newCategory] || { lastPage: 1, scrollPos: 0 }; + let searchQueryForNewCategory = ''; if (newCategory === SEARCH_CATEGORY) { - fetchMovies(newCategory, categoryHistory.lastPage, prev.searchQuery); + searchQueryForNewCategory = params.searchQuery || (newCategoryHistory.searchQuery || ''); + setSearchInput(searchQueryForNewCategory); // Sync search input } else { - fetchMovies(newCategory, categoryHistory.lastPage); + setSearchInput(''); // Clear search input if not search category } - return { - ...prev, - lastCategory: newCategory - }; + setParams(prev => ({ ...prev, lastCategory: newCategory })); + // console.log(`Category Change: Fetching ${newCategory}, page ${newCategoryHistory.lastPage}`); + fetchMovies(newCategory, newCategoryHistory.lastPage, searchQueryForNewCategory); }); - }, [removeScrollListener, saveScrollPosition, setParams, fetchMovies]); + }, [handleTransition, setParams, fetchMovies, params.history, params.searchQuery]); + - // 分页处理 - 修改以支持搜索 const handlePageChange = useCallback((_, page) => { - removeScrollListener(); - saveScrollPosition(); + // Save scroll position for the current page of the current category BEFORE fetching new page data + handleScrollPosition('save', currentCategory); + isPaginating.current = true; // Signal that this is a pagination action setParams(prev => ({ ...prev, history: { ...prev.history, - [currentCategory]: { - ...prev.history[currentCategory], - lastPage: page, - scrollPos: 0 - } + [currentCategory]: { ...currentCategoryHistory, lastPage: page } } })); - // 如果是搜索类别,传递搜索查询 - if (currentCategory === SEARCH_CATEGORY) { - fetchMovies(currentCategory, page, params.searchQuery); - } else { - fetchMovies(currentCategory, page); - } + const searchQueryForPageChange = currentCategory === SEARCH_CATEGORY ? (params.searchQuery || currentCategoryHistory.searchQuery || '') : ''; + // console.log(`Page Change: Fetching ${currentCategory}, page ${page}, search: "${searchQueryForPageChange}"`); + fetchMovies(currentCategory, page, searchQueryForPageChange); + // Scrolling to top will be handled by useEffect[loading] due to isPaginating.current + }, [setParams, fetchMovies, currentCategory, params.searchQuery, currentCategoryHistory, handleScrollPosition]); - window.scrollTo(0, 0); - }, [removeScrollListener, saveScrollPosition, setParams, fetchMovies, currentCategory, params.searchQuery]); - // 电影卡片点击处理 - const handleMovieCardClick = useCallback(() => { - removeScrollListener(); - saveScrollPosition(); - }, [removeScrollListener, saveScrollPosition]); - - // 新增:处理搜索提交 const handleSearchSubmit = useCallback(() => { - if (!searchInput.trim()) return; + const trimmedSearch = searchInput.trim(); + if (!trimmedSearch) return; - removeScrollListener(); - saveScrollPosition(); - - setParams(prev => { - // 更新搜索查询并切换到搜索类别 - const updatedParams = { + handleTransition(() => { + setParams(prev => ({ ...prev, lastCategory: SEARCH_CATEGORY, - searchQuery: searchInput.trim(), + searchQuery: trimmedSearch, // Update global search query history: { ...prev.history, - [SEARCH_CATEGORY]: { - ...prev.history[SEARCH_CATEGORY], - lastPage: 1 // 新搜索总是从第一页开始 + [SEARCH_CATEGORY]: { // Reset search history to page 1 for new search + lastPage: 1, + scrollPos: 0, // New search starts at top + searchQuery: trimmedSearch } } - }; - - // 执行搜索请求 - fetchMovies(SEARCH_CATEGORY, 1, searchInput.trim()); - - return updatedParams; + })); + // console.log(`Search Submit: Fetching ${SEARCH_CATEGORY}, page 1, search: "${trimmedSearch}"`); + fetchMovies(SEARCH_CATEGORY, 1, trimmedSearch); }); - }, [removeScrollListener, saveScrollPosition, setParams, fetchMovies, searchInput]); + }, [handleTransition, setParams, fetchMovies, searchInput]); + - // 新增:清除搜索 const handleClearSearch = useCallback(() => { - if (!searchInput) return; + if (!searchInput && currentCategory !== SEARCH_CATEGORY) return; setSearchInput(''); - - // 如果当前在搜索类别,则清除后切换到默认类别 + // If we are currently in search results, transition to default category if (currentCategory === SEARCH_CATEGORY) { - setParams(prev => ({ - ...prev, - lastCategory: DEFAULT_CATEGORY, - searchQuery: '' - })); - - // 获取默认类别的数据 - const categoryHistory = params.history[DEFAULT_CATEGORY] || { lastPage: 1 }; - fetchMovies(DEFAULT_CATEGORY, categoryHistory.lastPage); + handleTransition(() => { + const defaultCategoryHistory = params.history[DEFAULT_CATEGORY] || { lastPage: 1, scrollPos: 0 }; + setParams(prev => ({ + ...prev, + lastCategory: DEFAULT_CATEGORY, + searchQuery: '', // Clear global search query + // We can choose to preserve or reset params.history[SEARCH_CATEGORY].searchQuery + // For now, let's clear it in history as well if we are clearing and navigating away + history: { + ...prev.history, + [SEARCH_CATEGORY]: { ...params.history[SEARCH_CATEGORY], searchQuery: '' } + } + })); + // console.log(`Clear Search (was search category): Fetching ${DEFAULT_CATEGORY}, page ${defaultCategoryHistory.lastPage}`); + fetchMovies(DEFAULT_CATEGORY, defaultCategoryHistory.lastPage); + }); } else { - // 不在搜索类别,只清除输入框 - setParams(prev => ({ - ...prev, - searchQuery: '' - })); + // If not in search results, just clear the persisted search query + setParams(prev => ({ ...prev, searchQuery: '' })); } - }, [currentCategory, fetchMovies, params.history, searchInput, setParams]); + }, [currentCategory, fetchMovies, params.history, searchInput, setParams, handleTransition]); - // 新增:处理搜索输入变化 - const handleSearchChange = useCallback((e) => { - setSearchInput(e.target.value); - }, []); - // 新增:处理搜索输入键盘事件 + const handleRename = useCallback(async (oldName, newName) => { + try { + const response = await axios.post('/movie/rename', { old_name: oldName, new_name: newName }); + if (response.data.code === 200) { + // Refresh current view data, but skip scroll restoration + const pageToRefresh = currentCategoryHistory.lastPage; + const searchQueryForRefresh = currentCategory === SEARCH_CATEGORY ? (params.searchQuery || currentCategoryHistory.searchQuery || '') : ''; + // console.log(`Rename: Refreshing ${currentCategory}, page ${pageToRefresh}, search: "${searchQueryForRefresh}" with skipRestore`); + await fetchMovies(currentCategory, pageToRefresh, searchQueryForRefresh, { skipRestore: true }); + } else { + throw new Error(response.data.message || '重命名失败'); + } + } catch (error) { + console.error('重命名错误:', error); + throw error; // Re-throw to allow MovieCard to handle it if needed + } + }, [fetchMovies, currentCategory, currentCategoryHistory, params.searchQuery]); + + + const handleMovieCardClick = useCallback(() => { + // This is a navigation, so save scroll state + // console.log(`Movie Card Click: Saving scroll for ${currentCategory} before navigating away.`); + handleScrollPosition('save', currentCategory); + // No need for manageScrollListener('remove') here as the component might unmount + // or if it's an in-app navigation, the next component will manage its own scroll. + }, [handleScrollPosition, currentCategory]); + + const handleSearchChange = useCallback((e) => setSearchInput(e.target.value), []); const handleSearchKeyDown = useCallback((e) => { - if (e.key === 'Enter') { - handleSearchSubmit(); - } + if (e.key === 'Enter') handleSearchSubmit(); }, [handleSearchSubmit]); - // 组件卸载清理 - useEffect(() => removeScrollListener, [removeScrollListener]); + // Cleanup scroll listener on unmount + useEffect(() => { + return () => { + manageScrollListener('remove'); + }; + }, [manageScrollListener]); - // 分页组件复用 - const paginationComponent = ( + + const paginationComponent = movies.length > 0 && pagination.pages > 1 && ( { /> ); - const handleRename = useCallback(async (oldName, newName) => { - try { - const response = await axios.post('/movie/rename', { - old_name: oldName, - new_name: newName - }); - - if (response.data.code === 200) { - // 重命名成功后重新加载当前页面数据 - const category = params.lastCategory; - const categoryHistory = params.history[category] || { lastPage: 1 }; - - if (category === SEARCH_CATEGORY) { - await fetchMovies(category, categoryHistory.lastPage, params.searchQuery); - } else { - await fetchMovies(category, categoryHistory.lastPage); - } - } else { - throw new Error(response.data.message || '重命名失败'); - } - } catch (error) { - console.error('重命名错误:', error); - throw error; - } - }, [fetchMovies, params.lastCategory, params.history, params.searchQuery]); - return ( - {/* 添加搜索框 */} { onChange={handleSearchChange} onKeyDown={handleSearchKeyDown} InputProps={{ - startAdornment: ( - - - - ), + startAdornment: , endAdornment: searchInput && ( - - - + ) }} @@ -384,28 +377,33 @@ const Main = () => { - {/* 显示当前搜索状态 */} {currentCategory === SEARCH_CATEGORY && (
- 搜索: "{params.searchQuery}" ({pagination.total} 个结果) + 搜索: "{params.searchQuery || currentCategoryHistory.searchQuery}" ({pagination.total} 个结果)
)} {paginationComponent} - {loading ? ( - - ) : ( + {loading && } + + {!loading && movies.length === 0 && ( +
+ 没有找到结果。 +
+ )} + + {!loading && movies.length > 0 && ( {movies.map(item => ( diff --git a/src/components/MovieCard.jsx b/src/components/MovieCard.jsx index 80e2af0..80fa91b 100644 --- a/src/components/MovieCard.jsx +++ b/src/components/MovieCard.jsx @@ -1,11 +1,27 @@ import React, { useState } from 'react'; -import { Card, CardContent, CardMedia, Typography, IconButton, TextField, Dialog, DialogActions, DialogContent, DialogTitle, Button } from '@mui/material'; +import { + Card, + CardContent, + CardMedia, + Typography, + IconButton, + TextField, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Button, + Snackbar, + Alert +} from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; import { styled } from '@mui/system'; const MovieCard = ({ movie, config, onRename }) => { const [newName, setNewName] = useState(''); const [openDialog, setOpenDialog] = useState(false); + const [error, setError] = useState(null); + const [openSnackbar, setOpenSnackbar] = useState(false); const truncateFilename = (filename, maxLength) => { return filename.length > maxLength @@ -15,7 +31,7 @@ const MovieCard = ({ movie, config, onRename }) => { const handleRenameClick = (e) => { e.stopPropagation(); - e.preventDefault(); // 添加阻止默认行为 + e.preventDefault(); const lastDotIndex = movie.filename.lastIndexOf('.'); const nameWithoutExt = lastDotIndex === -1 ? movie.filename @@ -35,7 +51,7 @@ const MovieCard = ({ movie, config, onRename }) => { const handleRenameSubmit = async (e) => { if (e) { e.stopPropagation(); - e.preventDefault(); // 添加阻止默认行为 + e.preventDefault(); } if (!newName.trim()) return; @@ -55,11 +71,21 @@ const MovieCard = ({ movie, config, onRename }) => { await onRename(movie.filename, fullNewName); handleDialogClose(); } catch (error) { - console.error('重命名失败:', error); + + // 统一错误处理,优先显示后端返回的 message,其次显示 error.message,最后显示默认错误 + setError( + error.response?.data?.message || + error.message || + '重命名失败,请稍后重试' + ); + setOpenSnackbar(true); } }; - // 处理输入框回车键提交 + const handleCloseSnackbar = () => { + setOpenSnackbar(false); + }; + const handleKeyPress = (e) => { if (e.key === 'Enter') { e.stopPropagation(); @@ -68,7 +94,6 @@ const MovieCard = ({ movie, config, onRename }) => { } }; - // 阻止对话框内容区域的点击事件冒泡 const handleDialogContentClick = (e) => { e.stopPropagation(); }; @@ -123,7 +148,7 @@ const MovieCard = ({ movie, config, onRename }) => { @@ -138,29 +163,42 @@ const MovieCard = ({ movie, config, onRename }) => { variant="standard" value={newName} onChange={(e) => setNewName(e.target.value)} - onKeyPress={handleKeyPress} // 支持回车键提交 + onKeyPress={handleKeyPress} helperText="请不要修改文件后缀" - onClick={(e) => e.stopPropagation()} // 防止输入框点击冒泡 + onClick={(e) => e.stopPropagation()} /> + + + + {error} + + ); }; -export default MovieCard; \ No newline at end of file +export default MovieCard; + + diff --git a/src/components/VideoPlayer.jsx b/src/components/VideoPlayer.jsx index 25360e5..5630dd5 100644 --- a/src/components/VideoPlayer.jsx +++ b/src/components/VideoPlayer.jsx @@ -1,43 +1,153 @@ -import {React, useContext, useState} from 'react'; -import { useParams } from 'react-router-dom'; +import React, { useContext, useState, useRef, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; import Container from '@mui/material/Container'; import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ConfigContext from '../Config'; import ReactPlayer from 'react-player'; - - - const VideoPlayer = () => { const config = useContext(ConfigContext); const { filename } = useParams(); + const navigate = useNavigate(); + const playerRef = useRef(null); - const [isFullScreen, setIsFullScreen] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isMobile, setIsMobile] = useState(false); - const handleFullScreenChange = (player) => { - setIsFullScreen(!isFullScreen); - if (isFullScreen) { - player.exitFullscreen(); - } else { - player.requestFullscreen(); - } + // 检测移动设备 + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth <= 768); + }; + + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + // 解码文件名 + const decodedFilename = decodeURIComponent(filename); + const videoUrl = `${window.location.origin}/res/${filename}`; + + const handleReady = () => { + setLoading(false); }; + const handleError = (error) => { + console.error('Video error:', error); + setError('视频加载失败'); + setLoading(false); + }; + + // 错误页面 + if (error) { + return ( + + + navigate(-1)} sx={{ mr: 1 }}> + + + + {decodedFilename} + + + + {error} + + + ); + } return ( - - - {filename} - - - + + {/* 标题栏 */} + + navigate(-1)} + sx={{ color: 'white', mr: 1 }} + size={isMobile ? 'small' : 'medium'} + > + + + + {decodedFilename} + + + + {/* 视频播放器容器 */} + + {loading && ( + + 加载中... + + )} + + + + + {/* 底部信息(仅桌面端显示) */} + {!isMobile && ( + + + 提示:双击视频可全屏播放 + + + )} + ); }; + export default VideoPlayer; \ No newline at end of file