diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..209072a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.13-slim + +# 设置工作目录 +WORKDIR /app + +# 安装依赖 +RUN apt-get update && apt-get install -y --no-install-recommends nginx +RUN pip install --no-cache-dir beautifulsoup4 fastapi requests uvicorn hydra-core curl_cffi + +# 复制应用代码 +COPY . /app +RUN rm -rf /etc/nginx/sites-enabled/default && cp /app/nginx.example.conf /etc/nginx/sites-enabled/default + +CMD ["sh", "-c", "python3 main.py & nginx -g 'daemon off;'"] + +EXPOSE 80 +EXPOSE 8000 \ No newline at end of file diff --git a/README.md b/README.md index 69b21c6..9cd88c1 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,34 @@ ## AvHub - Adult Video Content Resource Toolkit +**AvHub** is an open-source tool dedicated to the retrieval and management of adult video resources, offering three core features: -AvHub is an open-source tool designed to help users efficiently search and organize adult video content through magnet links. The project focuses on two core functionalities: +--- -1. **AV Code Search Engine** - Quickly locate magnet links using Japanese adult video identification codes (番号) through integrated search capabilities. +### **Key Features** +● 🔗 **Magnet Link Search**: Accurately finds magnet links corresponding to video codes. +● 📅 **Timely Hentai Resource Updates**: Automatically updates and archives monthly hentai resources. +● 📊 **Random Video Recommendation**: Random playback functionality based on crawled data. +● 🌐 **Multi-language Support**: Supports multiple language interfaces to meet global user needs. +● 🎨 **Multiple Theme Options**: Offers various theme color schemes to enhance user experience. -2. **Monthly H-Anime Collections** - Automatically curated monthly compilations of adult anime content, providing organized access to the latest releases. +--- -**Key Features** -* 🔍 Cross-platform search optimization -* 📅 Scheduled content updates tracker -* 🔗 Magnet link verification system -* 📊 Community-driven content database +### **Technology Stack** +• **Frontend**: + • Built with **Tailwind CSS** for a modern, responsive interface. + • Integrated with **hls.js** for smooth video playback. +• **Backend**: + • Developed using **FastAPI**, a Python framework, to provide efficient and stable API services. +• **Privacy Protection**: + Strictly adheres to privacy principles and does not directly host any resource files. All data is retrieved through third-party links. -Built with modern web technologies, AvHub emphasizes user privacy and does not host any content directly. The project welcomes community contributions to improve search algorithms and content curation systems while adhering to open-source principles. +--- -*Note: Users are responsible for checking local laws regarding adult content access.* +### **Legal Disclaimer** +Users must comply with the laws and regulations of their respective regions. AvHub is solely a resource retrieval tool and does not involve the distribution or storage of any resources. + +--- + +**AvHub** is built with modern web technologies, aiming to provide users with an efficient and convenient adult video resource management experience. + +### **License** +This project is provided under a **Apache License 2.0** license that can be found in the [LICENSE](LICENSE) file. By using, distributing, or contributing to this project, you agree to the terms and conditions of this license. \ No newline at end of file diff --git a/README_CN.md b/README_CN.md index 47ff6a6..d69363c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,19 +1,35 @@ ## AvHub - 成人影视资源管理工具 -AvHub 是一款专注成人影视资源检索与管理的开源工具,主要提供两大核心功能: +**AvHub** 是一款专注成人影视资源检索与管理的开源工具,主要提供三大核心功能: -1. **AV番号搜索引擎** - 通过日本成人影片编号(番号)实现精准磁力链接检索,支持多平台资源聚合查询 +--- -2. **月度里番合集** - 自动整理当月成人动画作品,形成系统化的发行日历与资源归档 +### **核心特性** +● 🔗 **番号磁力链搜索**:精准查找番号对应的磁力链接。 +● 📅 **里番资源定时内容更新追踪**:自动更新并归档月度里番资源。 +● 📊 **随机视频推荐**:基于爬虫数据的随机播放功能。 +● 🌐 **多语言支持**:支持多种语言界面,满足全球用户需求。 +● 🎨 **多种主题配色切换**:提供多种主题配色,提升用户体验。 -**核心特性** -● 🔍 跨平台搜索优化引擎 -● 📅 定时内容更新追踪机制 -● 🔗 磁链验证系统(健康度/存活率检测) -● 📊 社区驱动的数据库更新模式 +--- -基于现代Web技术开发,项目严格遵循隐私保护原则,不直接托管任何资源文件。我们欢迎开发者共同完善智能推荐算法和内容审核系统,持续推进工具的技术合规性建设。 +### **技术栈** +• **前端**: + • 使用 **Tailwind CSS** 构建现代化、响应式界面。 + • 集成 **hls.js** 实现流畅的视频播放体验。 +• **后端**: + • 基于 **Python** 的 **FastAPI** 框架开发,提供高效、稳定的 API 服务。 +• **隐私保护**: + 严格遵循隐私保护原则,不直接托管任何资源文件,所有数据均通过第三方链接获取。 -*法律声明:用户需自行遵守所在地区相关法律法规* +--- + +### **法律声明** +用户需自行遵守所在地区相关法律法规。AvHub 仅为资源检索工具,不涉及任何资源的分发与存储。 + +--- + +**AvHub** 基于现代 Web 技术开发,致力于为用户提供高效、便捷的成人影视资源管理体验。 + +### **License** +This project is provided under a **Apache License 2.0** license that can be found in the [LICENSE](LICENSE) file. By using, distributing, or contributing to this project, you agree to the terms and conditions of this license. diff --git a/data/config.yaml b/data/config.yaml new file mode 100644 index 0000000..e1ca6e2 --- /dev/null +++ b/data/config.yaml @@ -0,0 +1,26 @@ +# config.yaml +app: + cors_origins: ["*"] + cors_credentials: true + cors_methods: ["*"] + cors_headers: ["*"] + +files: + hacg_json_path: "/app/data/hacg.json" + video_urls_txt_path: "/app/data/video_urls.txt" + +av_spider: + source_url: "https://missav.ai/cn/search/" + proxy_url: "http://192.168.50.2:7890" # http or socks5 proxy + +hacg_spider: + hacg_url: "" + +logging: + log_file: "main.log" + level: "INFO" + +hydra: + run: + dir: "." + output_subdir: null \ No newline at end of file diff --git a/data/hacg.json b/data/hacg.json new file mode 100644 index 0000000..5c05a9c --- /dev/null +++ b/data/hacg.json @@ -0,0 +1,73 @@ +{ + "2025年01月合集": "magnet:?xt=urn:btih:2146ffbecbed4d77b8b5ca6665b5c9e9949187ff", + "2024年12月合集": "magnet:?xt=urn:btih:827da30f3f6fc851f6c97758bb0618f755de14df", + "2024年11月合集": "magnet:?xt=urn:btih:080528a3185d9717084612541651c99dd541f9be", + "2024年10月合集": "magnet:?xt=urn:btih:309fa89ea1f05aa61d19e2faa1c23ebf3b30bd85", + "2024年09月合集": "magnet:?xt=urn:btih:e4fa8fb91a67f6b13efbbfe32f63d9b53bedd59e", + "2024年08月合集": "magnet:?xt=urn:btih:8ee70c99288a03f091c7bfdccf0954aa403bb150", + "2024年07月合集": "magnet:?xt=urn:btih:fb1c366e05af1a9eca23aee3810793b4212d4497", + "2024年06月合集": "magnet:?xt=urn:btih:423a7e4677193d3ce71de16152e341001e990352", + "2024年05月合集": "magnet:?xt=urn:btih:453bb0d3a5dcf082fb9b46c5cdc11d766de49406", + "2024年04月合集": "magnet:?xt=urn:btih:b496ccad674d6aa8c180e317747627ac01ecb23a", + "[桜都字幕组] 2020年7月合集": "magnet:?xt=urn:btih:03023f5686546918b90d0e0fec05948988594bf4", + "2022年09月合集": "magnet:?xt=urn:btih:a2d07b599126ef7e16d5d354ff1d47b414ff36dd", + "2024年03月合集": "magnet:?xt=urn:btih:3ca3e98867bc5571b21a76bb8f76b13279f5c25a", + "[桜都字幕组]2019年4月合集": "magnet:?xt=urn:btih:e7eca92cb2eead0abdddd57a691ee7c1bfd8aec1", + "[Animaker] To Love 系列3D同人动画(7、8、9月合集)": "magnet:?xt=urn:btih:5672503e4c76c154df21cdf2608a8bc33f25f3ba", + "[桜都字幕组] 2020年04月合集": "magnet:?xt=urn:btih:f574aca0560e54251559c10b716517958a10b40d", + "[鹰小队翻译组]2022年7月合集": "magnet:?xt=urn:btih:f1e9ba7e81da36a596da6da40708cce7e55f7985", + "2024年02月合集": "magnet:?xt=urn:btih:fe4febd0581492f01c583ca2201c9af7ef697365", + "2024年01月合集": "magnet:?xt=urn:btih:14a6d8231d9a6095fd2f53e05d47c319643201af", + "2023年12月合集": "magnet:?xt=urn:btih:3158bc6a3ebf4b33fe4c2e8a64c7d0c6c42a0acf", + "2023年11月合集": "magnet:?xt=urn:btih:80929ba4ef85abcd64c4d34baf4999cf1b32f61f", + "2023年10月合集": "magnet:?xt=urn:btih:160b29b4227e15352774c7b5152b0a6934f95d55", + "2023年09月合集": "magnet:?xt=urn:btih:21e37638bcc4931fa8daa1baeb27cfe82bd6f5f7", + "2023年08月合集": "magnet:?xt=urn:btih:2954d70aac32977f6b2869284d1489bb2f3eafea", + "2023年07月合集": "magnet:?xt=urn:btih:504e65916c9332e8d96bcdbfb9eef45f64575005", + "2023年06月合集": "magnet:?xt=urn:btih:946d0f92451d6aba8b14ef87b2a1987a9fda47a1", + "2023年05月合集": "magnet:?xt=urn:btih:f6a0102fa3d4f9e0ebe1f63069d2191173d2281d", + "2023年04月合集": "magnet:?xt=urn:btih:57b5b9525da08e988c8c69e733d913ffd6241918", + "2023年03月合集": "magnet:?xt=urn:btih:c709813729f3b9102364d9301fadc42c223a6bef", + "[b8er4u] AI生成 2023年2月合集": "magnet:?xt=urn:btih:6bba028b2f4e93b6c4d5eb9056c7396ea6bff8e5", + "2023年01月合集": "magnet:?xt=urn:btih:434da3bff16336c146ad3cdae00680527e7ceec9", + "2022年12月合集": "magnet:?xt=urn:btih:e701b02f8e0df25c7fff7eb6e1732d63c6e63791", + "2022年10月合集": "magnet:?xt=urn:btih:a6b30fc2291ef826533dafc720d9a23209b865d6", + "[鹰] 2022年8月合集": "magnet:?xt=urn:btih:4ee88182ae3e1964cd51386ee002bcf71be2cf2c", + "[鹰小队翻译组]2022年6月合集[1080P]": "magnet:?xt=urn:btih:fb2d3b4b83c8f3f4e3d2a5b7027589181760b4e5", + "[次元字幕组] 2022年5月合集": "magnet:?xt=urn:btih:f3e5801dd1876e462395567a14ecd14e22ecdbde", + "[桜都字幕组] 2021年12月合集": "magnet:?xt=urn:btih:d5a7231fcc570a9cd8bb936e94ee46b5e770b3ad", + "2021年9月合集": "magnet:?xt=urn:btih:1f6eb3787fae3a07a3c9762b75c48711778b48df", + "2021年8月合集": "magnet:?xt=urn:btih:e699f7ffc01ca00c385e7a1e1d89bc1f895a691a", + "[Taka.Sub] 2021年6月合集": "magnet:?xt=urn:btih:2f66c7bc52d5a1c3cd8b6a3717012765a42644f6", + "[鹰/Taka.Sub] 2021年5月合集": "magnet:?xt=urn:btih:9f700db0e80b9ead35381d3a11bd7cd992623d93", + "[鹰] 2021年4月合集": "magnet:?xt=urn:btih:61741e8e3962579fc9daa2ce6338d1eb6bf02d6e", + "[鹰] 2021年3月合集": "magnet:?xt=urn:btih:5dac7d69eef3dd3f975eba9492837bc93bf139bb", + "[桜都字幕组]2021年02月合集": "magnet:?xt=urn:btih:b7cc06d92fcb28f600525f6ebba291dc15e57d98", + "[鹰组/Taka.Sub]2021年2月合集": "magnet:?xt=urn:btih:4da945b18057009f6b8b6a053817e33aa3bfee55", + "[鹰组/Taka.Sub] 2021年1月合集": "magnet:?xt=urn:btih:36e8be5e0e9b6bc99bbcf6bccaa9ea545288b2d5", + "[桜都字幕组] 2020年12月合集": "magnet:?xt=urn:btih:a489ec8d90bb38c860b0d213f80dceb5ca64b0ba", + "[桜都字幕组] 2020年11月合集": "magnet:?xt=urn:btih:9424b9b7d721cb01a21a9f13547896d143ddd1d9", + "[桜都字幕组] 2020年10月合集": "magnet:?xt=urn:btih:7235d5b8497b98ca33caf2004dc6e9935cc47b5a", + "[桜都字幕组] 2020年9月合集": "magnet:?xt=urn:btih:669ed492944d5591a9e396a5a483940742440714", + "[桜都字幕组] 2020年8月合集": "magnet:?xt=urn:btih:404a3c7b4ee8b52bf8a8c00103c9545b303055b7", + "[桜都字幕组] 2020年6月合集": "magnet:?xt=urn:btih:60b6333809cc77aac3e181f4a2a2822f8b0f510b", + "[桜都字幕组]2020年05月合集": "magnet:?xt=urn:btih:9746006fda6b602cb5ce1ff728793d9635ce5e40", + "[桜都字幕组] 2020年03月合集": "magnet:?xt=urn:btih:7fce086c3db8e0f41ece40dc92e3f4c3aa16b535", + "[桜都字幕组]2020年2月合集": "magnet:?xt=urn:btih:91fb97619ef887d439a2142a2f9530b080cfbfd0", + "[桜都字幕组]2020年01月合集": "magnet:?xt=urn:btih:640b258ba41031ad7b2fa54bff1b4cad020a13a7", + "[桜都字幕组] 2019年12月合集": "magnet:?xt=urn:btih:058072f2fb052957245d47809a74d8cf8d737eda", + "[桜都字幕组] 2019年11月合集": "magnet:?xt=urn:btih:ad9178bb24f9863399b97d5d188b5dd51ddd36e4", + "[桜都字幕组] 2019年10月合集": "magnet:?xt=urn:btih:cbe5da5383cb7b99bb0707dd820163d3357d7ccc", + "[桜都字幕组] 2019年9月合集": "magnet:?xt=urn:btih:14ee20500e5c96d0ca0b2e6e576a282d5e80107a", + "[桜都字幕组] 2019年8月合集": "magnet:?xt=urn:btih:925aaeac1ae5b5937e09193124cefed719b4cf6b", + "[桜都字幕组] 2019年7月合集": "magnet:?xt=urn:btih:d109cb9631e5f0c7a6556bcb45b3b0b6b9abfd65", + "[桜都字幕组] 2019年6月合集": "magnet:?xt=urn:btih:2c5c7a75177046d36695252e380b233a3764699c", + "[桜都字幕组] 2019年5月合集": "magnet:?xt=urn:btih:53f0d34186883b7752dfd407703cd0b5a4ead203", + "2019年2月合集动画合集": "magnet:?xt=urn:btih:672551a849b3f78946149b208eaf4a3fb57413f1", + "[Haretahoo.sub] 2019年1月合集": "magnet:?xt=urn:btih:cda3841265b9861480845e3484ac269764dbb803", + "[桜都字幕组] 2018年10月合集": "magnet:?xt=urn:btih:7275d4206183911184d36e9c077483fb604ae0d4", + "[桜都字幕组] 2018年9月合集": "magnet:?xt=urn:btih:9c9e63ad2b861f83c494ebfb2e3aaa099280219d", + "[桜都字幕组] 2018年8月合集": "magnet:?xt=urn:btih:5cfb88b252ccbb6225698ec8257a6a5a90b79892", + "[脸肿字幕组]2018年07月合集(标准版)": "magnet:?xt=urn:btih:b7f466aca198f5a16a58259fa3920caed16e034b", + "[脸肿字幕组/Haretahoo.sub] 2018年06月合集(赠品版)": "magnet:?xt=urn:btih:ce66bb0c55bdffa3930652c565739ddb68044d1f" +} \ No newline at end of file diff --git a/data/video_urls.txt b/data/video_urls.txt new file mode 100644 index 0000000..381ac83 --- /dev/null +++ b/data/video_urls.txt @@ -0,0 +1,10 @@ +https://videos1.bysshxd.com/20230726/Pg0dW2NlmPpg1/index.m3u8 +https://videos1.bysshxd.com/20230728/NJrY0K4386kRo/index.m3u8 +https://videos1.bysshxd.com/20230727/MJQA2oYdmVvJj/index.m3u8 +https://videos1.bysshxd.com/20230728/PRx1jzovobvJb/index.m3u8 +https://videos1.bysshxd.com/20230728/LJB97V6r9PnRy/index.m3u8 +https://videos1.bysshxd.com/20230728/QR1kwvNwlM7gB/index.m3u8 +https://videos1.bysshxd.com/20230726/qGWQNZYPWBEJl/index.m3u8 +https://videos1.bysshxd.com/20230728/Qg8zwYbrwaVRw/index.m3u8 +https://videos1.bysshxd.com/20230728/1GwbVQ52xaLgr/index.m3u8 +https://videos1.bysshxd.com/20230802/5G2kw3KpaY2R2/index.m3u8 diff --git a/main.py b/main.py new file mode 100644 index 0000000..34f3c78 --- /dev/null +++ b/main.py @@ -0,0 +1,137 @@ +import sys +import argparse +import os +import re +import subprocess +import requests +import json +import ast +from bs4 import BeautifulSoup +from typing import Union +from fastapi import FastAPI +from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from fastapi import FastAPI, HTTPException +import random +from utils.spider import * +import hydra +from utils.logger import setup_logger + +@hydra.main(config_path='data/', config_name='config', version_base=None) +def main(cfg: DictConfig): + # 初始化日志记录器 + logger = setup_logger(cfg) + + app = FastAPI() + + @app.on_event("startup") + async def startup_event(): + global logger + logger = setup_logger(cfg) + + app.add_middleware( + CORSMiddleware, + allow_origins=cfg.app.cors_origins, + allow_credentials=cfg.app.cors_credentials, + allow_methods=cfg.app.cors_methods, + allow_headers=cfg.app.cors_headers, + ) + + def get_image_url(video_url: str) -> str: + try: + # 构建图片目录URL + image_dir_url = video_url.replace('index.m3u8', 'image/') + + # 发送请求获取目录内容 + response = requests.get(image_dir_url, timeout=20) # 设置超时时间防止长时间等待 + response.raise_for_status() # 如果响应状态码不是200,抛出HTTPError + + # 解析HTML并提取链接 + soup = BeautifulSoup(response.text, 'html.parser') + a_tags = soup.find_all('a', href=True) # 只查找有href属性的标签 + + # 分离出.webp和其他格式链接,并排除上级目录链接 + links = [image_dir_url + tag['href'] for tag in a_tags if tag['href'] != '../'] + webp_links = [link for link in links if link.endswith('.webp')] + + # 优先返回.webp链接,如果没有则从其他链接中随机返回 + if not links: + logger.warning("No image links found.") + return None + return random.choice(webp_links or links) + except Exception as e: + logger.error(f"获取图片URL失败: {str(e)}") + return None + + def read_random_line(file_path: str) -> tuple[str, str]: + """Reads a random line from a given file and returns video URL and image URL.""" + if not os.path.isfile(file_path): + logger.error("File not found") + raise HTTPException(status_code=404, detail="File not found") + + with open(file_path, 'r') as file: + lines = file.readlines() + + if not lines: + logger.error("File is empty") + raise HTTPException(status_code=400, detail="File is empty") + + random_line = random.choice(lines).strip() + img_url = get_image_url(random_line) + + return random_line, img_url + + @app.get("/v1/hacg") + async def read_hacg(): + try: + with open(cfg.files.hacg_json_path, 'r', encoding='utf-8') as file: + data = json.load(file) + logger.info("HACG data fetched successfully") + return JSONResponse({"data": data}, headers={'content-type': 'application/json;charset=utf-8'}) + except Exception as e: + logger.error(f"Failed to fetch HACG data: {str(e)}") + raise HTTPException(status_code=500, detail="Internal Server Error") + + @app.get("/v1/avcode/{code_str}") + async def crawl_av(code_str: str): + crawler = AVSpider(av_code=code_str, + source_url=cfg.av_spider.source_url, + proxy_url=cfg.av_spider.proxy_url, + cfg=cfg) + video_links = crawler.get_video_url() + all_magnet_links = [] + + for link in video_links: + magnet_links = crawler.get_magnet_links(link) + all_magnet_links.extend(magnet_links) + + if not all_magnet_links: + logger.error("No magnet links found for AV code: %s", code_str) + raise HTTPException(status_code=404, detail="No magnet links found") + + logger.info("Magnet links found for AV code: %s", code_str) + return {"status": "succeed", "data": [str(item) for item in all_magnet_links]} + + @app.get("/v1/get_video") + async def get_random_video_url(): + """Returns a random video URL and its corresponding image URL.""" + try: + file_path = cfg.files.video_urls_txt_path + video_url, img_url = read_random_line(file_path) + logger.info("Random video URL and image URL fetched successfully") + return { + "url": video_url, + "img_url": img_url or "" # 如果没有找到图片,使用默认图片 + } + except Exception as e: + logger.error(f"Failed to fetch random video URL: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) + +if __name__ == "__main__": + main() + + + diff --git a/nginx.example.conf b/nginx.example.conf new file mode 100644 index 0000000..a748323 --- /dev/null +++ b/nginx.example.conf @@ -0,0 +1,17 @@ +server { + listen 80; + listen [::]:80; + server_name _; + index index.php index.html index.htm default.php default.htm default.html; + root /app; + + location /api/ { + proxy_pass http://127.0.0.1:8000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + + } +} \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/hacg_spider.py b/utils/hacg_spider.py new file mode 100644 index 0000000..4ea3be3 --- /dev/null +++ b/utils/hacg_spider.py @@ -0,0 +1,93 @@ +import requests +from bs4 import BeautifulSoup +import re +import json +import os + +class HACGScraper: + def __init__(self, url, filepath): + self.url = url + self.filepath = filepath + + def get_pages(self): + response = requests.get(self.url) + html_content = response.text + + soup = BeautifulSoup(html_content, 'html.parser') + div_ele = soup.find('div', class_='wp-pagenavi') + page_text = div_ele.get_text() if div_ele else '' + + pages = None + if "共" in page_text: + pages = int(page_text.split('共')[1].split('页')[0]) + + return pages + + def get_links(self, page): + url = f'{self.url}' + response = requests.get(url) + html_content = response.text + + soup = BeautifulSoup(html_content, 'html.parser') + links = {} + for a_tag in soup.find_all('a'): + href = a_tag.get('href') + text = a_tag.get_text(strip=True) + if "月合集" in text: + links[text] = href + + magnet_links = {} + for title, link in links.items(): + response = requests.get(link) + + if response.status_code == 200: + content = response.text + matches = re.findall(r'\b[a-f0-9]{40}\b', content) + if matches: + magnet_links[title] = f'magnet:?xt=urn:btih:{matches[0]}' + else: + print(f"请求失败,状态码: {response.status_code}") + + return magnet_links + + def update_json_file(self): + if not os.path.exists(self.filepath) or os.path.getsize(self.filepath) == 0: + results = {} + total_pages = self.get_pages() + for i in range(1, total_pages + 1): + new_data = self.get_links(i) + results.update(new_data) + print(f'Page {i} processed (Full Update)') + else: + with open(self.filepath, 'r', encoding='utf-8') as file: + results = json.load(file) + + total_pages = self.get_pages() + for i in range(1, total_pages + 1): + new_data = self.get_links(i) + all_exists = True + + for title, magnet_link in new_data.items(): + if title not in results or results[title] != magnet_link: + all_exists = False + break + + if not all_exists: + results = {**new_data, **results} + print(f'Page {i} processed (Incremental Update)') + + if all_exists: + print(f"第 {i} 页数据已存在于 JSON 文件中,停止更新") + break + + with open(self.filepath, 'w', encoding='utf-8') as file: + json.dump(results, file, ensure_ascii=False, indent=4) + + print("JSON文件已更新") + +# 使用示例 +scraper = HACGScraper(url='https://www.hacg.mov/wp/page/1?s=%E5%90%88%E9%9B%86&submit=%E6%90%9C%E7%B4%A2', filepath=r"C:\Users\levywang\OneDrive\Code\avhub_v2\data\hacg.json") +scraper.update_json_file() + + + diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..00abb46 --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,25 @@ +import logging +from omegaconf import DictConfig + +def setup_logger(cfg: DictConfig): + logger = logging.getLogger(__name__) + if not logger.hasHandlers(): # 检查是否已经有处理器 + logger.setLevel(getattr(logging, cfg.logging.level.upper())) + + # 创建文件处理器和流处理器 + file_handler = logging.FileHandler(cfg.logging.log_file) + stream_handler = logging.StreamHandler() + + # 设置日志格式 + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + stream_handler.setFormatter(formatter) + + # 添加处理器到日志器 + logger.addHandler(file_handler) + logger.addHandler(stream_handler) + + return logger + + + diff --git a/utils/spider.py b/utils/spider.py new file mode 100644 index 0000000..3547a3f --- /dev/null +++ b/utils/spider.py @@ -0,0 +1,213 @@ +import re +import json +import os +from bs4 import BeautifulSoup +from curl_cffi import requests +from omegaconf import DictConfig +from utils.logger import setup_logger + +class AVSpider: + def __init__(self, av_code, source_url, proxy_url, cfg: DictConfig): + self.source_url = source_url + self.av_code = av_code.lower() + self.proxy_url = proxy_url + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36', + 'Content-Type': 'application/json' + } + self.proxies = { + "http": self.proxy_url, + "https": self.proxy_url + } + self.logger = setup_logger(cfg) + + def get_video_url(self) -> list: + """ + 获取视频页面的链接。 + + :return: 包含视频页面链接的列表。 + """ + code_str = self.av_code.replace('-', '') + match = re.match(r'([a-zA-Z]+)(\d+)', code_str) + if not match: + self.logger.error(f"Invalid AV code format: {self.av_code}") + return [] + + letters, digits = match.groups() + code_str = f"{letters.lower()}-{digits}" + url = f"{self.source_url}{code_str}" + try: + response = requests.get(url, proxies=self.proxies, headers=self.headers) + response.raise_for_status() + except requests.RequestException as e: + self.logger.error(f"请求失败: {e}") + return [] + + html_content = response.text + + soup = BeautifulSoup(html_content, 'html.parser') + unique_links = set() + + for a_tag in soup.find_all('a'): + alt_text = a_tag.get('alt') + if alt_text and code_str in alt_text: + href = a_tag.get('href') + if href: + unique_links.add(href) + + self.logger.info(f"Found video URLs: {unique_links}") + + return list(unique_links) + + def get_magnet_links(self, link: str) -> list: + """ + 从视频页面中提取磁力链接。 + + :param link: 视频页面的 URL。 + :return: 包含磁力链接的列表。 + """ + try: + response = requests.get(link, proxies=self.proxies, headers=self.headers) + response.raise_for_status() + except requests.RequestException as e: + self.logger.error(f"请求失败: {e}") + return [] + + html_content = response.text + + soup = BeautifulSoup(html_content, 'html.parser') + target_table = soup.find('table', class_='min-w-full') + + result = [] + if target_table is not None: + rows = target_table.find_all('tr') + for row in rows: + cols = row.find_all('td') + data = [] + + for col in cols: + links = col.find_all('a', rel='nofollow') + if links: + for l in links: + href = l['href'] + if "keepshare.org" not in href: + data.append(href) + + text = col.get_text(strip=True) + if text != "下载" and "keepshare.org" not in text: + data.append(text) + + result.append(data) + + self.logger.info(f"Magnet links extracted from {link}") + + return result + + +class HacgSpider: + def __init__(self, url, filepath, cfg: DictConfig): + self.url = url + self.filepath = filepath + self.logger = setup_logger(cfg) + + def get_pages(self): + try: + response = requests.get(self.url) + response.raise_for_status() + except requests.RequestException as e: + self.logger.error(f"请求失败: {e}") + return None + + html_content = response.text + + soup = BeautifulSoup(html_content, 'html.parser') + div_ele = soup.find('div', class_='wp-pagenavi') + page_text = div_ele.get_text() if div_ele else '' + + pages = None + if "共" in page_text: + pages = int(page_text.split('共')[1].split('页')[0]) + + self.logger.info(f"Total pages found: {pages}") + + return pages + + def get_links(self, page): + url = f'{self.url}?page={page}&s=%E5%90%88%E9%9B%86&submit=%E6%90%9C%E7%B4%A2' + try: + response = requests.get(url) + response.raise_for_status() + except requests.RequestException as e: + self.logger.error(f"请求失败: {e}") + return {} + + html_content = response.text + + soup = BeautifulSoup(html_content, 'html.parser') + links = {} + for a_tag in soup.find_all('a'): + href = a_tag.get('href') + text = a_tag.get_text(strip=True) + if "月合集" in text: + links[text] = href + + magnet_links = {} + for title, link in links.items(): + try: + response = requests.get(link) + response.raise_for_status() + except requests.RequestException as e: + self.logger.error(f"请求失败: {e}") + continue + + content = response.text + matches = re.findall(r'\b[a-f0-9]{40}\b', content) + if matches: + magnet_links[title] = f'magnet:?xt=urn:btih:{matches[0]}' + + self.logger.info(f"Magnet links extracted from page {page}: {magnet_links}") + + return magnet_links + + def update_json_file(self): + if not os.path.exists(self.filepath) or os.path.getsize(self.filepath) == 0: + results = {} + total_pages = self.get_pages() + if total_pages is None: + self.logger.error("无法获取总页数") + return + + for i in range(1, total_pages + 1): + new_data = self.get_links(i) + results.update(new_data) + self.logger.info(f'Page {i} processed (Full Update)') + else: + with open(self.filepath, 'r', encoding='utf-8') as file: + results = json.load(file) + + total_pages = self.get_pages() + if total_pages is None: + self.logger.error("无法获取总页数") + return + + for i in range(1, total_pages + 1): + new_data = self.get_links(i) + all_exists = True + + for title, magnet_link in new_data.items(): + if title not in results or results[title] != magnet_link: + all_exists = False + break + + if not all_exists: + results = {**new_data, **results} + self.logger.info(f'Page {i} processed (Incremental Update)') + + if all_exists: + self.logger.info(f"第 {i} 页数据已存在于 JSON 文件中,停止更新") + break + + with open(self.filepath, 'w', encoding='utf-8') as file: + json.dump(results, file, ensure_ascii=False, indent=4) + + self.logger.info("JSON文件已更新") \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..2e01785 --- /dev/null +++ b/web/index.html @@ -0,0 +1,185 @@ + + + + + + AvHub - 成人资源管理平台 + + + + + + +
+ +
+ +
+ + + + +
+ + +
+ +
+
+ + + +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + +
+ +
+ + +
+
+ + + + + + +
+ + + + + + + + +
+ + + + 已复制到剪贴板 +
+ + + + + diff --git a/web/script.js b/web/script.js new file mode 100644 index 0000000..dd8431b --- /dev/null +++ b/web/script.js @@ -0,0 +1,1507 @@ +// Tab切换功能 + +function switchTab(tabName) { + + // 更新按钮状态 + + document.querySelectorAll('.tab-button').forEach(button => { + + button.classList.remove('active'); + + if (button.dataset.tab === tabName) { + + button.classList.add('active'); + + } + + }); + + + + // 更新内容显示 + + document.querySelectorAll('.tab-content').forEach(content => { + + content.classList.add('hidden'); + + }); + + document.getElementById(`${tabName}Tab`).classList.remove('hidden'); + + + + // 如果切换到合集标签页,且还没有加载过数据,则加载数据 + + if (tabName === 'collections' && !document.getElementById('collectionList').children.length) { + + loadCollections(); + + } + + // 如果切换到视频播放标签页,则加载视频 + if (tabName === 'player') { + loadVideo(); + } + +} + + + +// 添加 API 配置 +const API_CONFIG = { + BASE_URL: 'https://api.imwlw.com/v1', + ENDPOINTS: { + SEARCH: '/avcode', + COLLECTIONS: '/hacg', + VIDEO: '/get_video' + } +}; + +// 搜索磁力链接 + +async function searchMagnet() { + + const input = document.getElementById('searchInput'); + + const resultsDiv = document.getElementById('searchResults'); + + const searchTerm = input.value.trim(); + + const notification = document.getElementById('notification'); + + const container = document.getElementById('coverImageContainer'); + + const regex = /^[A-Za-z][\w\s]*\d$/; + + if (!searchTerm || !regex.test(searchTerm)) { + + // 空搜索警告通知 + + notification.innerHTML = ` + + + + + + + + ${translations[currentLang].emptySearchWarning} + + `; + + notification.style.background = '#dc2626'; // 红色背景 + + notification.classList.add('show'); + + if (container) { + + container.classList.add('hidden'); + + } + + setTimeout(() => { + + notification.classList.remove('show'); + + notification.style.background = ''; // 重置背景色为默认值 + + }, 3000); + + return; + + } + + // 隐藏之前的图片和搜索结果 + + if (container) { + + container.classList.add('hidden'); + + container.style.opacity = '0'; + + } + + resultsDiv.innerHTML = ''; + + // 显示加载动画 + + const loadingTemplate = document.getElementById('loadingTemplate'); + + resultsDiv.innerHTML = loadingTemplate.innerHTML; + + setLanguage(currentLang); // 更新加载文本的语言 + + try { + + const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}/${searchTerm}`); + + const data = await response.json(); + + if (Array.isArray(data.data) && data.data.length > 0) { + + // 先显示搜索结果 + + const formattedResults = data.data.map(result => { + + if (Array.isArray(result)) { + + return result; + + } + + // 如果结果是字符串,尝试解析 + + try { + + return JSON.parse(result.replace(/'/g, '"')); + + } catch (e) { + + console.error('解析结果出错:', e); + + return null; + + } + + }).filter(result => result !== null); + + displaySearchResults(formattedResults); + + // 等待搜索结果渲染完成后再显示图片 + + setTimeout(() => showCoverImage(searchTerm), 300); + + } else { + + resultsDiv.innerHTML = `

${translations[currentLang].noResults}

`; + + // 没有搜索结果时隐藏图片 + + if (container) { + + container.classList.add('hidden'); + + } + + } + + } catch (error) { + + console.error('搜索出错:', error); + + resultsDiv.innerHTML = `

${translations[currentLang].searchError}

`; + + // 搜索出错时隐藏图片 + + if (container) { + + container.classList.add('hidden'); + + } + + } + +} + +// 显示搜索结果 + +function displaySearchResults(results) { + + const resultsDiv = document.getElementById('searchResults'); + + if (!results || !results.length) { + + resultsDiv.innerHTML = `

${translations[currentLang].noResults}

`; + + // 没有搜索结果时隐藏图片 + + const container = document.getElementById('coverImageContainer'); + + if (container) { + + container.classList.add('hidden'); + + } + + return; + + } + + const html = results.map(([magnet, title, size, date]) => { + + const tags = extractTags(title); + + const tagsHtml = tags.map(tag => { + + return `
${getTagLabel(tag.type)}
`; + + }).join(''); + + return ` + +
+ +
+ +

${title}

+ +
+ + ${tagsHtml} + +
+ +

+ + ${translations[currentLang].size}: ${size} | ${translations[currentLang].date}: ${date} + +

+ + + +
+ +
+ + `; + + }).join(''); + + resultsDiv.innerHTML = html; + +} + +// 处理番号格式并显示封面图片 + +function showCoverImage(searchTerm) { + + const container = document.getElementById('coverImageContainer'); + + const image = document.getElementById('coverImage'); + + const modal = document.getElementById('imageModal'); + + const modalImage = document.getElementById('modalImage'); + + // 如果搜索词为空,隐藏图片 + + if (!searchTerm) { + + container.classList.add('hidden'); + + return; + + } + + // 正则表达式匹配番号格式 + + const avMatch = searchTerm.match(/([a-zA-Z]+)[-]?(\d+)/i); + + if (avMatch) { + + // 提取番号的字母和数字部分 + + const prefix = avMatch[1].toLowerCase(); + + const number = avMatch[2].padStart(3, '0'); // 确保数字部分至少有3位 + + // 构建标准格式的番号 (例如: ipx-096) + + const formattedAV = `${prefix}-${number}`; + + // 构建图片URL + + const imageUrl = `https://fourhoi.com/${formattedAV}/cover-n.jpg`; + + // 设置图片源并显示容器 + + image.src = imageUrl; + + container.style.opacity = '0'; + + container.classList.remove('hidden'); + + // 移除之前的 loaded 类 + image.classList.remove('loaded'); + + // 处理图片加载完成 + image.onload = () => { + requestAnimationFrame(() => { + container.style.transition = 'opacity 0.3s ease'; + container.style.opacity = '1'; + image.classList.add('loaded'); + }); + }; + + // 处理图片加载错误 + image.onerror = () => { + container.classList.add('hidden'); + }; + + // 点击图片显示大图 + + container.onclick = () => { + + modalImage.src = imageUrl; + + modal.classList.remove('hidden'); + + setTimeout(() => { + + modal.classList.add('active'); + + }, 10); + + }; + + } else { + + // 如果不是番号格式,隐藏图片容器 + + container.classList.add('hidden'); + + } + +} + +// 视频播放功能 +let hls = null; +let autoplayEnabled = false; + +// 初始化自动播放设置 +function initializeAutoplaySettings() { + const autoplayToggle = document.getElementById('autoplayToggle'); + if (autoplayToggle) { + // 从localStorage读取自动播放设置 + autoplayEnabled = localStorage.getItem('autoplayEnabled') === 'true'; + autoplayToggle.checked = autoplayEnabled; + + // 监听自动播放设置变化 + autoplayToggle.addEventListener('change', (e) => { + autoplayEnabled = e.target.checked; + localStorage.setItem('autoplayEnabled', autoplayEnabled); + }); + } +} + +async function loadVideo() { + const videoPlayer = document.getElementById('videoPlayer'); + const notification = document.getElementById('notification'); + const sourceUrlElement = document.getElementById('videoSourceUrl'); + + try { + // 添加加载中状态 + videoPlayer.classList.add('loading'); + + // 显示加载中通知 + notification.innerHTML = ` + + + + ${translations[currentLang].loadingVideo} + `; + notification.classList.add('show'); + + // 预加载封面图 + const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.VIDEO}`); + const data = await response.json(); + + // 创建一个Image对象来预加载封面图 + const img = new Image(); + img.src = data.img_url; + + // 等待封面图加载完成 + await new Promise((resolve) => { + img.onload = resolve; + img.onerror = resolve; // 如果加载失败也继续 + }); + + // 销毁之前的HLS实例 + if (hls) { + hls.destroy(); + hls = null; + } + + // 设置视频封面 + videoPlayer.poster = data.img_url; + + // 更新视频源地址显示 + sourceUrlElement.textContent = data.url; + + // 根据视频URL类型选择播放方式 + if (data.url.endsWith('.m3u8')) { + if (Hls.isSupported()) { + hls = new Hls(); + hls.loadSource(data.url); + hls.attachMedia(videoPlayer); + hls.on(Hls.Events.MANIFEST_PARSED, () => { + if (autoplayEnabled) { + videoPlayer.play().catch(e => console.error('Auto-play failed:', e)); + } + }); + } else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) { + videoPlayer.src = data.url; + if (autoplayEnabled) { + videoPlayer.play().catch(e => console.error('Auto-play failed:', e)); + } + } + } else { + videoPlayer.src = data.url; + if (autoplayEnabled) { + videoPlayer.play().catch(e => console.error('Auto-play failed:', e)); + } + } + + // 确保播放器控件可见 + videoPlayer.addEventListener('webkitfullscreenchange', () => { + if (document.webkitFullscreenElement) { + videoPlayer.style.display = 'block'; + videoPlayer.controls = true; + } + }); + + videoPlayer.addEventListener('fullscreenchange', () => { + if (document.fullscreenElement) { + videoPlayer.style.display = 'block'; + videoPlayer.controls = true; + } + }); + + videoPlayer.classList.remove('loading'); + notification.classList.remove('show'); + } catch (error) { + console.error('加载视频出错:', error); + videoPlayer.classList.remove('loading'); + sourceUrlElement.textContent = ''; + + notification.innerHTML = ` + + + + ${translations[currentLang].videoError} + `; + notification.style.background = '#dc2626'; + notification.classList.add('show'); + setTimeout(() => { + notification.classList.remove('show'); + notification.style.background = ''; + }, 3000); + } +} + +// 初始化视频播放器 +document.addEventListener('DOMContentLoaded', () => { + initializeAutoplaySettings(); + initializeCopyButton(); + + const nextVideoButton = document.getElementById('nextVideo'); + if (nextVideoButton) { + nextVideoButton.addEventListener('click', loadVideo); + } +}); + +// 初始化复制按钮功能 +function initializeCopyButton() { + const copyButton = document.getElementById('copySourceUrl'); + const notification = document.getElementById('notification'); + + if (copyButton) { + copyButton.addEventListener('click', async () => { + const sourceUrlElement = document.getElementById('videoSourceUrl'); + const sourceUrl = sourceUrlElement?.textContent; + if (!sourceUrl) return; + + try { + await navigator.clipboard.writeText(sourceUrl); + + // 显示复制成功提示 + if (notification) { + notification.innerHTML = ` + + + + ${translations[currentLang].copied} + `; + notification.style.background = '#10B981'; // 成功绿色 + notification.classList.add('show'); + setTimeout(() => { + notification.classList.remove('show'); + notification.style.background = ''; + }, 2000); + } + + // 更新按钮状态 + copyButton.classList.add('copied'); + const textElement = copyButton.querySelector('.tab-text'); + if (textElement) { + const originalText = textElement.textContent; + textElement.textContent = translations[currentLang].copied; + + setTimeout(() => { + copyButton.classList.remove('copied'); + textElement.textContent = originalText; + }, 2000); + } + } catch (err) { + console.error('复制失败:', err); + if (notification) { + notification.innerHTML = ` + + + + ${translations[currentLang].copyFailed} + `; + notification.style.background = '#dc2626'; + notification.classList.add('show'); + setTimeout(() => { + notification.classList.remove('show'); + notification.style.background = ''; + }, 3000); + } + } + }); + } +} + +// 语言配置 +const translations = { + zh: { + search: 'AV搜索', + collections: '里番合集', + player: '视频播放', + searchPlaceholder: '请输入AV番号...', + searchButton: '搜索', + copyButton: '复制链接', + noResults: '未找到相关结果', + searchError: '搜索出错,请稍后重试', + size: '大小', + date: '日期', + emptySearchWarning: '搜索词为空或有误,请重新输入', + copySuccess: '已复制到剪贴板', + copyError: '复制失败,请手动复制', + loading: '正在搜索中', + pageSize: '每页显示', + items: '条', + total: '共', + currentPage: '当前第', + page: '页', + prevPage: '上一页', + nextPage: '下一页', + goToPage: '跳转到', + sortByDate: '按日期排序', + sortBySize: '按大小排序', + newest: '最新', + oldest: '最早', + largest: '最大', + smallest: '最小', + next: '下一个', + loadingVideo: '正在加载视频...', + videoError: '视频加载失败,请稍后重试', + nsfw: '⚠️ 警告:该内容包含成人内容 (NSFW),请确保您已年满18岁', + autoplay: '自动播放', + sourceUrl: '视频源地址', + copy: '复制', + copied: '已复制', + copyFailed: '复制失败' + }, + en: { + search: 'AV Search', + collections: 'Anime Collection', + player: 'Video Player', + searchPlaceholder: 'Enter AV number...', + searchButton: 'Search', + copyButton: 'Copy Link', + noResults: 'No results found', + searchError: 'Search error, please try again later', + size: 'Size', + date: 'Date', + emptySearchWarning: 'The search term is empty or incorrect, please re-enter', + copySuccess: 'Copied to clipboard', + copyError: 'Copy failed, please copy manually', + loading: 'Searching', + pageSize: 'Show', + items: 'items', + total: 'Total', + currentPage: 'Page', + page: '', + prevPage: 'Previous', + nextPage: 'Next', + goToPage: 'Go to page', + sortByDate: 'Sort by date', + sortBySize: 'Sort by size', + newest: 'Newest', + oldest: 'Oldest', + largest: 'Largest', + smallest: 'Smallest', + next: 'Next', + loadingVideo: 'Loading video...', + videoError: 'Failed to load video, please try again later', + nsfw: '⚠️ Warning: This content contains adult material (NSFW), ensure you are 18+', + autoplay: 'Auto Play', + sourceUrl: 'Video Source URL', + copy: 'Copy', + copied: 'Copied', + copyFailed: 'Copy Failed' + } +}; + +// 语言图标配置 +const LANGUAGES = { + zh: { + icon: ` + + `, + label: '中文' + }, + en: { + icon: ` + + `, + label: 'English' + } +}; + +// 当前语言 +let currentLang = 'zh'; + +// 主题配置 +const THEMES = { + dark: { + icon: ` + + `, + label: { zh: '夜间', en: 'Dark' } + }, + light: { + icon: ` + + `, + label: { zh: '日间', en: 'Light' } + }, + emerald: { + icon: ` + + `, + label: { zh: '翠绿', en: 'Emerald' } + }, + ocean: { + icon: ` + + + + `, + label: { zh: '海蓝', en: 'Ocean' } + }, + amethyst: { + icon: ` + + `, + label: { zh: '紫晶', en: 'Amethyst' } + } +}; + +// 排序配置 +const SORT_OPTIONS = { + 'date-desc': { + icon: ` + + `, + label: { zh: '最新日期', en: 'Newest' } + }, + 'date-asc': { + icon: ` + + `, + label: { zh: '最早日期', en: 'Oldest' } + }, + 'size-desc': { + icon: ` + + `, + label: { zh: '文件最大', en: 'Largest' } + }, + 'size-asc': { + icon: ` + + `, + label: { zh: '文件最小', en: 'Smallest' } + } +}; + +// 初始化 +document.addEventListener('DOMContentLoaded', () => { + // 设置初始主题 + const savedTheme = localStorage.getItem('theme') || 'dark'; + toggleTheme(savedTheme); + + // 设置初始语言 + const savedLang = localStorage.getItem('language') || 'zh'; + setLanguage(savedLang); + + // 初始化所有按钮 + initializeButtons(); + + // 加载合集列表 + loadCollections(); + + // 添加事件监听器 + + // 语言切换按钮 + const langButton = document.getElementById('languageToggle'); + if (langButton) { + langButton.onclick = () => showLanguageMenu(langButton); + } + + // 主题切换按钮 + const themeButton = document.getElementById('themeToggle'); + if (themeButton) { + themeButton.onclick = () => showThemeMenu(themeButton); + } + + // 搜索输入框添加回车键触发搜索 + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.addEventListener('keypress', function(event) { + if (event.key === 'Enter') { + event.preventDefault(); + searchMagnet(); + } + }); + } + + // 回到顶部按钮 + const backToTopButton = document.getElementById('backToTop'); + if (backToTopButton) { + // 初始隐藏按钮 + backToTopButton.classList.add('hidden'); + + // 监听滚动事件 + window.addEventListener('scroll', function() { + if (window.scrollY > 300) { + backToTopButton.classList.remove('hidden'); + } else { + backToTopButton.classList.add('hidden'); + } + }); + + // 点击事件 + backToTopButton.addEventListener('click', function() { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); + }); + } + + // 初始化大图预览功能 + const modal = document.getElementById('imageModal'); + const closeButton = document.getElementById('closeModal'); + + // 关闭按钮点击事件 + if (closeButton) { + closeButton.onclick = () => { + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); + }; + } + + // 点击模态框背景关闭 + if (modal) { + modal.onclick = (e) => { + if (e.target === modal) { + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); + } + }; + } + + // ESC键关闭模态框 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal && !modal.classList.contains('hidden')) { + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); + } + }); + + // 下一个视频按钮 + const nextVideoButton = document.getElementById('nextVideo'); + if (nextVideoButton) { + nextVideoButton.addEventListener('click', loadVideo); + } +}); + +// 切换主题功能 +function toggleTheme(themeName) { + // 移除所有主题类 + document.body.removeAttribute('data-theme'); + // 设置新主题 + document.body.setAttribute('data-theme', themeName); + // 保存主题设置 + localStorage.setItem('theme', themeName); + + // 更新主题按钮图标 + const themeButton = document.getElementById('themeToggle'); + if (themeButton) { + themeButton.innerHTML = THEMES[themeName].icon; + } + + // 重新初始化所有按钮事件 + initializeButtons(); +} + +// 初始化所有按钮事件 +function initializeButtons() { + // 初始化标签页按钮 + document.querySelectorAll('.tab-button').forEach(button => { + const tabName = button.dataset.tab; + button.onclick = () => switchTab(tabName); + }); + + // 初始化主题切换按钮 + const themeButton = document.getElementById('themeToggle'); + if (themeButton) { + themeButton.onclick = () => showThemeMenu(themeButton); + } + + // 初始化语言切换按钮 + const langButton = document.getElementById('languageToggle'); + if (langButton) { + langButton.onclick = () => showLanguageMenu(langButton); + } + + // 初始化排序按钮 + const sortButton = document.getElementById('sortButton'); + if (sortButton) { + sortButton.onclick = () => showSortMenu(sortButton); + } +} + +// 显示语言菜单 +function showLanguageMenu(button) { + const existingMenu = document.querySelector('.language-menu'); + if (existingMenu) { + existingMenu.remove(); + return; + } + + const menu = document.createElement('div'); + menu.className = 'theme-menu language-menu'; + + // 对号图标 SVG + const checkmarkSvg = ` + + `; + + menu.innerHTML = ` + + + `; + + // 将菜单添加到 body + document.body.appendChild(menu); + + // 计算菜单位置 + const buttonRect = button.getBoundingClientRect(); + const menuRect = menu.getBoundingClientRect(); + + // 确保菜单不会超出视口 + let top = buttonRect.bottom; + let left = Math.min( + buttonRect.left, + window.innerWidth - menuRect.width - 10 + ); + + // 如果菜单会超出底部,则显示在按钮上方 + if (top + menuRect.height > window.innerHeight) { + top = buttonRect.top - menuRect.height; + } + + menu.style.position = 'fixed'; + menu.style.top = `${top}px`; + menu.style.left = `${left}px`; + menu.style.zIndex = '1000'; + + // 窗口滚动时更新菜单位置 + const updateMenuPosition = () => { + const updatedRect = button.getBoundingClientRect(); + let newTop = updatedRect.bottom; + + // 如果菜单会超出底部,则显示在按钮上方 + if (newTop + menuRect.height > window.innerHeight) { + newTop = updatedRect.top - menuRect.height; + } + + menu.style.top = `${newTop}px`; + menu.style.left = `${Math.min(updatedRect.left, window.innerWidth - menuRect.width - 10)}px`; + }; + + window.addEventListener('scroll', updateMenuPosition); + window.addEventListener('resize', updateMenuPosition); + + menu.addEventListener('click', (e) => { + const langItem = e.target.closest('.theme-menu-item'); + if (langItem) { + const newLang = langItem.dataset.lang; + setLanguage(newLang); + menu.remove(); + window.removeEventListener('scroll', updateMenuPosition); + window.removeEventListener('resize', updateMenuPosition); + } + }); + + // 点击其他区域关闭菜单 + const closeMenu = (e) => { + if (!button.contains(e.target) && !menu.contains(e.target)) { + menu.remove(); + document.removeEventListener('click', closeMenu); + window.removeEventListener('scroll', updateMenuPosition); + window.removeEventListener('resize', updateMenuPosition); + } + }; + + // 使用 re·uestAnimationFrame 延迟添加点击事件,避免立即触发 + requestAnimationFrame(() => { + document.addEventListener('click', closeMenu); + }); + + // ESC 键关闭菜单 + const handleEscape = (e) => { + if (e.key === 'Escape') { + menu.remove(); + document.removeEventListener('keydown', handleEscape); + document.removeEventListener('click', closeMenu); + window.removeEventListener('scroll', updateMenuPosition); + window.removeEventListener('resize', updateMenuPosition); + } + }; + document.addEventListener('keydown', handleEscape); +} + +// 语言切换功能 +function setLanguage(lang) { + currentLang = lang; + localStorage.setItem('language', lang); + + // 不再需要更新语言按钮文本,因为我们使用固定的"文"字符作为图标 + // const languageButton = document.getElementById('languageToggle'); + // if (languageButton) { + // languageButton.querySelector('.language-text').textContent = + // lang === 'zh' ? '中文' : 'English'; + // } + + // 更新所有带有 data-zh 和 data-en 属性的元素 + document.querySelectorAll('[data-zh][data-en]').forEach(el => { + el.textContent = el.getAttribute(`data-${lang}`); + }); + + // 更新搜索框占位符 + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.placeholder = searchInput.getAttribute(`data-${lang}-placeholder`); + } + + // 更新排序按钮文本 + const sortButton = document.getElementById('sortButton'); + if (sortButton && sortButton.value) { + const sortOption = SORT_OPTIONS[sortButton.value]; + if (sortOption) { + sortButton.innerHTML = ` + ${sortOption.icon} + ${sortOption.label[lang]} + `; + } + } + + // 更新主题菜单文本 + const themeMenuItems = document.querySelectorAll('.theme-menu-item'); + themeMenuItems.forEach(item => { + const themeName = item.dataset.theme; + if (themeName && THEMES[themeName]) { + const label = item.querySelector('.theme-label'); + if (label) { + label.textContent = THEMES[themeName].label[lang]; + } + } + }); + + // 更新分页控件文本 + updatePaginationText(); + + // 更新所有按钮文本 + document.querySelectorAll('button').forEach(button => { + // 更新搜索按钮 + if (button.classList.contains('search-button')) { + const searchText = button.querySelector('.tab-text'); + if (searchText) { + searchText.textContent = translations[lang].searchButton; + } + } + + // 更新复制按钮 + if (button.classList.contains('copy-button')) { + button.textContent = translations[lang].copyButton; + } + }); + + // 重新渲染搜索结果 + const searchResults = document.getElementById('searchResults'); + if (searchResults && searchResults.children.length > 0) { + const firstChild = searchResults.firstElementChild; + if (!firstChild.classList.contains('loading-container') && + firstChild.tagName.toLowerCase() !== 'p') { + try { + const results = Array.from(searchResults.children).map(item => { + const title = item.querySelector('h3').textContent; + const info = item.querySelector('p').textContent; + const [size, date] = info.split('|').map(str => str.split(':')[1].trim()); + const magnet = item.querySelector('button').getAttribute('onclick').split("'")[1]; + return [magnet, title, size, date]; + }); + displaySearchResults(results); + } catch (error) { + console.error('解析搜索结果失败:', error); + } + } + } + + // 重新加载合集 + const collectionsTab = document.getElementById('collectionsTab'); + if (collectionsTab && !collectionsTab.classList.contains('hidden')) { + loadCollections(); + } + + // 更新加载动画文本 + const loadingText = document.querySelector('.loading-text'); + if (loadingText) { + loadingText.textContent = translations[lang].loading; + } + + // 更新所有错误和提示消息 + document.querySelectorAll('.notification, .error-message, .info-message').forEach(el => { + const messageKey = el.dataset.messageKey; + if (messageKey && translations[lang][messageKey]) { + el.textContent = translations[lang][messageKey]; + } + }); +} + +// 更新分页控件文本 +function updatePaginationText() { + const paginationElements = document.querySelectorAll('.pagination-container'); + paginationElements.forEach(container => { + const prevBtn = container.querySelector('.prev-page'); + const nextBtn = container.querySelector('.next-page'); + const pageInfo = container.querySelector('.page-info'); + const pageSizeSelect = container.querySelector('.page-size-select'); + + if (prevBtn) prevBtn.textContent = translations[currentLang].prevPage; + if (nextBtn) nextBtn.textContent = translations[currentLang].nextPage; + + if (pageInfo) { + const currentPage = pageInfo.dataset.currentPage; + const totalPages = pageInfo.dataset.totalPages; + const pageSize = pageInfo.dataset.pageSize; + + pageInfo.textContent = `${translations[currentLang].currentPage}${currentPage}${translations[currentLang].page} / ${translations[currentLang].total}${totalPages}${translations[currentLang].page}`; + } + + if (pageSizeSelect) { + const label = pageSizeSelect.previousElementSibling; + if (label) { + label.textContent = `${translations[currentLang].pageSize}: `; + } + const suffix = pageSizeSelect.nextElementSibling; + if (suffix) { + suffix.textContent = ` ${translations[currentLang].items}`; + } + } + }); +} + +// 解析文件大小 +function parseFileSize(sizeStr) { + try { + // 确保输入是字符串 + sizeStr = String(sizeStr).trim(); + + // 匹配数字和单位 + const match = sizeStr.match(/^([\d.]+)\s*([KMGT]?B)$/i); + if (!match) return 0; + + const [, value, unit] = match; + const size = parseFloat(value); + + // 转换到字节 + switch (unit.toUpperCase()) { + case 'KB': + return size * 1024; + case 'MB': + return size * 1024 * 1024; + case 'GB': + return size * 1024 * 1024 * 1024; + case 'TB': + return size * 1024 * 1024 * 1024 * 1024; + case 'B': + default: + return size; + } + } catch (error) { + console.error('解析文件大小错误:', error); + return 0; + } +} + +// 排序功能 +function sortResults(sortType) { + const resultsDiv = document.getElementById('searchResults'); + const results = Array.from(resultsDiv.children); + + if (results.length === 0 || results[0].classList.contains('loading-container')) { + return; + } + + results.sort((a, b) => { + try { + const aInfo = a.querySelector('p').textContent; + const bInfo = b.querySelector('p').textContent; + + // 提取大小和日期 + const [aSize, aDate] = aInfo.split('|').map(str => str.split(':')[1].trim()); + const [bSize, bDate] = bInfo.split('|').map(str => str.split(':')[1].trim()); + + switch (sortType) { + case 'date-desc': + return new Date(bDate || 0) - new Date(aDate || 0); + case 'date-asc': + return new Date(aDate || 0) - new Date(bDate || 0); + case 'size-desc': + return parseFileSize(bSize) - parseFileSize(aSize); + case 'size-asc': + return parseFileSize(aSize) - parseFileSize(bSize); + default: + return 0; + } + } catch (error) { + console.error('排序比较错误:', error); + return 0; + } + }); + + // 清空并重新添加排序后的结果 + resultsDiv.innerHTML = ''; + results.forEach(result => resultsDiv.appendChild(result)); +} + +// 显示动漫合集 +function displayCollections(collections) { + const collectionList = document.getElementById('collectionList'); + + // 清空现有内容 + collectionList.innerHTML = ''; + + // 检查collections是否为数组 + if (Array.isArray(collections)) { + // 处理数组类型的数据 + collections.forEach(collection => { + const collectionItem = document.createElement('div'); + collectionItem.className = 'magnet-item p-6 rounded-xl'; + collectionItem.innerHTML = ` +
+

${collection.title}

+ +
+ `; + collectionList.appendChild(collectionItem); + }); + } else if (collections && typeof collections === 'object') { + // 处理对象类型的数据 + Object.entries(collections).forEach(([title, link]) => { + const collectionItem = document.createElement('div'); + collectionItem.className = 'magnet-item p-6 rounded-xl'; + collectionItem.innerHTML = ` +
+

${title}

+ +
+ `; + collectionList.appendChild(collectionItem); + }); + } else { + // 没有数据或数据格式不正确 + collectionList.innerHTML = `

${translations[currentLang].noResults}

`; + } +} + +// 获取标签文字 +function getTagLabel(type) { + const tagLabels = { + hd: { zh: '高清', en: 'HD' }, + subtitle: { zh: '字幕', en: 'SUB' }, + uncensored: { zh: '无码', en: 'Uncensored' }, + chinese: { zh: '中文', en: 'Chinese' }, + leak: { zh: '破解', en: 'Leaked' } + }; + return tagLabels[type][currentLang]; +} + +// 提取标签 +function extractTags(title) { + const tags = []; + const tagMap = { + 'HD': {type: 'hd', priority: 1}, + 'FHD': {type: 'hd', priority: 1}, + '字幕': {type: 'subtitle', priority: 2}, + '-C': {type: 'subtitle', priority: 2}, + '無修正': {type: 'uncensored', priority: 3}, + '无码': {type: 'uncensored', priority: 3}, + 'uncensored': {type: 'uncensored', priority: 3}, + '中文': {type: 'chinese', priority: 4}, + '破解': {type: 'leak', priority: 5}, + 'leak': {type: 'leak', priority: 5} + }; + + Object.entries(tagMap).forEach(([keyword, {type, priority}]) => { + if (title.toLowerCase().includes(keyword.toLowerCase())) { + if (!tags.find(t => t.type === type)) { + tags.push({type, priority}); + } + } + }); + + return tags.sort((a, b) => a.priority - b.priority); +} + +// 获取标签样式 +function getTagStyle(tag) { + // 更新标签样式为玻璃态设计 + const styleMap = { + '高清': 'bg-blue-500/20 text-blue-300', + '字幕': 'bg-green-500/20 text-green-300', + '无码': 'bg-red-500/20 text-red-300', + '有码': 'bg-yellow-500/20 text-yellow-300', + '中文': 'bg-purple-500/20 text-purple-300', + '无修正': 'bg-pink-500/20 text-pink-300', + '破解版': 'bg-indigo-500/20 text-indigo-300' + }; + + return styleMap[tag] || 'bg-gray-500/20 text-gray-300'; +} + +// 加载动漫合集 +async function loadCollections() { + try { + const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.COLLECTIONS}`); + const data = await response.json(); + displayCollections(data.data); + } catch (error) { + console.error('加载合集失败:', error); + } +} + +// 复制到剪贴板 +function copyToClipboard(text) { + const notification = document.getElementById('notification'); + + navigator.clipboard.writeText(text).then(() => { + // 成功复制通知 + notification.innerHTML = ` + + + + ${translations[currentLang].copySuccess} + `; + notification.style.setProperty('background', '#1bb76e'); + notification.classList.add('show'); + + setTimeout(() => { + notification.classList.remove('show'); + notification.style.background = ''; // 重置背景色为默认值 + }, 3000); + }).catch(err => { + // 复制失败通知 + notification.innerHTML = ` + + + + ${translations[currentLang].copyError} + `; + notification.style.background = '#dc2626'; + notification.classList.add('show'); + + setTimeout(() => { + notification.classList.remove('show'); + notification.style.background = ''; + }, 3000); + }); +} + +// 修改排序下拉菜单 +function showSortMenu(button) { + const existingMenu = document.querySelector('.sort-menu'); + if (existingMenu) { + existingMenu.remove(); + return; + } + + const currentSort = button.value; + const sortMenu = document.createElement('div'); + sortMenu.className = 'sort-menu'; + + button.parentElement.style.position = 'relative'; + + sortMenu.innerHTML = Object.entries(SORT_OPTIONS).map(([value, option]) => ` + + `).join(''); + + button.parentElement.appendChild(sortMenu); + + sortMenu.addEventListener('click', (e) => { + const sortItem = e.target.closest('.theme-menu-item'); + if (sortItem) { + const newSort = sortItem.dataset.sort; + button.value = newSort; + sortResults(newSort); + button.innerHTML = ` + ${SORT_OPTIONS[newSort].icon} + ${SORT_OPTIONS[newSort].label[currentLang]} + `; + sortMenu.remove(); + } + }); + + document.addEventListener('click', (e) => { + if (!button.contains(e.target) && !sortMenu.contains(e.target)) { + sortMenu.remove(); + } + }, { once: true }); +} + +// 显示主题菜单 +function showThemeMenu(button) { + const existingMenu = document.querySelector('.theme-menu'); + if (existingMenu) { + existingMenu.remove(); + return; + } + + const currentTheme = localStorage.getItem('theme') || 'dark'; + const themeMenu = document.createElement('div'); + themeMenu.className = 'theme-menu'; + + themeMenu.innerHTML = Object.entries(THEMES).map(([name, theme]) => ` + + `).join(''); + + // 将菜单添加到 body 而不是按钮中 + document.body.appendChild(themeMenu); + + // 计算菜单位置 + const buttonRect = button.getBoundingClientRect(); + const menuRect = themeMenu.getBoundingClientRect(); + + // 确保菜单不会超出视口 + let top = buttonRect.bottom; + let left = Math.min( + buttonRect.left, + window.innerWidth - menuRect.width - 10 + ); + + // 如果菜单会超出底部,则显示在按钮上方 + if (top + menuRect.height > window.innerHeight) { + top = buttonRect.top - menuRect.height; + } + + themeMenu.style.position = 'fixed'; + themeMenu.style.top = `${top}px`; + themeMenu.style.left = `${left}px`; + themeMenu.style.zIndex = '1000'; + + // 窗口滚动时更新菜单位置 + const updateMenuPosition = () => { + const updatedRect = button.getBoundingClientRect(); + let newTop = updatedRect.bottom; + + // 如果菜单会超出底部,则显示在按钮上方 + if (newTop + menuRect.height > window.innerHeight) { + newTop = updatedRect.top - menuRect.height; + } + + themeMenu.style.top = `${newTop}px`; + themeMenu.style.left = `${Math.min(updatedRect.left, window.innerWidth - menuRect.width - 10)}px`; + }; + + window.addEventListener('scroll', updateMenuPosition); + window.addEventListener('resize', updateMenuPosition); + + // 点击菜单项时切换主题并关闭菜单 + themeMenu.addEventListener('click', (e) => { + const themeItem = e.target.closest('.theme-menu-item'); + if (themeItem) { + const newTheme = themeItem.dataset.theme; + toggleTheme(newTheme); + themeMenu.remove(); + window.removeEventListener('scroll', updateMenuPosition); + window.removeEventListener('resize', updateMenuPosition); + } + }); + + // 点击其他区域关闭菜单 + const closeMenu = (e) => { + if (!button.contains(e.target) && !themeMenu.contains(e.target)) { + themeMenu.remove(); + document.removeEventListener('click', closeMenu); + window.removeEventListener('scroll', updateMenuPosition); + window.removeEventListener('resize', updateMenuPosition); + } + }; + + // 使用 requestAnimationFrame 延迟添加点击事件,避免立即触发 + requestAnimationFrame(() => { + document.addEventListener('click', closeMenu); + }); + + // ESC 键关闭菜单 + const handleEscape = (e) => { + if (e.key === 'Escape') { + themeMenu.remove(); + document.removeEventListener('keydown', handleEscape); + document.removeEventListener('click', closeMenu); + window.removeEventListener('scroll', updateMenuPosition); + window.removeEventListener('resize', updateMenuPosition); + } + }; + document.addEventListener('keydown', handleEscape); +} \ No newline at end of file diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..d733cfe --- /dev/null +++ b/web/style.css @@ -0,0 +1,1414 @@ +:root { + /* 默认深色主题 */ + --primary-color: #1bb76e; + --primary-hover: #1da462; + --background-dark: #141518; + --card-dark: #2c2f34; + --header-dark: #1b1b1b; + --text-primary: #ffffff; + --text-secondary: #b4b9c2; + --border-color: #41454c; +} + +/* 绿色主题 */ +[data-theme="green"] { + --primary-color: #10b981; + --primary-hover: #059669; + --background-dark: #064e3b; + --card-dark: #065f46; + --header-dark: #064e3b; + --text-primary: #ffffff; + --text-secondary: #a7f3d0; + --border-color: #047857; +} + +/* 蓝色主题 */ +[data-theme="blue"] { + --primary-color: #3b82f6; + --primary-hover: #2563eb; + --background-dark: #1e3a8a; + --card-dark: #1e40af; + --header-dark: #1e3a8a; + --text-primary: #ffffff; + --text-secondary: #bfdbfe; + --border-color: #3b82f6; +} + +/* 紫色主题 */ +[data-theme="purple"] { + --primary-color: #8b5cf6; + --primary-hover: #7c3aed; + --background-dark: #4c1d95; + --card-dark: #5b21b6; + --header-dark: #4c1d95; + --text-primary: #ffffff; + --text-secondary: #ddd6fe; + --border-color: #7c3aed; +} + +/* 橙色主题 */ +[data-theme="orange"] { + --primary-color: #f97316; + --primary-hover: #ea580c; + --background-dark: #7c2d12; + --card-dark: #9a3412; + --header-dark: #7c2d12; + --text-primary: #ffffff; + --text-secondary: #fed7aa; + --border-color: #ea580c; +} + +/* 粉色主题 */ +[data-theme="pink"] { + --primary-color: #ec4899; + --primary-hover: #db2777; + --background-dark: #831843; + --card-dark: #9d174d; + --header-dark: #831843; + --text-primary: #ffffff; + --text-secondary: #fbcfe8; + --border-color: #db2777; +} + +/* 主题配色 */ +[data-theme="emerald"] { + --primary-color: #10b981; + --primary-hover: #059669; + --background-dark: #064e3b; + --card-dark: #065f46; + --header-dark: #064e3b; + --text-primary: #ffffff; + --text-secondary: #a7f3d0; + --border-color: #047857; + --primary-color-rgb: 16, 185, 129; +} + +[data-theme="ocean"] { + --primary-color: #3b82f6; + --primary-hover: #2563eb; + --background-dark: #1e3a8a; + --card-dark: #1e40af; + --header-dark: #1e3a8a; + --text-primary: #ffffff; + --text-secondary: #bfdbfe; + --border-color: #3b82f6; + --primary-color-rgb: 59, 130, 246; +} + +[data-theme="amethyst"] { + --primary-color: #8b5cf6; + --primary-hover: #7c3aed; + --background-dark: #4c1d95; + --card-dark: #5b21b6; + --header-dark: #4c1d95; + --text-primary: #ffffff; + --text-secondary: #ddd6fe; + --border-color: #7c3aed; + --primary-color-rgb: 139, 92, 246; +} + +/* 浅色主题 */ +[data-theme="light"] { + --primary-color: #1bb76e; + --primary-hover: #1da462; + --background-dark: #f3f4f6; + --card-dark: #ffffff; + --header-dark: #ffffff; + --text-primary: #1f2937; + --text-secondary: #4b5563; + --border-color: #e5e7eb; + --primary-color-rgb: 27, 183, 110; +} + +/* 浅色主题特殊处理 */ +[data-theme="light"] .theme-menu, +[data-theme="light"] .sort-menu { + background: #ffffff; + border-color: #e5e7eb; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +[data-theme="light"] .settings-button { + background: #ffffff; + border-color: #e5e7eb; + color: #1f2937; +} + +[data-theme="light"] .magnet-item { + background: #ffffff; + border-color: #e5e7eb; + color: #1f2937; +} + +[data-theme="light"] .search-button, +[data-theme="light"] .copy-button { + color: #ffffff; +} + +[data-theme="light"] .theme-menu-item { + color: #1f2937; +} + +[data-theme="light"] .theme-menu-item:hover { + color: #ffffff; +} + +[data-theme="light"] .sort-menu .theme-menu-item { + color: #1f2937; +} + +[data-theme="light"] .sort-menu .theme-menu-item:hover { + color: #ffffff; +} + +[data-theme="light"] input { + background: #ffffff; + border-color: #e5e7eb; + color: #1f2937; +} + +[data-theme="light"] .tab-button { + color: #6b7280; +} + +[data-theme="light"] .tab-button:hover { + color: #374151; +} + +[data-theme="light"] .tab-button.active { + color: var(--primary-color); + background: rgba(var(--primary-color-rgb), 0.1); +} + +[data-theme="light"] .notification { + background: var(--primary-color); + color: #ffffff; + border: 1px solid #e5e7eb; +} + +body { + background-color: var(--background-dark); + color: var(--text-primary); + min-height: 100vh; +} + +.bg-header { + background-color: var(--header-dark); +} + +.bg-card { + background-color: var(--card-dark); +} + +.magnet-item { + transition: all 0.2s ease; + background: var(--card-dark); + border: 1px solid var(--border-color); + border-radius: 4px; + overflow: hidden; + animation: fadeIn 0.3s ease-out; +} + +.magnet-item:hover { + transform: translateY(-1px); + border-color: var(--primary-color); +} + +.search-button, .copy-button { + background: var(--primary-color); + color: white; + font-weight: 600; + border-radius: 4px; + transition: all 0.2s ease; +} + +.search-button:hover, .copy-button:hover { + background: var(--primary-hover); + transform: translateY(-1px); +} + +.search-button:active, .copy-button:active { + transform: translateY(0); +} + +.copy-button { + background: var(--primary-color); + color: white; + font-weight: 600; + border-radius: 4px; + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +.copy-button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.2), + transparent + ); + transition: 0.5s; +} + +.copy-button:hover::before { + left: 100%; +} + +.copy-button:hover { + background: var(--primary-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(27, 183, 110, 0.2); +} + +.tab-container { + display: flex; + gap: 8px; + padding: 4px; + background: var(--card-dark); + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.tab-button { + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + transition: all 0.2s; + color: var(--text-secondary); +} + +.tab-button:hover { + color: var(--text-primary); +} + +.tab-button.active { + color: var(--primary-color); + background: rgba(var(--primary-color-rgb), 0.1); +} + +.tab-content { + display: block; +} + +.tab-content.hidden { + display: none; +} + +.tag { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + font-weight: 500; + color: white; +} + +.tag[data-type="hd"] { + background: #3b82f6; +} + +.tag[data-type="subtitle"] { + background: #22c55e; +} + +.tag[data-type="uncensored"] { + background: #ef4444; +} + +.tag[data-type="chinese"] { + background: #a855f7; +} + +.tag[data-type="leak"] { + background: #f59e0b; +} + +/* 确保语言切换按钮与主题切换按钮大小一致 */ +#languageToggle { + display: flex; + align-items: center; + justify-content: center; +} + +/* 修改标签颜色 */ +.tag[data-type="hd"] { + background: rgba(59, 130, 246, 0.1); + color: #3b82f6; + border-color: rgba(59, 130, 246, 0.2); +} + +.tag[data-type="subtitle"] { + background: rgba(34, 197, 94, 0.1); + color: #22c55e; + border-color: rgba(34, 197, 94, 0.2); +} + +.tag[data-type="uncensored"] { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; + border-color: rgba(239, 68, 68, 0.2); +} + +.tag[data-type="chinese"] { + background: rgba(168, 85, 247, 0.1); + color: #a855f7; + border-color: rgba(168, 85, 247, 0.2); +} + +.tag[data-type="leak"] { + background: rgba(245, 158, 11, 0.1); + color: #f59e0b; + border-color: rgba(245, 158, 11, 0.2); +} + +/* 亮色主题下的标签样式微调 */ +[data-theme="light"] .tag { + opacity: 0.9; +} + +/* 设置区域样式 */ +.settings-container { + position: fixed; + top: 16px; + right: 5%; + display: flex; + gap: 8px; + z-index: 1000; +} + +/* 设置按钮样式 */ +.settings-button { + position: relative; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + height: 40px; + background: var(--card-dark); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.settings-button svg { + width: 20px; + height: 20px; +} + +/* 语言切换按钮文字样式 */ +.language-text { + line-height: 20px; +} + +/* 主题切换按钮样式 */ +#themeToggle { + padding: 8px; + width: 40px; +} + +/* 主题菜单样式 */ +.theme-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + background: var(--card-dark); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(8px); + width: max-content; + min-width: 100px; + z-index: 1001; +} + +.theme-menu-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + width: 100%; + border-radius: 6px; + transition: all 0.2s; + cursor: pointer; +} + +/* 亮色主题下的菜单样式 */ +[data-theme="light"] .theme-menu { + background: #ffffff; + border-color: #e5e7eb; +} + +[data-theme="light"] .theme-menu-item { + color: #1f2937; +} + +[data-theme="light"] .theme-menu-item:hover { + background: rgba(var(--primary-color-rgb), 0.1); +} + +/* 深色主题下的菜单样式 */ +[data-theme="dark"] .theme-menu { + background: #1f2937; + border-color: #374151; +} + +[data-theme="dark"] .theme-menu-item { + color: #ffffff; +} + +[data-theme="dark"] .theme-menu-item:hover { + background: rgba(var(--primary-color-rgb), 0.1); +} + +/* 主题选项图标颜色 */ +.theme-menu-item[data-theme="dark"] svg { + color: #94a3b8; +} + +.theme-menu-item[data-theme="light"] svg { + color: #fbbf24; +} + +.theme-menu-item[data-theme="emerald"] svg { + color: #10b981; +} + +.theme-menu-item[data-theme="ocean"] svg { + color: #3b82f6; +} + +.theme-menu-item[data-theme="amethyst"] svg { + color: #8b5cf6; +} + +/* 主题选项悬停效果 */ +.theme-menu-item:hover { + background: rgba(var(--primary-color-rgb), 0.1); +} + +/* 当前选中的主题样式 */ +.theme-menu-item[data-active="true"] { + background: rgba(var(--primary-color-rgb), 0.1); +} + +.theme-menu-item svg { + width: 20px; + height: 20px; + flex-shrink: 0; +} + +/* 搜索框样式修复 */ +[data-theme="light"] input { + background: #ffffff; + border: 1px solid #e5e7eb; + color: #1f2937; +} + +[data-theme="light"] input::placeholder { + color: #9ca3af; +} + +#searchInput { + background: var(--card-dark); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: 6px; +} + +#searchInput::placeholder { + color: var(--text-secondary); +} + +/* 排序按钮容器样式 */ +.sort-button-container { + position: relative; + display: flex; + justify-content: flex-end; + margin-bottom: 16px; +} + +/* 排序按钮样式 */ +#sortButton { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--card-dark); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + min-width: 120px; +} + +#sortButton:hover { + border-color: var(--primary-color); + color: var(--text-primary); +} + +#sortButton svg { + flex-shrink: 0; +} + +#sortButton span { + white-space: nowrap; +} + +/* 排序菜单样式优化 */ +.sort-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + background: var(--card-dark); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(8px); + width: max-content; + min-width: 120px; + max-width: 160px; + z-index: 100; +} + +.sort-menu .theme-menu-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + width: 100%; + color: var(--text-primary); + transition: all 0.2s; + border-radius: 6px; + font-size: 14px; +} + +.sort-menu .theme-menu-item:hover { + background: var(--primary-color); + color: white; +} + +.sort-menu .theme-menu-item svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.sort-menu .theme-menu-item span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 语言切换按钮样式 */ +.language-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + background: var(--card-dark); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(8px); + width: max-content; + min-width: 100px; + z-index: 1001; +} + +.language-menu .theme-menu-item { + padding: 8px 12px; + gap: 8px; +} + +.language-menu .theme-menu-item svg { + flex-shrink: 0; + width: 16px; + height: 16px; +} + +.language-menu .theme-menu-item span { + flex-grow: 1; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 当前选中的语言样式 */ +.language-menu .theme-menu-item[data-active="true"] { + color: var(--primary-color); + background: rgba(var(--primary-color-rgb), 0.1); +} + +.language-menu .theme-menu-item[data-active="true"]:hover { + color: white; + background: var(--primary-color); +} + +/* 确保语言切换按钮与主题切换按钮大小一致 */ +#languageToggle { + display: flex; + align-items: center; + justify-content: center; +} + +/* 输入框和选择框样式优化 */ +input, select { + background: var(--card-dark); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: 4px; + transition: all 0.2s ease; + padding: 8px 12px; +} + +/* 输入框占位符颜色 */ +input::placeholder { + color: var(--text-secondary); +} + +/* 选择框选项样式 */ +select option { + background: var(--card-dark); + color: var(--text-primary); + padding: 8px; +} + +/* 排序选择框样式 */ +#sortSelect { + background-color: var(--card-dark); + color: var(--text-primary); + border: 1px solid var(--border-color); + padding: 6px 28px 6px 12px; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23b4b9c2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 16px; +} + +#sortSelect:hover { + border-color: var(--primary-color); +} + +#sortSelect:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(27, 183, 110, 0.2); +} + +/* 暗色主题下的输入框和选择框 */ +body:not(.light-theme) input, +body:not(.light-theme) select { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.1); + color: var(--text-primary); +} + +body:not(.light-theme) input:hover, +body:not(.light-theme) select:hover { + border-color: rgba(255, 255, 255, 0.2); +} + +body:not(.light-theme) input:focus, +body:not(.light-theme) select:focus { + border-color: var(--primary-color); + background: rgba(255, 255, 255, 0.1); +} + +body:not(.light-theme) input::placeholder { + color: rgba(255, 255, 255, 0.5); +} + +/* 暗色主题下的选择框选项 */ +body:not(.light-theme) select option { + background: var(--background-dark); + color: var(--text-primary); +} + +/* 亮色主题下的输入框和选择框 */ +body.light-theme input, +body.light-theme select { + background: #ffffff; + border: 1px solid #e5e7eb; + color: #333333; +} + +body.light-theme input::placeholder { + color: #9ca3af; +} + +body.light-theme input:hover, +body.light-theme select:hover { + border-color: #d1d5db; +} + +body.light-theme input:focus, +body.light-theme select:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(27, 183, 110, 0.1); +} + +body.light-theme select option { + background: #ffffff; + color: #333333; +} + +/* 消息通知样式 */ +.notification { + position: fixed; + bottom: 24px; + right: 24px; + background: var(--primary-color); + color: #ffffff; + padding: 12px 24px; + border-radius: 8px; + font-weight: 500; + opacity: 0; + transform: translateY(20px); + transition: all 0.3s ease; + z-index: 1000; + display: flex; + align-items: center; + gap: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.notification.show { + opacity: 1; + transform: translateY(0); +} + +.notification svg { + width: 20px; + height: 20px; +} + +/* 修复复制按钮点击后的文字颜色 */ +[data-theme="light"] .copy-button.success { + background: #10b981; + color: #ffffff; +} + +[data-theme="light"] .copy-button.copied { + background: #10b981; + color: #ffffff; +} + +/* 确保复制按钮在所有状态下文字颜色保持一致 */ +.copy-button, +.copy-button:hover, +.copy-button:active, +.copy-button.success, +.copy-button.copied { + color: #ffffff; +} + +[data-theme="light"] .copy-button, +[data-theme="light"] .copy-button:hover, +[data-theme="light"] .copy-button:active, +[data-theme="light"] .copy-button.success, +[data-theme="light"] .copy-button.copied { + color: #ffffff; +} + +/* 加载动画容器 */ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + padding: 32px; +} + +/* 加载动画 */ +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid var(--border-color); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + transform-origin: center center; +} + +.loading-text { + color: var(--text-secondary); + font-size: 14px; + position: relative; + display: inline-block; +} + +.loading-text::after { + content: ''; + animation: dots 1.5s steps(4, end) infinite; +} + +@keyframes dots { + 0%, 20% { content: ''; } + 40% { content: '.'; } + 60% { content: '..'; } + 80%, 100% { content: '...'; } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* 亮色主题下的标签页容器样式 */ +[data-theme="light"] .tab-container { + background: #ffffff; + border-color: #e5e7eb; +} + +/* Logo 样式 */ +.logo-container { + position: fixed; + top: 16px; + left: 16px; + z-index: 50; + background: rgba(0, 0, 0, 0.2); + padding: 4px 8px; + border-radius: 8px; + backdrop-filter: blur(8px); +} + +.logo { + display: inline-flex; + align-items: center; + font-family: Arial, sans-serif; + font-weight: 800; + font-size: 32px; + text-decoration: none; + padding: 4px 8px; + border-radius: 6px; + transition: all 0.2s ease; + letter-spacing: 0.5px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.logo:hover { + transform: translateY(-1px); + filter: brightness(1.1); +} + +.logo-av { + color: #ffffff; +} + +.logo-hub { + color: #000000; + background-color: #ff9000; + padding: 2px 8px; + border-radius: 4px; + margin-left: 1px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* 亮色主题适配 */ +[data-theme="light"] .logo-container { + background: rgba(255, 255, 255, 0.2); +} + +[data-theme="light"] .logo-av { + color: #000000; +} + +/* 移动端适配 */ +@media (max-width: 640px) { + .logo { + font-size: 24px; + } + + .logo-container { + padding: 2px 6px; + } +} + +/* 语言图标样式 */ +.lang-icon { + font-size: 16px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; +} + +/* 回到顶部按钮样式 */ +.back-to-top { + position: fixed; + bottom: 30px; + right: 30px; + background-color: var(--primary-color); + color: var(--text-color); + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + z-index: 100; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); +} + +.back-to-top:hover { + transform: translateY(-3px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); +} + +.back-to-top.hidden { + opacity: 0; + visibility: hidden; + transform: translateY(20px); + pointer-events: none; +} + +.back-to-top svg { + width: 20px; + height: 20px; +} + +/* 视频源地址样式 */ +.video-source { + background: var(--bg-color); + border: 1px solid var(--border-color); + margin-bottom: 1rem; +} + +.source-url { + color: var(--text-color); + padding: 0.25rem 0; + word-break: break-all; +} + +.copy-button { + background: var(--primary-color); + color: var(--bg-color); + transition: all 0.2s; +} + +.copy-button:hover { + opacity: 0.9; +} + +.copy-button.copied { + background: var(--success-color); +} + +/* 深色主题适配 */ +[data-theme="dark"] .video-source { + background: rgba(255, 255, 255, 0.05); +} + +[data-theme="dark"] .source-url { + color: rgba(255, 255, 255, 0.9); +} + +/* 封面图片区域样式 */ +.cover-image-container { + position: relative; + width: 100%; + max-width: 800px; + margin: 20px auto; + text-align: center; + transition: all 0.3s ease; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); + cursor: pointer; + min-height: 300px; /* 添加最小高度 */ +} + +.cover-image-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + rgba(255,255,255,0.1) 25%, + rgba(255,255,255,0.2) 50%, + rgba(255,255,255,0.1) 75% + ); + animation: shimmer 1.5s infinite; + z-index: 1; +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } +} + +.cover-image { + opacity: 0; + transition: opacity 0.3s ease, transform 0.3s ease; + position: relative; + z-index: 2; +} + +.cover-image.loaded { + opacity: 1; +} + +/* 大图预览模态框样式 */ +.image-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.image-modal.active { + opacity: 1; + visibility: visible; +} + +.image-modal.hidden { + display: none; +} + +.modal-content { + position: relative; + max-width: 90%; + max-height: 90vh; +} + +.modal-image { + max-width: 100%; + max-height: 90vh; + object-fit: contain; +} + +.modal-close { + position: absolute; + top: -40px; + right: 0; + background: none; + border: none; + color: white; + cursor: pointer; + padding: 8px; + transition: transform 0.2s ease; +} + +.modal-close:hover { + transform: scale(1.1); +} + +.modal-close svg { + width: 24px; + height: 24px; +} + +/* 添加键盘操作提示 */ +.modal-content::after { + content: 'ESC 关闭'; + position: absolute; + bottom: -30px; + left: 50%; + transform: translateX(-50%); + color: rgba(255, 255, 255, 0.6); + font-size: 14px; +} + +/* 下一个按钮样式 */ +.next-button { + background-color: var(--primary-color); + color: var(--text-color); + border-radius: 0.375rem; + font-weight: 500; + transition: all 0.2s; +} + +.next-button:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +.next-button:active { + transform: translateY(0); +} + +/* 视频播放器容器样式 */ +#playerTab .max-w-4xl { + width: 100%; + padding: 0 1rem; + height: 100%; + display: flex; + flex-direction: column; +} + +/* 视频播放器容器 */ +#playerTab .relative { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} + +/* 视频播放器过渡动画 */ +#videoPlayer { + max-height: calc(100vh - 300px); /* 减小最大高度,为按钮留出空间 */ + object-fit: contain; + background-color: #000; + transition: all 0.3s ease-in-out; + min-height: 200px; + width: 100%; + margin: 0 auto; +} + +/* 视频加载中状态 */ +#videoPlayer.loading { + opacity: 0.6; + filter: brightness(0.8); +} + +/* 下一个按钮容器 */ +#playerTab .flex.justify-center { + margin-top: 1rem; + padding-bottom: 1rem; + flex-shrink: 0; /* 防止按钮被压缩 */ +} + +/* 视频播放器过渡动画 */ +#videoPlayer { + max-height: calc(100vh - 300px); /* 减小最大高度,为按钮留出空间 */ + object-fit: contain; + background-color: #000; + transition: all 0.3s ease-in-out; + min-height: 200px; + width: 100%; + margin: 0 auto; +} + +/* 视频加载中状态 */ +#videoPlayer.loading { + opacity: 0.6; + filter: brightness(0.8); +} + +/* NSFW 警告样式 */ +.nsfw-warning { + color: #dc2626; + font-weight: bold; + font-size: 1.1rem; + padding: 0.5rem; + background-color: rgba(220, 38, 38, 0.1); + border-radius: 0.375rem; + border: 1px solid rgba(220, 38, 38, 0.2); +} + +/* 视频控制区域样式 */ +.video-controls { + background: rgba(0, 0, 0, 0.05); + padding: 0.75rem 1rem; + border-radius: 0.5rem; +} + +/* 自动播放开关样式 */ +.toggle-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + background-color: #ccc; + border-radius: 12px; + transition: all 0.3s; +} + +.toggle-switch::after { + content: ''; + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: white; + top: 2px; + left: 2px; + transition: all 0.3s; +} + +#autoplayToggle:checked + label .toggle-switch { + background-color: var(--primary-color); +} + +#autoplayToggle:checked + label .toggle-switch::after { + transform: translateX(20px); +} + +/* 适配移动端全屏问题 */ +#videoPlayer::-webkit-media-controls { + will-change: transform; +} + +#videoPlayer::-webkit-media-controls-panel { + display: flex !important; + opacity: 1 !important; +} + +#videoPlayer::-webkit-media-controls-play-button { + display: flex !important; + opacity: 1 !important; +} + +/* 确保视频控件在全屏时可见 */ +#videoPlayer:fullscreen::-webkit-media-controls-panel, +#videoPlayer:-webkit-full-screen::-webkit-media-controls-panel { + display: flex !important; + opacity: 1 !important; +} + +/* 修改消息通知样式,确保在浅色主题下字体为白色 */ +[data-theme="light"] .notification { + background: var(--primary-color); + color: #ffffff; +} + +/* 特殊处理加载中状态的通知 */ +[data-theme="light"] .notification.loading { + background: #f3f4f6; + color: #1f2937; + border: 1px solid #e5e7eb; +} + +[data-theme="light"] .notification.loading svg { + color: #1f2937; +} + +/* 优化视频播放页面在浅色主题下的对比度 */ +[data-theme="light"] .video-source { + background: #f3f4f6; + border-color: #e5e7eb; +} + +[data-theme="light"] .source-url { + color: #1f2937; +} + +[data-theme="light"] .source-label { + color: #4b5563; +} + +[data-theme="light"] .video-controls { + background: #f3f4f6; + border: 1px solid #e5e7eb; + color: #1f2937; +} + +[data-theme="light"] .toggle-switch { + background-color: #d1d5db; +} + +[data-theme="light"] .nsfw-warning { + background-color: rgba(220, 38, 38, 0.1); + border-color: rgba(220, 38, 38, 0.2); + color: #dc2626; +} + +/* 确保复制按钮在浅色主题下的文字颜色为白色 */ +[data-theme="light"] .copy-button { + color: #ffffff; +} + +/* 确保下一个按钮在浅色主题下的文字颜色为白色 */ +[data-theme="light"] .next-button { + color: #ffffff; +} + +/* 主题图标样式优化 */ +[data-theme="dark"] #themeToggle svg { + color: #94a3b8; +} + +[data-theme="light"] #themeToggle svg { + color: #fbbf24; +} + +/* 修改主题菜单中的图标 */ +.theme-menu-item[data-theme="dark"] svg { + /* 月亮图标 */ + color: #94a3b8; +} + +.theme-menu-item[data-theme="light"] svg { + /* 太阳图标 */ + color: #fbbf24; +} + +.theme-menu-item[data-theme="emerald"] svg { + /* 翠绿主题图标 */ + color: #10b981; +} + +.theme-menu-item[data-theme="ocean"] svg { + /* 海蓝主题图标 */ + color: #3b82f6; +} + +.theme-menu-item[data-theme="amethyst"] svg { + /* 紫晶主题图标 */ + color: #8b5cf6; +} + +/* 修复日间主题下自动播放文字颜色 */ +[data-theme="light"] .autoplay-toggle label { + color: #1f2937; +} + +/* 里番合集描述文本样式 */ +.collection-description { + text-align: center; + color: var(--text-secondary); + margin-bottom: 2rem; + padding: 1rem; + font-size: 0.9rem; + border-bottom: 1px solid var(--border-color); + max-width: 6xl; + margin-left: auto; + margin-right: auto; +} + +[data-theme="light"] .collection-description { + color: #6b7280; +}