优化漫画界面
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# 强制所有文本文件在仓库中用 LF
|
||||
* text=auto eol=lf
|
||||
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"Region CSS": {
|
||||
"prefix": "regc",
|
||||
"body": [
|
||||
"/* #region /**=========== ${1} =========== */",
|
||||
"$0",
|
||||
"/* #endregion /**======== ${1} =========== */"
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
// Tailwind CSS Intellisense
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"aaron-bond.better-comments"
|
||||
]
|
||||
}
|
||||
Vendored
-17
@@ -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": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/start"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-17
@@ -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"
|
||||
}
|
||||
-193
@@ -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 (",
|
||||
" <div>",
|
||||
" $0",
|
||||
" </div>",
|
||||
" )",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"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 (",
|
||||
" <div className={clsxm(['', className])} {...rest}>",
|
||||
" $0",
|
||||
" </div>",
|
||||
" )",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
//#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 (",
|
||||
" <Layout>",
|
||||
" <Seo templateTitle='${1:${TM_FILENAME_BASE/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}}' />\n",
|
||||
" <main>\n",
|
||||
" <section className=''>",
|
||||
" <div className='layout py-20 min-h-screen'>",
|
||||
" $0",
|
||||
" </div>",
|
||||
" </section>",
|
||||
" </main>",
|
||||
" </Layout>",
|
||||
" )",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"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<typeof getStaticProps>"
|
||||
},
|
||||
"Infer Get Server Side Props": {
|
||||
"prefix": "igssp",
|
||||
"body": "InferGetServerSidePropsType<typeof getServerSideProps>"
|
||||
},
|
||||
"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}')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 的版本号机制
|
||||
- 版本信息面板,展示本地变更日志和远程更新日志
|
||||
|
||||
@@ -10,6 +10,44 @@ import { MangaChapter, MangaDetail, MangaReadRecord, MangaShelfItem } from '@/li
|
||||
|
||||
import ProxyImage from '@/components/ProxyImage';
|
||||
|
||||
function MangaDetailSkeleton() {
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<div className='grid gap-6 rounded-[28px] border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-950 md:grid-cols-[260px_1fr]'>
|
||||
<div className='aspect-[3/4] animate-pulse rounded-3xl bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='space-y-4'>
|
||||
<div className='h-8 w-2/3 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='flex gap-2'>
|
||||
<div className='h-7 w-24 animate-pulse rounded-full bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='h-7 w-20 animate-pulse rounded-full bg-gray-200 dark:bg-gray-800' />
|
||||
</div>
|
||||
<div className='space-y-3'>
|
||||
<div className='h-4 w-full animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='h-4 w-11/12 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='h-4 w-4/5 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-3'>
|
||||
<div className='h-12 w-32 animate-pulse rounded-2xl bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='h-12 w-40 animate-pulse rounded-2xl bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='h-12 w-32 animate-pulse rounded-2xl bg-gray-200 dark:bg-gray-800' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-[28px] border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-950'>
|
||||
<div className='mb-4 h-6 w-32 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='space-y-3'>
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={index} className='rounded-2xl border border-gray-200 px-4 py-3 dark:border-gray-700'>
|
||||
<div className='h-4 w-1/3 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='mt-2 h-3 w-1/4 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 <div className='text-sm text-gray-500'>加载中...</div>;
|
||||
if (!detail) return <MangaDetailSkeleton />;
|
||||
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
|
||||
@@ -8,11 +8,31 @@ import { MangaReadRecord } from '@/lib/manga.types';
|
||||
|
||||
import MangaCard from '@/components/MangaCard';
|
||||
|
||||
function MangaHistorySkeleton() {
|
||||
return (
|
||||
<div className='grid grid-cols-2 gap-4 md:grid-cols-4 xl:grid-cols-6'>
|
||||
{Array.from({ length: 12 }).map((_, index) => (
|
||||
<div key={index} className='overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-950'>
|
||||
<div className='aspect-[3/4] w-full animate-pulse bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='space-y-3 p-3'>
|
||||
<div className='h-4 w-3/4 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='h-3 w-2/3 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MangaHistoryPage() {
|
||||
const [history, setHistory] = useState<Record<string, MangaReadRecord>>({});
|
||||
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() {
|
||||
<div className='mb-4 flex items-center gap-2 text-sm text-gray-500'>
|
||||
<History className='h-4 w-4 text-violet-500' /> 共 {historyList.length} 条阅读记录
|
||||
</div>
|
||||
{historyList.length === 0 ? (
|
||||
{loading ? (
|
||||
<MangaHistorySkeleton />
|
||||
) : historyList.length === 0 ? (
|
||||
<div className='rounded-2xl bg-gray-50 p-10 text-center text-sm text-gray-500 dark:bg-gray-900/50'>
|
||||
暂无阅读历史
|
||||
</div>
|
||||
|
||||
+22
-2
@@ -8,6 +8,21 @@ import { MangaSearchItem, MangaShelfItem, MangaSource } from '@/lib/manga.types'
|
||||
|
||||
import MangaCard from '@/components/MangaCard';
|
||||
|
||||
function MangaCardSkeleton({ withButton = false }: { withButton?: boolean }) {
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<div className='overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-950'>
|
||||
<div className='aspect-[3/4] w-full animate-pulse bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='space-y-3 p-3'>
|
||||
<div className='h-4 w-3/4 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='h-3 w-1/2 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
</div>
|
||||
</div>
|
||||
{withButton && <div className='h-9 w-full animate-pulse rounded-2xl bg-gray-200 dark:bg-gray-800' />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MangaPage() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [sources, setSources] = useState<MangaSource[]>([]);
|
||||
@@ -108,10 +123,15 @@ export default function MangaPage() {
|
||||
<section>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<h2 className='text-lg font-semibold'>搜索结果</h2>
|
||||
{loading && <span className='text-sm text-gray-500'>搜索中...</span>}
|
||||
</div>
|
||||
{error && <div className='mb-4 text-sm text-red-500'>{error}</div>}
|
||||
{results.length === 0 ? (
|
||||
{loading ? (
|
||||
<div className='grid grid-cols-2 gap-4 md:grid-cols-4 xl:grid-cols-6'>
|
||||
{Array.from({ length: 12 }).map((_, index) => (
|
||||
<MangaCardSkeleton key={index} withButton />
|
||||
))}
|
||||
</div>
|
||||
) : results.length === 0 ? (
|
||||
<div className='rounded-2xl bg-gray-50 p-10 text-center text-sm text-gray-500 dark:bg-gray-900/50'>
|
||||
请输入关键词开始搜索漫画
|
||||
</div>
|
||||
|
||||
+482
-40
@@ -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 (
|
||||
<div className='flex min-h-[calc(100vh-8rem)] overflow-hidden' style={{ gap: `${pageGap}px` }}>
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<div key={index} className='flex min-w-full items-center justify-center px-1'>
|
||||
<div className='h-full min-h-[calc(100vh-8rem)] w-full animate-pulse bg-gray-100 dark:bg-gray-900' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (readMode === 'single' || readMode === 'double') {
|
||||
return (
|
||||
<div className='flex min-h-[calc(100vh-8rem)] items-center justify-center'>
|
||||
<div
|
||||
className={`grid w-full max-w-6xl ${readMode === 'double' ? 'md:grid-cols-2' : 'grid-cols-1'}`}
|
||||
style={{ gap: `${pageGap}px` }}
|
||||
>
|
||||
{Array.from({ length: readMode === 'double' ? 2 : 1 }).map((_, index) => (
|
||||
<div key={index} className='min-h-[calc(100vh-8rem)] animate-pulse bg-gray-100 dark:bg-gray-900' />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col' style={{ gap: `${pageGap}px` }}>
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div key={index} className='aspect-[3/4] animate-pulse bg-gray-100 dark:bg-gray-900' />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<MangaDetail | null>(null);
|
||||
const [showChapterComplete, setShowChapterComplete] = useState(false);
|
||||
|
||||
const verticalPageRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||
const horizontalContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const pendingRecordRef = useRef<MangaReadRecord | null>(null);
|
||||
const pendingRecordDirtyRef = useRef(false);
|
||||
const saveInFlightRef = useRef(false);
|
||||
const restoredChapterKeyRef = useRef<string | null>(null);
|
||||
const previousReadModeRef = useRef<ReadMode>('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<MangaChapter | null>(() => {
|
||||
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<HTMLDivElement>) => {
|
||||
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() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{chapterListOpen && (
|
||||
<div className='fixed inset-0 z-40 bg-black/30' onClick={() => setChapterListOpen(false)}>
|
||||
<div
|
||||
className='absolute right-0 top-14 h-[calc(100vh-3.5rem)] w-full max-w-sm overflow-y-auto border-l border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-950 sm:top-16 sm:h-[calc(100vh-4rem)]'
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className='p-4'>
|
||||
<div className='mb-3 flex items-center justify-end'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex items-center gap-2 rounded-2xl border border-gray-200 px-3 py-2 text-sm text-gray-700 transition hover:bg-gray-100 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-900'
|
||||
onClick={() => setChapterListDesc((prev) => !prev)}
|
||||
>
|
||||
{chapterListDesc ? <ArrowDownWideNarrow className='h-4 w-4' /> : <ArrowUpWideNarrow className='h-4 w-4' />}
|
||||
{chapterListDesc ? '倒序' : '正序'}
|
||||
</button>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
{orderedChapterList.map((chapter) => {
|
||||
const active = chapter.id === chapterId;
|
||||
return (
|
||||
<Link
|
||||
key={chapter.id}
|
||||
href={`/manga/read?mangaId=${mangaId}&sourceId=${sourceId}&chapterId=${chapter.id}&title=${encodeURIComponent(title)}&cover=${encodeURIComponent(cover)}&sourceName=${encodeURIComponent(sourceName)}&chapterName=${encodeURIComponent(chapter.name)}`}
|
||||
className={`group relative block rounded-2xl px-4 py-3 text-sm transition ${
|
||||
active
|
||||
? 'bg-sky-600 text-white'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-900'
|
||||
}`}
|
||||
onClick={() => setChapterListOpen(false)}
|
||||
>
|
||||
<span className='block truncate'>{chapter.name}</span>
|
||||
<div className='absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-gray-800 dark:bg-gray-900 text-white text-sm rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 ease-out whitespace-nowrap z-[100] pointer-events-none'>
|
||||
<div className='text-sm'>{chapter.name}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className='relative min-h-[calc(100vh-5rem)] select-none px-2 py-3 sm:px-3'
|
||||
onClick={handleReaderClick}
|
||||
>
|
||||
{showChapterComplete && (
|
||||
<div className='fixed inset-0 z-30 flex items-center justify-center bg-black/60 px-4' onClick={() => setShowChapterComplete(false)}>
|
||||
<div
|
||||
className='w-full max-w-sm rounded-3xl border border-gray-200 bg-white p-6 text-center shadow-xl dark:border-gray-700 dark:bg-gray-950'
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className='text-lg font-semibold text-gray-900 dark:text-gray-100'>{chapterName} 阅读完毕</div>
|
||||
<div className='mt-2 text-sm text-gray-500 dark:text-gray-400'>
|
||||
{nextChapter ? '当前章节已读完,可继续阅读下一话' : '当前章节已读完'}
|
||||
</div>
|
||||
<div className='mt-6 flex flex-col gap-3'>
|
||||
{nextChapter ? (
|
||||
<Link
|
||||
href={`/manga/read?mangaId=${mangaId}&sourceId=${sourceId}&chapterId=${nextChapter.id}&title=${encodeURIComponent(title)}&cover=${encodeURIComponent(cover)}&sourceName=${encodeURIComponent(sourceName)}&chapterName=${encodeURIComponent(nextChapter.name)}`}
|
||||
className='rounded-2xl bg-sky-600 px-4 py-3 text-sm font-medium text-white transition hover:bg-sky-700'
|
||||
>
|
||||
下一话:{nextChapter.name}
|
||||
</Link>
|
||||
) : null}
|
||||
<button
|
||||
type='button'
|
||||
className='rounded-2xl border border-gray-200 px-4 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-900'
|
||||
onClick={() => setShowChapterComplete(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`fixed right-3 top-1/2 z-20 h-40 w-1 -translate-y-1/2 overflow-hidden rounded-full bg-gray-200/80 transition-all duration-200 dark:bg-gray-700/80 ${
|
||||
controlsVisible ? 'opacity-100' : 'pointer-events-none opacity-0'
|
||||
@@ -356,10 +787,16 @@ export default function MangaReadPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{pages.length === 0 ? (
|
||||
<div className='rounded-[24px] bg-gray-50 p-10 text-center text-sm text-gray-500 dark:bg-gray-900/50'>
|
||||
加载漫画图片中...
|
||||
{pages.length > 0 && (
|
||||
<div className='pointer-events-none fixed bottom-4 left-1/2 z-20 -translate-x-1/2 rounded-full bg-black/15 px-2 py-0.5 text-sm font-medium text-white/90 backdrop-blur-sm dark:bg-white/10 dark:text-white/85'>
|
||||
{Math.min(activePage + 1, pages.length)}/{pages.length}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pages.length === 0 && <MangaReadSkeleton readMode={readMode} pageGap={pageGap} />}
|
||||
|
||||
{pages.length === 0 ? (
|
||||
null
|
||||
) : readMode === 'vertical' ? (
|
||||
<div className='flex flex-col' style={{ gap: `${pageGap}px` }}>
|
||||
{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'
|
||||
>
|
||||
<ProxyImage originalSrc={page} alt={`${chapterName}-${index + 1}`} className={imageClassName} />
|
||||
<ProxyImage
|
||||
originalSrc={page}
|
||||
alt={`${chapterName}-${index + 1}`}
|
||||
className={imageClassName}
|
||||
onLoad={handleVerticalImageLoad}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -383,7 +825,7 @@ export default function MangaReadPage() {
|
||||
>
|
||||
{pages.map((page, index) => (
|
||||
<div key={`${page}-${index}`} className='flex min-w-full snap-center items-center justify-center px-1'>
|
||||
<div className='w-full overflow-hidden rounded-[24px] bg-gray-100 shadow-sm dark:bg-gray-900'>
|
||||
<div className='w-full overflow-hidden bg-gray-100 shadow-sm dark:bg-gray-900'>
|
||||
<ProxyImage originalSrc={page} alt={`${chapterName}-${index + 1}`} className={imageClassName} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -395,7 +837,7 @@ export default function MangaReadPage() {
|
||||
{pagedItems.map((page, index) => (
|
||||
<div
|
||||
key={`${page}-${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'
|
||||
>
|
||||
<ProxyImage
|
||||
originalSrc={page}
|
||||
|
||||
@@ -8,11 +8,34 @@ import { MangaShelfItem } from '@/lib/manga.types';
|
||||
|
||||
import MangaCard from '@/components/MangaCard';
|
||||
|
||||
function MangaShelfSkeleton() {
|
||||
return (
|
||||
<div className='grid grid-cols-2 gap-4 md:grid-cols-4 xl:grid-cols-6'>
|
||||
{Array.from({ length: 12 }).map((_, index) => (
|
||||
<div key={index} className='space-y-2'>
|
||||
<div className='overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-950'>
|
||||
<div className='aspect-[3/4] w-full animate-pulse bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='space-y-3 p-3'>
|
||||
<div className='h-4 w-3/4 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
<div className='h-3 w-1/2 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-9 w-full animate-pulse rounded-2xl bg-gray-200 dark:bg-gray-800' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MangaShelfPage() {
|
||||
const [shelf, setShelf] = useState<Record<string, MangaShelfItem>>({});
|
||||
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() {
|
||||
<div className='mb-4 flex items-center gap-2 text-sm text-gray-500'>
|
||||
<BookOpen className='h-4 w-4 text-emerald-500' /> 共 {shelfList.length} 本漫画
|
||||
</div>
|
||||
{shelfList.length === 0 ? (
|
||||
{loading ? (
|
||||
<MangaShelfSkeleton />
|
||||
) : shelfList.length === 0 ? (
|
||||
<div className='rounded-2xl bg-gray-50 p-10 text-center text-sm text-gray-500 dark:bg-gray-900/50'>
|
||||
暂无书架内容
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className='min-h-screen bg-gray-50 text-gray-900 dark:bg-black dark:text-gray-100'>
|
||||
<header className='fixed inset-x-0 top-0 z-[999] border-b border-gray-200/70 bg-white/85 backdrop-blur-xl shadow-sm dark:border-gray-800/80 dark:bg-gray-950/85'>
|
||||
<div className='mx-auto flex h-14 max-w-7xl items-center gap-3 px-3 sm:h-16 sm:px-6'>
|
||||
<div className='flex min-w-0 items-center gap-2'>
|
||||
{meta.backHref ? (
|
||||
<Link
|
||||
href={meta.backHref}
|
||||
className='flex h-10 w-10 items-center justify-center rounded-full text-gray-600 transition hover:bg-gray-100 hover:text-sky-600 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
>
|
||||
<ChevronLeft className='h-5 w-5' />
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href='/'
|
||||
className='flex h-10 items-center rounded-full px-3 text-sm font-semibold text-sky-600 transition hover:bg-sky-50 dark:hover:bg-sky-950/40'
|
||||
>
|
||||
{siteName}
|
||||
</Link>
|
||||
)}
|
||||
<div className='min-w-0'>
|
||||
<div className='mx-auto flex h-14 max-w-7xl items-center gap-3 px-3 sm:h-16 sm:px-6'>
|
||||
<div className='flex min-w-0 items-center gap-2'>
|
||||
{meta.backHref ? (
|
||||
<Link
|
||||
href={meta.backHref}
|
||||
className='flex h-10 w-10 items-center justify-center rounded-full text-gray-600 transition hover:bg-gray-100 hover:text-sky-600 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
>
|
||||
<ChevronLeft className='h-5 w-5' />
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href='/'
|
||||
className='flex h-10 items-center rounded-full px-3 text-sm font-semibold text-sky-600 transition hover:bg-sky-50 dark:hover:bg-sky-950/40'
|
||||
>
|
||||
{siteName}
|
||||
</Link>
|
||||
)}
|
||||
<div className='min-w-0'>
|
||||
<div className='group relative'>
|
||||
<div className='truncate text-sm font-semibold sm:text-base'>{meta.title}</div>
|
||||
{meta.subtitle && (
|
||||
<div className='truncate text-xs text-gray-500 dark:text-gray-400'>
|
||||
{meta.subtitle}
|
||||
</div>
|
||||
)}
|
||||
<div className='absolute top-full left-1/2 transform -translate-x-1/2 mt-2 px-3 py-2 bg-gray-800 dark:bg-gray-900 text-white text-sm rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 ease-out z-[100] pointer-events-none w-max max-w-[85vw] whitespace-normal break-words text-center sm:max-w-none sm:whitespace-nowrap'>
|
||||
<div className='text-sm'>{meta.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
{meta.subtitle && (
|
||||
<div className='truncate text-xs text-gray-500 dark:text-gray-400'>
|
||||
{meta.subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className='ml-auto hidden items-center gap-2 lg:flex'>
|
||||
{sectionTabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const active = isActive(tab.href);
|
||||
return (
|
||||
<Link
|
||||
key={tab.href}
|
||||
href={tab.href}
|
||||
className={`inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm transition ${
|
||||
active
|
||||
? 'bg-sky-600 text-white shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-100 hover:text-sky-600 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Icon className='h-4 w-4' />
|
||||
{tab.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<nav className='ml-auto hidden items-center gap-2 lg:flex'>
|
||||
{sectionTabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const active = isActive(tab.href);
|
||||
return (
|
||||
<Link
|
||||
key={tab.href}
|
||||
href={tab.href}
|
||||
className={`inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm transition ${
|
||||
active
|
||||
? 'bg-sky-600 text-white shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-100 hover:text-sky-600 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Icon className='h-4 w-4' />
|
||||
{tab.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className={`${isReadingPage ? 'flex' : 'hidden md:flex'} items-center gap-2`}>
|
||||
{isReadingPage ? (
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex h-10 w-10 items-center justify-center rounded-full text-gray-600 transition hover:bg-gray-100 hover:text-sky-600 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
onClick={() => {
|
||||
window.dispatchEvent(new CustomEvent('manga-read-toggle-settings'));
|
||||
}}
|
||||
aria-label='阅读设置'
|
||||
>
|
||||
<Settings2 className='h-5 w-5' />
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex h-10 w-10 items-center justify-center rounded-full text-gray-600 transition hover:bg-gray-100 hover:text-sky-600 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
onClick={() => {
|
||||
window.dispatchEvent(new CustomEvent('manga-read-toggle-chapters'));
|
||||
}}
|
||||
aria-label='章节列表'
|
||||
>
|
||||
<List className='h-5 w-5' />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex h-10 w-10 items-center justify-center rounded-full text-gray-600 transition hover:bg-gray-100 hover:text-sky-600 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
onClick={() => {
|
||||
window.dispatchEvent(new CustomEvent('manga-read-toggle-settings'));
|
||||
}}
|
||||
aria-label='阅读设置'
|
||||
>
|
||||
<Settings2 className='h-5 w-5' />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ThemeToggle />
|
||||
@@ -134,15 +151,16 @@ export default function MangaLayout({ children }: MangaLayoutProps) {
|
||||
</header>
|
||||
|
||||
<main
|
||||
className='mx-auto max-w-7xl px-3 pb-24 pt-20 sm:px-6 sm:pb-28 sm:pt-24'
|
||||
style={{ paddingBottom: isReadingPage ? undefined : 'calc(5rem + env(safe-area-inset-bottom))' }}
|
||||
className={`mx-auto max-w-7xl px-3 pt-20 sm:px-6 sm:pt-24 ${
|
||||
isReadingPage ? 'pb-24 sm:pb-28 lg:pb-10' : 'pb-[calc(5rem+env(safe-area-inset-bottom))] sm:pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-10'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{!isReadingPage && (
|
||||
<nav
|
||||
className='fixed inset-x-0 bottom-0 z-[998] border-t border-gray-200/70 bg-white/92 backdrop-blur-xl dark:border-gray-800/80 dark:bg-gray-950/92'
|
||||
className='fixed inset-x-0 bottom-0 z-[998] border-t border-gray-200/70 bg-white/92 backdrop-blur-xl dark:border-gray-800/80 dark:bg-gray-950/92 lg:hidden'
|
||||
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
|
||||
>
|
||||
<div className='mx-auto grid max-w-3xl grid-cols-4'>
|
||||
@@ -157,16 +175,12 @@ export default function MangaLayout({ children }: MangaLayoutProps) {
|
||||
>
|
||||
<Icon
|
||||
className={`h-5 w-5 ${
|
||||
active
|
||||
? 'text-sky-600 dark:text-sky-400'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
active ? 'text-sky-600 dark:text-sky-400' : 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={
|
||||
active
|
||||
? 'text-sky-600 dark:text-sky-400'
|
||||
: 'text-gray-600 dark:text-gray-300'
|
||||
active ? 'text-sky-600 dark:text-sky-400' : 'text-gray-600 dark:text-gray-300'
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
|
||||
Reference in New Issue
Block a user