+
提示信息
-
@@ -273,14 +342,18 @@
currentIndex: 0,
isPlaying: false,
favorites: new Set(),
- playMode: 'sequence', // sequence | random
+ playMode: 'sequence', // sequence | random | favorites
+ viewMode: 'stream', // stream | grid
+ isAutoplay: true, // 默认开启自动连播
isClearScreen: false,
currentTab: 'home', // home | favorites | my
- currentLibraryId: null
+ currentLibraryId: null,
+ isScaleFill: true, // 视频缩放状态
+ isMuted: false // 全局静音状态
};
- // 配置信息
- this.config = { server: '', apiKey: '', useStatic: true };
+ // 配置信息修改点:添加 user, token, userId
+ this.config = { server: '', user: '', token: '', userId: '', useStatic: true, autoplay: true, deleteMode: false };
// 触摸状态缓存
this.touch = { startY: 0, startTime: 0 };
@@ -299,8 +372,21 @@
this.loadFavorites();
this.bindEvents();
+ // 根据配置设置自动连播状态(只影响视频自动播放,不隐藏界面)
+ this.state.isAutoplay = this.config.autoplay !== false;
+
+ // 初始化视图模式按钮图标(默认竖版,显示gallery-vertical)
+ const viewBtn = this.dom.viewModeBtn;
+ viewBtn.innerHTML = `
`;
+
+ // 初始化播放模式按钮图标(根据自动连播配置)
+ const playBtn = this.dom.playModeBtn;
+ playBtn.innerHTML = `
`;
+
+ lucide.createIcons();
+
// 检查配置并启动
- if (this.config.server && this.config.apiKey) {
+ if (this.config.server && this.config.token) {
this.fetchVideos();
} else {
setTimeout(() => this.toggleModal('profilePage', true), 500);
@@ -310,41 +396,122 @@
cacheDOM() {
const ids = [
'videoContainer', 'clickToPlayOverlay', 'loading', 'toast',
- 'videoTitle', 'videoDescription', 'progressLine', 'currentTime', 'totalTime',
- 'playModeBtn', 'libraryBtn', 'favoritesBtn', 'myBtn',
- 'favoriteBtn', 'clearScreenBtn', 'fullscreenBtn', 'muteBtn',
- 'configServer', 'configKey', 'configStatic'
+ 'videoInfoArea', 'videoTitle', 'videoDescription', 'progressLine', 'currentTime', 'totalTime',
+ 'playModeBtn', 'viewModeBtn', 'libraryBtn', 'myBtn',
+ 'favoriteBtn', 'scaleBtn', 'muteBtn', 'fullscreenBtn', 'deleteBtn',
+ 'mediaInfoModal', 'mediaInfoContent', 'closeMediaInfoBtn',
+ 'configServer', 'configUser', 'configPwd', 'configStatic', 'configAutoplay', 'configDeleteMode'
];
ids.forEach(id => this.dom[id] = document.getElementById(id));
+ this.dom.rightToolbar = document.querySelector('.right-toolbar');
}
// --- 配置与数据管理 ---
loadConfig() {
this.config.server = localStorage.getItem('emby_server') || '';
- this.config.apiKey = localStorage.getItem('emby_key') || '';
+ this.config.user = localStorage.getItem('emby_user') || '';
+ this.config.token = localStorage.getItem('emby_token') || '';
+ this.config.userId = localStorage.getItem('emby_uid') || '';
this.config.useStatic = localStorage.getItem('emby_static') !== 'false';
+ this.config.autoplay = localStorage.getItem('emby_autoplay') !== 'false';
+ this.config.deleteMode = localStorage.getItem('emby_delete_mode') === 'true';
// 填充 UI
- this.dom.configServer.value = this.config.server;
- this.dom.configKey.value = this.config.apiKey;
- this.dom.configStatic.checked = this.config.useStatic;
+ if (this.dom.configServer) this.dom.configServer.value = this.config.server;
+ if (this.dom.configUser) this.dom.configUser.value = this.config.user;
+ if (this.dom.configPwd && this.config.token) {
+ this.dom.configPwd.value = '****';
+ }
+ if (this.dom.configStatic) this.dom.configStatic.checked = this.config.useStatic;
+ if (this.dom.configAutoplay) this.dom.configAutoplay.checked = this.config.autoplay;
+ if (this.dom.configDeleteMode) this.dom.configDeleteMode.checked = this.config.deleteMode;
+
+ // Sync UI based on config
+ this.dom.deleteBtn.style.display = this.config.deleteMode ? 'flex' : 'none';
+ if (this.dom.playModeBtn) {
+ this.dom.playModeBtn.innerHTML = `
`;
+ lucide.createIcons();
+ }
}
- saveConfig() {
- const server = this.dom.configServer.value.trim();
- const key = this.dom.configKey.value.trim();
+ // 核心修改点:AuthenticateByName 登录逻辑
+ async saveConfig() {
+ const server = this.dom.configServer.value.trim().replace(/\/$/, "");
+ const user = this.dom.configUser.value.trim();
+ const pwd = this.dom.configPwd.value.trim();
const isStatic = this.dom.configStatic.checked;
+ const autoplay = this.dom.configAutoplay.checked;
+ const deleteMode = this.dom.configDeleteMode.checked;
- if (!server || !key) return this.showToast('请填写完整信息');
+ if (!server || !user) return this.showToast('请填写完整信息');
- localStorage.setItem('emby_server', server);
- localStorage.setItem('emby_key', key);
- localStorage.setItem('emby_static', isStatic);
+ this.toggleLoading(true);
+ try {
+ // 如果是用旧 token 更新设置,跳过登录
+ if (pwd === '****' && this.config.token && this.config.server === server && this.config.user === user) {
+ localStorage.setItem('emby_static', isStatic);
+ localStorage.setItem('emby_autoplay', autoplay);
+ localStorage.setItem('emby_delete_mode', deleteMode);
+ this.config.useStatic = isStatic;
+ this.config.autoplay = autoplay;
+ this.config.deleteMode = deleteMode;
+ this.state.isAutoplay = autoplay;
- this.config = { server, apiKey: key, useStatic: isStatic };
- this.showToast('配置已保存');
- this.toggleModal('profilePage', false);
- this.fetchVideos(); // 重新加载
+ this.dom.deleteBtn.style.display = deleteMode ? 'flex' : 'none';
+ if (this.dom.playModeBtn) {
+ this.dom.playModeBtn.innerHTML = `
`;
+ lucide.createIcons();
+ }
+
+ this.showToast('✅ 配置已保存');
+ this.toggleModal('profilePage', false);
+ return; // 提前退出,不重新拉取视频
+ }
+
+ const res = await fetch(`${server}/emby/Users/AuthenticateByName`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Emby-Authorization': `Emby Client="EmbyX", Device="Web", DeviceId="EmbyX-Device", Version="1.1"`
+ },
+ body: JSON.stringify({ Username: user, Pw: pwd })
+ });
+ const data = await res.json();
+
+ if (data.AccessToken) {
+ localStorage.setItem('emby_server', server);
+ localStorage.setItem('emby_user', user);
+ localStorage.setItem('emby_token', data.AccessToken);
+ localStorage.setItem('emby_uid', data.SessionInfo.UserId);
+ localStorage.setItem('emby_static', isStatic);
+ localStorage.setItem('emby_autoplay', autoplay);
+ localStorage.setItem('emby_delete_mode', deleteMode);
+
+ this.config = { server, user, token: data.AccessToken, userId: data.SessionInfo.UserId, useStatic: isStatic, autoplay, deleteMode };
+
+ // 更新密码框为掩码
+ this.dom.configPwd.value = '****';
+
+ // 更新删除按钮显示状态
+ this.dom.deleteBtn.style.display = deleteMode ? 'flex' : 'none';
+
+ // 更新循环播放按钮状态
+ if (this.dom.playModeBtn) {
+ this.dom.playModeBtn.innerHTML = `
`;
+ lucide.createIcons();
+ }
+
+ this.showToast('登录成功且配置已保存');
+ this.toggleModal('profilePage', false);
+ this.fetchVideos(); // 只有全新登录或换账号才重新拉取视频
+ } else {
+ this.showToast('认证失败,请检查账号密码');
+ }
+ } catch (e) {
+ this.showToast('无法连接服务器');
+ } finally {
+ this.toggleLoading(false);
+ }
}
resetConfig() {
@@ -354,110 +521,372 @@
}
}
- loadFavorites() {
- const saved = localStorage.getItem('emby_favorites');
- if (saved) this.state.favorites = new Set(JSON.parse(saved));
+ async loadFavorites() {
+ const { server, token, userId } = this.config;
+ if (!server || !token || !userId) {
+ const saved = localStorage.getItem('emby_favorites');
+ if (saved) this.state.favorites = new Set(JSON.parse(saved));
+ return;
+ }
+ try {
+ const res = await fetch(`${server}/emby/Users/${userId}/Items?api_key=${token}&Recursive=true&Filters=IsFavorite&Fields=Id`);
+ if (res.ok) {
+ const data = await res.json();
+ if (data && data.Items) {
+ this.state.favorites = new Set(data.Items.map(i => String(i.Id)));
+ localStorage.setItem('emby_favorites', JSON.stringify([...this.state.favorites]));
+ this.updateUI();
+ }
+ }
+ } catch (e) {
+ console.error('加载远端收藏夹失败:', e);
+ const saved = localStorage.getItem('emby_favorites');
+ if (saved) this.state.favorites = new Set(JSON.parse(saved));
+ }
}
// --- 核心播放逻辑 ---
-
- /**
- * 获取视频列表
- * @param {string} parentId - 媒体库 ID (可选)
- * @param {string} ids - 指定视频 ID 列表 (用于收藏夹模式)
- */
- async fetchVideos(parentId = null, ids = null) {
+
+ async fetchVideos(parentId = null, ids = null, isLoadMore = false) {
this.toggleLoading(true);
try {
- const { server, apiKey } = this.config;
- let url = `${server}/emby/Items?api_key=${apiKey}&Recursive=true&IncludeItemTypes=Video&Limit=100&Fields=Overview,Path,RunTimeTicks`;
+ const { server, token, userId } = this.config;
+ if (!server || !token) {
+ this.showToast('请先配置服务器');
+ return;
+ }
+
+ if (!isLoadMore) {
+ this.state.startIndex = 0;
+ }
+
+ const libraryId = parentId || this.state.currentLibraryId;
+
+ let url = `${server}/emby/Users/${userId}/Items?api_key=${token}&Recursive=true&IncludeItemTypes=Video&Limit=100&StartIndex=${this.state.startIndex || 0}&Fields=Overview,Path,RunTimeTicks`;
if (ids) {
url += `&Ids=${ids}`;
+ } else if (libraryId === 'favorites') {
+ url += `&SortBy=DateCreated&SortOrder=Descending&Filters=IsFavorite`;
} else {
url += `&SortBy=DateCreated&SortOrder=Descending`;
- if (parentId) url += `&ParentId=${parentId}`;
+ if (libraryId) {
+ url += `&ParentId=${libraryId}`;
+ }
}
const res = await fetch(url);
- const data = await res.json();
+ if (!res.ok) {
+ throw new Error(`服务器错误: ${res.status}`);
+ }
+ let data = await res.json();
if (data && data.Items && data.Items.length > 0) {
+ if (this.state.playMode === 'random') {
+ data.Items = data.Items.sort(() => Math.random() - 0.5);
+ }
this.state.videos = data.Items;
this.state.currentIndex = 0;
- this.renderSlides();
- this.loadVideo(0);
+
+ if (this.state.viewMode === 'grid') {
+ this.renderGridView();
+ } else {
+ this.renderSlides();
+ this.loadVideo(0);
+ }
+ } else if (isLoadMore) {
+ this.showToast('已循环回到头部');
+ this.state.startIndex = 0;
+ this.fetchVideos(parentId, ids, false);
} else {
this.showToast('没有找到视频');
this.dom.videoContainer.innerHTML = '';
}
} catch (e) {
- console.error(e);
- this.showToast('无法连接服务器');
+ console.error('获取视频失败:', e);
+ this.showToast('无法连接服务器: ' + (e.message || '未知错误'));
} finally {
this.toggleLoading(false);
}
}
/**
- * 渲染视频卡片结构 (DOM)
+ * 核心修改点:渲染视频卡片结构 (三窗格复用 DOM)
*/
renderSlides() {
this.dom.videoContainer.innerHTML = '';
- this.state.videos.forEach((video, index) => {
+ this.dom.videoContainer.className = 'relative w-full h-full transition-transform duration-300 ease-out';
+ this.dom.videoContainer.style.transform = `translateY(-${this.state.currentIndex * 100}%)`;
+
+ // 只初始化 3 个物理槽位
+ for (let i = 0; i < 3; i++) {
const el = document.createElement('div');
el.className = 'slide-item';
- el.id = `slide-${index}`;
- // 初始位置:第一个在视野内,其他在下方
- el.style.transform = index === 0 ? 'translateY(0)' : 'translateY(100%)';
-
- const posterSrc = this.getImageSrc(video.Id);
-
- // 修复:在模板中显式添加 hidden 类,与 JS 的 remove('hidden') 配合
+ el.id = `slide-${i}`;
+ el.style.display = 'none';
el.innerHTML = `
-
-
-
`;
this.dom.videoContainer.appendChild(el);
+ }
+ this.dom.slides = [
+ document.getElementById('slide-0'),
+ document.getElementById('slide-1'),
+ document.getElementById('slide-2')
+ ];
+
+ lucide.createIcons();
+ this.updateUI();
+ }
+
+ renderGridView() {
+ this.dom.slides = null;
+ const container = this.dom.videoContainer;
+ container.innerHTML = '';
+ // 完全重置为绝对定位的、可滚动的原生网格层
+ container.className = 'absolute inset-0 z-10 bg-black/40 overflow-hidden flex flex-col';
+ container.style.transform = 'none';
+
+ // 背景海报模糊层
+ const currentVideo = this.state.videos[this.state.currentIndex];
+ const bgPosterStr = currentVideo ? this.getImageSrc(currentVideo) : '';
+ const bgLayer = document.createElement('div');
+ bgLayer.className = 'absolute inset-0 bg-cover bg-center opacity-70 scale-110 blur-xl saturate-150 z-[-1] pointer-events-none';
+ if (bgPosterStr) {
+ const imgL = new Image();
+ imgL.crossOrigin = 'anonymous';
+ imgL.referrerPolicy = 'no-referrer';
+ imgL.src = bgPosterStr;
+ imgL.onerror = () => {
+ bgLayer.style.backgroundImage = `url('${this.getImageSrc(null)}')`;
+ };
+ bgLayer.style.backgroundImage = `url('${bgPosterStr}')`;
+ }
+ container.appendChild(bgLayer);
+
+ const gridWrapper = document.createElement('div');
+ const safeHeader = document.createElement('div');
+
+ let libraryName = '全部视频';
+ if (this.state.currentLibraryId === 'favorites') {
+ libraryName = '收藏夹';
+ } else if (this.state.currentLibraryId) {
+ const libIdStr = String(this.state.currentLibraryId);
+ // 尝试从视图/播放列表数据缓存中寻找库名
+ const cachedViewsStr = localStorage.getItem('emby_views_cache');
+ if (cachedViewsStr) {
+ try {
+ const cached = JSON.parse(cachedViewsStr);
+ let found = null;
+ if (cached.views && cached.views.Items) {
+ found = cached.views.Items.find(i => String(i.Id) === libIdStr);
+ }
+ if (!found && cached.playlists && cached.playlists.Items) {
+ found = cached.playlists.Items.find(i => String(i.Id) === libIdStr);
+ }
+ if (found) libraryName = found.Name;
+ } catch (e) { }
+ }
+ }
+
+ let countStr = this.state.videos.length === 100 ? '99+' : this.state.videos.length;
+ safeHeader.className = 'w-full pt-[calc(env(safe-area-inset-top)+8px)] bg-black/40 backdrop-blur-md pb-3 px-3 border-b border-gray-800/50 flex items-center gap-2';
+ safeHeader.innerHTML = `
${libraryName}${countStr}${this.state.videos.length >= 100 ? `` : ''}
+ `;
+ container.appendChild(safeHeader);
+
+ if (this.state.videos.length >= 100) {
+ const refBtn = document.getElementById('gridRefreshBtn');
+ if (refBtn) {
+ refBtn.onclick = (e) => {
+ e.stopPropagation();
+ this.state.startIndex = (this.state.startIndex || 0) + 100;
+ this.fetchVideos(null, null, true);
+ };
+ }
+ }
+
+ // 创建专门的内部滚动容器
+ const scrollArea = document.createElement('div');
+ scrollArea.className = 'flex-1 min-h-0 overflow-y-auto overflow-x-hidden relative w-full pb-32 overscroll-contain';
+ scrollArea.style.WebkitOverflowScrolling = 'touch';
+ container.appendChild(scrollArea);
+
+ // MOUSE DRAG SUPPORT FOR GRID
+ let isDown = false;
+ let startY;
+ let scrollTop;
+ scrollArea.addEventListener('mousedown', (e) => {
+ isDown = true;
+ scrollArea.style.cursor = 'grabbing';
+ startY = e.pageY - scrollArea.offsetTop;
+ scrollTop = scrollArea.scrollTop;
});
+ scrollArea.addEventListener('mouseleave', () => {
+ isDown = false;
+ scrollArea.style.cursor = '';
+ });
+ scrollArea.addEventListener('mouseup', () => {
+ isDown = false;
+ scrollArea.style.cursor = '';
+ });
+ scrollArea.addEventListener('mousemove', (e) => {
+ if (!isDown) return;
+ e.preventDefault();
+ const y = e.pageY - scrollArea.offsetTop;
+ const walk = (y - startY) * 2;
+ scrollArea.scrollTop = scrollTop - walk;
+ });
+
+ gridWrapper.className = 'grid grid-cols-3 gap-1 content-start w-full relative z-10 px-1 pt-2';
+ scrollArea.appendChild(gridWrapper);
+
+ this.state.videos.forEach((video, index) => {
+ const el = document.createElement('div');
+ const isActive = index === this.state.currentIndex;
+ el.className = `aspect-[3/4] relative cursor-pointer overflow-hidden rounded-sm active:scale-95 transition-transform ${isActive ? 'border-2 border-primary box-border' : 'bg-gray-800'}`;
+ el.onclick = () => {
+ this.state.currentIndex = index;
+ this.toggleViewMode();
+ };
+
+ const img = document.createElement('img');
+ img.crossOrigin = 'anonymous';
+ img.referrerPolicy = 'no-referrer';
+ img.src = this.getImageSrc(video);
+ img.className = 'w-full h-full object-cover';
+ img.loading = 'lazy';
+ img.onerror = () => {
+ img.onerror = null;
+ img.src = this.getImageSrc(null);
+ };
+ el.appendChild(img);
+
+ if (video.RunTimeTicks) {
+ const duration = Math.floor(video.RunTimeTicks / 10000000);
+ const mins = Math.floor(duration / 60);
+ const secs = duration % 60;
+ const timeBadge = document.createElement('div');
+ timeBadge.className = 'absolute bottom-1 right-1 bg-black/70 text-white text-[10px] px-1 py-0.5 rounded';
+ timeBadge.textContent = `${mins}:${secs.toString().padStart(2, '0')}`;
+ el.appendChild(timeBadge);
+ }
+
+ gridWrapper.appendChild(el);
+ });
+
+ // 取消原来的清空逻辑,恢复底层信息显示以便能看到网格状态或当前焦点视频
+ const focusVideo = this.state.videos[this.state.currentIndex];
+ if (focusVideo) {
+ this.dom.videoTitle.textContent = focusVideo.Name || '未知视频';
+ this.dom.videoDescription.textContent = focusVideo.Overview || '没有简介...';
+ }
+
+ lucide.createIcons();
+ }
+
+ // 新增:缓冲渲染逻辑
+ renderBuffer() {
+ if (this.state.viewMode !== 'stream' || !this.dom.slides) return;
+
+ this.dom.videoContainer.style.transform = `translateY(-${this.state.currentIndex * 100}%)`;
+
+ const offsets = [-1, 0, 1];
+ offsets.forEach((offset) => {
+ const videoIdx = this.state.currentIndex + offset;
+ if (videoIdx < 0 || videoIdx >= this.state.videos.length) return;
+
+ const slideIdx = (videoIdx % 3 + 3) % 3;
+ const slideEl = this.dom.slides[slideIdx];
+ const videoData = this.state.videos[videoIdx];
+
+ slideEl.style.transform = `translateY(${videoIdx * 100}%)`;
+ slideEl.style.display = 'block';
+
+ if (slideEl.dataset.id !== videoData.Id) {
+ slideEl.dataset.id = videoData.Id;
+ const posterSrc = this.getImageSrc(videoData);
+
+ const posterEl = document.getElementById(`poster-${slideIdx}`);
+ posterEl.style.backgroundImage = `url('${posterSrc}')`;
+
+ const imgLoader = new Image();
+ imgLoader.crossOrigin = 'anonymous';
+ imgLoader.referrerPolicy = 'no-referrer';
+ imgLoader.src = posterSrc;
+ imgLoader.onerror = () => {
+ posterEl.style.backgroundImage = `url('${this.getImageSrc(null)}')`;
+ };
+
+ const videoEl = document.getElementById(`video-p${slideIdx}`);
+ videoEl.classList.add('hidden');
+ videoEl.onerror = null; // 防止清空src触发旧的onerror重试机制
+ videoEl.removeAttribute('src'); // 使用 removeAttribute 代替 src='' 更加健壮
+ videoEl.load();
+ document.getElementById(`playBtn-${slideIdx}`).classList.remove('paused');
+ }
+ });
+
+ // 隐藏范围外的卡片
+ for (let i = 0; i < 3; i++) {
+ const activeIndices = offsets.map(o => (this.state.currentIndex + o));
+ const currentSlideVideoIdx = activeIndices.find(idx => ((idx % 3 + 3) % 3) === i);
+ if (currentSlideVideoIdx === undefined || currentSlideVideoIdx < 0 || currentSlideVideoIdx >= this.state.videos.length) {
+ this.dom.slides[i].style.display = 'none';
+ this.dom.slides[i].dataset.id = '';
+ const videoEl = document.getElementById(`video-p${i}`);
+ if (videoEl) {
+ videoEl.onerror = null;
+ videoEl.removeAttribute('src');
+ videoEl.load();
+ }
+ }
+ }
this.updateUI();
}
/**
- * 加载并准备播放指定索引的视频
+ * 核心修改点:适应三窗格的加载逻辑
*/
loadVideo(index) {
- // 1. 清理旧视频 (暂停并隐藏)
- this.state.videos.forEach((_, i) => {
- if (i !== index) {
- const v = document.getElementById(`video-${i}`);
- if (v) {
- v.pause();
- v.classList.add('hidden');
- v.src = '';
- // 优化 4: 强制清理资源
- v.removeAttribute('src');
- v.load();
- }
- }
- });
+ if (this.state.viewMode !== 'stream') return;
- // 2. 加载新视频
- const item = this.state.videos[index];
- if (!item) return;
+ this.renderBuffer(); // 确保DOM就位
+
+ const videoData = this.state.videos[this.state.currentIndex];
+ const slideIdx = (this.state.currentIndex % 3 + 3) % 3;
+ const videoEl = document.getElementById(`video-p${slideIdx}`);
+
+ if (!videoEl || !videoData) return;
+
+ const src = this.getVideoSrc(videoData.Id);
+ if (videoEl.src !== src) {
+ videoEl.src = src;
+ videoEl.load();
+ }
+
+ // 同步全局静音状态
+ videoEl.muted = this.state.isMuted;
+ const muteBtn = this.dom.muteBtn;
+ muteBtn.innerHTML = `
`;
+ lucide.createIcons();
- const videoEl = document.getElementById(`video-${index}`);
- videoEl.src = this.getVideoSrc(item.Id);
-
- // 显式移除 hidden 类,使 display: none 失效
videoEl.classList.remove('hidden');
- // 3. 绑定时间更新事件 (进度条)
+ // 添加缓冲提示状态
+ videoEl.onwaiting = () => this.toggleLoading(true);
+ videoEl.onplaying = () => this.toggleLoading(false);
+ videoEl.oncanplay = () => this.toggleLoading(false);
+
videoEl.ontimeupdate = () => {
if (videoEl.duration) {
const pct = (videoEl.currentTime / videoEl.duration) * 100;
@@ -467,42 +896,98 @@
}
};
- this.updateUI();
+ videoEl.onended = () => {
+ if (this.state.viewMode === 'stream' && this.state.isAutoplay) {
+ this.nextVideo();
+ }
+ };
+
+ videoEl.onerror = () => {
+ console.warn('视频播放失败,处理降级重试逻辑');
+ this.toggleLoading(false);
+ this.retryWithAltMode(this.state.currentIndex, videoData);
+ };
+
+ videoEl.dataset.retryCount = '0';
}
playVideo(index) {
- const v = document.getElementById(`video-${index}`);
- const btn = document.getElementById(`playBtn-${index}`);
+ if (this.state.viewMode !== 'stream') return;
+ const slideIdx = (this.state.currentIndex % 3 + 3) % 3;
+ const v = document.getElementById(`video-p${slideIdx}`);
+ const btn = document.getElementById(`playBtn-${slideIdx}`);
if (v) {
- v.play().then(() => {
- this.state.isPlaying = true;
- btn.classList.remove('paused');
- }).catch(() => {
- this.state.isPlaying = false;
- btn.classList.add('paused');
- });
+ const playPromise = v.play();
+ if (playPromise !== undefined) {
+ playPromise.then(() => {
+ this.state.isPlaying = true;
+ if (btn) btn.classList.remove('paused');
+ }).catch(error => {
+ console.error('播放失败:', error);
+ this.state.isPlaying = false;
+ this.toggleLoading(false);
+ if (btn) btn.classList.add('paused');
+ if (error.name === 'NotAllowedError') {
+ this.showToast('需要用户交互才能播放');
+ }
+ });
+ }
}
}
+ retryWithAltMode(index, item) {
+ const slideIdx = (this.state.currentIndex % 3 + 3) % 3;
+ const v = document.getElementById(`video-p${slideIdx}`);
+ if (!v) return;
+
+ const retryCount = parseInt(v.dataset.retryCount || '0');
+
+ if (this.config.useStatic) {
+ // 如果强制开启了不转码,则直接失败报错
+ let formatInfo = "未知";
+ if (item.MediaSources && item.MediaSources.length > 0) {
+ try {
+ formatInfo = item.MediaSources[0].MediaStreams.find(s => s.Type === 'Video')?.Codec || "未知";
+ } catch (e) { }
+ }
+ this.showToast(`当前设备不支持此格式 (${formatInfo})`);
+ return;
+ }
+
+ // 如果取消了"不转码"(允许转码),这里我们进行1次重试,使用转码流
+ if (retryCount >= 1) {
+ this.showToast('转码播放仍失败,格式不支持');
+ return;
+ }
+
+ v.dataset.retryCount = String(retryCount + 1);
+ const { server, token } = this.config;
+ // 转码模式 Container=mp4
+ v.src = `${server}/emby/Videos/${item.Id}/stream?api_key=${token}&Container=mp4`;
+ v.load();
+ this.playVideo(index);
+ this.showToast('尝试转码播放中...');
+ }
+
togglePlay() {
- const idx = this.state.currentIndex;
- const v = document.getElementById(`video-${idx}`);
- const btn = document.getElementById(`playBtn-${idx}`);
+ const slideIdx = (this.state.currentIndex % 3 + 3) % 3;
+ const v = document.getElementById(`video-p${slideIdx}`);
+ const btn = document.getElementById(`playBtn-${slideIdx}`);
if (!v) return;
if (v.paused) {
v.play();
this.state.isPlaying = true;
- btn.classList.remove('paused');
+ if (btn) btn.classList.remove('paused');
} else {
v.pause();
this.state.isPlaying = false;
- btn.classList.add('paused');
+ if (btn) btn.classList.add('paused');
}
}
// --- 导航与手势 ---
-
+
nextVideo() {
let nextIndex;
if (this.state.playMode === 'random') {
@@ -513,7 +998,6 @@
if (nextIndex >= this.state.videos.length) {
this.showToast('已经是最后一个视频了');
- this.resetSlidePosition();
return;
}
this.switchSlide(nextIndex, 'up');
@@ -521,92 +1005,165 @@
prevVideo() {
if (this.state.currentIndex === 0) {
- this.resetSlidePosition();
return;
}
this.switchSlide(this.state.currentIndex - 1, 'down');
}
switchSlide(newIndex, direction) {
- const currentEl = document.getElementById(`slide-${this.state.currentIndex}`);
- const nextEl = document.getElementById(`slide-${newIndex}`);
-
- // 简单的上下切换动画
- if (direction === 'up') {
- currentEl.style.transform = 'translateY(-100%)';
- nextEl.style.transform = 'translateY(0)';
- } else {
- currentEl.style.transform = 'translateY(100%)';
- nextEl.style.transform = 'translateY(0)';
- }
+ const slideIdx = (this.state.currentIndex % 3 + 3) % 3;
+ const curV = document.getElementById(`video-p${slideIdx}`);
+ if (curV) curV.pause(); // 切换前暂停当前播放
this.state.currentIndex = newIndex;
this.loadVideo(newIndex);
this.playVideo(newIndex);
}
- resetSlidePosition() {
- // 回弹效果
- const el = document.getElementById(`slide-${this.state.currentIndex}`);
- el.style.transform = 'translateY(0)';
- }
-
// --- 辅助功能 ---
getVideoSrc(id) {
- const { server, apiKey, useStatic } = this.config;
- return useStatic
- ? `${server}/emby/Videos/${id}/stream?api_key=${apiKey}&Static=true`
- : `${server}/emby/Videos/${id}/stream?api_key=${apiKey}&Container=mp4`;
+ const { server, token, useStatic } = this.config;
+ // 不管配置如何,第一次请求永远优先尝试 Static 原画直连
+ return `${server}/emby/Videos/${id}/stream?api_key=${token}&Static=true`;
}
- getImageSrc(id) {
- const { server, apiKey } = this.config;
- // 优化 5: 减小图片体积 MaxHeight=400
- return `${server}/emby/Items/${id}/Images/Primary?api_key=${apiKey}&MaxHeight=400&Quality=90`;
+ getImageSrc(video) {
+ const { server, token } = this.config;
+ const fallbackUrl = 'https://img.5nav.eu.org/file/AgACAgUAAxkDAAMCaaGfIOLBPKq4KFJNqL2_suU1LDQAAqgOaxv_dBFV0MGoMD85wu0BAAMCAAN5AAM6BA.jpg';
+
+ if (typeof video === 'object' && video !== null) {
+ if (video.ImageTags && video.ImageTags.Primary) {
+ return `${server}/emby/Items/${video.Id}/Images/Primary?api_key=${token}`;
+ }
+ return fallbackUrl;
+ }
+
+ // 此时传入的是纯 ID 字符串(旧逻辑残留处理)
+ if (!video) return fallbackUrl;
+ return `${server}/emby/Items/${video}/Images/Primary?api_key=${token}`;
}
async showLibraries() {
this.toggleLoading(true);
- try {
- const res = await fetch(`${this.config.server}/emby/Library/VirtualFolders?api_key=${this.config.apiKey}`);
- const data = await res.json();
- const list = document.getElementById('librariesList');
- list.innerHTML = '';
+ const list = document.getElementById('librariesList');
+ list.innerHTML = '';
- data.forEach(lib => {
- const div = document.createElement('div');
- const isSelected = lib.Id === this.state.currentLibraryId;
-
- div.className = `p-3 rounded mb-2 cursor-pointer text-sm flex justify-between items-center transition-colors ${
- isSelected ? 'bg-gray-700 text-primary font-bold border border-primary/30' : 'bg-gray-800 text-white hover:bg-gray-700'
- }`;
-
- div.innerHTML = `
${lib.Name}${isSelected ? '
' : ''}`;
-
- div.onclick = () => {
- this.state.currentLibraryId = lib.Id;
- this.state.currentTab = 'home';
- this.updateTabHighlight();
- this.fetchVideos(lib.Id);
- this.toggleModal('libraryModal', false);
- };
- list.appendChild(div);
- });
+ try {
+ const userId = this.config.userId;
+
+ const playlistsRes = await fetch(`${this.config.server}/emby/Users/${userId}/Items?Recursive=true&IncludeItemTypes=Playlist&api_key=${this.config.token}`);
+ const playlistsData = await playlistsRes.json();
+
+ const viewsRes = await fetch(`${this.config.server}/emby/Users/${userId}/Views?api_key=${this.config.token}`);
+ const viewsDataRaw = viewsRes.ok ? await viewsRes.json() : { Items: [] };
+
+ // 缓存完整的视图和列表数据供网格模式提取名称
+ localStorage.setItem('emby_views_cache', JSON.stringify({ playlists: playlistsData, views: viewsDataRaw }));
+
+ // 过滤掉基于特定收集方式的视图避免重叠
+ const filteredViews = viewsDataRaw.Items ? viewsDataRaw.Items.filter(item => item.CollectionType !== 'playlists' && item.CollectionType !== 'boxsets') : [];
+
+ const favDiv = document.createElement('div');
+ const isFavSelected = this.state.currentLibraryId === 'favorites';
+ favDiv.className = `col-span-1 p-2.5 rounded cursor-pointer text-sm flex items-center gap-2 ${isFavSelected ? 'bg-primary/20 text-primary font-bold border border-primary/50' : 'bg-white/10 text-gray-300 hover:bg-white/20'}`;
+ favDiv.innerHTML = `
收藏夹${isFavSelected ? '
' : ''}`;
+ favDiv.onclick = () => {
+ this.state.currentLibraryId = 'favorites';
+ this.state.currentTab = 'favorites';
+ if (this.state.favorites.size === 0) {
+ this.showToast('暂无收藏');
+ return;
+ }
+ this.state.currentLibraryType = 'favorites';
+ this.fetchVideos();
+ this.toggleModal('libraryModal', false);
+ };
+ list.appendChild(favDiv);
+
+ if (playlistsData && playlistsData.Items && playlistsData.Items.length > 0) {
+ const headerPl = document.createElement('div');
+ headerPl.className = 'col-span-2 text-xs text-gray-500 mt-2 mb-1 pl-1';
+ headerPl.textContent = '播放列表';
+ list.appendChild(headerPl);
+
+ playlistsData.Items.forEach(pl => {
+ const div = document.createElement('div');
+ const isSelected = pl.Id === this.state.currentLibraryId;
+ div.className = `col-span-1 p-2.5 rounded cursor-pointer text-sm flex items-center gap-2 ${isSelected ? 'bg-primary/20 text-primary font-bold border border-primary/50' : 'bg-white/10 text-gray-300 hover:bg-white/20'}`;
+ div.innerHTML = `
${pl.Name}${isSelected ? '
' : ''}`;
+ div.onclick = () => {
+ this.state.currentLibraryId = pl.Id;
+ this.state.currentLibraryType = 'playlist';
+ this.state.currentTab = 'home';
+ this.updateTabHighlight();
+ this.fetchVideos(pl.Id);
+ this.toggleModal('libraryModal', false);
+ };
+ list.appendChild(div);
+ });
+ }
+
+ if (filteredViews && filteredViews.length > 0) {
+ const headerLib = document.createElement('div');
+ headerLib.className = 'col-span-2 text-xs text-gray-500 mt-2 mb-1 pl-1';
+ headerLib.textContent = '媒体库';
+ list.appendChild(headerLib);
+
+ filteredViews.forEach(lib => {
+ const div = document.createElement('div');
+ const isSelected = lib.Id === this.state.currentLibraryId;
+ div.className = `col-span-1 p-2.5 rounded cursor-pointer text-sm flex items-center gap-2 ${isSelected ? 'bg-primary/20 text-primary font-bold border border-primary/50' : 'bg-white/10 text-gray-300 hover:bg-white/20'}`;
+ div.innerHTML = `
${lib.Name}${isSelected ? '
' : ''}`;
+ div.onclick = () => {
+ this.state.currentLibraryId = lib.Id;
+ this.state.currentLibraryType = 'library';
+ this.state.currentTab = 'home';
+ this.updateTabHighlight();
+ this.fetchVideos(lib.Id);
+ this.toggleModal('libraryModal', false);
+ };
+ list.appendChild(div);
+ });
+ }
+
+ lucide.createIcons();
this.toggleModal('libraryModal', true);
- } catch(e) {
+ } catch (e) {
+ console.error('获取媒体库失败:', e);
this.showToast('获取媒体库失败');
+
+ const favDiv = document.createElement('div');
+ favDiv.className = 'p-3 rounded mb-2 cursor-pointer text-sm flex items-center gap-3 bg-gray-800 text-white';
+ favDiv.innerHTML = `
收藏夹`;
+ favDiv.onclick = () => {
+ if (this.state.favorites.size === 0) {
+ this.showToast('暂无收藏');
+ return;
+ }
+ this.state.currentLibraryId = 'favorites';
+ this.state.currentLibraryType = 'favorites';
+ this.fetchVideos();
+ this.toggleModal('libraryModal', false);
+ };
+ list.appendChild(favDiv);
+
+ lucide.createIcons();
+ this.toggleModal('libraryModal', true);
} finally {
this.toggleLoading(false);
}
}
- toggleFavorite() {
+ async toggleFavorite() {
const item = this.state.videos[this.state.currentIndex];
if (!item) return;
const id = String(item.Id);
+ const isFav = this.state.favorites.has(id);
+ const { server, token, userId } = this.config;
- if (this.state.favorites.has(id)) {
+ // 乐观更新
+ if (isFav) {
this.state.favorites.delete(id);
this.showToast('已取消收藏');
} else {
@@ -615,6 +1172,16 @@
}
localStorage.setItem('emby_favorites', JSON.stringify([...this.state.favorites]));
this.updateUI();
+
+ // 远端同步
+ if (server && token && userId) {
+ try {
+ const method = isFav ? 'DELETE' : 'POST';
+ await fetch(`${server}/emby/Users/${userId}/FavoriteItems/${id}?api_key=${token}`, { method });
+ } catch (e) {
+ console.error('收藏同步失败:', e);
+ }
+ }
}
// --- UI 更新与交互 ---
@@ -622,182 +1189,385 @@
updateUI() {
const item = this.state.videos[this.state.currentIndex];
if (item) {
- // 1. 更新文本信息
this.dom.videoTitle.textContent = item.Name;
this.dom.videoDescription.textContent = item.Overview || '暂无简介';
-
- // 2. 更新收藏图标
- const icon = this.dom.favoriteBtn.querySelector('i');
- if (this.state.favorites.has(String(item.Id))) {
- icon.className = 'fa fa-heart text-primary text-4xl drop-shadow-md';
- } else {
- icon.className = 'fa fa-heart text-white text-4xl drop-shadow-md';
- }
+
+ const favBtn = document.getElementById('favoriteBtn');
+ const isFav = this.state.favorites.has(String(item.Id));
+ favBtn.innerHTML = `
`;
+ lucide.createIcons();
}
}
updateTabHighlight() {
- // 重置底部导航颜色
- this.dom.favoritesBtn.className = 'flex flex-col items-center justify-center w-1/4 text-gray-400 cursor-pointer';
this.dom.myBtn.className = 'flex flex-col items-center justify-center w-1/4 text-gray-400 cursor-pointer';
-
- // 高亮当前
- if (this.state.currentTab === 'favorites') {
- this.dom.favoritesBtn.className = 'flex flex-col items-center justify-center w-1/4 text-white cursor-pointer';
- } else if (this.state.currentTab === 'my') {
+ this.dom.libraryBtn.className = 'flex flex-col items-center justify-center w-1/4 text-gray-400 cursor-pointer';
+ this.dom.playModeBtn.className = 'flex flex-col items-center justify-center w-1/4 text-gray-400 cursor-pointer';
+ this.dom.viewModeBtn.className = 'flex flex-col items-center justify-center w-1/4 text-gray-400 cursor-pointer';
+
+ if (this.state.currentTab === 'my') {
this.dom.myBtn.className = 'flex flex-col items-center justify-center w-1/4 text-white cursor-pointer';
+ } else if (this.state.currentTab === 'library') {
+ this.dom.libraryBtn.className = 'flex flex-col items-center justify-center w-1/4 text-white cursor-pointer';
}
}
- // 修复:清屏模式自动隐藏逻辑
- toggleClearScreen() {
- this.state.isClearScreen = !this.state.isClearScreen;
+ toggleAutoplay() {
const app = document.getElementById('app');
- const icon = this.dom.clearScreenBtn.querySelector('i');
- const text = this.dom.clearScreenBtn.querySelector('span');
-
- if (this.state.isClearScreen) {
- app.classList.add('interface-hidden');
- icon.className = 'fa fa-eye text-white text-4xl drop-shadow-md';
- text.textContent = '退出';
- this.showToast('已清屏 (5秒后自动隐藏)');
-
- // 进入清屏模式,启动计时
- this.resetClearScreenTimer();
- } else {
+ if (app.classList.contains('interface-hidden')) {
app.classList.remove('interface-hidden');
- icon.className = 'fa fa-eye-slash text-white text-4xl drop-shadow-md';
- text.textContent = '清屏';
-
- // 退出清屏模式,清除计时器,确保按钮可见
- if (this.csTimer) clearTimeout(this.csTimer);
- this.dom.clearScreenBtn.classList.remove('opacity-0', 'pointer-events-none');
+ } else {
+ app.classList.add('interface-hidden');
}
}
-
- resetClearScreenTimer() {
- // 仅在清屏模式下生效
- if (!this.state.isClearScreen) return;
- // 1. 显示按钮
- this.dom.clearScreenBtn.classList.remove('opacity-0', 'pointer-events-none');
-
- // 2. 清除旧计时器
+ toggleDeleteMode() {
+ const item = this.state.videos[this.state.currentIndex];
+ if (!item) return;
+
+ const firstConfirm = confirm('确定要删除此视频吗?');
+ if (!firstConfirm) return;
+
+ const finalConfirm = confirm('⚠️ 警告:这将删除媒体库中的原文件!\n\n确认删除 ' + (item.Name || '此视频') + ' 吗?');
+ if (!finalConfirm) return;
+
+ fetch(`${this.config.server}/emby/Items/${item.Id}?api_key=${this.config.token}`, {
+ method: 'DELETE'
+ }).then(() => {
+ this.showToast('已删除');
+ this.state.videos.splice(this.state.currentIndex, 1);
+ if (this.state.viewMode === 'grid') {
+ this.renderGridView();
+ } else {
+ this.renderSlides();
+ }
+ }).catch(() => {
+ this.showToast('删除失败');
+ });
+ }
+
+ showInterfaceTemp() {
+ const app = document.getElementById('app');
if (this.csTimer) clearTimeout(this.csTimer);
-
- // 3. 启动新计时器 (5秒后隐藏)
- this.csTimer = setTimeout(() => {
- this.dom.clearScreenBtn.classList.add('opacity-0', 'pointer-events-none');
- }, 5000);
+ if (document.fullscreenElement) {
+ this.csTimer = setTimeout(() => {
+ app.classList.add('interface-hidden');
+ }, 3000);
+ }
}
togglePlayMode() {
const isSeq = this.state.playMode === 'sequence';
this.state.playMode = isSeq ? 'random' : 'sequence';
-
+
const btn = this.dom.playModeBtn;
- btn.querySelector('span').textContent = isSeq ? '随机播放' : '顺序播放';
- btn.querySelector('i').className = isSeq ? 'fa fa-random text-lg' : 'fa fa-repeat text-lg';
-
+ btn.innerHTML = `
`;
+
+ lucide.createIcons();
this.showToast(isSeq ? '已切换随机播放' : '已切换顺序播放');
}
+ toggleViewMode() {
+ if (this.state.currentLibraryType === 'favorites' || this.state.currentLibraryType === 'playlist') {
+ this.showToast('当前媒体源不支持切换视图');
+ return;
+ }
+
+ const isStream = this.state.viewMode === 'stream';
+ this.state.viewMode = isStream ? 'grid' : 'stream';
+
+ const btn = this.dom.viewModeBtn;
+ btn.innerHTML = `
`;
+
+ lucide.createIcons();
+
+ if (this.state.viewMode === 'grid') {
+ const app = document.getElementById('app');
+ if (app) app.classList.remove('interface-hidden');
+ this.renderGridView();
+ } else {
+ this.renderSlides();
+ this.loadVideo(this.state.currentIndex);
+ this.playVideo(this.state.currentIndex);
+ }
+ }
+
+ toggleScaleMode() {
+ this.state.isScaleFill = !this.state.isScaleFill;
+ const videos = document.querySelectorAll('.video-fullscreen');
+ videos.forEach((v, index) => {
+ v.classList.remove('object-cover', 'object-contain');
+ v.classList.add(this.state.isScaleFill ? 'object-cover' : 'object-contain');
+
+ const poster = document.getElementById(`poster-${index}`);
+ const overlay = v.parentElement.querySelector('.bg-black\\/40');
+ if (this.state.isScaleFill) {
+ if (poster) poster.classList.add('hidden');
+ if (overlay) overlay.classList.add('hidden');
+ } else {
+ if (poster) poster.classList.remove('hidden');
+ if (overlay) overlay.classList.remove('hidden');
+ }
+ });
+
+ const scaleBtn = this.dom.scaleBtn;
+ scaleBtn.innerHTML = `
`;
+ lucide.createIcons();
+
+ }
+
+ async showMediaInfo() {
+ const item = this.state.videos[this.state.currentIndex];
+ if (!item) return;
+
+ this.dom.mediaInfoModal.classList.remove('hidden');
+ this.dom.mediaInfoContent.innerHTML = `
`;
+ lucide.createIcons();
+
+ try {
+ const res = await fetch(`${this.config.server}/emby/Users/${this.config.userId}/Items/${item.Id}?api_key=${this.config.token}&Fields=MediaSources`);
+ if (!res.ok) throw new Error('API Error');
+ const data = await res.json();
+
+ if (data && data.MediaSources && data.MediaSources.length > 0) {
+ const src = data.MediaSources[0];
+ const c = src.Container || '未知';
+ const w = src.MediaStreams?.find(s => s.Type === 'Video')?.Width || '?';
+ const h = src.MediaStreams?.find(s => s.Type === 'Video')?.Height || '?';
+ const vCodec = src.MediaStreams?.find(s => s.Type === 'Video')?.Codec || '未知';
+ const aCodec = src.MediaStreams?.find(s => s.Type === 'Audio')?.Codec || '未知';
+ const bitrate = src.Bitrate ? (src.Bitrate / 1000000).toFixed(2) + ' Mbps' : '未知';
+ const transcodeState = this.config.useStatic ? '直接播放 (强制)' : '自动转码/直推';
+
+ this.dom.mediaInfoContent.innerHTML = `
+
+ 分辨率
+ ${w} × ${h}
+
+
+ 封装格式
+ ${c.toUpperCase()}
+
+
+ 视频编码
+ ${vCodec.toUpperCase()}
+
+
+ 音频编码
+ ${aCodec.toUpperCase()}
+
+
+ 总码率
+ ${bitrate}
+
+
+ 播放策略
+ ${transcodeState}
+
+ `;
+ } else {
+ this.dom.mediaInfoContent.innerHTML = `
无法获取媒体信息
`;
+ }
+ } catch (e) {
+ this.dom.mediaInfoContent.innerHTML = `
获取数据失败
`;
+ }
+ }
+
// --- 事件绑定 ---
bindEvents() {
- // 1. 播放控制
this.dom.clickToPlayOverlay.onclick = () => {
this.dom.clickToPlayOverlay.style.display = 'none';
this.playVideo(this.state.currentIndex);
};
-
- // 2. 右侧工具栏
- this.dom.favoriteBtn.onclick = (e) => { e.stopPropagation(); this.toggleFavorite(); };
- this.dom.clearScreenBtn.onclick = (e) => {
- e.stopPropagation();
- this.toggleClearScreen();
- };
- this.dom.muteBtn.onclick = (e) => {
+
+ this.dom.favoriteBtn.onclick = (e) => {
e.stopPropagation();
- const v = document.getElementById(`video-${this.state.currentIndex}`);
- if (v) {
- v.muted = !v.muted;
- this.dom.muteBtn.querySelector('i').className = v.muted ? 'fa fa-volume-off text-white text-4xl drop-shadow-md' : 'fa fa-volume-up text-white text-4xl drop-shadow-md';
- this.showToast(v.muted ? '已静音' : '已开启声音');
+ this.toggleFavorite();
+ const favBtn = document.getElementById('favoriteBtn');
+ const isFav = this.state.favorites.has(String(this.state.videos[this.state.currentIndex]?.Id));
+ favBtn.innerHTML = `
`;
+ lucide.createIcons();
+ };
+
+ this.dom.scaleBtn.onclick = (e) => {
+ e.stopPropagation();
+ this.toggleScaleMode();
+ };
+
+ this.dom.videoInfoArea.onclick = (e) => {
+ e.stopPropagation();
+ this.showMediaInfo();
+ };
+
+ this.dom.closeMediaInfoBtn.onclick = (e) => {
+ e.stopPropagation();
+ this.dom.mediaInfoModal.classList.add('hidden');
+ };
+
+ this.dom.mediaInfoModal.onclick = (e) => {
+ if (e.target === e.currentTarget) {
+ this.dom.mediaInfoModal.classList.add('hidden');
}
};
-
- // 修复:全屏按钮图标逻辑
- this.dom.fullscreenBtn.onclick = (e) => {
- e.stopPropagation();
- if (!document.fullscreenElement) {
- document.documentElement.requestFullscreen().catch(()=>{});
- } else {
- document.exitFullscreen();
+
+ this.dom.rightToolbar.addEventListener('click', (e) => {
+ const app = document.getElementById('app');
+ if (app.classList.contains('interface-hidden')) {
+ app.classList.remove('interface-hidden');
+ this.showInterfaceTemp();
}
- };
- // 监听全屏变化事件
- document.addEventListener('fullscreenchange', () => {
- const icon = this.dom.fullscreenBtn.querySelector('i');
- if(document.fullscreenElement) {
- icon.className = 'fa fa-compress text-white text-4xl drop-shadow-md';
- } else {
- icon.className = 'fa fa-expand text-white text-4xl drop-shadow-md';
- }
});
- // 3. 底部导航
+ this.dom.muteBtn.onclick = (e) => {
+ e.stopPropagation();
+ this.state.isMuted = !this.state.isMuted;
+
+ const slideIdx = (this.state.currentIndex % 3 + 3) % 3;
+ const v = document.getElementById(`video-p${slideIdx}`);
+ if (v) {
+ v.muted = this.state.isMuted;
+ }
+
+ const muteBtn = this.dom.muteBtn;
+ muteBtn.innerHTML = `
`;
+ lucide.createIcons();
+ };
+
+ this.dom.fullscreenBtn.onclick = (e) => {
+ e.stopPropagation();
+ const app = document.getElementById('app');
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen().then(() => {
+ this.showInterfaceTemp();
+ }).catch(() => { });
+ } else {
+ if (app.classList.contains('interface-hidden')) {
+ app.classList.remove('interface-hidden');
+ this.showInterfaceTemp();
+ } else {
+ document.exitFullscreen();
+ app.classList.remove('interface-hidden');
+ }
+ }
+ };
+ document.addEventListener('fullscreenchange', () => {
+ const isFull = !!document.fullscreenElement;
+ const fullscreenBtn = this.dom.fullscreenBtn;
+ fullscreenBtn.innerHTML = `
`;
+ lucide.createIcons();
+
+ if (!isFull) {
+ const app = document.getElementById('app');
+ app.classList.remove('interface-hidden');
+ if (this.csTimer) clearTimeout(this.csTimer);
+ }
+ });
+
+ this.dom.deleteBtn.onclick = (e) => {
+ e.stopPropagation();
+ this.toggleDeleteMode();
+ };
+
+ this.dom.viewModeBtn = document.getElementById('viewModeBtn');
+ this.dom.viewModeBtn.onclick = () => this.toggleViewMode();
this.dom.playModeBtn.onclick = () => this.togglePlayMode();
this.dom.libraryBtn.onclick = () => this.showLibraries();
- this.dom.favoritesBtn.onclick = () => {
- if (this.state.favorites.size === 0) return this.showToast('暂无收藏');
- this.state.currentTab = 'favorites';
- this.state.currentLibraryId = null;
- this.updateTabHighlight();
- this.fetchVideos(null, [...this.state.favorites].join(','));
- };
this.dom.myBtn.onclick = () => this.toggleModal('profilePage', true);
- // 4. 弹窗操作
document.getElementById('closeLibraryBtn').onclick = () => this.toggleModal('libraryModal', false);
+
+ const libraryModal = document.getElementById('libraryModal');
+ libraryModal.onclick = (e) => {
+ if (e.target === e.currentTarget) {
+ this.toggleModal('libraryModal', false);
+ }
+ };
+
document.getElementById('closeProfileBtn').onclick = () => this.toggleModal('profilePage', false);
document.getElementById('saveConfigBtn').onclick = () => this.saveConfig();
document.getElementById('resetConfigBtn').onclick = () => this.resetConfig();
- // 5. 进度条点击
document.getElementById('progressBarContainer').onclick = (e) => {
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
- const v = document.getElementById(`video-${this.state.currentIndex}`);
+ const slideIdx = (this.state.currentIndex % 3 + 3) % 3;
+ const v = document.getElementById(`video-p${slideIdx}`);
if (v && v.duration) v.currentTime = ((e.clientX - rect.left) / rect.width) * v.duration;
};
- // 6. 触摸手势逻辑
const container = this.dom.videoContainer;
container.addEventListener('touchstart', (e) => {
+ if (this.state.viewMode !== 'stream') return;
this.touch.startY = e.touches[0].clientY;
this.touch.startTime = Date.now();
}, { passive: true });
container.addEventListener('touchend', (e) => {
+ if (this.state.viewMode !== 'stream') return;
const deltaY = e.changedTouches[0].clientY - this.touch.startY;
const time = Date.now() - this.touch.startTime;
if (Math.abs(deltaY) < 10 && time < 200) {
+ const app = document.getElementById('app');
+ if (app.classList.contains('interface-hidden')) {
+ app.classList.remove('interface-hidden');
+ this.showInterfaceTemp();
+ return;
+ }
if (e.target.closest('.slide-item')) {
this.togglePlay();
- // 如果在清屏模式,点击屏幕唤醒按钮
- if(this.state.isClearScreen) this.resetClearScreenTimer();
}
} else if (Math.abs(deltaY) > 50) {
deltaY < 0 ? this.nextVideo() : this.prevVideo();
}
});
-
- // PC 点击兼容
+
+ // PC 鼠标滑动支持
+ let isDragging = false;
+ container.addEventListener('mousedown', (e) => {
+ if (this.state.viewMode !== 'stream') return;
+ isDragging = true;
+ this.touch.startY = e.clientY;
+ this.touch.startTime = Date.now();
+ });
+ container.addEventListener('mousemove', (e) => {
+ if (this.state.viewMode !== 'stream') return;
+ if (isDragging) e.preventDefault();
+ });
+ container.addEventListener('mouseleave', () => {
+ isDragging = false;
+ });
+ container.addEventListener('mouseup', (e) => {
+ if (this.state.viewMode !== 'stream') return;
+ if (!isDragging) return;
+ isDragging = false;
+ const deltaY = e.clientY - this.touch.startY;
+ const time = Date.now() - this.touch.startTime;
+
+ if (Math.abs(deltaY) < 10 && time < 200) {
+ const app = document.getElementById('app');
+ if (app.classList.contains('interface-hidden')) {
+ app.classList.remove('interface-hidden');
+ this.showInterfaceTemp();
+ return;
+ }
+ if (e.target.closest('.slide-item')) {
+ this.togglePlay();
+ }
+ } else if (Math.abs(deltaY) > 50) {
+ deltaY < 0 ? this.nextVideo() : this.prevVideo();
+ }
+ });
+
container.addEventListener('click', (e) => {
- if (!('ontouchstart' in window) && e.target.closest('.slide-item')) {
- this.togglePlay();
- if(this.state.isClearScreen) this.resetClearScreenTimer();
+ if (this.state.viewMode !== 'stream') return;
+ if (!('ontouchstart' in window)) {
+ const app = document.getElementById('app');
+ if (app.classList.contains('interface-hidden')) {
+ app.classList.remove('interface-hidden');
+ this.showInterfaceTemp();
+ return;
+ }
}
});
}
@@ -807,10 +1577,9 @@
const el = document.getElementById(id);
if (id === 'profilePage') {
if (show) { el.classList.add('active'); this.state.currentTab = 'my'; }
- else { el.classList.remove('active'); } // 关闭时 tab 状态不立即恢复,由用户操作决定
- if(show) this.updateTabHighlight();
+ else { el.classList.remove('active'); }
+ if (show) this.updateTabHighlight();
} else {
- // 普通弹窗 (libraryModal)
if (show) el.classList.remove('hidden'); else el.classList.add('hidden');
}
}
@@ -835,8 +1604,11 @@
}
}
- // 启动应用
- window.onload = () => { window.app = new EmbyApp(); };
+ window.onload = () => {
+ lucide.createIcons();
+ window.app = new EmbyApp();
+ };
-
+
+