From d3b267380fa4967c3f997fd152101339eec52659 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Thu, 16 Apr 2026 23:28:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=BC=AB=E7=94=BB=E7=95=8C?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 2 + .vscode/css.code-snippets | 10 - .vscode/extensions.json | 9 - .vscode/launch.json | 17 - .vscode/settings.json | 17 - .vscode/typescriptreact.code-snippets | 193 --- CHANGELOG | 1836 ++++++++++++------------- src/app/manga/detail/page.tsx | 41 +- src/app/manga/history/page.tsx | 26 +- src/app/manga/page.tsx | 24 +- src/app/manga/read/page.tsx | 522 ++++++- src/app/manga/shelf/page.tsx | 29 +- src/components/manga/MangaLayout.tsx | 142 +- start.sh | 0 14 files changed, 1593 insertions(+), 1275 deletions(-) create mode 100644 .gitattributes delete mode 100644 .vscode/css.code-snippets delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json delete mode 100644 .vscode/typescriptreact.code-snippets mode change 100755 => 100644 start.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..58e8a44 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# 强制所有文本文件在仓库中用 LF +* text=auto eol=lf \ No newline at end of file diff --git a/.vscode/css.code-snippets b/.vscode/css.code-snippets deleted file mode 100644 index 82cc815..0000000 --- a/.vscode/css.code-snippets +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Region CSS": { - "prefix": "regc", - "body": [ - "/* #region /**=========== ${1} =========== */", - "$0", - "/* #endregion /**======== ${1} =========== */" - ] - } -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 44bf298..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "recommendations": [ - // Tailwind CSS Intellisense - "bradlc.vscode-tailwindcss", - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint", - "aaron-bond.better-comments" - ] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index fef730b..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "skipFiles": [ - "/**" - ], - "program": "${workspaceFolder}/start" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a775463..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "css.validate": false, - "editor.formatOnSave": true, - "editor.tabSize": 2, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit" - }, - "[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - // Tailwind CSS Autocomplete, add more if used in projects - "tailwindCSS.classAttributes": [ - "class", - "className", - "classNames", - "containerClassName" - ], - "typescript.preferences.importModuleSpecifier": "non-relative" -} diff --git a/.vscode/typescriptreact.code-snippets b/.vscode/typescriptreact.code-snippets deleted file mode 100644 index 1fce740..0000000 --- a/.vscode/typescriptreact.code-snippets +++ /dev/null @@ -1,193 +0,0 @@ -{ - //#region //*=========== React =========== - "import React": { - "prefix": "ir", - "body": ["import * as React from 'react';"] - }, - "React.useState": { - "prefix": "us", - "body": [ - "const [${1}, set${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}] = React.useState<$3>(${2:initial${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}})$0" - ] - }, - "React.useEffect": { - "prefix": "uf", - "body": ["React.useEffect(() => {", " $0", "}, []);"] - }, - "React.useReducer": { - "prefix": "ur", - "body": [ - "const [state, dispatch] = React.useReducer(${0:someReducer}, {", - " ", - "})" - ] - }, - "React.useRef": { - "prefix": "urf", - "body": ["const ${1:someRef} = React.useRef($0)"] - }, - "React Functional Component": { - "prefix": "rc", - "body": [ - "import * as React from 'react';\n", - "export default function ${1:${TM_FILENAME_BASE}}() {", - " return (", - "
", - " $0", - "
", - " )", - "}" - ] - }, - "React Functional Component with Props": { - "prefix": "rcp", - "body": [ - "import * as React from 'react';\n", - "import clsxm from '@/lib/clsxm';\n", - "type ${1:${TM_FILENAME_BASE}}Props= {\n", - "} & React.ComponentPropsWithoutRef<'div'>\n", - "export default function ${1:${TM_FILENAME_BASE}}({className, ...rest}: ${1:${TM_FILENAME_BASE}}Props) {", - " return (", - "
", - " $0", - "
", - " )", - "}" - ] - }, - //#endregion //*======== React =========== - - //#region //*=========== Commons =========== - "Region": { - "prefix": "reg", - "scope": "javascript, typescript, javascriptreact, typescriptreact", - "body": [ - "//#region //*=========== ${1} ===========", - "${TM_SELECTED_TEXT}$0", - "//#endregion //*======== ${1} ===========" - ] - }, - "Region CSS": { - "prefix": "regc", - "scope": "css, scss", - "body": [ - "/* #region /**=========== ${1} =========== */", - "${TM_SELECTED_TEXT}$0", - "/* #endregion /**======== ${1} =========== */" - ] - }, - //#endregion //*======== Commons =========== - - //#region //*=========== Next.js =========== - "Next Pages": { - "prefix": "np", - "body": [ - "import * as React from 'react';\n", - "import Layout from '@/components/layout/Layout';", - "import Seo from '@/components/Seo';\n", - "export default function ${1:${TM_FILENAME_BASE/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}}Page() {", - " return (", - " ", - " \n", - "
\n", - "
", - "
", - " $0", - "
", - "
", - "
", - "
", - " )", - "}" - ] - }, - "Next API": { - "prefix": "napi", - "body": [ - "import { NextApiRequest, NextApiResponse } from 'next';\n", - "export default async function handler(req: NextApiRequest, res: NextApiResponse) {", - " if (req.method === 'GET') {", - " res.status(200).json({ name: 'Bambang' });", - " } else {", - " res.status(405).json({ message: 'Method Not Allowed' });", - " }", - "}" - ] - }, - "Get Static Props": { - "prefix": "gsp", - "body": [ - "export const getStaticProps = async (context: GetStaticPropsContext) => {", - " return {", - " props: {}", - " };", - "}" - ] - }, - "Get Static Paths": { - "prefix": "gspa", - "body": [ - "export const getStaticPaths: GetStaticPaths = async () => {", - " return {", - " paths: [", - " { params: { $1 }}", - " ],", - " fallback: ", - " };", - "}" - ] - }, - "Get Server Side Props": { - "prefix": "gssp", - "body": [ - "export const getServerSideProps = async (context: GetServerSidePropsContext) => {", - " return {", - " props: {}", - " };", - "}" - ] - }, - "Infer Get Static Props": { - "prefix": "igsp", - "body": "InferGetStaticPropsType" - }, - "Infer Get Server Side Props": { - "prefix": "igssp", - "body": "InferGetServerSidePropsType" - }, - "Import useRouter": { - "prefix": "imust", - "body": ["import { useRouter } from 'next/router';"] - }, - "Import Next Image": { - "prefix": "imimg", - "body": ["import Image from 'next/image';"] - }, - "Import Next Link": { - "prefix": "iml", - "body": ["import Link from 'next/link';"] - }, - //#endregion //*======== Next.js =========== - - //#region //*=========== Snippet Wrap =========== - "Wrap with Fragment": { - "prefix": "ff", - "body": ["<>", "\t${TM_SELECTED_TEXT}", ""] - }, - "Wrap with clsx": { - "prefix": "cx", - "body": ["{clsx([${TM_SELECTED_TEXT}$0])}"] - }, - "Wrap with clsxm": { - "prefix": "cxm", - "body": ["{clsxm([${TM_SELECTED_TEXT}$0, className])}"] - }, - //#endregion //*======== Snippet Wrap =========== - - "Logger": { - "prefix": "lg", - "body": [ - "logger({ ${1:${CLIPBOARD}} }, '${TM_FILENAME} line ${TM_LINE_NUMBER}')" - ] - } -} diff --git a/CHANGELOG b/CHANGELOG index 86fe0a3..c9e2779 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,918 +1,918 @@ -## [217.0.0] - 2026-04-10 -### Added -- 新增注册邀请码功能 -- 增加弹幕内置源 -- 增加netlify部署支持 -- 观影室增加屏幕共享功能 -- 详情面板新增照片墙功能 -- 新增夸克网盘的转存与播放功能 -- 豆瓣数据源增加备用源功能 - -### Changed -- 移除音乐功能 - -### Fixed -- 修复弹幕选集分组无法鼠标滑轮滚动 -- 修复高级推荐报错无限刷新 - -## [216.0.0] - 2026-03-30 -### Added -- 新增视频源脚本 -- 私人影库增加本机转码功能 -- tvbox订阅增加黄色过滤 -- 播放记录显示直链播放的链接 -- 磁链增加代理配置 -- 弹幕选集面板增强 -- 电视直播聚合同名节目 -- douban页面大屏自动预加载第二页 -- 增加docker lite镜像 -- 详情面板增加外部跳转 - -### Changed -- GlobalError自动消失 -- 站点配置子服务配置项折叠 - -### Fixed -- 修复手动选择弹幕因缓存问题无法变更弹幕集数 -- 修复当前集拉回开头显示恢复进度按钮 -- 修复视频源权重的一些问题 - -## [215.0.0] - 2026-03-20 -### Added -- 增加主动恢复进度按钮 -- 新增播放记录面板 -- 弹幕搜索面板标题增加tooltip -- 影视搜索新增列表视图 -- pansou增加重试按钮 -- 新增ai评论生成 -- 增加一键render部署 -- 电视直播增加三种代理模式 - -### Changed -- 优化直链播放m3u8体验 -- 搜索页面海量数据下使用虚拟滚动提高性能 -- 优化emby代理内存泄漏问题 -- 电视直播代理控制权从用户端改为管理端 -- 获取视频源详情不再依赖title - -### Fixed -- 修复search页面僵尸历史记录tag -- 修复弹幕搜索框挤压 -- 修复搜索页面加载条显示顺序错误 -- 修复继续观看渐进式加载的一些问题 - -## [214.1.0] - 2026-03-10 -### Changed -- emby兼容jellyfin -- emby支持无密码登录 -- ai问片移除think块 -- ai问片可点击标题播放 -- 优化ai问片流式处理,系统提示词增加日期,优化决策json提取 - -### Fixed -- 修复filesystem模式取消下载不删除文件 -- 修复未配置TVBOX_SUBSCRIBE_TOKEN不代理emby图片 - -## [214.0.0] - 2026-03-04 -### Added -- 下载新增File System Api模式,实现浏览器本地播放 -- emby增加自定义ua和图片代理 -- tvbox支持根据用户细分视频源 -- 新增下载线程数调整设置 -- DetailPanel选集自动跟随 -- 源名字中增加分辨率显示 -- 换源增加全部重试 - -### Changed -- 数据迁移支持并发提升性能 -- 首页信息空数据不缓存 -- 播放页面的弹窗在大屏下使用抽屉式 -- 电视直播放行mp4 -- 定时任务支持控制是否同步返回 -- 下载按钮支持拖动 -- 网络设置改名数据源设置并关闭默认折叠 -- cloudflare添加minify参数以压缩大小 -- videocard直播不显示详情 - -### Fixed -- 修复集数屏蔽设置面板层级 -- 修复侧边栏私人影库选项不高亮 - -## [213.0.0] - 2026-02-24 -### Added -- 私人影库新加追番订阅功能 -- 新增进度条图标个性化功能 -- 新增下载任务数设置 -- 播放页面显示详情按钮 -- 详情面板增加演员数据 -- 继续播放增加剧集更新提示 -- tvbox订阅增加默认jar包 -- 定时任务增加冷却 - -### Changed -- 详情面板支持切换数据源 -- AI问片关闭弹窗不中断 -- 定时任务性能优化,支持并发处理 - -### Fixed -- 修复通知已读不更新菜单状态 -- 修正生态应用selene下载链接 - -## [212.2.0] - 2026-02-13 -### Added -- 新增生态应用模块 -- 新增精确搜索功能 - -### Changed -- 求片按钮位置调整 - -### Fixed -- 修复音乐模块移动端无法调节音量 -- 修复emby无法删除最后一个服务 -- 修复cloudflare非d1部署报错 -- 修复音乐模块openlist缓存未启用时仍走代理 -- 修复postgrep数据方式导入导出无法使用 -- 修复openlist扫描目录包含文件时报错 - -## [212.1.0] - 2026-02-08 -### Added -- 新增 Vercel Postgres 数据库支持 - -### Changed -- 自动创建站长账号,避免外键约束导致无法操作关联表 -- 即将上映恢复长按菜单,上映天数改为点击显示 - -## [212.0.0] - 2026-02-07 -### Added -- 接入Tunehub实现音乐播放 -- 网络直播支持观影室同步 -- 详情图片点击可预览 -- oidc登录图标自动识别多平台 - -### Changed -- 获取用户站长无信息时返回默认值兜底 -- 继续观看使用渐进式加载模式 -- openlist根路径主动移除bom防止获取根目录失效 -- webgpu缓冲区大小调整 -- 设备管理可识别OrionTv - -### Fixed -- 修复与upstash合并数据源时丢失的用户缓存信息 -- 修复登录页面不处理error参数 -- 修复safari视频切换集数无法清理旧视频 -- 修复部分tmdb接口在cloudflare下无法使用 - -## [211.0.0] - 2026-02-01 -### Added -- cloudflare部署增加d1支持 -- 电视直播支持flv播放 -- 登录注册页面输入框增加图标 - -### Changed -- 豆瓣预告片改为前端获取 -- 豆瓣预告片可播放声音 -- 轮播图缓存采用乐观更新策略 -- 管理面板无权限时显示错误提示 - -## [210.2.0] - 2026-01-29 -### Added -- 外部播放器增加app打开 -- 新增tvbox屏蔽源配置 -- 新增视频源权重功能 - -### Changed -- 设备管理当前设备置顶且显示黄色边框 -- live分片代理移除content-length发送 - -### Fixed -- 修复tmdbkey错误和未登录时获取外部观影室密钥无限重定向 - -## [210.1.3] - 2026-01-28 -### Fixed -- 修复跳转登录不登出 - -## [210.1.2] - 2026-01-27 -### Fixed -- 修复刷新token失效无限重定向 - -## [210.1.1] - 2026-01-27 -### Fixed -- 修复token过期重定向无法续期的问题 - -## [210.1.0] - 2026-01-27 -### Added -- 增加直链播放 -- 直播兼容txt格式 - -### Changed -- 首页轮播图和继续观看可隐藏 -- 刷新token改为前端进行防止redis数据库方式报错 - -## [210.0.0] - 2026-01-25 -### Added -- 新增网络直播功能 -- 动漫磁力搜索增加蜜柑 -- 动漫磁力搜索增加动漫花园 -- pansou增加关键词过滤 -- cloudflare workers部署支持 - -### Changed -- 去广告增强 -- 登录验证机制改为Token+Refresh Token形式 -- 移除旧版用户操作残留 - -### Fixed -- 修复缓存弹幕限制后不显示原始数量 -- 修复测速缓存导致的错误测速数据 -- 修复videocard,2000年只显示0年 - -## [209.1.0] - 2026-01-21 -### Added -- 增加禁用自动匹配弹幕 -- 增加弹幕上限设置 -- 增加禁用弹幕热力图开关 - -### Changed -- 恢复无数据库支持 -- 反爬增强 - -### Fixed -- 修复订阅去广告开关不正确修改链接问题 - -## [209.0.0] - 2026-01-18 -### Added -- 新增收藏更新邮件发送 -- 新增测速超时设置 -- tmdb详情支持查看季度信息,集数信息 -- tmdb支持设置反代 -- tmdb图片支持自定义设置url -- 豆瓣图片增加百度图片代理 - -### Changed -- 统一upstash和redis数据层 -- 优化docker镜像大小 -- Cloudflare Turnstile开启前检测sitekey和Secretkey不能为空 -- 豆瓣反爬增强 -- 竖向videocard的source name调整到海报右下角 -- 取消小雅获取剧集强制刷新 -- 本地设置选项卡增加图标 - -### Fixed -- 修复详情不应用豆瓣图片策略 - -## [208.0.0] - 2026-01-16 -### Added -- ai问片增加非流式响应 -- 新增下集弹幕预加载 -- openlist和小雅增加禁用预览方式播放 -- 管理配置增加主动重载 -- emby增加排序功能 -- 播放页面标题自动跟随影片标题 -- emby数据增加导入导出 - -### Changed -- 小雅现在会自动刷新播放链接 -- 缓冲策略从下拉框改为滑块 -- openlist扫描前增加tmdbkey配置检测 -- 优化firefox下的弹幕热力图 -- 小雅获取剧集时刷新文件夹 - -### Fixed -- 修复redis方式数据导入报错 -- 修复小雅不过滤空url - - -## [207.0.0] - 2026-01-13 -### Added -- 私人影库增加小雅支持 -- 测速增加码率测算 -- 私人影库增加求片功能 -- 搜索增加简繁转换器 -- openlist多目录扫描支持 - -### Changed -- 视频源保留关键字增加xiaoya,emby -- 年代筛选的年代改为动态生成 -- openlist支持视频预览方式播放 - -### Fixed -- 修复upstash方式数据导入报错 -- 修复新加载弹幕不显示弹幕数量 -- 修复生产环境emby分类返回空数组 - -## [206.3.0] - 2026-01-10 -### Added -- emby支持配置多源 -- emby支持配置高级参数(转码mp4,代理等) -- 新增自定义ai问片默认消息 - -### Changed -- 优化搜索逻辑和聚合逻辑 -- 加载缓存弹幕时显示元信息 -- 服务器代理豆瓣图片反防盗链增强 - -### Fixed -- 修复emby分类切换时加载旧数据 - -## [206.2.1] - 2026-01-08 -### Fixed -- 修正豆瓣图片源 - -## [206.2.0] - 2026-01-07 -### Added -- 轮播图数据源增加豆瓣 -- 弹幕开关状态持久化 -- play页面新增复制视频链接 - -### Changed -- 搜索聚合规则增强 -- emby剧集弹幕匹配优化 -- 搜索来源筛选提升私人影库权重 -- 私人影库未配置openlist时自动跳转emby -- 优化弹幕加载逻辑 - -### Fixed -- 修复私人影库报错导致搜索无结果 -- 修复无法保存更多推荐数据源 - -## [206.1.0] - 2026-01-05 -### Added -- 新增手动上传弹幕功能 - -### Changed -- 修改页面进度条逻辑 -- openlist和emby源改为异步搜索 -- 提高移动端选集面板高度 - -### Fixed -- 修复live页面500报错 - - -## [206.0.0] - 2026-01-04 -### Added -- 私人影库增加emby支持 -- 清空增加确认框 -- 增加页面切换进度条 -- 增加首页模块配置 - -### Changed -- 私人影库支持解析ova集数 -- 优化弹幕匹配逻辑 -- 修改本地设置面板可折叠 -- 关闭预告片后不再检测连通性,检测连通性时增加query保证无缓存检测 - -### Fixed -- 修正视频源代理模式外部播放器链接 -- 修复删除配置文件源提示成功的问题 - -## [205.1.0] - 2026-01-02 -### Added -- 播放器显示截屏按钮 -- 播放器在移动端增加快进快退控件 -- 私人影库增加扫描模式 -- 定时任务接口增加鉴权 -- tmdb轮播图支持预告片显示 - -### Changed -- 调整竖向videocard样式 -- 源站寻片应用黄色过滤器 -- 调整播放页大屏幕下封面图大小 -- 热力图不再使用artplayer内置方式,改为自定义实现 - -### Fixed -- 修复私人影库分页问题 -- 修复同一视频频繁弹窗弹幕选择 -- 彻底移除旧版用户导入导出并修复导入密码二次hash的问题 - - -## [205.0.1] - 2026-01-01 -### Changed -- 迁移跳过配置到新数据结构 - -## [205.0.0] - 2026-01-01 -### Added -- oidc注册支持设置linuxdo信任等级 -- 播放页增加更多推荐 -- 新增首页轮播图 -- 播放页面增加背景图 -- 增加动漫磁力搜索 -- 视频源增加代理开关 -- 新增预缓冲和缓冲策略 -- 新增ai问片功能 -- 新增播放记录自动清理 -- tmdb增加轮询 -- 视频卡新增详情功能 -- 新增源站寻片功能 - -### Changed -- 大幅提高播放页性能 -- 私人影库扫描增加季度支持与年份辅助扫描 -- 移除首页的收藏夹切换卡,改成从用户菜单进入 -- 美化继续观看模块 -- tvbox支持私人影库播放 -- 继续观看列表使用虚拟滚动 -- 重构弹幕缓存 -- 迁移观看记录和收藏到新数据结构 -- 跳过配置改成弹窗配置 -- 首页推荐数据增加1小时缓存 -- 不再支持无数库使用 -- 更换logo - -### Fixed -- 修复/api/server-config暴露externalServerAuth的安全问题 -- 修复换源列表,私人影库不显示图像 -- 修复无法切换源到私人影库 -- 修正admin权限问题 -- 修复定时任务的openlist扫描 -- 修复导入用户丢失问题 -- 修复暂停保存两次播放记录 - -## [204.0.0] - 2025-12-25 - -### Added -- ⚠️⚠️⚠️更新此版本前务必进行备份!!!⚠️⚠️⚠️ -- 新增私人影视库功能(实验性) -- 增加弹幕热力图 -- 增加盘搜搜索资源 - -### Changed -- 完全重构用户数据存储结构 -- 提高所有弹幕接口的超时时间 -- 优化完结标识判断 -- 即将上映移动端字体大小调整 -- tmdb增加代理支持 -- 剧集更新检测改为服务器后台定时执行 - -## [203.2.2] - 2025-12-20 - -### Fixed -- 修复IOS端换集报错播放器初始化失败 -- 修复超分切换时重复渲染 - -## [203.2.0] - 2025-12-19 - -### Added -- 首页新增短剧推荐 -- 新增剧集更新检查 - -### Changed -- 优化弹幕匹配规则 -- 弹幕请求超时修改为2分钟 -- 视频加载失败增加提示 - -## [203.1.0] - 2025-12-18 - -### Added -- 直播页面增加超分功能 -- 直播节目单增加Timeline视图 -- 直播页面增加外部播放器 - -### Changed -- 直播观看记录会出现在继续观看列表了 -- 直播页面切换频道现在地址栏会跟随变化 -- 直播首次加载节目单改为异步 - -## [203.0.0] - 2025-12-17 - -### Added -- 首页新增即将上映模块 -- 新增剧集屏蔽功能 -- 换源增加重新测试和质量排序 - -### Changed -- IOS全屏体验优化 -- 站点配置中的注册相关配置移动到了新的注册配置大项中 -- 默认站名从MoonTV改为MoonTVPlus -- 用户管理现在会给oidc注册的用户显示oidc标识 - -### Fixed -- 修复压缩节目单无法加载 -- 修复Docker环境下离线下载无法开启 - -## [202.0.0] - 2025-12-14 - -### Added -- 新增离线下载功能 -- 增加注册功能 -- 增加oidc登录功能 -- 增加自定义主题 - -### Changed -- 视频下载现在支持选集下载 - -### Fixed -- 修复webrtc连接容易断开的问题 -- 修复语音聊天中转模式回声 - -## [201.0.1] - 2025-12-10 - -### Fixed -- 修复因弹幕缓存导致弹幕过滤功能失效的bug - -## [201.0.0] - 2025-12-10 - -### Added -- 增加观影室,支持多人同步观影、实时聊天、语音通话等功能(实验性) -- 增加完整下载,通过合并m3u8片段实现完整视频下载 -- 播放详情页增加评分显示 - -### Changed -- 现在本地弹幕缓存会主动清除了 -### Fixed -- 修正tvbox订阅请求时机错误 - -## [200.4.0] - 2025-12-06 - -### Added -- 增加tvbox订阅功能,自带去广告 -- 增加弹幕本地缓存 - -### Changed -- 外部播放器使用代理m3u8时现在可以增加鉴权了 - -## [200.3.0] - 2025-12-05 - -### Added -- 增加自定义去广告功能 - -### Changed -- 现在外部播放器支持去广告了 - -### Fixed -- 修复首页/api/favorites接口重复请求 - -## [200.2.0] - 2025-12-04 - -### Added -- 新增弹幕过滤功能 -- 配置文件现在支持上传json -- 搜索结果现在支持缓存 - -### Changed -- 登录页面现在可以保存和查看密码 -- 优化弹幕自动匹配 -- 优化超分功能 - -### Fixed -- 弹幕自动加载失败现在有提示了 - -## [200.1.0] - 2025-12-03 - -### Changed -- 评论抓取功能更改为懒加载 -- 评论抓取功能增加开关,默认为关闭 - -### Fixed -- 修复播放页500错误 - -## [200.0.0] - 2025-12-02 - -### Added -- 新增外部播放器跳转 -- 新增视频超分 -- 新增弹幕抓取 -- 新增评论抓取 - -### Fixed -- 修复首页卡顿 - -## [100.0.0] - 2025-08-26 - -### Added - -- 新增对 SITE_BASE 环境变量的支持,解决 m3u8 重写时 base url 错误的问题 - -### Changed - -- 移除授权相关逻辑 -- 移除代码混淆 -- 移除 melody-cdn-sharon - -## [4.3.0] - 2025-08-26 - -### Added - -- 支持将 IPTV 频道添加到收藏中 - -### Changed - -- 禁用 flv 直播,仅支持 m3u8 直播 -- 降低代理 ts 分片的内存占用 - -## [4.2.1] - 2025-08-26 - -### Fixed - -- 修复直播源加载失败或离开页面后依然无限加载的问题 - -## [4.2.0] - 2025-08-26 - -### Added - -- 支持 flv 直播和直播地址解析到 mp4 的处理 -- 增加直播台标的 proxy 以防止 cors -- 支持播放页选集分组的滚动翻页 - -### Changed - -- 管理后台页面的按钮增加加载中的 UI - -### Fixed - -- /api/proxy/m3u8 仅对 m3u8 内容反序列化,降低内存和 CPU 消耗 - -## [4.1.1] - 2025-08-25 - -### Changed - -- 增加对 url-tvg 和多 epg url 的支持 - -### Fixed - -- 修复 epg 数据清洗中去重叠逻辑未考虑日期导致的问题 - -## [4.1.0] - 2025-08-24 - -### Added - -- 解析 m3u 自带的 epg 和自定义 epg,增加今日节目单 - -### Changed - -- 直播源数据刷新改为并发刷新 - -## [4.0.0] - 2025-08-24 - -### Added - -- 增加 iptv 订阅和播放功能 - -### Changed - -- 搜索页面视频卡片移动端/右键菜单添加豆瓣链接 -- 搜索建议遵循色情过滤 - -## [3.2.1] - 2025-08-22 - -### Changed - -- 新增色色过滤分类 -- 调整搜索建议框层级 - -## [3.2.0] - 2025-08-22 - -### Added - -- 视频源管理支持批量启用、禁用、删除 -- 用户管理支持批量设置用户组 -- 视频卡片右键/长按菜单新增新标签页播放 - -### Changed - -- 视频卡片移动端 hover 时仅保留播放按钮 -- 微调管理页面 UI 和视频卡片右键/长按菜单中的收藏样式 - -### Fixed - -- 修复了搜索栏 enter 键自动选中第一个建议项的问题 - -## [3.1.2] - 2025-08-22 - -### Fixed - -- 修复移动端卡片无法点击的问题 - -## [3.1.1] - 2025-08-21 - -### Fixed - -- 修复了视频卡片 hover 的非播放按钮点击后进入播放页的问题 - -## [3.1.0] - 2025-08-21 - -### Added - -- 增加用户组管理和用户组播放源限制 -- 增加管理面板视频源有效性检查 -- 搜索栏增加一键删除按钮 - -### Changed - -- 放宽授权心跳对于网络问题的判断标准 -- 统一管理面板弹窗使用 createPortal -- VideoCard 允许移动端响应 hover 事件 -- 移动端布局 header 常驻,搜索按钮移动到 header 右侧 -- 调大搜索接口超时时间 - -### Fixed - -- 修复 bangumi 返回的整数评分无小数导致 UI 不对齐的问题 - -## [3.0.2] - 2025-08-20 - -### Changed - -- 优化机器码生成逻辑 - -### Fixed - -- 修复 redis url 不支持 rediss 协议的问题 - -## [3.0.1] - 2025-08-20 - -### Fixed - -- 修复授权初始化错误 - -## [3.0.0] - 2025-08-20 - -### Added - -- 防盗卖加固 -- 支持自定义用户可用视频源 - -### Changed - -- 右键视频卡片可弹出操作菜单 - -### Fixed - -- 过滤掉集数为 0 的搜索结果 - -## [2.7.1] - 2025-08-17 - -### Fixed - -- 修复 iOS 下版本面板可穿透滚动背景的问题 - -## [2.7.0] - 2025-08-17 - -### Added - -- 视频卡片新增移动端操作面板,优化触控屏操作体验 - -### Changed - -- 优化集数标题的匹配和展示逻辑 - -### Fixed - -- 修复设置面板和修改密码面板背景可被拖动的问题 - -## [2.6.0] - 2025-08-17 - -### Added - -- 新增搜索流式输出接口,并设置流式搜索为默认搜索接口,优化搜索体验 -- 新增源站搜索结果内存缓存,粒度为源站+关键词+页数,缓存 10 分钟 -- 新增豆瓣 CDN provided by @JohnsonRan - -### Changed - -- 搜索结果默认为无排序状态,不再默认按照年份排序 -- 常规搜索接口无结果时,不再设置响应的缓存头 -- 移除豆瓣数据源中的 cors-anywhere 方式 - -### Fixed - -- 数据导出时导出站长密码,保证迁移到新账户时原站长用户可正常登录 -- 聚合卡片优化移动端源信息展示 - -## [2.4.1] - 2025-08-15 - -### Fixed - -- 对导入和 db 读取的配置文件做自检,防止 USERNAME 修改导致用户状态异常 - -## [2.4.0] - 2025-08-15 - -### Added - -- 支持 kvrocks 存储(持久化 kv 存储) - -### Fixed - -- 修复搜索结果排序不稳定的问题 -- 导入数据时同时更新内存缓存的管理员配置 - -## [2.3.0] - 2025-08-15 - -### Added - -- 支持站长导入导出整站数据 - -### Changed - -- 仅允许站长操作配置文件 -- 微调搜索结果过滤面板的移动端样式 - -## [2.2.1] - 2025-08-14 - -### Fixed - -- 修复了筛选 panel 打开时滚动页面 panel 不跟随的问题 - -## [2.2.0] - 2025-08-14 - -### Added - -- 搜索结果支持按播放源、标题和年份筛选,支持按年份排序 -- 搜索界面视频卡片展示年份信息,聚合卡片展示播放源 - -### Fixed - -- 修复 /api/search/resources 返回空的问题 -- 修复 upstash 实例无法编辑自定义分类的问题 - -## [2.1.0] - 2025-08-13 - -### Added - -- 支持通过订阅获取配置文件 - -### Changed - -- 微调部分文案和 UI -- 删除部分无用代码 - -## [2.0.1] - 2025-08-13 - -### Changed - -- 版本检查和变更日志请求 Github - -### Fixed - -- 微调管理面板样式 - -## [2.0.0] - 2025-08-13 - -### Added - -- 支持配置文件在线配置和编辑 -- 搜索页搜索框实时联想 -- 去除对 localstorage 模式的支持 - -### Changed - -- 播放记录删除按钮改为垃圾桶图标以消除歧义 - -### Fixed - -- 限制设置面板的最大长度,防止超出视口 - -## [1.1.1] - 2025-08-12 - -### Changed -- 修正 zwei 提供的 cors proxy 地址 -- 移除废弃代码 - -### Fixed -- [运维] docker workflow release 日期使用东八区日期 - -## [1.1.0] - 2025-08-12 - -### Added -- 每日新番放送功能,展示每日新番放送的番剧 - -### Fixed -- 修复远程 CHANGELOG 无法提取变更内容的问题 - -## [1.0.5] - 2025-08-12 - -### Changed -- 实现基于 Git 标签的自动 Release 工作流 - -## [1.0.4] - 2025-08-11 - -### Added -- 优化版本管理工作流,实现单点修改 - -### Changed -- 版本号现在从 CHANGELOG 自动提取,无需手动维护 VERSION.txt - -## [1.0.3] - 2025-08-11 - -### Changed - -- 升级播放器 Artplayer 至版本 5.2.5 - -## [1.0.2] - 2025-08-11 - -### Changed - -- 版本号比较机制恢复为数字比较,仅当最新版本大于本地版本时才认为有更新 -- [运维] 自动替换 version.ts 中的版本号为 VERSION.txt 中的版本号 - -## [1.0.1] - 2025-08-11 - -### Fixed - -- 修复版本检查功能,只要与最新版本号不一致即认为有更新 - -## [1.0.0] - 2025-08-10 - -### Added - -- 基于 Semantic Versioning 的版本号机制 -- 版本信息面板,展示本地变更日志和远程更新日志 +## [217.0.0] - 2026-04-10 +### Added +- 新增注册邀请码功能 +- 增加弹幕内置源 +- 增加netlify部署支持 +- 观影室增加屏幕共享功能 +- 详情面板新增照片墙功能 +- 新增夸克网盘的转存与播放功能 +- 豆瓣数据源增加备用源功能 + +### Changed +- 移除音乐功能 + +### Fixed +- 修复弹幕选集分组无法鼠标滑轮滚动 +- 修复高级推荐报错无限刷新 + +## [216.0.0] - 2026-03-30 +### Added +- 新增视频源脚本 +- 私人影库增加本机转码功能 +- tvbox订阅增加黄色过滤 +- 播放记录显示直链播放的链接 +- 磁链增加代理配置 +- 弹幕选集面板增强 +- 电视直播聚合同名节目 +- douban页面大屏自动预加载第二页 +- 增加docker lite镜像 +- 详情面板增加外部跳转 + +### Changed +- GlobalError自动消失 +- 站点配置子服务配置项折叠 + +### Fixed +- 修复手动选择弹幕因缓存问题无法变更弹幕集数 +- 修复当前集拉回开头显示恢复进度按钮 +- 修复视频源权重的一些问题 + +## [215.0.0] - 2026-03-20 +### Added +- 增加主动恢复进度按钮 +- 新增播放记录面板 +- 弹幕搜索面板标题增加tooltip +- 影视搜索新增列表视图 +- pansou增加重试按钮 +- 新增ai评论生成 +- 增加一键render部署 +- 电视直播增加三种代理模式 + +### Changed +- 优化直链播放m3u8体验 +- 搜索页面海量数据下使用虚拟滚动提高性能 +- 优化emby代理内存泄漏问题 +- 电视直播代理控制权从用户端改为管理端 +- 获取视频源详情不再依赖title + +### Fixed +- 修复search页面僵尸历史记录tag +- 修复弹幕搜索框挤压 +- 修复搜索页面加载条显示顺序错误 +- 修复继续观看渐进式加载的一些问题 + +## [214.1.0] - 2026-03-10 +### Changed +- emby兼容jellyfin +- emby支持无密码登录 +- ai问片移除think块 +- ai问片可点击标题播放 +- 优化ai问片流式处理,系统提示词增加日期,优化决策json提取 + +### Fixed +- 修复filesystem模式取消下载不删除文件 +- 修复未配置TVBOX_SUBSCRIBE_TOKEN不代理emby图片 + +## [214.0.0] - 2026-03-04 +### Added +- 下载新增File System Api模式,实现浏览器本地播放 +- emby增加自定义ua和图片代理 +- tvbox支持根据用户细分视频源 +- 新增下载线程数调整设置 +- DetailPanel选集自动跟随 +- 源名字中增加分辨率显示 +- 换源增加全部重试 + +### Changed +- 数据迁移支持并发提升性能 +- 首页信息空数据不缓存 +- 播放页面的弹窗在大屏下使用抽屉式 +- 电视直播放行mp4 +- 定时任务支持控制是否同步返回 +- 下载按钮支持拖动 +- 网络设置改名数据源设置并关闭默认折叠 +- cloudflare添加minify参数以压缩大小 +- videocard直播不显示详情 + +### Fixed +- 修复集数屏蔽设置面板层级 +- 修复侧边栏私人影库选项不高亮 + +## [213.0.0] - 2026-02-24 +### Added +- 私人影库新加追番订阅功能 +- 新增进度条图标个性化功能 +- 新增下载任务数设置 +- 播放页面显示详情按钮 +- 详情面板增加演员数据 +- 继续播放增加剧集更新提示 +- tvbox订阅增加默认jar包 +- 定时任务增加冷却 + +### Changed +- 详情面板支持切换数据源 +- AI问片关闭弹窗不中断 +- 定时任务性能优化,支持并发处理 + +### Fixed +- 修复通知已读不更新菜单状态 +- 修正生态应用selene下载链接 + +## [212.2.0] - 2026-02-13 +### Added +- 新增生态应用模块 +- 新增精确搜索功能 + +### Changed +- 求片按钮位置调整 + +### Fixed +- 修复音乐模块移动端无法调节音量 +- 修复emby无法删除最后一个服务 +- 修复cloudflare非d1部署报错 +- 修复音乐模块openlist缓存未启用时仍走代理 +- 修复postgrep数据方式导入导出无法使用 +- 修复openlist扫描目录包含文件时报错 + +## [212.1.0] - 2026-02-08 +### Added +- 新增 Vercel Postgres 数据库支持 + +### Changed +- 自动创建站长账号,避免外键约束导致无法操作关联表 +- 即将上映恢复长按菜单,上映天数改为点击显示 + +## [212.0.0] - 2026-02-07 +### Added +- 接入Tunehub实现音乐播放 +- 网络直播支持观影室同步 +- 详情图片点击可预览 +- oidc登录图标自动识别多平台 + +### Changed +- 获取用户站长无信息时返回默认值兜底 +- 继续观看使用渐进式加载模式 +- openlist根路径主动移除bom防止获取根目录失效 +- webgpu缓冲区大小调整 +- 设备管理可识别OrionTv + +### Fixed +- 修复与upstash合并数据源时丢失的用户缓存信息 +- 修复登录页面不处理error参数 +- 修复safari视频切换集数无法清理旧视频 +- 修复部分tmdb接口在cloudflare下无法使用 + +## [211.0.0] - 2026-02-01 +### Added +- cloudflare部署增加d1支持 +- 电视直播支持flv播放 +- 登录注册页面输入框增加图标 + +### Changed +- 豆瓣预告片改为前端获取 +- 豆瓣预告片可播放声音 +- 轮播图缓存采用乐观更新策略 +- 管理面板无权限时显示错误提示 + +## [210.2.0] - 2026-01-29 +### Added +- 外部播放器增加app打开 +- 新增tvbox屏蔽源配置 +- 新增视频源权重功能 + +### Changed +- 设备管理当前设备置顶且显示黄色边框 +- live分片代理移除content-length发送 + +### Fixed +- 修复tmdbkey错误和未登录时获取外部观影室密钥无限重定向 + +## [210.1.3] - 2026-01-28 +### Fixed +- 修复跳转登录不登出 + +## [210.1.2] - 2026-01-27 +### Fixed +- 修复刷新token失效无限重定向 + +## [210.1.1] - 2026-01-27 +### Fixed +- 修复token过期重定向无法续期的问题 + +## [210.1.0] - 2026-01-27 +### Added +- 增加直链播放 +- 直播兼容txt格式 + +### Changed +- 首页轮播图和继续观看可隐藏 +- 刷新token改为前端进行防止redis数据库方式报错 + +## [210.0.0] - 2026-01-25 +### Added +- 新增网络直播功能 +- 动漫磁力搜索增加蜜柑 +- 动漫磁力搜索增加动漫花园 +- pansou增加关键词过滤 +- cloudflare workers部署支持 + +### Changed +- 去广告增强 +- 登录验证机制改为Token+Refresh Token形式 +- 移除旧版用户操作残留 + +### Fixed +- 修复缓存弹幕限制后不显示原始数量 +- 修复测速缓存导致的错误测速数据 +- 修复videocard,2000年只显示0年 + +## [209.1.0] - 2026-01-21 +### Added +- 增加禁用自动匹配弹幕 +- 增加弹幕上限设置 +- 增加禁用弹幕热力图开关 + +### Changed +- 恢复无数据库支持 +- 反爬增强 + +### Fixed +- 修复订阅去广告开关不正确修改链接问题 + +## [209.0.0] - 2026-01-18 +### Added +- 新增收藏更新邮件发送 +- 新增测速超时设置 +- tmdb详情支持查看季度信息,集数信息 +- tmdb支持设置反代 +- tmdb图片支持自定义设置url +- 豆瓣图片增加百度图片代理 + +### Changed +- 统一upstash和redis数据层 +- 优化docker镜像大小 +- Cloudflare Turnstile开启前检测sitekey和Secretkey不能为空 +- 豆瓣反爬增强 +- 竖向videocard的source name调整到海报右下角 +- 取消小雅获取剧集强制刷新 +- 本地设置选项卡增加图标 + +### Fixed +- 修复详情不应用豆瓣图片策略 + +## [208.0.0] - 2026-01-16 +### Added +- ai问片增加非流式响应 +- 新增下集弹幕预加载 +- openlist和小雅增加禁用预览方式播放 +- 管理配置增加主动重载 +- emby增加排序功能 +- 播放页面标题自动跟随影片标题 +- emby数据增加导入导出 + +### Changed +- 小雅现在会自动刷新播放链接 +- 缓冲策略从下拉框改为滑块 +- openlist扫描前增加tmdbkey配置检测 +- 优化firefox下的弹幕热力图 +- 小雅获取剧集时刷新文件夹 + +### Fixed +- 修复redis方式数据导入报错 +- 修复小雅不过滤空url + + +## [207.0.0] - 2026-01-13 +### Added +- 私人影库增加小雅支持 +- 测速增加码率测算 +- 私人影库增加求片功能 +- 搜索增加简繁转换器 +- openlist多目录扫描支持 + +### Changed +- 视频源保留关键字增加xiaoya,emby +- 年代筛选的年代改为动态生成 +- openlist支持视频预览方式播放 + +### Fixed +- 修复upstash方式数据导入报错 +- 修复新加载弹幕不显示弹幕数量 +- 修复生产环境emby分类返回空数组 + +## [206.3.0] - 2026-01-10 +### Added +- emby支持配置多源 +- emby支持配置高级参数(转码mp4,代理等) +- 新增自定义ai问片默认消息 + +### Changed +- 优化搜索逻辑和聚合逻辑 +- 加载缓存弹幕时显示元信息 +- 服务器代理豆瓣图片反防盗链增强 + +### Fixed +- 修复emby分类切换时加载旧数据 + +## [206.2.1] - 2026-01-08 +### Fixed +- 修正豆瓣图片源 + +## [206.2.0] - 2026-01-07 +### Added +- 轮播图数据源增加豆瓣 +- 弹幕开关状态持久化 +- play页面新增复制视频链接 + +### Changed +- 搜索聚合规则增强 +- emby剧集弹幕匹配优化 +- 搜索来源筛选提升私人影库权重 +- 私人影库未配置openlist时自动跳转emby +- 优化弹幕加载逻辑 + +### Fixed +- 修复私人影库报错导致搜索无结果 +- 修复无法保存更多推荐数据源 + +## [206.1.0] - 2026-01-05 +### Added +- 新增手动上传弹幕功能 + +### Changed +- 修改页面进度条逻辑 +- openlist和emby源改为异步搜索 +- 提高移动端选集面板高度 + +### Fixed +- 修复live页面500报错 + + +## [206.0.0] - 2026-01-04 +### Added +- 私人影库增加emby支持 +- 清空增加确认框 +- 增加页面切换进度条 +- 增加首页模块配置 + +### Changed +- 私人影库支持解析ova集数 +- 优化弹幕匹配逻辑 +- 修改本地设置面板可折叠 +- 关闭预告片后不再检测连通性,检测连通性时增加query保证无缓存检测 + +### Fixed +- 修正视频源代理模式外部播放器链接 +- 修复删除配置文件源提示成功的问题 + +## [205.1.0] - 2026-01-02 +### Added +- 播放器显示截屏按钮 +- 播放器在移动端增加快进快退控件 +- 私人影库增加扫描模式 +- 定时任务接口增加鉴权 +- tmdb轮播图支持预告片显示 + +### Changed +- 调整竖向videocard样式 +- 源站寻片应用黄色过滤器 +- 调整播放页大屏幕下封面图大小 +- 热力图不再使用artplayer内置方式,改为自定义实现 + +### Fixed +- 修复私人影库分页问题 +- 修复同一视频频繁弹窗弹幕选择 +- 彻底移除旧版用户导入导出并修复导入密码二次hash的问题 + + +## [205.0.1] - 2026-01-01 +### Changed +- 迁移跳过配置到新数据结构 + +## [205.0.0] - 2026-01-01 +### Added +- oidc注册支持设置linuxdo信任等级 +- 播放页增加更多推荐 +- 新增首页轮播图 +- 播放页面增加背景图 +- 增加动漫磁力搜索 +- 视频源增加代理开关 +- 新增预缓冲和缓冲策略 +- 新增ai问片功能 +- 新增播放记录自动清理 +- tmdb增加轮询 +- 视频卡新增详情功能 +- 新增源站寻片功能 + +### Changed +- 大幅提高播放页性能 +- 私人影库扫描增加季度支持与年份辅助扫描 +- 移除首页的收藏夹切换卡,改成从用户菜单进入 +- 美化继续观看模块 +- tvbox支持私人影库播放 +- 继续观看列表使用虚拟滚动 +- 重构弹幕缓存 +- 迁移观看记录和收藏到新数据结构 +- 跳过配置改成弹窗配置 +- 首页推荐数据增加1小时缓存 +- 不再支持无数库使用 +- 更换logo + +### Fixed +- 修复/api/server-config暴露externalServerAuth的安全问题 +- 修复换源列表,私人影库不显示图像 +- 修复无法切换源到私人影库 +- 修正admin权限问题 +- 修复定时任务的openlist扫描 +- 修复导入用户丢失问题 +- 修复暂停保存两次播放记录 + +## [204.0.0] - 2025-12-25 + +### Added +- ⚠️⚠️⚠️更新此版本前务必进行备份!!!⚠️⚠️⚠️ +- 新增私人影视库功能(实验性) +- 增加弹幕热力图 +- 增加盘搜搜索资源 + +### Changed +- 完全重构用户数据存储结构 +- 提高所有弹幕接口的超时时间 +- 优化完结标识判断 +- 即将上映移动端字体大小调整 +- tmdb增加代理支持 +- 剧集更新检测改为服务器后台定时执行 + +## [203.2.2] - 2025-12-20 + +### Fixed +- 修复IOS端换集报错播放器初始化失败 +- 修复超分切换时重复渲染 + +## [203.2.0] - 2025-12-19 + +### Added +- 首页新增短剧推荐 +- 新增剧集更新检查 + +### Changed +- 优化弹幕匹配规则 +- 弹幕请求超时修改为2分钟 +- 视频加载失败增加提示 + +## [203.1.0] - 2025-12-18 + +### Added +- 直播页面增加超分功能 +- 直播节目单增加Timeline视图 +- 直播页面增加外部播放器 + +### Changed +- 直播观看记录会出现在继续观看列表了 +- 直播页面切换频道现在地址栏会跟随变化 +- 直播首次加载节目单改为异步 + +## [203.0.0] - 2025-12-17 + +### Added +- 首页新增即将上映模块 +- 新增剧集屏蔽功能 +- 换源增加重新测试和质量排序 + +### Changed +- IOS全屏体验优化 +- 站点配置中的注册相关配置移动到了新的注册配置大项中 +- 默认站名从MoonTV改为MoonTVPlus +- 用户管理现在会给oidc注册的用户显示oidc标识 + +### Fixed +- 修复压缩节目单无法加载 +- 修复Docker环境下离线下载无法开启 + +## [202.0.0] - 2025-12-14 + +### Added +- 新增离线下载功能 +- 增加注册功能 +- 增加oidc登录功能 +- 增加自定义主题 + +### Changed +- 视频下载现在支持选集下载 + +### Fixed +- 修复webrtc连接容易断开的问题 +- 修复语音聊天中转模式回声 + +## [201.0.1] - 2025-12-10 + +### Fixed +- 修复因弹幕缓存导致弹幕过滤功能失效的bug + +## [201.0.0] - 2025-12-10 + +### Added +- 增加观影室,支持多人同步观影、实时聊天、语音通话等功能(实验性) +- 增加完整下载,通过合并m3u8片段实现完整视频下载 +- 播放详情页增加评分显示 + +### Changed +- 现在本地弹幕缓存会主动清除了 +### Fixed +- 修正tvbox订阅请求时机错误 + +## [200.4.0] - 2025-12-06 + +### Added +- 增加tvbox订阅功能,自带去广告 +- 增加弹幕本地缓存 + +### Changed +- 外部播放器使用代理m3u8时现在可以增加鉴权了 + +## [200.3.0] - 2025-12-05 + +### Added +- 增加自定义去广告功能 + +### Changed +- 现在外部播放器支持去广告了 + +### Fixed +- 修复首页/api/favorites接口重复请求 + +## [200.2.0] - 2025-12-04 + +### Added +- 新增弹幕过滤功能 +- 配置文件现在支持上传json +- 搜索结果现在支持缓存 + +### Changed +- 登录页面现在可以保存和查看密码 +- 优化弹幕自动匹配 +- 优化超分功能 + +### Fixed +- 弹幕自动加载失败现在有提示了 + +## [200.1.0] - 2025-12-03 + +### Changed +- 评论抓取功能更改为懒加载 +- 评论抓取功能增加开关,默认为关闭 + +### Fixed +- 修复播放页500错误 + +## [200.0.0] - 2025-12-02 + +### Added +- 新增外部播放器跳转 +- 新增视频超分 +- 新增弹幕抓取 +- 新增评论抓取 + +### Fixed +- 修复首页卡顿 + +## [100.0.0] - 2025-08-26 + +### Added + +- 新增对 SITE_BASE 环境变量的支持,解决 m3u8 重写时 base url 错误的问题 + +### Changed + +- 移除授权相关逻辑 +- 移除代码混淆 +- 移除 melody-cdn-sharon + +## [4.3.0] - 2025-08-26 + +### Added + +- 支持将 IPTV 频道添加到收藏中 + +### Changed + +- 禁用 flv 直播,仅支持 m3u8 直播 +- 降低代理 ts 分片的内存占用 + +## [4.2.1] - 2025-08-26 + +### Fixed + +- 修复直播源加载失败或离开页面后依然无限加载的问题 + +## [4.2.0] - 2025-08-26 + +### Added + +- 支持 flv 直播和直播地址解析到 mp4 的处理 +- 增加直播台标的 proxy 以防止 cors +- 支持播放页选集分组的滚动翻页 + +### Changed + +- 管理后台页面的按钮增加加载中的 UI + +### Fixed + +- /api/proxy/m3u8 仅对 m3u8 内容反序列化,降低内存和 CPU 消耗 + +## [4.1.1] - 2025-08-25 + +### Changed + +- 增加对 url-tvg 和多 epg url 的支持 + +### Fixed + +- 修复 epg 数据清洗中去重叠逻辑未考虑日期导致的问题 + +## [4.1.0] - 2025-08-24 + +### Added + +- 解析 m3u 自带的 epg 和自定义 epg,增加今日节目单 + +### Changed + +- 直播源数据刷新改为并发刷新 + +## [4.0.0] - 2025-08-24 + +### Added + +- 增加 iptv 订阅和播放功能 + +### Changed + +- 搜索页面视频卡片移动端/右键菜单添加豆瓣链接 +- 搜索建议遵循色情过滤 + +## [3.2.1] - 2025-08-22 + +### Changed + +- 新增色色过滤分类 +- 调整搜索建议框层级 + +## [3.2.0] - 2025-08-22 + +### Added + +- 视频源管理支持批量启用、禁用、删除 +- 用户管理支持批量设置用户组 +- 视频卡片右键/长按菜单新增新标签页播放 + +### Changed + +- 视频卡片移动端 hover 时仅保留播放按钮 +- 微调管理页面 UI 和视频卡片右键/长按菜单中的收藏样式 + +### Fixed + +- 修复了搜索栏 enter 键自动选中第一个建议项的问题 + +## [3.1.2] - 2025-08-22 + +### Fixed + +- 修复移动端卡片无法点击的问题 + +## [3.1.1] - 2025-08-21 + +### Fixed + +- 修复了视频卡片 hover 的非播放按钮点击后进入播放页的问题 + +## [3.1.0] - 2025-08-21 + +### Added + +- 增加用户组管理和用户组播放源限制 +- 增加管理面板视频源有效性检查 +- 搜索栏增加一键删除按钮 + +### Changed + +- 放宽授权心跳对于网络问题的判断标准 +- 统一管理面板弹窗使用 createPortal +- VideoCard 允许移动端响应 hover 事件 +- 移动端布局 header 常驻,搜索按钮移动到 header 右侧 +- 调大搜索接口超时时间 + +### Fixed + +- 修复 bangumi 返回的整数评分无小数导致 UI 不对齐的问题 + +## [3.0.2] - 2025-08-20 + +### Changed + +- 优化机器码生成逻辑 + +### Fixed + +- 修复 redis url 不支持 rediss 协议的问题 + +## [3.0.1] - 2025-08-20 + +### Fixed + +- 修复授权初始化错误 + +## [3.0.0] - 2025-08-20 + +### Added + +- 防盗卖加固 +- 支持自定义用户可用视频源 + +### Changed + +- 右键视频卡片可弹出操作菜单 + +### Fixed + +- 过滤掉集数为 0 的搜索结果 + +## [2.7.1] - 2025-08-17 + +### Fixed + +- 修复 iOS 下版本面板可穿透滚动背景的问题 + +## [2.7.0] - 2025-08-17 + +### Added + +- 视频卡片新增移动端操作面板,优化触控屏操作体验 + +### Changed + +- 优化集数标题的匹配和展示逻辑 + +### Fixed + +- 修复设置面板和修改密码面板背景可被拖动的问题 + +## [2.6.0] - 2025-08-17 + +### Added + +- 新增搜索流式输出接口,并设置流式搜索为默认搜索接口,优化搜索体验 +- 新增源站搜索结果内存缓存,粒度为源站+关键词+页数,缓存 10 分钟 +- 新增豆瓣 CDN provided by @JohnsonRan + +### Changed + +- 搜索结果默认为无排序状态,不再默认按照年份排序 +- 常规搜索接口无结果时,不再设置响应的缓存头 +- 移除豆瓣数据源中的 cors-anywhere 方式 + +### Fixed + +- 数据导出时导出站长密码,保证迁移到新账户时原站长用户可正常登录 +- 聚合卡片优化移动端源信息展示 + +## [2.4.1] - 2025-08-15 + +### Fixed + +- 对导入和 db 读取的配置文件做自检,防止 USERNAME 修改导致用户状态异常 + +## [2.4.0] - 2025-08-15 + +### Added + +- 支持 kvrocks 存储(持久化 kv 存储) + +### Fixed + +- 修复搜索结果排序不稳定的问题 +- 导入数据时同时更新内存缓存的管理员配置 + +## [2.3.0] - 2025-08-15 + +### Added + +- 支持站长导入导出整站数据 + +### Changed + +- 仅允许站长操作配置文件 +- 微调搜索结果过滤面板的移动端样式 + +## [2.2.1] - 2025-08-14 + +### Fixed + +- 修复了筛选 panel 打开时滚动页面 panel 不跟随的问题 + +## [2.2.0] - 2025-08-14 + +### Added + +- 搜索结果支持按播放源、标题和年份筛选,支持按年份排序 +- 搜索界面视频卡片展示年份信息,聚合卡片展示播放源 + +### Fixed + +- 修复 /api/search/resources 返回空的问题 +- 修复 upstash 实例无法编辑自定义分类的问题 + +## [2.1.0] - 2025-08-13 + +### Added + +- 支持通过订阅获取配置文件 + +### Changed + +- 微调部分文案和 UI +- 删除部分无用代码 + +## [2.0.1] - 2025-08-13 + +### Changed + +- 版本检查和变更日志请求 Github + +### Fixed + +- 微调管理面板样式 + +## [2.0.0] - 2025-08-13 + +### Added + +- 支持配置文件在线配置和编辑 +- 搜索页搜索框实时联想 +- 去除对 localstorage 模式的支持 + +### Changed + +- 播放记录删除按钮改为垃圾桶图标以消除歧义 + +### Fixed + +- 限制设置面板的最大长度,防止超出视口 + +## [1.1.1] - 2025-08-12 + +### Changed +- 修正 zwei 提供的 cors proxy 地址 +- 移除废弃代码 + +### Fixed +- [运维] docker workflow release 日期使用东八区日期 + +## [1.1.0] - 2025-08-12 + +### Added +- 每日新番放送功能,展示每日新番放送的番剧 + +### Fixed +- 修复远程 CHANGELOG 无法提取变更内容的问题 + +## [1.0.5] - 2025-08-12 + +### Changed +- 实现基于 Git 标签的自动 Release 工作流 + +## [1.0.4] - 2025-08-11 + +### Added +- 优化版本管理工作流,实现单点修改 + +### Changed +- 版本号现在从 CHANGELOG 自动提取,无需手动维护 VERSION.txt + +## [1.0.3] - 2025-08-11 + +### Changed + +- 升级播放器 Artplayer 至版本 5.2.5 + +## [1.0.2] - 2025-08-11 + +### Changed + +- 版本号比较机制恢复为数字比较,仅当最新版本大于本地版本时才认为有更新 +- [运维] 自动替换 version.ts 中的版本号为 VERSION.txt 中的版本号 + +## [1.0.1] - 2025-08-11 + +### Fixed + +- 修复版本检查功能,只要与最新版本号不一致即认为有更新 + +## [1.0.0] - 2025-08-10 + +### Added + +- 基于 Semantic Versioning 的版本号机制 +- 版本信息面板,展示本地变更日志和远程更新日志 diff --git a/src/app/manga/detail/page.tsx b/src/app/manga/detail/page.tsx index 576d161..7e472fe 100644 --- a/src/app/manga/detail/page.tsx +++ b/src/app/manga/detail/page.tsx @@ -10,6 +10,44 @@ import { MangaChapter, MangaDetail, MangaReadRecord, MangaShelfItem } from '@/li import ProxyImage from '@/components/ProxyImage'; +function MangaDetailSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 8 }).map((_, index) => ( +
+
+
+
+ ))} +
+
+
+ ); +} + function formatChapterMeta(chapter: MangaChapter): string { if (typeof chapter.pageCount === 'number' && chapter.pageCount > 0) { return `${chapter.pageCount} 页`; @@ -104,7 +142,8 @@ export default function MangaDetailPage() { const chapterHref = (chapter: MangaChapter) => `/manga/read?mangaId=${mangaId}&sourceId=${sourceId}&chapterId=${chapter.id}&title=${encodeURIComponent(detail?.title || '')}&cover=${encodeURIComponent(detail?.cover || '')}&sourceName=${encodeURIComponent(detail?.sourceName || '')}&chapterName=${encodeURIComponent(chapter.name)}`; - if (!detail) return
加载中...
; + if (!detail) return ; + return (
diff --git a/src/app/manga/history/page.tsx b/src/app/manga/history/page.tsx index dfc03f1..b52320f 100644 --- a/src/app/manga/history/page.tsx +++ b/src/app/manga/history/page.tsx @@ -8,11 +8,31 @@ import { MangaReadRecord } from '@/lib/manga.types'; import MangaCard from '@/components/MangaCard'; +function MangaHistorySkeleton() { + return ( +
+ {Array.from({ length: 12 }).map((_, index) => ( +
+
+
+
+
+
+
+ ))} +
+ ); +} + export default function MangaHistoryPage() { const [history, setHistory] = useState>({}); + const [loading, setLoading] = useState(true); useEffect(() => { - getAllMangaReadRecords().then(setHistory).catch(() => undefined); + getAllMangaReadRecords() + .then(setHistory) + .catch(() => undefined) + .finally(() => setLoading(false)); }, []); const historyList = useMemo( @@ -25,7 +45,9 @@ export default function MangaHistoryPage() {
共 {historyList.length} 条阅读记录
- {historyList.length === 0 ? ( + {loading ? ( + + ) : historyList.length === 0 ? (
暂无阅读历史
diff --git a/src/app/manga/page.tsx b/src/app/manga/page.tsx index d1d21c4..62bf3aa 100644 --- a/src/app/manga/page.tsx +++ b/src/app/manga/page.tsx @@ -8,6 +8,21 @@ import { MangaSearchItem, MangaShelfItem, MangaSource } from '@/lib/manga.types' import MangaCard from '@/components/MangaCard'; +function MangaCardSkeleton({ withButton = false }: { withButton?: boolean }) { + return ( +
+
+
+
+
+
+
+
+ {withButton &&
} +
+ ); +} + export default function MangaPage() { const [query, setQuery] = useState(''); const [sources, setSources] = useState([]); @@ -108,10 +123,15 @@ export default function MangaPage() {

搜索结果

- {loading && 搜索中...}
{error &&
{error}
} - {results.length === 0 ? ( + {loading ? ( +
+ {Array.from({ length: 12 }).map((_, index) => ( + + ))} +
+ ) : results.length === 0 ? (
请输入关键词开始搜索漫画
diff --git a/src/app/manga/read/page.tsx b/src/app/manga/read/page.tsx index 85c43e7..3658bb7 100644 --- a/src/app/manga/read/page.tsx +++ b/src/app/manga/read/page.tsx @@ -3,8 +3,10 @@ import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import { type MouseEvent, useEffect, useMemo, useRef, useState } from 'react'; +import { ArrowDownWideNarrow, ArrowUpWideNarrow } from 'lucide-react'; -import { saveMangaReadRecord } from '@/lib/db.client'; +import { getAllMangaReadRecords, saveMangaReadRecord } from '@/lib/db.client'; +import type { MangaChapter, MangaDetail, MangaReadRecord } from '@/lib/manga.types'; import ProxyImage from '@/components/ProxyImage'; @@ -14,6 +16,7 @@ type ScaleMode = 'fit' | 'original'; const READ_MODE_STORAGE_KEY = 'mangaReadMode'; const SCALE_MODE_STORAGE_KEY = 'mangaScaleMode'; const PAGE_GAP_STORAGE_KEY = 'mangaPageGap'; +const SAVE_INTERVAL_MS = 10000; const READ_MODE_OPTIONS: Array<{ value: ReadMode; label: string }> = [ { value: 'single', label: '单页' }, @@ -27,6 +30,43 @@ const SCALE_MODE_OPTIONS: Array<{ value: ScaleMode; label: string }> = [ { value: 'original', label: '原始大小' }, ]; +function MangaReadSkeleton({ readMode, pageGap }: { readMode: ReadMode; pageGap: number }) { + if (readMode === 'horizontal') { + return ( +
+ {Array.from({ length: 2 }).map((_, index) => ( +
+
+
+ ))} +
+ ); + } + + if (readMode === 'single' || readMode === 'double') { + return ( +
+
+ {Array.from({ length: readMode === 'double' ? 2 : 1 }).map((_, index) => ( +
+ ))} +
+
+ ); + } + + return ( +
+ {Array.from({ length: 4 }).map((_, index) => ( +
+ ))} +
+ ); +} + export default function MangaReadPage() { const searchParams = useSearchParams(); const mangaId = searchParams.get('mangaId') || ''; @@ -44,9 +84,42 @@ export default function MangaReadPage() { const [pageGap, setPageGap] = useState(0); const [controlsVisible, setControlsVisible] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); + const [chapterListOpen, setChapterListOpen] = useState(false); + const [chapterListDesc, setChapterListDesc] = useState(false); + const [mangaDetail, setMangaDetail] = useState(null); + const [showChapterComplete, setShowChapterComplete] = useState(false); const verticalPageRefs = useRef>([]); const horizontalContainerRef = useRef(null); + const pendingRecordRef = useRef(null); + const pendingRecordDirtyRef = useRef(false); + const saveInFlightRef = useRef(false); + const restoredChapterKeyRef = useRef(null); + const previousReadModeRef = useRef('vertical'); + const requestVerticalPageSyncRef = useRef<(() => void) | null>(null); + const currentVerticalPageIndexRef = useRef(0); + + const getCurrentVerticalPageIndex = () => { + if (!verticalPageRefs.current.length) return 0; + const topAnchor = 80; + let currentIndex = 0; + + for (let index = 0; index < verticalPageRefs.current.length; index += 1) { + const node = verticalPageRefs.current[index]; + if (!node) continue; + + const rect = node.getBoundingClientRect(); + if (rect.top <= topAnchor) { + currentIndex = index; + } + } + + return currentIndex; + }; + + const handleVerticalImageLoad = () => { + requestVerticalPageSyncRef.current?.(); + }; useEffect(() => { if (typeof window === 'undefined') return; @@ -83,6 +156,7 @@ export default function MangaReadPage() { const handleToggleSettings = () => { setSettingsOpen((prev) => !prev); setControlsVisible(false); + setChapterListOpen(false); }; window.addEventListener('manga-read-toggle-settings', handleToggleSettings); @@ -91,6 +165,19 @@ export default function MangaReadPage() { }; }, []); + useEffect(() => { + const handleToggleChapters = () => { + setChapterListOpen((prev) => !prev); + setControlsVisible(false); + setSettingsOpen(false); + }; + + window.addEventListener('manga-read-toggle-chapters', handleToggleChapters); + return () => { + window.removeEventListener('manga-read-toggle-chapters', handleToggleChapters); + }; + }, []); + useEffect(() => { if (!chapterId) return; fetch(`/api/manga/pages?chapterId=${encodeURIComponent(chapterId)}`) @@ -99,27 +186,145 @@ export default function MangaReadPage() { .catch(() => setPages([])); }, [chapterId]); + useEffect(() => { + if (!mangaId || !sourceId) return; + + const params = new URLSearchParams({ + mangaId, + sourceId, + title, + cover, + sourceName, + }); + + fetch(`/api/manga/detail?${params.toString()}`) + .then((res) => res.json()) + .then((data) => setMangaDetail(data)) + .catch(() => setMangaDetail(null)); + }, [cover, mangaId, sourceId, sourceName, title]); + useEffect(() => { setActivePage(0); - }, [chapterId, readMode]); + restoredChapterKeyRef.current = null; + }, [chapterId]); + + useEffect(() => { + if (!pages.length || !mangaId || !sourceId || !chapterId) return; + + const chapterKey = `${sourceId}+${mangaId}+${chapterId}`; + if (restoredChapterKeyRef.current === chapterKey) return; + + let cancelled = false; + + getAllMangaReadRecords() + .then((records) => { + if (cancelled) return; + + const record = records[`${sourceId}+${mangaId}`]; + if (!record || record.chapterId !== chapterId) { + restoredChapterKeyRef.current = chapterKey; + return; + } + + const nextPage = Math.min(Math.max(record.pageIndex || 0, 0), Math.max(pages.length - 1, 0)); + setActivePage(nextPage); + restoredChapterKeyRef.current = chapterKey; + + if (readMode === 'vertical') { + window.setTimeout(() => { + const node = verticalPageRefs.current[nextPage]; + node?.scrollIntoView({ block: 'start' }); + }, 0); + return; + } + + if (readMode === 'horizontal') { + window.setTimeout(() => { + const container = horizontalContainerRef.current; + if (!container) return; + container.scrollTo({ + left: container.clientWidth * nextPage, + behavior: 'auto', + }); + }, 0); + } + }) + .catch(() => undefined); + + return () => { + cancelled = true; + }; + }, [chapterId, mangaId, pages.length, readMode, sourceId]); + + useEffect(() => { + setShowChapterComplete(false); + }, [activePage, chapterId, readMode]); useEffect(() => { if (readMode !== 'vertical' || !pages.length || !mangaId || !sourceId || !chapterId) return; - const observer = new IntersectionObserver( - (entries) => { - const visible = entries - .filter((entry) => entry.isIntersecting) - .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0]; - if (!visible) return; - const index = Number((visible.target as HTMLElement).dataset.index || 0); - setActivePage(index); - }, - { rootMargin: '-15% 0px -70% 0px', threshold: 0.2 } - ); + let ticking = false; + let rafId = 0; + let lastScrollTop = -1; + let lastInnerHeight = -1; + const scrollingElement = document.scrollingElement || document.documentElement; - verticalPageRefs.current.forEach((node) => node && observer.observe(node)); - return () => observer.disconnect(); + const updateActivePageFromViewport = () => { + ticking = false; + const nextIndex = getCurrentVerticalPageIndex(); + currentVerticalPageIndexRef.current = nextIndex; + + setActivePage((prev) => (prev === nextIndex ? prev : nextIndex)); + }; + + const requestUpdate = () => { + if (ticking) return; + ticking = true; + window.requestAnimationFrame(updateActivePageFromViewport); + }; + + requestVerticalPageSyncRef.current = requestUpdate; + + const watchScrollPosition = () => { + const nextScrollTop = scrollingElement.scrollTop; + const nextInnerHeight = window.innerHeight; + + if (nextScrollTop !== lastScrollTop || nextInnerHeight !== lastInnerHeight) { + lastScrollTop = nextScrollTop; + lastInnerHeight = nextInnerHeight; + requestUpdate(); + } + + rafId = window.requestAnimationFrame(watchScrollPosition); + }; + + requestUpdate(); + rafId = window.requestAnimationFrame(watchScrollPosition); + window.addEventListener('scroll', requestUpdate, { passive: true }); + document.addEventListener('scroll', requestUpdate, { passive: true, capture: true }); + window.addEventListener('resize', requestUpdate); + + let resizeObserver: ResizeObserver | null = null; + if (typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver(() => { + requestUpdate(); + }); + + verticalPageRefs.current.forEach((node) => { + if (node) { + resizeObserver?.observe(node); + } + }); + } + + return () => { + requestVerticalPageSyncRef.current = null; + window.cancelAnimationFrame(rafId); + window.removeEventListener('scroll', requestUpdate); + document.removeEventListener('scroll', requestUpdate, true); + window.removeEventListener('resize', requestUpdate); + resizeObserver?.disconnect(); + }; }, [readMode, pages, mangaId, sourceId, chapterId]); useEffect(() => { @@ -138,28 +343,137 @@ export default function MangaReadPage() { }, [readMode, pages.length]); useEffect(() => { - if (!pages.length || !mangaId || !sourceId || !chapterId) return; - const timer = window.setTimeout(() => { - saveMangaReadRecord(sourceId, mangaId, { - title, - cover, - sourceId, - sourceName, - mangaId, - chapterId, - chapterName, - pageIndex: activePage, - pageCount: pages.length, - saveTime: Date.now(), - }).catch(() => undefined); - }, 300); + if (!pages.length) { + previousReadModeRef.current = readMode; + return; + } - return () => window.clearTimeout(timer); - }, [activePage, chapterId, chapterName, cover, mangaId, pages.length, sourceId, sourceName, title]); + const previousReadMode = previousReadModeRef.current; + previousReadModeRef.current = readMode; + if (previousReadMode === readMode) return; + + const targetPage = Math.min(Math.max(activePage, 0), Math.max(pages.length - 1, 0)); + + if (readMode === 'vertical') { + window.setTimeout(() => { + const node = verticalPageRefs.current[targetPage]; + node?.scrollIntoView({ block: 'start' }); + }, 0); + return; + } + + if (readMode === 'horizontal') { + window.setTimeout(() => { + const container = horizontalContainerRef.current; + if (!container) return; + container.scrollTo({ + left: container.clientWidth * targetPage, + behavior: 'auto', + }); + }, 0); + } + }, [activePage, pages.length, readMode]); + + useEffect(() => { + if (!pages.length || !mangaId || !sourceId || !chapterId) return; + const currentPageIndex = readMode === 'vertical' ? currentVerticalPageIndexRef.current : activePage; + + pendingRecordRef.current = { + title, + cover, + sourceId, + sourceName, + mangaId, + chapterId, + chapterName, + pageIndex: currentPageIndex, + pageCount: pages.length, + saveTime: Date.now(), + }; + pendingRecordDirtyRef.current = true; + }, [activePage, chapterId, chapterName, cover, mangaId, pages.length, readMode, sourceId, sourceName, title]); + + useEffect(() => { + if (!mangaId || !sourceId || !chapterId) return; + + const flushPendingRecord = () => { + const record = pendingRecordRef.current; + if (!record || !pendingRecordDirtyRef.current || saveInFlightRef.current) return; + + const recordToSave = + readMode === 'vertical' + ? { + ...record, + pageIndex: getCurrentVerticalPageIndex(), + } + : record; + + saveInFlightRef.current = true; + saveMangaReadRecord(sourceId, mangaId, { + ...recordToSave, + saveTime: Date.now(), + }) + .then(() => { + pendingRecordRef.current = recordToSave; + pendingRecordDirtyRef.current = false; + }) + .catch(() => undefined) + .finally(() => { + saveInFlightRef.current = false; + }); + }; + + const flushPendingRecordOnLeave = () => { + if (!pendingRecordDirtyRef.current || saveInFlightRef.current) return; + flushPendingRecord(); + }; + + const handleVisibilityChange = () => { + if (document.visibilityState === 'hidden') { + flushPendingRecordOnLeave(); + } + }; + + const intervalId = window.setInterval(flushPendingRecord, SAVE_INTERVAL_MS); + window.addEventListener('pagehide', flushPendingRecordOnLeave); + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + window.clearInterval(intervalId); + window.removeEventListener('pagehide', flushPendingRecordOnLeave); + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [chapterId, mangaId, readMode, sourceId]); const hideTransientUi = () => { setControlsVisible(false); setSettingsOpen(false); + setChapterListOpen(false); + setShowChapterComplete(false); + }; + + const isAtChapterEnd = () => { + if (!pages.length) return false; + + if (readMode === 'double') { + return activePage >= Math.max(pages.length - 2, 0); + } + + if (readMode === 'vertical') { + const scrollBottom = window.scrollY + window.innerHeight; + const pageBottom = document.documentElement.scrollHeight; + return activePage >= pages.length - 1 && scrollBottom >= pageBottom - 24; + } + + return activePage >= pages.length - 1; + }; + + const openChapterComplete = () => { + if (!isAtChapterEnd()) return false; + setShowChapterComplete(true); + setControlsVisible(false); + setSettingsOpen(false); + return true; }; const clampPage = (page: number) => { @@ -197,17 +511,20 @@ export default function MangaReadPage() { const goNext = () => { if (!pages.length) return; if (readMode === 'vertical') { + if (openChapterComplete()) return; window.scrollBy({ top: window.innerHeight * 0.85, behavior: 'smooth' }); hideTransientUi(); return; } if (readMode === 'horizontal') { + if (openChapterComplete()) return; const nextPage = clampPage(activePage + 1); setActivePage(nextPage); scrollHorizontalToPage(nextPage); hideTransientUi(); return; } + if (openChapterComplete()) return; setActivePage((prev) => clampPage(prev + (readMode === 'double' ? 2 : 1))); hideTransientUi(); }; @@ -217,6 +534,36 @@ export default function MangaReadPage() { [activePage, pages.length] ); + const nextChapter = useMemo(() => { + const chapters = mangaDetail?.chapters || []; + if (!chapters.length) return null; + + const orderedChapters = [...chapters].sort((a, b) => { + const chapterDiff = (a.chapterNumber ?? 0) - (b.chapterNumber ?? 0); + if (chapterDiff !== 0) return chapterDiff; + return (a.uploadDate ?? 0) - (b.uploadDate ?? 0); + }); + + const currentIndex = orderedChapters.findIndex((item) => item.id === chapterId); + if (currentIndex === -1 || currentIndex >= orderedChapters.length - 1) return null; + + return orderedChapters[currentIndex + 1]; + }, [chapterId, mangaDetail?.chapters]); + + const chapterList = useMemo(() => { + const chapters = mangaDetail?.chapters || []; + return [...chapters].sort((a, b) => { + const chapterDiff = (a.chapterNumber ?? 0) - (b.chapterNumber ?? 0); + if (chapterDiff !== 0) return chapterDiff; + return (a.uploadDate ?? 0) - (b.uploadDate ?? 0); + }); + }, [mangaDetail?.chapters]); + + const orderedChapterList = useMemo( + () => (chapterListDesc ? [...chapterList].reverse() : chapterList), + [chapterList, chapterListDesc] + ); + const pagedItems = useMemo(() => { if (readMode === 'single') { return pages[activePage] ? [pages[activePage]] : []; @@ -231,11 +578,20 @@ export default function MangaReadPage() { if (scaleMode === 'original') { return 'mx-auto h-auto w-auto max-w-none object-none'; } + if (readMode === 'single' || readMode === 'double') { + return 'mx-auto max-h-[calc(100vh-8rem)] w-auto max-w-full object-contain'; + } return 'h-auto w-full object-contain'; - }, [scaleMode]); + }, [readMode, scaleMode]); const handleReaderClick = (event: MouseEvent) => { if (settingsOpen) return; + if (readMode === 'vertical') { + setControlsVisible((prev) => !prev); + setShowChapterComplete(false); + return; + } + const { clientX } = event; const width = window.innerWidth; const leftBoundary = width / 3; @@ -341,10 +697,85 @@ export default function MangaReadPage() {
)} + {chapterListOpen && ( +
setChapterListOpen(false)}> +
event.stopPropagation()} + > +
+
+ +
+
+ {orderedChapterList.map((chapter) => { + const active = chapter.id === chapterId; + return ( + setChapterListOpen(false)} + > + {chapter.name} +
+
{chapter.name}
+
+ + ); + })} +
+
+
+
+ )} +
+ {showChapterComplete && ( +
setShowChapterComplete(false)}> +
event.stopPropagation()} + > +
{chapterName} 阅读完毕
+
+ {nextChapter ? '当前章节已读完,可继续阅读下一话' : '当前章节已读完'} +
+
+ {nextChapter ? ( + + 下一话:{nextChapter.name} + + ) : null} + +
+
+
+ )} +
- {pages.length === 0 ? ( -
- 加载漫画图片中... + {pages.length > 0 && ( +
+ {Math.min(activePage + 1, pages.length)}/{pages.length}
+ )} + + {pages.length === 0 && } + + {pages.length === 0 ? ( + null ) : readMode === 'vertical' ? (
{pages.map((page, index) => ( @@ -369,9 +806,14 @@ export default function MangaReadPage() { verticalPageRefs.current[index] = node; }} data-index={index} - className='overflow-hidden rounded-[24px] bg-gray-100 shadow-sm dark:bg-gray-900' + className='overflow-hidden bg-gray-100 shadow-sm dark:bg-gray-900' > - +
))}
@@ -383,7 +825,7 @@ export default function MangaReadPage() { > {pages.map((page, index) => (
-
+
@@ -395,7 +837,7 @@ export default function MangaReadPage() { {pagedItems.map((page, index) => (
+ {Array.from({ length: 12 }).map((_, index) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+ ); +} + export default function MangaShelfPage() { const [shelf, setShelf] = useState>({}); + const [loading, setLoading] = useState(true); useEffect(() => { - getAllMangaShelf().then(setShelf).catch(() => undefined); + getAllMangaShelf() + .then(setShelf) + .catch(() => undefined) + .finally(() => setLoading(false)); }, []); const shelfList = useMemo( @@ -35,7 +58,9 @@ export default function MangaShelfPage() {
共 {shelfList.length} 本漫画
- {shelfList.length === 0 ? ( + {loading ? ( + + ) : shelfList.length === 0 ? (
暂无书架内容
diff --git a/src/components/manga/MangaLayout.tsx b/src/components/manga/MangaLayout.tsx index 29b3fb4..f5bd42d 100644 --- a/src/components/manga/MangaLayout.tsx +++ b/src/components/manga/MangaLayout.tsx @@ -1,6 +1,6 @@ -'use client'; +'use client'; -import { BookOpen, ChevronLeft, History, Home, Search, Settings2 } from 'lucide-react'; +import { BookOpen, ChevronLeft, History, Home, List, Search, Settings2 } from 'lucide-react'; import Link from 'next/link'; import { usePathname, useSearchParams } from 'next/navigation'; @@ -62,66 +62,83 @@ export default function MangaLayout({ children }: MangaLayoutProps) { return (
-
-
- {meta.backHref ? ( - - - - ) : ( - - {siteName} - - )} -
+
+
+ {meta.backHref ? ( + + + + ) : ( + + {siteName} + + )} +
+
{meta.title}
- {meta.subtitle && ( -
- {meta.subtitle} -
- )} +
+
{meta.title}
+
+ {meta.subtitle && ( +
+ {meta.subtitle} +
+ )}
+
- +
{isReadingPage ? ( - + <> + + + ) : ( <> @@ -134,15 +151,16 @@ export default function MangaLayout({ children }: MangaLayoutProps) {
{children}
{!isReadingPage && (