This commit is contained in:
eson 2025-08-14 00:28:29 +08:00
parent 0a8b633539
commit b17e9dea28
5 changed files with 100 additions and 142 deletions

View File

@ -3,102 +3,20 @@ package main
import ( import (
"log" "log"
"net/http" "net/http"
"sort"
"strconv" "strconv"
"strings" "strings"
"sort"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// 重命名电影的处理函数 // GetCategoryList 获取分类列表
func PostRename(c *gin.Context) {
response := gin.H{
"code": http.StatusInternalServerError,
"message": "An unexpected error occurred.",
}
// 定义请求结构体
type RenameRequest struct {
OldName string `json:"old_name" binding:"required"`
NewName string `json:"new_name" binding:"required"`
}
var req RenameRequest
if err := c.ShouldBindJSON(&req); err != nil {
response["code"] = http.StatusBadRequest
response["message"] = "Invalid request body: " + err.Error()
c.JSON(http.StatusBadRequest, response)
return
}
oldName := req.OldName
newName := req.NewName
if oldName == "" || newName == "" {
response["code"] = http.StatusBadRequest
response["message"] = "Old name and new name are required."
c.JSON(http.StatusBadRequest, response)
return
}
if oldName == newName {
response["code"] = http.StatusBadRequest
response["message"] = "Old name and new name cannot be the same."
c.JSON(http.StatusBadRequest, response)
return
}
MovieDictLock.Lock()
defer MovieDictLock.Unlock()
// 检查新名称是否已存在
if _, exists := MovieDict[newName]; exists {
response["code"] = http.StatusConflict
response["message"] = "New name already exists."
c.JSON(http.StatusConflict, response)
return
}
// 查找旧名称对应的电影
movie, exists := MovieDict[oldName]
if exists {
// 更新电影信息
err := movie.RenameVideo(newName) // 重命名视频文件
if err != nil {
response["code"] = http.StatusInternalServerError
response["message"] = "Failed to rename video file."
c.JSON(http.StatusInternalServerError, response)
return
}
delete(MovieDict, oldName) // 删除旧名称的条目
MovieDict[newName] = movie // 添加新名称的条目
response["code"] = http.StatusOK
response["message"] = "Movie renamed successfully."
response["data"] = gin.H{"old_name": oldName, "new_name": newName}
c.JSON(http.StatusOK, response)
log.Printf("Movie renamed from %s to %s successfully", oldName, newName)
return
}
// 如果旧名称不存在返回404错误
response["code"] = http.StatusNotFound
response["message"] = "Movie not found."
c.JSON(http.StatusNotFound, response)
log.Printf("Movie rename failed: %s not found", oldName)
}
func GetCategoryList(c *gin.Context) { func GetCategoryList(c *gin.Context) {
response := gin.H{ response := gin.H{
"code": http.StatusInternalServerError, "code": http.StatusOK,
"message": "An unexpected error occurred.", "message": "Success",
"data": []string{}, "data": Categories,
} }
response["code"] = http.StatusOK
response["message"] = "Success"
response["data"] = Categories
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
@ -227,3 +145,55 @@ func MovieList(c *gin.Context) {
response["data"] = responseData response["data"] = responseData
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
// PostRename 重命名文件
func PostRename(c *gin.Context) {
var requestData struct {
OldName string `json:"old_name"`
NewName string `json:"new_name"`
}
response := gin.H{
"code": http.StatusOK,
"message": "Success",
"data": nil,
}
if err := c.ShouldBindJSON(&requestData); err != nil {
response["code"] = http.StatusBadRequest
response["message"] = "Invalid request data: " + err.Error()
c.JSON(http.StatusBadRequest, response)
return
}
if requestData.OldName == "" || requestData.NewName == "" {
response["code"] = http.StatusBadRequest
response["message"] = "Both old_name and new_name are required"
c.JSON(http.StatusBadRequest, response)
return
}
// 查找要重命名的电影
movie, exists := MovieDict[requestData.OldName]
if !exists {
response["code"] = http.StatusNotFound
response["message"] = "Movie not found"
c.JSON(http.StatusNotFound, response)
return
}
// 执行重命名
if err := movie.RenameVideo(requestData.NewName); err != nil {
response["code"] = http.StatusInternalServerError
response["message"] = "Failed to rename video: " + err.Error()
c.JSON(http.StatusInternalServerError, response)
return
}
// 更新全局字典
delete(MovieDict, requestData.OldName)
MovieDict[requestData.NewName] = movie
response["message"] = "Rename successful"
c.JSON(http.StatusOK, response)
}

View File

@ -10,6 +10,9 @@ import (
func main() { func main() {
initMovie() initMovie()
// 设置为发布模式
gin.SetMode(gin.ReleaseMode)
eg := gin.Default() eg := gin.Default()
eg.Use(Cors()) eg.Use(Cors())
eg.Static("/res", "movie/") eg.Static("/res", "movie/")

View File

@ -75,7 +75,7 @@ var Movies []*Movie // 存储所有电影信息的全局切
var MovieDict = make(map[string]*Movie) // 存储需要处理缩略图的视频字典 var MovieDict = make(map[string]*Movie) // 存储需要处理缩略图的视频字典
var MovieDictLock sync.Mutex // 保护MovieDict的并发访问 var MovieDictLock sync.Mutex // 保护MovieDict的并发访问
var IsRemakePNG = true // 是否重新生成所有PNG缩略图 var IsRemakePNG = false // 是否重新生成所有PNG缩略图
var Categories = []string{ // 分类 var Categories = []string{ // 分类
"15min", "30min", "60min", "大于60min", "最新添加"} "15min", "30min", "60min", "大于60min", "最新添加"}

View File

@ -1,9 +1,10 @@
// ...existing code...
import React, { useState, useEffect, useContext, useCallback, useRef } from 'react'; import React, { useState, useEffect, useContext, useCallback, useRef } from 'react';
import axios from 'axios'; import axios from 'axios';
import Container from '@mui/material/Container'; import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Pagination from '@mui/material/Pagination'; import Pagination from '@mui/material/Pagination';
import { Link } from 'react-router-dom'; // useLocation removed as not directly used for this fix import { Link } from 'react-router-dom';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment'; import InputAdornment from '@mui/material/InputAdornment';
@ -11,9 +12,10 @@ import IconButton from '@mui/material/IconButton';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear'; import ClearIcon from '@mui/icons-material/Clear';
import ConfigContext from './Config'; // Ensure this path is correct import ConfigContext from './Config';
import MovieCard from './components/MovieCard'; // Ensure this path is correct import MovieCard from './components/MovieCard';
import CategoryNav from './components/CategoryNav'; // Ensure this path is correct import CategoryNav from './components/CategoryNav';
// ...existing code...
// //
const categories = [ const categories = [
@ -71,9 +73,9 @@ const Main = () => {
]), ]),
}); });
const isMounted = useRef(false);
const isPopStateNav = useRef(false);
const scrollTimeoutRef = useRef(null); const scrollTimeoutRef = useRef(null);
const isPopStateNav = useRef(false);
const isMounted = useRef(false);
const fetchMovies = useCallback(async (category, page, search = '', options = {}) => { const fetchMovies = useCallback(async (category, page, search = '', options = {}) => {
setLoading(true); setLoading(true);
@ -114,7 +116,7 @@ const Main = () => {
isPopStateNav.current = false; isPopStateNav.current = false;
} }
// IMPORTANT: Removed duplicated scroll logic block that was here // IMPORTANT: Removed duplicated scroll logic block that was here
}, []); // No dependencies that change frequently, setPersistedParams is stable }, [isPopStateNav]);
const navigateAndFetch = useCallback((newCategory, newPage, newSearchQuery = '', options = {}) => { const navigateAndFetch = useCallback((newCategory, newPage, newSearchQuery = '', options = {}) => {
const { replace = false, preserveScroll = false, isPopStatePaging = false } = options; const { replace = false, preserveScroll = false, isPopStatePaging = false } = options;
@ -122,33 +124,25 @@ const Main = () => {
setActiveCategory(newCategory); setActiveCategory(newCategory);
setCurrentPage(newPage); setCurrentPage(newPage);
setActiveSearchQuery(newSearchQuery); setActiveSearchQuery(newSearchQuery);
if (newCategory === SEARCH_CATEGORY) { if (newCategory === SEARCH_CATEGORY) {
setSearchInput(newSearchQuery); setSearchInput(newSearchQuery);
} else { } else {
setSearchInput(''); setSearchInput('');
} }
// Determine scroll position for history state const scrollForHistory = preserveScroll
// If preserving scroll, use current window.scrollY or a remembered scroll for the target category if available ? (window.history.state?.appState?.scrollPos || window.scrollY)
// Otherwise, new navigations (not popstate) typically scroll to 0 unless specified. : 0;
let scrollForHistory = 0;
if (preserveScroll) {
// For general preserveScroll (like back/fwd), use current scrollY.
// For specific category change restorations, this might be overridden by options.restoreScrollPos in fetchMovies.
scrollForHistory = window.history.state?.appState?.scrollPos || window.scrollY;
}
// If navigating to a category with a known scroll position, that should be prioritized for restoration.
// This is handled by passing restoreScrollPos to fetchMovies. For history state, use `scrollForHistory`.
const historyState = { const historyState = {
category: newCategory, category: newCategory,
page: newPage, page: newPage,
searchQuery: newSearchQuery, searchQuery: newSearchQuery,
scrollPos: scrollForHistory, // This is the scroll position *at the moment of navigation* scrollPos: scrollForHistory,
}; };
const url = window.location.pathname; // Keep URL simple, no query params in URL itself for now const url = window.location.pathname;
const browserHistoryState = window.history.state?.appState; const browserHistoryState = window.history.state?.appState;
const needsPush = !browserHistoryState || const needsPush = !browserHistoryState ||
browserHistoryState.category !== newCategory || browserHistoryState.category !== newCategory ||
@ -168,14 +162,14 @@ const Main = () => {
if (oldCategoryState && oldCategoryState !== newCategory) { if (oldCategoryState && oldCategoryState !== newCategory) {
newCategoryHistory[oldCategoryState] = { newCategoryHistory[oldCategoryState] = {
...newCategoryHistory[oldCategoryState], ...newCategoryHistory[oldCategoryState],
scrollPos: window.scrollY, // Save scroll of category being left scrollPos: window.scrollY,
}; };
} }
newCategoryHistory[newCategory] = { newCategoryHistory[newCategory] = {
...newCategoryHistory[newCategory], ...newCategoryHistory[newCategory],
lastPage: newPage, lastPage: newPage,
scrollPos: scrollForHistory, // Store the scroll we intend to be at for the new state scrollPos: scrollForHistory,
...(newCategory === SEARCH_CATEGORY && { searchQuery: newSearchQuery }), ...(newCategory === SEARCH_CATEGORY && { searchQuery: newSearchQuery }),
}; };
@ -185,20 +179,11 @@ const Main = () => {
}; };
}); });
// Determine scroll position for fetching movies const scrollPosForFetch = preserveScroll ? scrollForHistory : 0;
// If preserving scroll, it means we want to restore to where we were or a specific point
let scrollPosForFetch = scrollForHistory; // Default to the scrollForHistory calculated
if (!preserveScroll) { // If not preserving, new main navigations usually go to top
scrollPosForFetch = 0;
}
// If navigating to a category and it has a specific remembered scroll, that should be preferred
// This part becomes complex if we want category changes to always restore *their* scroll.
// For now, `preserveScroll` will try to keep `window.scrollY`, otherwise `0`.
// PopState correctly restores its specific scroll.
fetchMovies(newCategory, newPage, newSearchQuery, { fetchMovies(newCategory, newPage, newSearchQuery, {
restoreScrollPos: scrollPosForFetch, restoreScrollPos: scrollPosForFetch,
skipScrollRestore: false, // Allow fetchMovies to handle scroll unless explicitly told otherwise later skipScrollRestore: false,
isPopStatePaging: isPopStatePaging, isPopStatePaging: isPopStatePaging,
}); });
}, [fetchMovies, setPersistedParams]); }, [fetchMovies, setPersistedParams]);
@ -268,7 +253,7 @@ const Main = () => {
clearTimeout(scrollTimeoutRef.current); clearTimeout(scrollTimeoutRef.current);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigateAndFetch, setPersistedParams]); // Removed fetchMovies as navigateAndFetch calls it. }, [navigateAndFetch, setPersistedParams]);
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
@ -306,7 +291,7 @@ const Main = () => {
window.removeEventListener('scroll', handleScroll); window.removeEventListener('scroll', handleScroll);
clearTimeout(scrollTimeoutRef.current); clearTimeout(scrollTimeoutRef.current);
}; };
}, [loading, movies.length, setPersistedParams]); }, [loading, movies.length, setPersistedParams, isPopStateNav, scrollTimeoutRef]);
const handleCategoryChange = useCallback((newCategory) => { const handleCategoryChange = useCallback((newCategory) => {
@ -422,7 +407,7 @@ const Main = () => {
setActiveSearchQuery(''); setActiveSearchQuery('');
setPersistedParams(prev => ({ ...prev, lastKnownState: { ...prev.lastKnownState, searchQuery: '' } })); setPersistedParams(prev => ({ ...prev, lastKnownState: { ...prev.lastKnownState, searchQuery: '' } }));
} }
}, [activeCategory, navigateAndFetch, persistedParams.categoryHistory, setPersistedParams, fetchMovies]); }, [activeCategory, persistedParams.categoryHistory, setPersistedParams, fetchMovies]);
// MODIFIED handleRename // MODIFIED handleRename

View File

@ -122,30 +122,30 @@ const VideoPlayer = () => {
file: { file: {
attributes: { attributes: {
playsInline: true, // playsInline: true, //
preload: 'metadata' preload: 'metadata',
disablePictureInPicture: true //
} }
} }
}} }}
style={{ style={{
maxWidth: '100%', maxWidth: '100%',
maxHeight: '100%' maxHeight: '100%',
margin: 'auto' //
}} }}
/> />
</Box> </Box>
{/* 底部信息(仅桌面端显示) */} {/* 底部信息(移动端显示简化提示) */}
{!isMobile && (
<Box sx={{ <Box sx={{
p: 2, p: isMobile ? 1 : 2,
color: 'rgba(255,255,255,0.7)', color: 'rgba(255,255,255,0.7)',
textAlign: 'center', textAlign: 'center',
fontSize: '0.8rem' fontSize: '0.8rem'
}}> }}>
<Typography variant="caption"> <Typography variant="caption">
提示双击视频可全屏播放 {isMobile ? '双击可全屏' : '提示:双击视频可全屏播放'}
</Typography> </Typography>
</Box> </Box>
)}
</Box> </Box>
); );
}; };