- 修复循环播放
This commit is contained in:
@@ -3,6 +3,9 @@ mod models;
|
|||||||
mod server;
|
mod server;
|
||||||
mod storage;
|
mod storage;
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
// 在后台线程启动 HTTP 服务器
|
// 在后台线程启动 HTTP 服务器
|
||||||
@@ -13,6 +16,25 @@ pub fn run() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 等待 HTTP 服务器就绪后再打开窗口
|
||||||
|
for i in 0..50 {
|
||||||
|
if let Ok(mut s) = std::net::TcpStream::connect_timeout(
|
||||||
|
&"127.0.0.1:8000".parse().unwrap(),
|
||||||
|
Duration::from_millis(500),
|
||||||
|
) {
|
||||||
|
// 发送一个简单的 HTTP 请求测试
|
||||||
|
let _ = s.write_all(b"GET /api/state HTTP/1.0\r\n\r\n");
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
if s.read(&mut buf).is_ok() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
eprintln!("[dpm] 等待 HTTP 服务器启动...");
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|||||||
+49
-3
@@ -18,7 +18,7 @@ use std::fs;
|
|||||||
use tokio_stream::wrappers::BroadcastStream;
|
use tokio_stream::wrappers::BroadcastStream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
// ===================== 媒体目录 =====================
|
// ===================== 媒体目录 =====================
|
||||||
fn media_dir() -> PathBuf {
|
fn media_dir() -> PathBuf {
|
||||||
@@ -46,8 +46,50 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 智能查找 dist 目录(前端静态文件)
|
||||||
|
/// 支持多种运行场景:
|
||||||
|
/// - tauri dev 时 CWD = src-tauri/ → ../dist
|
||||||
|
/// - 二进制在 target/release/dpm → ../../dist
|
||||||
|
/// - 从项目根目录运行时 → dist
|
||||||
|
fn dist_dir() -> PathBuf {
|
||||||
|
let candidates: Vec<PathBuf> = {
|
||||||
|
let mut c = Vec::new();
|
||||||
|
// 1. 相对于当前工作目录
|
||||||
|
c.push(PathBuf::from("dist"));
|
||||||
|
c.push(PathBuf::from("../dist"));
|
||||||
|
// 2. 相对于可执行文件路径
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(dir) = exe.parent() {
|
||||||
|
// 二进制在 src-tauri/target/release/dpm → dist 在 dpm/dist/
|
||||||
|
c.push(dir.join("../../../dist"));
|
||||||
|
// macOS .app bundle: dpm.app/Contents/MacOS/dpm
|
||||||
|
c.push(dir.join("../Resources/dist"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c
|
||||||
|
};
|
||||||
|
|
||||||
|
for p in &candidates {
|
||||||
|
let canonical = p.canonicalize();
|
||||||
|
if let Ok(path) = canonical {
|
||||||
|
if path.join("index.html").exists() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback:直接尝试最常见的路径
|
||||||
|
let fallback = PathBuf::from("../dist");
|
||||||
|
eprintln!("[dpm] 警告: 找不到 dist 目录 (已尝试 {:?}),使用: {:?}", candidates, fallback);
|
||||||
|
fallback
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== 路由构建 =====================
|
// ===================== 路由构建 =====================
|
||||||
pub fn create_router(state: AppState) -> Router {
|
pub fn create_router(state: AppState) -> Router {
|
||||||
|
let dist = dist_dir();
|
||||||
|
println!("[dpm] 静态文件目录: {:?}", dist);
|
||||||
|
let dist_str = dist.to_string_lossy().to_string();
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/login", post(login_handler))
|
.route("/api/login", post(login_handler))
|
||||||
.route("/api/settings", get(get_settings).post(update_settings))
|
.route("/api/settings", get(get_settings).post(update_settings))
|
||||||
@@ -61,11 +103,15 @@ pub fn create_router(state: AppState) -> Router {
|
|||||||
.route("/api/control", post(control_handler))
|
.route("/api/control", post(control_handler))
|
||||||
.route("/api/state", get(get_state).post(update_state))
|
.route("/api/state", get(get_state).post(update_state))
|
||||||
.route("/api/events", get(sse_handler))
|
.route("/api/events", get(sse_handler))
|
||||||
|
// 媒体文件:用户目录 ~/Downloads/Media/
|
||||||
.nest_service("/file", ServeDir::new(media_dir_str()))
|
.nest_service("/file", ServeDir::new(media_dir_str()))
|
||||||
|
// SPA 静待文件回退
|
||||||
.fallback_service(
|
.fallback_service(
|
||||||
ServeDir::new("../dist")
|
ServeDir::new(&dist_str)
|
||||||
.append_index_html_on_directories(true)
|
.append_index_html_on_directories(true)
|
||||||
.fallback(ServeFile::new("../dist/index.html")),
|
.fallback(tower_http::services::ServeFile::new(
|
||||||
|
format!("{}/index.html", dist_str),
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.pine.dpm",
|
"identifier": "com.pine.dpm",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "yarn dev",
|
"beforeDevCommand": "TAURI_DEV_HOST=0.0.0.0 yarn dev",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
"beforeBuildCommand": "yarn build",
|
"beforeBuildCommand": "yarn build",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
|
|||||||
+110
-123
@@ -36,137 +36,138 @@ export default function Screen() {
|
|||||||
const [soundBlocked, setSoundBlocked] = useState(false);
|
const [soundBlocked, setSoundBlocked] = useState(false);
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const [loaderText, setLoaderText] = useState('加载播放列表中...');
|
const [loaderText, setLoaderText] = useState('加载播放列表中...');
|
||||||
|
const [mediaKey, setMediaKey] = useState(0); // 强制重新渲染,用于单项目循环
|
||||||
const [showVideo, setShowVideo] = useState(true);
|
const [showVideo, setShowVideo] = useState(true);
|
||||||
const [currentSrc, setCurrentSrc] = useState('');
|
|
||||||
|
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
const imgRef = useRef(null);
|
const imgRef = useRef(null);
|
||||||
const timerRef = useRef(null);
|
const timerRef = useRef(null);
|
||||||
|
|
||||||
|
// ref 存储可变值,避免回调中的闭包过期
|
||||||
|
const listRef = useRef(list);
|
||||||
|
const indexRef = useRef(index);
|
||||||
|
const pausedRef = useRef(paused);
|
||||||
|
const playModeRef = useRef(playMode);
|
||||||
|
const imageDurationRef = useRef(imageDuration);
|
||||||
|
const soundBlockedRef = useRef(soundBlocked);
|
||||||
|
|
||||||
|
useEffect(() => { listRef.current = list; }, [list]);
|
||||||
|
useEffect(() => { indexRef.current = index; }, [index]);
|
||||||
|
useEffect(() => { pausedRef.current = paused; }, [paused]);
|
||||||
|
useEffect(() => { playModeRef.current = playMode; }, [playMode]);
|
||||||
|
useEffect(() => { imageDurationRef.current = imageDuration; }, [imageDuration]);
|
||||||
|
useEffect(() => { soundBlockedRef.current = soundBlocked; }, [soundBlocked]);
|
||||||
|
|
||||||
const getUrl = useCallback((item) => {
|
const getUrl = useCallback((item) => {
|
||||||
return item.source === 'url' ? item.relative_path : `/file/${item.relative_path}`;
|
return item.source === 'url' ? item.relative_path : `/file/${item.relative_path}`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const reportState = useCallback((status) => {
|
// ============ 核心播放控制 ============
|
||||||
const item = list[index] || null;
|
|
||||||
api.updateState({
|
|
||||||
status,
|
|
||||||
index,
|
|
||||||
name: item ? item.name : '',
|
|
||||||
type: item ? item.type : '',
|
|
||||||
}).catch(() => {});
|
|
||||||
}, [list, index]);
|
|
||||||
|
|
||||||
const updateStatusUI = useCallback(() => { }, []); // CSS handles visual feedback
|
/** 稳定的 next — 始终推进索引(不检查 paused),永不停止 */
|
||||||
|
const next = useCallback(() => {
|
||||||
const play = useCallback(() => {
|
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
if (paused) return;
|
const len = listRef.current.length;
|
||||||
const item = list[index];
|
if (len === 0) return;
|
||||||
if (!item) return;
|
const mode = playModeRef.current;
|
||||||
const url = getUrl(item);
|
setIndex(prev => {
|
||||||
|
if (mode === 'random') {
|
||||||
if (item.type === 'video') {
|
return Math.floor(Math.random() * len);
|
||||||
setShowVideo(true);
|
|
||||||
setCurrentSrc(url);
|
|
||||||
const video = videoRef.current;
|
|
||||||
if (video) {
|
|
||||||
video.style.display = 'block';
|
|
||||||
video.src = url;
|
|
||||||
video.load();
|
|
||||||
video.muted = soundBlocked;
|
|
||||||
const p = video.play();
|
|
||||||
if (p !== undefined) {
|
|
||||||
p.then(() => {
|
|
||||||
reportState('playing');
|
|
||||||
}).catch(() => {
|
|
||||||
setSoundBlocked(true);
|
|
||||||
video.muted = true;
|
|
||||||
video.play().catch(() => {});
|
|
||||||
reportState('playing');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
// 关键:始终循环,(prev + 1) % len 保证永不越界
|
||||||
// Image
|
return (prev + 1) % len;
|
||||||
setShowVideo(false);
|
});
|
||||||
setCurrentSrc(url);
|
// 单项目时 index 不变((0+1)%1=0),用 mediaKey 强制渲染
|
||||||
reportState('playing');
|
setMediaKey(k => k + 1);
|
||||||
timerRef.current = setTimeout(next, imageDuration * 1000);
|
}, []);
|
||||||
}
|
|
||||||
}, [list, index, paused, getUrl, soundBlocked, imageDuration, reportState]);
|
|
||||||
|
|
||||||
const skip = useCallback((delta) => {
|
const skip = useCallback((delta) => {
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
|
const len = listRef.current.length;
|
||||||
|
if (len === 0) return;
|
||||||
|
const mode = playModeRef.current;
|
||||||
setIndex(prev => {
|
setIndex(prev => {
|
||||||
if (playMode === 'random') {
|
if (mode === 'random') {
|
||||||
return Math.floor(Math.random() * list.length);
|
return Math.floor(Math.random() * len);
|
||||||
}
|
}
|
||||||
return (prev + delta + list.length) % list.length;
|
return (prev + delta + len) % len;
|
||||||
});
|
});
|
||||||
}, [playMode, list.length]);
|
setMediaKey(k => k + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const next = useCallback(() => {
|
// ============ 播放效果 ============
|
||||||
if (paused) return;
|
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
|
||||||
setIndex(prev => {
|
|
||||||
if (playMode === 'random') {
|
|
||||||
return Math.floor(Math.random() * list.length);
|
|
||||||
}
|
|
||||||
return (prev + 1) % list.length;
|
|
||||||
});
|
|
||||||
}, [paused, playMode, list.length]);
|
|
||||||
|
|
||||||
// Play when index changes
|
// index / mediaKey 变化时播放对应项
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded && list.length > 0) {
|
if (!loaded || list.length === 0 || paused) return;
|
||||||
const item = list[index];
|
|
||||||
if (item) {
|
|
||||||
if (item.type === 'video') {
|
|
||||||
setShowVideo(true);
|
|
||||||
setCurrentSrc(getUrl(item));
|
|
||||||
} else {
|
|
||||||
const url = getUrl(item);
|
|
||||||
setShowVideo(false);
|
|
||||||
setCurrentSrc(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [index, loaded]);
|
|
||||||
|
|
||||||
// Separate effect for playback triggered by src changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loaded || list.length === 0 || !currentSrc) return;
|
|
||||||
const item = list[index];
|
const item = list[index];
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
|
// 同步状态到后端
|
||||||
|
api.updateState({
|
||||||
|
status: 'playing',
|
||||||
|
index,
|
||||||
|
name: item.name || '',
|
||||||
|
media_type: item.type || '',
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
if (item.type === 'video') {
|
if (item.type === 'video') {
|
||||||
|
setShowVideo(true);
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (video) {
|
if (!video) return;
|
||||||
video.style.display = 'block';
|
const url = getUrl(item);
|
||||||
video.src = currentSrc;
|
video.src = url;
|
||||||
video.load();
|
video.load();
|
||||||
video.muted = soundBlocked;
|
video.muted = soundBlocked;
|
||||||
const p = video.play();
|
video.volume = volume;
|
||||||
if (p !== undefined) {
|
const p = video.play();
|
||||||
p.then(() => reportState('playing'))
|
if (p !== undefined) {
|
||||||
.catch(() => {
|
p.catch(() => {
|
||||||
setSoundBlocked(true);
|
setSoundBlocked(true);
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.play().catch(() => {});
|
video.play().catch(() => {});
|
||||||
reportState('playing');
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// 图片
|
||||||
|
setShowVideo(false);
|
||||||
|
const url = getUrl(item);
|
||||||
|
imgRef.current.src = url;
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
reportState('playing');
|
|
||||||
timerRef.current = setTimeout(next, imageDuration * 1000);
|
timerRef.current = setTimeout(next, imageDuration * 1000);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [index]);
|
}, [index, mediaKey, loaded, paused]);
|
||||||
|
|
||||||
// Load playlist on mount
|
// 恢复播放:paused 从 true → false 时触发
|
||||||
|
useEffect(() => {
|
||||||
|
if (paused || !loaded || list.length === 0) return;
|
||||||
|
const item = list[index];
|
||||||
|
if (!item || item.type !== 'video') return;
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (video && video.paused && video.src) {
|
||||||
|
video.play().catch(() => {
|
||||||
|
setSoundBlocked(true);
|
||||||
|
video.muted = true;
|
||||||
|
video.play().catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [paused]);
|
||||||
|
|
||||||
|
// ============ 事件监听 ============
|
||||||
|
|
||||||
|
// Video ended → next(稳定,永不失效)
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
const handler = () => next();
|
||||||
|
video.addEventListener('ended', handler);
|
||||||
|
return () => video.removeEventListener('ended', handler);
|
||||||
|
// mediaKey 变化时 video 元素被 key 强制重建,需要重新绑定
|
||||||
|
}, [next, mediaKey]);
|
||||||
|
|
||||||
|
// 加载播放列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -185,7 +186,7 @@ export default function Screen() {
|
|||||||
}
|
}
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
|
|
||||||
// Try play with sound
|
// 尝试启用声音
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (video) {
|
if (video) {
|
||||||
video.muted = false;
|
video.muted = false;
|
||||||
@@ -194,21 +195,18 @@ export default function Screen() {
|
|||||||
testPlay.then(() => {
|
testPlay.then(() => {
|
||||||
setSoundBlocked(false);
|
setSoundBlocked(false);
|
||||||
video.pause();
|
video.pause();
|
||||||
reportState('idle');
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setSoundBlocked(true);
|
setSoundBlocked(true);
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
reportState('idle');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {/* ignore */}
|
} catch {/* ignore */}
|
||||||
};
|
};
|
||||||
load();
|
load();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Connect SSE
|
// SSE 连接
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const es = new EventSource('/api/events');
|
const es = new EventSource('/api/events');
|
||||||
es.onmessage = (e) => {
|
es.onmessage = (e) => {
|
||||||
@@ -219,12 +217,10 @@ export default function Screen() {
|
|||||||
setPaused(true);
|
setPaused(true);
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
if (videoRef.current) videoRef.current.pause();
|
if (videoRef.current) videoRef.current.pause();
|
||||||
reportState('paused');
|
|
||||||
break;
|
break;
|
||||||
case 'play':
|
case 'play':
|
||||||
setPaused(false);
|
setPaused(false);
|
||||||
play();
|
// 由 paused → false 的 useEffect 自动恢复播放
|
||||||
reportState('playing');
|
|
||||||
break;
|
break;
|
||||||
case 'next':
|
case 'next':
|
||||||
setPaused(false);
|
setPaused(false);
|
||||||
@@ -245,13 +241,13 @@ export default function Screen() {
|
|||||||
};
|
};
|
||||||
return () => es.close();
|
return () => es.close();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [play, skip, reportState]);
|
}, [skip]);
|
||||||
|
|
||||||
const reloadPlaylist = useCallback(async () => {
|
const reloadPlaylist = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await api.getPlaylist();
|
const data = await api.getPlaylist();
|
||||||
const newList = data.files || [];
|
const newList = data.files || [];
|
||||||
const currentItem = list[index] || null;
|
const currentItem = listRef.current[indexRef.current] || null;
|
||||||
|
|
||||||
if (newList.length === 0) {
|
if (newList.length === 0) {
|
||||||
setList([]);
|
setList([]);
|
||||||
@@ -276,7 +272,7 @@ export default function Screen() {
|
|||||||
setIndex(prev => Math.min(prev, newList.length - 1));
|
setIndex(prev => Math.min(prev, newList.length - 1));
|
||||||
}
|
}
|
||||||
} catch {/* ignore */}
|
} catch {/* ignore */}
|
||||||
}, [list, index]);
|
}, []);
|
||||||
|
|
||||||
const applySettings = useCallback((msg) => {
|
const applySettings = useCallback((msg) => {
|
||||||
if (msg.volume !== undefined) {
|
if (msg.volume !== undefined) {
|
||||||
@@ -285,7 +281,7 @@ export default function Screen() {
|
|||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (video) {
|
if (video) {
|
||||||
video.volume = vol;
|
video.volume = vol;
|
||||||
if (soundBlocked) {
|
if (soundBlockedRef.current) {
|
||||||
video.muted = false;
|
video.muted = false;
|
||||||
video.play().then(() => {
|
video.play().then(() => {
|
||||||
setSoundBlocked(false);
|
setSoundBlocked(false);
|
||||||
@@ -300,7 +296,7 @@ export default function Screen() {
|
|||||||
}
|
}
|
||||||
if (msg.play_mode !== undefined) setPlayMode(msg.play_mode);
|
if (msg.play_mode !== undefined) setPlayMode(msg.play_mode);
|
||||||
if (msg.image_duration !== undefined) setImageDuration(msg.image_duration);
|
if (msg.image_duration !== undefined) setImageDuration(msg.image_duration);
|
||||||
}, [soundBlocked]);
|
}, []);
|
||||||
|
|
||||||
const enableSound = useCallback(() => {
|
const enableSound = useCallback(() => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
@@ -316,23 +312,14 @@ export default function Screen() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Video ended handler
|
|
||||||
useEffect(() => {
|
|
||||||
const video = videoRef.current;
|
|
||||||
if (!video) return;
|
|
||||||
const handler = () => next();
|
|
||||||
video.addEventListener('ended', handler);
|
|
||||||
return () => video.removeEventListener('ended', handler);
|
|
||||||
}, [next]);
|
|
||||||
|
|
||||||
// Global click for sound unblock
|
// Global click for sound unblock
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
if (soundBlocked) enableSound();
|
if (soundBlockedRef.current) enableSound();
|
||||||
};
|
};
|
||||||
document.addEventListener('click', handler);
|
document.addEventListener('click', handler);
|
||||||
return () => document.removeEventListener('click', handler);
|
return () => document.removeEventListener('click', handler);
|
||||||
}, [soundBlocked, enableSound]);
|
}, [enableSound]);
|
||||||
|
|
||||||
// Cleanup timer on unmount
|
// Cleanup timer on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -355,10 +342,11 @@ export default function Screen() {
|
|||||||
{/* Loader */}
|
{/* Loader */}
|
||||||
<Loader hidden={loaded && list.length > 0} text={loaderText} />
|
<Loader hidden={loaded && list.length > 0} text={loaderText} />
|
||||||
|
|
||||||
{/* Media Players */}
|
{/* Media Players — key={mediaKey} 强制重建以实现单项目循环 */}
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
id="player"
|
id="player"
|
||||||
|
key={mediaKey}
|
||||||
autoPlay
|
autoPlay
|
||||||
playsInline
|
playsInline
|
||||||
controlsList="nodownload"
|
controlsList="nodownload"
|
||||||
@@ -368,7 +356,6 @@ export default function Screen() {
|
|||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
id="imgPlayer"
|
id="imgPlayer"
|
||||||
alt=""
|
alt=""
|
||||||
src={!showVideo && loaded && currentItem ? getUrl(currentItem) : undefined}
|
|
||||||
style={{ display: !showVideo && loaded ? 'block' : 'none' }}
|
style={{ display: !showVideo && loaded ? 'block' : 'none' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user