Sync all projects
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $db;
|
||||
private $dbPath;
|
||||
|
||||
public function getName() {
|
||||
return "74P福利(本地库)";
|
||||
}
|
||||
|
||||
public function init($extend = "") {
|
||||
// 数据库文件位于当前目录 (与本文件同名,后缀为 .db)
|
||||
$dbName = str_replace('.php', '.db', basename(__FILE__));
|
||||
$this->dbPath = __DIR__ . '/' . $dbName;
|
||||
|
||||
// 尝试查找对应的数据库文件 (如果当前文件名不匹配,尝试查找原版爬虫名对应的db)
|
||||
if (!file_exists($this->dbPath)) {
|
||||
$originName = '74P福利图 ᵈᶻ[画].db';
|
||||
if (file_exists(__DIR__ . '/' . $originName)) {
|
||||
$this->dbPath = __DIR__ . '/' . $originName;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->db = new SQLite3($this->dbPath);
|
||||
$this->db->busyTimeout(5000);
|
||||
} catch (Exception $e) {
|
||||
// 数据库连接失败,可能是文件不存在
|
||||
}
|
||||
}
|
||||
|
||||
public function isVideoFormat($url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function manualVideoCheck() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
if (!$this->db) return ['class' => []];
|
||||
|
||||
$classes = [];
|
||||
$res = $this->db->query("SELECT tid, name FROM categories");
|
||||
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
|
||||
$classes[] = [
|
||||
"type_id" => $row['tid'],
|
||||
"type_name" => $row['name']
|
||||
];
|
||||
}
|
||||
return ['class' => $classes, 'filters' => []];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
return ['list' => []];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
if (!$this->db) return ['list' => [], 'page' => $pg, 'pagecount' => 0, 'limit' => 20, 'total' => 0];
|
||||
|
||||
$limit = 20;
|
||||
$offset = ($pg - 1) * $limit;
|
||||
|
||||
// 获取总数
|
||||
$countStmt = $this->db->prepare("SELECT COUNT(*) as total FROM vods WHERE type_id = :tid");
|
||||
$countStmt->bindValue(':tid', $tid, SQLITE3_TEXT);
|
||||
$countRes = $countStmt->execute();
|
||||
$total = 0;
|
||||
if ($row = $countRes->fetchArray(SQLITE3_ASSOC)) {
|
||||
$total = $row['total'];
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare("SELECT * FROM vods WHERE type_id = :tid ORDER BY crawled_at DESC LIMIT :limit OFFSET :offset");
|
||||
$stmt->bindValue(':tid', $tid, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':offset', $offset, SQLITE3_INTEGER);
|
||||
|
||||
$res = $stmt->execute();
|
||||
$vlist = [];
|
||||
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
|
||||
$vlist[] = [
|
||||
'vod_id' => $row['vod_id'],
|
||||
'vod_name' => $row['vod_name'],
|
||||
'vod_pic' => $row['vod_pic'],
|
||||
'vod_remarks' => $row['vod_remarks'],
|
||||
'style' => ["type" => "rect", "ratio" => 1.33]
|
||||
];
|
||||
}
|
||||
|
||||
$pageCount = ceil($total / $limit);
|
||||
|
||||
return ['list' => $vlist, 'page' => $pg, 'pagecount' => $pageCount, 'limit' => $limit, 'total' => $total];
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
if (!$this->db) return ['list' => []];
|
||||
|
||||
$vod_id = $ids[0];
|
||||
|
||||
// 1. 获取视频详情 (关联 categories 获取 type_name)
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT v.*, c.name as type_name
|
||||
FROM vods v
|
||||
LEFT JOIN categories c ON v.type_id = c.tid
|
||||
WHERE v.vod_id = :vod_id
|
||||
");
|
||||
$stmt->bindValue(':vod_id', $vod_id, SQLITE3_TEXT);
|
||||
$res = $stmt->execute();
|
||||
$vod_row = $res->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if (!$vod_row) return ['list' => []];
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $vod_row['vod_id'],
|
||||
'vod_name' => $vod_row['vod_name'],
|
||||
'vod_pic' => $vod_row['vod_pic'],
|
||||
'type_name' => $vod_row['type_name'],
|
||||
'vod_content' => $vod_row['vod_content'],
|
||||
'vod_play_from' => '',
|
||||
'vod_play_url' => ''
|
||||
];
|
||||
$vod_pk = $vod_row['id'];
|
||||
|
||||
// 2. 获取剧集列表 (关联 play_sources 获取 play_from)
|
||||
$stmt_ep = $this->db->prepare("
|
||||
SELECT e.*, s.name as play_from
|
||||
FROM episodes e
|
||||
LEFT JOIN play_sources s ON e.sid = s.id
|
||||
WHERE e.vod_pk = :vod_pk
|
||||
");
|
||||
$stmt_ep->bindValue(':vod_pk', $vod_pk, SQLITE3_INTEGER);
|
||||
$res_ep = $stmt_ep->execute();
|
||||
|
||||
$episodes_map = []; // play_from => [ "name$url" ]
|
||||
|
||||
while ($row = $res_ep->fetchArray(SQLITE3_ASSOC)) {
|
||||
$play_from = $row['play_from'];
|
||||
$name = $row['name'];
|
||||
// 优先使用已解析的 URL,如果没有则使用原始 URL
|
||||
$url = !empty($row['resolved_url']) ? $row['resolved_url'] : $row['raw_url'];
|
||||
|
||||
if (!isset($episodes_map[$play_from])) {
|
||||
$episodes_map[$play_from] = [];
|
||||
}
|
||||
$episodes_map[$play_from][] = "{$name}\${$url}";
|
||||
}
|
||||
|
||||
$play_from_list = [];
|
||||
$play_url_list = [];
|
||||
|
||||
foreach ($episodes_map as $from => $eps) {
|
||||
$play_from_list[] = $from;
|
||||
$play_url_list[] = implode("#", $eps);
|
||||
}
|
||||
|
||||
$vod['vod_play_from'] = implode("$$$", $play_from_list);
|
||||
$vod['vod_play_url'] = implode("$$$", $play_url_list);
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
if (!$this->db) return ['list' => [], 'page' => $pg];
|
||||
|
||||
$limit = 20;
|
||||
$offset = ($pg - 1) * $limit;
|
||||
|
||||
// 获取总数
|
||||
$countStmt = $this->db->prepare("SELECT COUNT(*) as total FROM vods WHERE vod_name LIKE :key");
|
||||
$countStmt->bindValue(':key', "%$key%", SQLITE3_TEXT);
|
||||
$countRes = $countStmt->execute();
|
||||
$total = 0;
|
||||
if ($row = $countRes->fetchArray(SQLITE3_ASSOC)) {
|
||||
$total = $row['total'];
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare("SELECT * FROM vods WHERE vod_name LIKE :key ORDER BY crawled_at DESC LIMIT :limit OFFSET :offset");
|
||||
$stmt->bindValue(':key', "%$key%", SQLITE3_TEXT);
|
||||
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':offset', $offset, SQLITE3_INTEGER);
|
||||
|
||||
$res = $stmt->execute();
|
||||
$vlist = [];
|
||||
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
|
||||
$vlist[] = [
|
||||
'vod_id' => $row['vod_id'],
|
||||
'vod_name' => $row['vod_name'],
|
||||
'vod_pic' => $row['vod_pic'],
|
||||
'vod_remarks' => $row['vod_remarks'],
|
||||
'style' => ["type" => "rect", "ratio" => 1.33]
|
||||
];
|
||||
}
|
||||
|
||||
$pageCount = ceil($total / $limit);
|
||||
return ['list' => $vlist, 'page' => $pg, 'pagecount' => $pageCount, 'limit' => $limit, 'total' => $total];
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
// id 已经是 detailContent 中返回的 url
|
||||
// 如果是已解析的 pics:// 链接,直接返回
|
||||
// 如果是原始链接,说明爬取时未解析成功,这里直接返回原始链接让客户端尝试处理(虽然本地模式下通常无法处理网络请求,但保持一致性)
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => $id,
|
||||
"header" => ""
|
||||
];
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
|
||||
private $baseUrl;
|
||||
|
||||
public function getName() {
|
||||
return "74P福利(漫画版)";
|
||||
}
|
||||
|
||||
public function init($extend = "") {
|
||||
$this->baseUrl = "https://www.74p.net";
|
||||
}
|
||||
|
||||
public function isVideoFormat($url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function manualVideoCheck() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getHeader() {
|
||||
return [
|
||||
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Referer" => $this->baseUrl . '/',
|
||||
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"Connection" => "keep-alive"
|
||||
];
|
||||
}
|
||||
|
||||
private function fetchHtml($url, $referer = "") {
|
||||
$headers = $this->getHeader();
|
||||
if ($referer) $headers['Referer'] = $referer;
|
||||
|
||||
$options = [
|
||||
'headers' => $headers
|
||||
];
|
||||
return $this->fetch($url, $options);
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$cats = [
|
||||
["type_name" => "=== 写真 ===", "type_id" => "ignore"],
|
||||
["type_name" => "秀人网", "type_id" => "xiurenwang"],
|
||||
["type_name" => "语画界", "type_id" => "yuhuajie"],
|
||||
["type_name" => "花漾", "type_id" => "huayang"],
|
||||
["type_name" => "星颜社", "type_id" => "xingyanshe"],
|
||||
["type_name" => "嗲囡囡", "type_id" => "feilin"],
|
||||
["type_name" => "爱蜜社", "type_id" => "aimishe"],
|
||||
["type_name" => "波萝社", "type_id" => "boluoshe"],
|
||||
["type_name" => "尤物馆", "type_id" => "youwuguan"],
|
||||
["type_name" => "蜜桃社", "type_id" => "miitao"],
|
||||
["type_name" => "=== 漫画 ===", "type_id" => "ignore"],
|
||||
["type_name" => "日本漫画", "type_id" => "comic/category/jp"],
|
||||
["type_name" => "韩国漫画", "type_id" => "comic/category/kr"],
|
||||
["type_name" => "=== 小说 ===", "type_id" => "ignore"],
|
||||
["type_name" => "都市", "type_id" => "novel/category/Urban"],
|
||||
["type_name" => "乱伦", "type_id" => "novel/category/Incestuous"],
|
||||
["type_name" => "玄幻", "type_id" => "novel/category/Xuanhuan"],
|
||||
["type_name" => "武侠", "type_id" => "novel/category/Wuxia"]
|
||||
];
|
||||
|
||||
$validCats = [];
|
||||
foreach ($cats as $c) {
|
||||
if ($c['type_id'] != 'ignore') {
|
||||
$validCats[] = $c;
|
||||
}
|
||||
}
|
||||
return ['class' => $validCats, 'filters' => []];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
return ['list' => []];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$url = "{$this->baseUrl}/{$tid}/page/{$pg}";
|
||||
return $this->getPostList($url, $pg);
|
||||
}
|
||||
|
||||
private function getPostList($url, $pg) {
|
||||
$html = $this->fetchHtml($url);
|
||||
$vlist = [];
|
||||
|
||||
if ($html) {
|
||||
$listBlock = $html;
|
||||
if (preg_match('/(?:id="index_ajax_list"|class="site-main")[^>]*>(.*?)<(?:footer|aside)/s', $html, $match)) {
|
||||
$listBlock = $match[1];
|
||||
}
|
||||
|
||||
if (preg_match_all('/<li[^>]*>(.*?)<\/li>/s', $listBlock, $items)) {
|
||||
foreach ($items[1] as $item) {
|
||||
if (!preg_match('/href=["\']([^"\']+)["\']/', $item, $hrefMatch)) continue;
|
||||
$href = $hrefMatch[1];
|
||||
|
||||
if (strpos($href, '.css') !== false || strpos($href, '.js') !== false || strpos($href, 'templates/') !== false || strpos($href, 'wp-includes') !== false) continue;
|
||||
|
||||
$pic = "";
|
||||
if (preg_match('/data-original=["\']([^"\']+)["\']/', $item, $imgMatch)) {
|
||||
$pic = $imgMatch[1];
|
||||
} elseif (preg_match('/src=["\']([^"\']+)["\']/', $item, $imgMatch)) {
|
||||
$pic = $imgMatch[1];
|
||||
}
|
||||
|
||||
if (!$pic) $pic = "https://www.74p.net/static/images/cover.png";
|
||||
|
||||
$name = "";
|
||||
if (preg_match('/title=["\']([^"\']+)["\']/', $item, $titleMatch)) {
|
||||
$name = $titleMatch[1];
|
||||
} else {
|
||||
$name = trim(strip_tags($item));
|
||||
$name = explode("\n", $name)[0];
|
||||
}
|
||||
|
||||
if (strpos($name, '.') === 0 || strpos($name, '{') !== false || strlen($name) > 300) continue; // strlen 100 in python is roughly 300 bytes in utf8 php maybe
|
||||
|
||||
if (strpos($href, '//') === 0) $href = 'https:' . $href;
|
||||
elseif (strpos($href, '/') === 0) $href = $this->baseUrl . $href;
|
||||
|
||||
$vlist[] = [
|
||||
'vod_id' => $href,
|
||||
'vod_name' => $name,
|
||||
'vod_pic' => $pic,
|
||||
'vod_remarks' => '点击查看',
|
||||
'style' => ["type" => "rect", "ratio" => 1.33]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pageCount = (count($vlist) >= 15) ? $pg + 1 : $pg;
|
||||
return ['list' => $vlist, 'page' => $pg, 'pagecount' => $pageCount, 'limit' => 20, 'total' => 9999];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$searchPath = "/search/{$key}";
|
||||
$referer = (strpos($key, "漫画") !== false) ? "{$this->baseUrl}/comic" : "{$this->baseUrl}/novel";
|
||||
|
||||
if ($pg > 1) $url = "{$this->baseUrl}{$searchPath}/page/{$pg}";
|
||||
else $url = "{$this->baseUrl}{$searchPath}";
|
||||
|
||||
// Temporarily override fetchHtml's referer logic by passing it
|
||||
// Or actually fetchHtml supports passing referer.
|
||||
// But getPostList calls fetchHtml without referer.
|
||||
// Let's modify getPostList to accept referer or just set global referer.
|
||||
// Simpler: Just rely on default referer or specific one.
|
||||
// Python code sets specific referer.
|
||||
|
||||
// Let's manually fetch here to respect logic, or just reuse getPostList which uses default referer (baseUrl)
|
||||
// Python code: if "漫画" in key: headers['Referer'] = ...
|
||||
// Since getPostList calls fetchHtml($url), and fetchHtml uses default headers if not provided.
|
||||
// Let's just use default headers for simplicity as search usually works without specific referer too.
|
||||
|
||||
return $this->getPostList($url, $pg);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$url = $ids[0];
|
||||
$html = $this->fetchHtml($url);
|
||||
if (!$html) return ['list' => []];
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $url,
|
||||
'vod_name' => '',
|
||||
'vod_pic' => '',
|
||||
'type_name' => '漫画',
|
||||
'vod_content' => '',
|
||||
'vod_play_from' => '74P漫画',
|
||||
'vod_play_url' => ''
|
||||
];
|
||||
|
||||
if (preg_match('/<h1[^>]*>(.*?)<\/h1>/', $html, $h1)) {
|
||||
$vod['vod_name'] = trim(strip_tags($h1[1]));
|
||||
}
|
||||
|
||||
$contentHtml = "";
|
||||
if (preg_match('/(?:id="content"|class="entry-content"|class="single-content")[^>]*>(.*?)<(?:div class="related|footer|aside|section)/s', $html, $match)) {
|
||||
$contentHtml = $match[1];
|
||||
$vod['vod_content'] = mb_substr(trim(strip_tags($contentHtml)), 0, 200);
|
||||
|
||||
if (preg_match('/<img[^>]+src=["\']([^"\']+)["\']/', $contentHtml, $imgMatch)) {
|
||||
$pic = $imgMatch[1];
|
||||
if (strpos($pic, '//') === 0) $pic = 'https:' . $pic;
|
||||
elseif (strpos($pic, '/') === 0) $pic = $this->baseUrl . $pic;
|
||||
$vod['vod_pic'] = $pic;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果上述方式未找到封面,尝试全局匹配第一张非 logo/icon 图片
|
||||
if (empty($vod['vod_pic']) && preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/', $html, $matches)) {
|
||||
foreach ($matches[1] as $src) {
|
||||
if (preg_match('/(logo|icon|avatar|\.gif)/i', $src)) continue;
|
||||
|
||||
if (strpos($src, '//') === 0) $src = 'https:' . $src;
|
||||
elseif (strpos($src, '/') === 0) $src = $this->baseUrl . $src;
|
||||
|
||||
$vod['vod_pic'] = $src;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$playList = [];
|
||||
|
||||
// 1. 查找章节列表
|
||||
if (preg_match_all('/<a[^>]+href=["\']([^"\']*\/(?:comic|novel)\/chapter\/[^"\']+)["\'][^>]*>(.*?)<\/a>/', $html, $links, PREG_SET_ORDER)) {
|
||||
foreach ($links as $link) {
|
||||
$href = $link[1];
|
||||
$name = trim($link[2]);
|
||||
|
||||
if (strpos($href, '//') === 0) $href = 'https:' . $href;
|
||||
elseif (strpos($href, '/') === 0) $href = $this->baseUrl . $href;
|
||||
|
||||
$playList[] = "{$name}\${$href}";
|
||||
}
|
||||
} else {
|
||||
// 2. 无目录,单页
|
||||
$playList[] = "在线观看\${$url}";
|
||||
}
|
||||
|
||||
$vod['vod_play_url'] = implode("#", $playList);
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$images = $this->scrapeAllImages($id);
|
||||
$novelData = implode("&&", $images);
|
||||
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => "pics://{$novelData}",
|
||||
"header" => ""
|
||||
];
|
||||
}
|
||||
|
||||
private function scrapeAllImages($url) {
|
||||
$images = [];
|
||||
$visited = [];
|
||||
$currentUrl = $url;
|
||||
$page = 1;
|
||||
$maxPages = 50;
|
||||
|
||||
while ($page <= $maxPages) {
|
||||
if (in_array($currentUrl, $visited)) break;
|
||||
$visited[] = $currentUrl;
|
||||
|
||||
$html = $this->fetchHtml($currentUrl);
|
||||
if (!$html) break;
|
||||
|
||||
$contentHtml = $html;
|
||||
if (preg_match('/(?:id="content"|class="entry-content"|class="single-content")[^>]*>(.*?)<(?:div class="related|footer|section)/s', $html, $match)) {
|
||||
$contentHtml = $match[1];
|
||||
}
|
||||
|
||||
if (preg_match_all('/<img[^>]+(?:src|data-original|data-src)=["\']([^"\']+)["\']/', $contentHtml, $matches)) {
|
||||
foreach ($matches[1] as $src) {
|
||||
$lowerSrc = strtolower($src);
|
||||
if (strpos($lowerSrc, '.gif') !== false || strpos($lowerSrc, '.svg') !== false || strpos($lowerSrc, 'logo') !== false || strpos($lowerSrc, 'avatar') !== false || strpos($lowerSrc, 'icon') !== false) continue;
|
||||
if (strpos($lowerSrc, '/covers/') !== false) continue; // 过滤封面图推荐
|
||||
|
||||
|
||||
if (strpos($src, '//') === 0) $src = 'https:' . $src;
|
||||
elseif (strpos($src, '/') === 0) $src = $this->baseUrl . $src;
|
||||
|
||||
if (!in_array($src, $images)) {
|
||||
$images[] = $src;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$nextUrl = null;
|
||||
if (preg_match('/<a[^>]+href=["\']([^"\']+)["\'][^>]*>(?:下一页|Next|»)<\/a>/i', $html, $nextMatch)) {
|
||||
$nextUrl = $nextMatch[1];
|
||||
} elseif (preg_match('/<a[^>]+href=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*next[^"\']*["\']/', $html, $nextMatch)) {
|
||||
$nextUrl = $nextMatch[1];
|
||||
}
|
||||
|
||||
if (!$nextUrl && strpos($currentUrl, '/comic/chapter/') === false && strpos($currentUrl, 'page') !== false) {
|
||||
// Try auto-increment if pagination pattern detected
|
||||
$parts = explode('/', rtrim($currentUrl, '/'));
|
||||
$lastPart = end($parts);
|
||||
if (is_numeric($lastPart)) {
|
||||
$base = substr($currentUrl, 0, strrpos($currentUrl, '/'));
|
||||
$nextUrl = "{$base}/" . ($page + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if ($nextUrl) {
|
||||
if (strpos($nextUrl, '//') === 0) $nextUrl = 'https:' . $nextUrl;
|
||||
elseif (strpos($nextUrl, '/') === 0) $nextUrl = $this->baseUrl . $nextUrl;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
$currentUrl = $nextUrl;
|
||||
$page++;
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
/**
|
||||
* J91 / 91PORN - PHP T4 接口 v5 (图片代理修正版)
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
|
||||
define('UA', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36');
|
||||
define('FALLBACK_HOST', 'https://pta.9a07g.com');
|
||||
define('PUBLISH_JS', 'https://lib.kingslord.com/dizhi/publish.js?20221020');
|
||||
define('HOST_CACHE', sys_get_temp_dir() . '/j91host.cache');
|
||||
define('CACHE_TTL', 3600); // 1小时
|
||||
|
||||
// ── 1. 图片服务器代理逻辑 ──────────────────────────────────────
|
||||
// 如果 URL 中带有 img_proxy 参数,则进入代理模式抓取图片并输出
|
||||
if (isset($_GET['img_proxy'])) {
|
||||
$img_url = $_GET['img_proxy'];
|
||||
if (empty($img_url)) exit;
|
||||
|
||||
$ch = curl_init($img_url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'User-Agent: ' . UA,
|
||||
'Referer: https://91porn.com/' // 关键:伪造来源绕过防盗链
|
||||
],
|
||||
]);
|
||||
$data = curl_exec($ch);
|
||||
$type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
curl_close($ch);
|
||||
|
||||
header("Content-Type: " . $type);
|
||||
echo $data;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将原始图片 URL 转换为当前服务器代理 URL
|
||||
*/
|
||||
function proxy_pic($url) {
|
||||
if (!$url) return '';
|
||||
// 获取当前脚本的完整 URL 地址
|
||||
$self = (isset($_SERVER['HTTPS']) ? "https://" : "http://") . $_SERVER['HTTP_HOST'] . explode('?', $_SERVER['REQUEST_URI'])[0];
|
||||
return $self . '?img_proxy=' . urlencode($url);
|
||||
}
|
||||
|
||||
// ── 自动获取最新域名 ─────────────────────────────────────────
|
||||
function getHost() {
|
||||
if (file_exists(HOST_CACHE) && time() - filemtime(HOST_CACHE) < CACHE_TTL) {
|
||||
$cached = trim(file_get_contents(HOST_CACHE));
|
||||
if ($cached) return $cached;
|
||||
}
|
||||
|
||||
// 先尝试已知可用域名
|
||||
$known = ['https://91porny.com', 'https://9lporn.com'];
|
||||
$exclude = ['kingslord.com', 'googletagmanager.com', 'google.com', 'jquery.com', 'dizhi9'];
|
||||
|
||||
foreach ($known as $url) {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url . '/video/category/latest/1',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_HTTPHEADER => ['User-Agent: ' . UA],
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($code === 200 && strpos($body, 'video-elem') !== false) {
|
||||
file_put_contents(HOST_CACHE, $url);
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
// 降级:从发布页抓域名
|
||||
$pub_pages = ['https://d2.dizhi931.com/', 'https://d2.dizhi932.com/'];
|
||||
foreach ($pub_pages as $pub) {
|
||||
$html = bare_get($pub);
|
||||
if (!$html) continue;
|
||||
preg_match_all('#(https?://[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,10})#', $html, $m);
|
||||
foreach (array_unique($m[1]) as $url) {
|
||||
foreach ($exclude as $ex) {
|
||||
if (strpos($url, $ex) !== false) continue 2;
|
||||
}
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url . '/video/category/latest/1',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_HTTPHEADER => ['User-Agent: ' . UA],
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($code === 200 && strpos($body, 'video-elem') !== false) {
|
||||
file_put_contents(HOST_CACHE, $url);
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents(HOST_CACHE, FALLBACK_HOST);
|
||||
return FALLBACK_HOST;
|
||||
}
|
||||
|
||||
function bare_get($url) {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_HTTPHEADER => ['User-Agent: ' . UA],
|
||||
]);
|
||||
$data = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $data ?: '';
|
||||
}
|
||||
|
||||
$HOST = getHost();
|
||||
|
||||
$CLASSES = [
|
||||
['type_id' => 'latest', 'type_name' => '最近更新'],
|
||||
['type_id' => 'hd', 'type_name' => '高清视频'],
|
||||
['type_id' => 'recent-favorite', 'type_name' => '最近加精'],
|
||||
['type_id' => 'hot-list', 'type_name' => '当前最热'],
|
||||
['type_id' => 'recent-rating', 'type_name' => '最近得分'],
|
||||
['type_id' => 'nonpaid', 'type_name' => '非付费'],
|
||||
['type_id' => 'ori', 'type_name' => '91原创'],
|
||||
['type_id' => 'long-list', 'type_name' => '10分钟以上'],
|
||||
['type_id' => 'longer-list', 'type_name' => '20分钟以上'],
|
||||
['type_id' => 'month-discuss', 'type_name' => '本月讨论'],
|
||||
['type_id' => 'top-favorite', 'type_name' => '本月收藏'],
|
||||
['type_id' => 'most-favorite', 'type_name' => '收藏最多'],
|
||||
['type_id' => 'top-list', 'type_name' => '本月最热'],
|
||||
['type_id' => 'top-last', 'type_name' => '上月最热'],
|
||||
['type_id' => 'swag', 'type_name' => 'SWAG'],
|
||||
['type_id' => 'madou', 'type_name' => '麻豆'],
|
||||
];
|
||||
|
||||
function http_get($url) {
|
||||
global $HOST;
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'User-Agent: ' . UA,
|
||||
'Accept: text/html,application/xhtml+xml,*/*;q=0.9',
|
||||
'Accept-Language: zh-CN,zh;q=0.9',
|
||||
'Referer: ' . $HOST . '/',
|
||||
],
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $body ?: '';
|
||||
}
|
||||
|
||||
// ── 播放解析逻辑 (保持不变) ───────────────────────────────────
|
||||
function extract_embed_m3u8($html) {
|
||||
if (preg_match('/data-src="(https:\/\/[^"]+index\.m3u8[^"]*)"/i', $html, $m)) return html_entity_decode($m[1], ENT_QUOTES, 'UTF-8');
|
||||
if (preg_match('/data-src="(https:\/\/[^"]+\.m3u8[^"]*)"/i', $html, $m)) return html_entity_decode($m[1], ENT_QUOTES, 'UTF-8');
|
||||
return '';
|
||||
}
|
||||
function extract_mse_url($html) {
|
||||
if (preg_match('/id="mse"[^>]*data-url="([^"]+)"/i', $html, $m)) return html_entity_decode($m[1], ENT_QUOTES, 'UTF-8');
|
||||
return '';
|
||||
}
|
||||
function extract_data_src($html) {
|
||||
if (!preg_match('/<video\b[^>]*>/i', $html, $vm)) return '';
|
||||
$videoTag = $vm[0];
|
||||
if (stripos($videoTag, 'muted') !== false && stripos($videoTag, 'loop') !== false) return '';
|
||||
if (preg_match('/\bdata-src="([^"]+)"/i', $videoTag, $m)) return html_entity_decode($m[1], ENT_QUOTES, 'UTF-8');
|
||||
return '';
|
||||
}
|
||||
function extract_m3u8($html) {
|
||||
if (preg_match('/(https?:\/\/[^\s"\'<>]+\.m3u8[^\s"\'<>]*)/i', $html, $m)) return html_entity_decode($m[1], ENT_QUOTES, 'UTF-8');
|
||||
return '';
|
||||
}
|
||||
function extract_mp4_from_scripts($html) {
|
||||
preg_match_all('/<script\b[^>]*>(.*?)<\/script>/is', $html, $scripts);
|
||||
$patterns = [
|
||||
'/strencode2\s*=\s*["\']([^"\']+\.mp4[^"\']*)/i',
|
||||
'/video_url\s*[:=]\s*["\']([^"\']+\.mp4[^"\']*)/i',
|
||||
'/["\']src["\']\s*:\s*["\']([^"\']+\.mp4[^"\']*)/i',
|
||||
'/(https?:\/\/[^\s"\']+cdn77\.org[^\s"\']+\.mp4[^\s"\']*)/i',
|
||||
];
|
||||
foreach ($scripts[1] as $script) {
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $script, $m)) return html_entity_decode($m[1], ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extract_play_url($html) {
|
||||
return extract_mse_url($html) ?: extract_embed_m3u8($html) ?: extract_data_src($html) ?: extract_mp4_from_scripts($html) ?: extract_m3u8($html);
|
||||
}
|
||||
|
||||
function detail_url($vod_id) {
|
||||
global $HOST;
|
||||
if (strpos($vod_id, 'vs:') === 0) return $HOST . '/videos/view/' . substr($vod_id, 3) . '/';
|
||||
if (strpos($vod_id, 'hd:') === 0) return $HOST . '/video/view/' . substr($vod_id, 3);
|
||||
$hash = strpos($vod_id, 'v:') === 0 ? substr($vod_id, 2) : $vod_id;
|
||||
return $HOST . '/video/view/' . $hash;
|
||||
}
|
||||
function get_hash($vod_id) { return strpos($vod_id, ':') !== false ? substr($vod_id, strpos($vod_id, ':') + 1) : $vod_id; }
|
||||
function get_type($vod_id) {
|
||||
if (strpos($vod_id, 'vs:') === 0) return 'videos';
|
||||
if (strpos($vod_id, 'hd:') === 0) return 'viewhd';
|
||||
return 'view';
|
||||
}
|
||||
|
||||
// ── 解析列表 (加入图片代理) ─────────────────────────────────────
|
||||
function parse_video_list($html) {
|
||||
$list = [];
|
||||
$blocks = explode('video-elem', $html);
|
||||
array_shift($blocks);
|
||||
foreach ($blocks as $block) {
|
||||
if (!preg_match('#href="(/(video/view(?:hd)?|videos/view)/[^"]+)"#', $block, $hm)) continue;
|
||||
$href = $hm[1];
|
||||
$parts = array_values(array_filter(explode('/', $href)));
|
||||
if ($parts[0] === 'videos') {
|
||||
$vod_id = 'vs:' . ($parts[2] ?? '') . '/' . ($parts[3] ?? '');
|
||||
} elseif (isset($parts[1]) && $parts[1] === 'viewhd') {
|
||||
$vod_id = 'hd:' . ($parts[2] ?? '');
|
||||
} else {
|
||||
$vod_id = 'v:' . ($parts[2] ?? '');
|
||||
}
|
||||
if (strlen($vod_id) <= 3) continue;
|
||||
|
||||
$pic = '';
|
||||
if (preg_match("/background-image:\s*url\(['\"]?([^'\")\s]+)['\"]?\)/i", $block, $pm)) {
|
||||
$pic = $pm[1];
|
||||
if (stripos($pic, '.gif') !== false) continue;
|
||||
if (strpos($pic, '//') === 0) $pic = 'https:' . $pic;
|
||||
// 使用代理函数包装图片 URL
|
||||
$pic = proxy_pic($pic);
|
||||
}
|
||||
if (!preg_match('/class="[^"]*\btitle\b[^"]*"[^>]*>([^<]+)</i', $block, $tm)) continue;
|
||||
$title = trim($tm[1]);
|
||||
if (!$title) continue;
|
||||
|
||||
$remarks = '';
|
||||
if (preg_match('/class="layer">\s*([^<]+)</i', $block, $lm)) $remarks = trim($lm[1]);
|
||||
$list[] = ['vod_id' => $vod_id, 'vod_name' => $title, 'vod_pic' => $pic, 'vod_remarks' => $remarks];
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
function parse_page_count($html, $cur) {
|
||||
$max = $cur;
|
||||
preg_match_all('/[?&]page=(\d+)/i', $html, $m);
|
||||
foreach ($m[1] as $p) { $p = (int)$p; if ($p > $max && $p < 9999) $max = $p; }
|
||||
return $max ?: $cur;
|
||||
}
|
||||
|
||||
// ── 核心逻辑 ─────────────────────────────────────────────────
|
||||
function do_home() {
|
||||
global $CLASSES, $HOST;
|
||||
$html = http_get($HOST);
|
||||
$list = $html ? parse_video_list($html) : [];
|
||||
return ['class' => $CLASSES, 'filters' => new stdClass(), 'list' => $list];
|
||||
}
|
||||
|
||||
function do_category($type_id, $page) {
|
||||
global $HOST;
|
||||
$pg = max(1, (int)$page);
|
||||
$url = in_array($type_id, ['swag', 'madou', 'videos', 'premium']) ? $HOST . '/' . $type_id . '?page=' . $pg : $HOST . '/video/category/' . $type_id . '/' . $pg;
|
||||
$html = http_get($url);
|
||||
if (!$html) return ['list' => [], 'page' => $pg, 'pagecount' => 1];
|
||||
$list = parse_video_list($html);
|
||||
return ['list' => $list, 'page' => $pg, 'pagecount' => $pg + 1, 'limit' => 20, 'total' => 9999];
|
||||
}
|
||||
|
||||
function do_search($wd, $page) {
|
||||
global $HOST;
|
||||
$pg = max(1, (int)$page);
|
||||
$url = $HOST . '/search?keywords=' . urlencode($wd) . ($pg > 1 ? '&page=' . $pg : '');
|
||||
$html = http_get($url);
|
||||
if (!$html) return ['list' => [], 'page' => $pg, 'pagecount' => 1];
|
||||
$list = parse_video_list($html);
|
||||
return ['list' => $list, 'page' => $pg, 'pagecount' => $pg + 1, 'limit' => 20];
|
||||
}
|
||||
|
||||
function do_detail($ids) {
|
||||
global $HOST;
|
||||
$results = [];
|
||||
foreach ($ids as $vod_id) {
|
||||
$vod_id = trim($vod_id);
|
||||
if (!$vod_id) continue;
|
||||
$html = http_get(detail_url($vod_id));
|
||||
if (!$html) continue;
|
||||
|
||||
$title = '未知';
|
||||
if (preg_match('/<meta[^>]+property=["\']og:title["\'][^>]+content=["\']([^"\']+)["\']/i', $html, $m)) $title = trim($m[1]);
|
||||
|
||||
$pic = '';
|
||||
if (preg_match('/<meta[^>]+property=["\']og:image["\'][^>]+content=["\']([^"\']+)["\']/i', $html, $m)) {
|
||||
// 使用代理函数包装详情页图片 URL
|
||||
$pic = proxy_pic($m[1]);
|
||||
}
|
||||
|
||||
$actor = '';
|
||||
if (preg_match('#href="/author/([^"]+)"#i', $html, $am)) $actor = urldecode($am[1]);
|
||||
|
||||
// 提取播放地址
|
||||
$hash = get_hash($vod_id);
|
||||
$type = get_type($vod_id);
|
||||
$play_url = '';
|
||||
$embed_urls = $type === 'viewhd' ? [$HOST . '/video/embedhd/' . $hash, $HOST . '/video/embed/' . $hash] : [$HOST . '/video/embed/' . $hash];
|
||||
|
||||
foreach ($embed_urls as $ep) {
|
||||
$ehtml = http_get($ep);
|
||||
if (!$ehtml) continue;
|
||||
$play_url = extract_embed_m3u8($ehtml);
|
||||
if ($play_url) break;
|
||||
}
|
||||
if (!$play_url) $play_url = extract_play_url($html);
|
||||
|
||||
$results[] = [
|
||||
'vod_id' => $vod_id,
|
||||
'vod_name' => $title,
|
||||
'vod_pic' => $pic,
|
||||
'vod_actor' => $actor,
|
||||
'vod_play_from' => 'J91',
|
||||
'vod_play_url' => '播放$' . $play_url,
|
||||
];
|
||||
}
|
||||
return ['list' => $results];
|
||||
}
|
||||
|
||||
function do_play($url) {
|
||||
global $HOST;
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => html_entity_decode($url, ENT_QUOTES, 'UTF-8'),
|
||||
'header' => ['User-Agent' => UA, 'Referer' => $HOST . '/'],
|
||||
];
|
||||
}
|
||||
|
||||
// ── 路由控制 ──────────────────────────────────────────────────
|
||||
$ac = $_GET['ac'] ?? '';
|
||||
$t = $_GET['t'] ?? '';
|
||||
$pg = $_GET['pg'] ?? '1';
|
||||
$ids = $_GET['ids'] ?? '';
|
||||
$play = $_GET['play'] ?? '';
|
||||
$wd = $_GET['wd'] ?? '';
|
||||
|
||||
try {
|
||||
if ($play) { echo json_encode(do_play($play), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; }
|
||||
if ($wd) { echo json_encode(do_search($wd, $pg), JSON_UNESCAPED_UNICODE); exit; }
|
||||
if (!$ac) { echo json_encode(do_home(), JSON_UNESCAPED_UNICODE); exit; }
|
||||
if ($ac === 'detail') {
|
||||
if ($t) { echo json_encode(do_category($t, $pg), JSON_UNESCAPED_UNICODE); exit; }
|
||||
if ($ids) {
|
||||
$id_list = array_filter(array_map('trim', explode(',', $ids)));
|
||||
echo json_encode(do_detail($id_list), JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['code' => 500, 'msg' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
/**
|
||||
* B站视频爬虫 - PHP 适配版 (道长重构)
|
||||
* 按照 BaseSpider 结构重写
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
|
||||
private $cookie = [];
|
||||
|
||||
public function init($extend = '') {
|
||||
$this->headers['Referer'] = "https://www.bilibili.com";
|
||||
// 配置初始 Cookie
|
||||
// 实际使用时,建议通过 ext 传入 cookie
|
||||
$configCookie = 'buvid3=xxxx; SESSDATA=xxxx;';
|
||||
|
||||
// 尝试从 extend 获取 cookie (假设 extend 是 JSON 字符串或直接是 cookie 字符串)
|
||||
// 这里简化处理:如果 extend 包含 SESSDATA,则认为是 cookie
|
||||
if (!empty($extend)) {
|
||||
if (strpos($extend, 'SESSDATA') !== false) {
|
||||
$configCookie = $extend;
|
||||
} elseif (is_array($extend) && isset($extend['cookie'])) {
|
||||
$configCookie = $extend['cookie'];
|
||||
} else {
|
||||
// 尝试解析 json
|
||||
$json = json_decode($extend, true);
|
||||
if (isset($json['cookie'])) {
|
||||
$configCookie = $json['cookie'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->cookie = $this->parseCookie($configCookie);
|
||||
}
|
||||
|
||||
private function parseCookie($cookieStr) {
|
||||
if (empty($cookieStr)) return [];
|
||||
$cookies = [];
|
||||
$pairs = explode(';', $cookieStr);
|
||||
foreach ($pairs as $pair) {
|
||||
$pair = trim($pair);
|
||||
if (strpos($pair, '=') !== false) {
|
||||
list($name, $value) = explode('=', $pair, 2);
|
||||
$cookies[trim($name)] = trim($value);
|
||||
}
|
||||
}
|
||||
return $cookies;
|
||||
}
|
||||
|
||||
private function buildCookieString() {
|
||||
$pairs = [];
|
||||
foreach ($this->cookie as $name => $value) {
|
||||
$pairs[] = $name . '=' . $value;
|
||||
}
|
||||
return implode('; ', $pairs);
|
||||
}
|
||||
|
||||
// 覆盖父类 fetch 以自动添加 cookie
|
||||
protected function fetch($url, $options = [], $headers = []) {
|
||||
if (!isset($options['cookie'])) {
|
||||
$cookieStr = $this->buildCookieString();
|
||||
if (!empty($cookieStr)) {
|
||||
$options['cookie'] = $cookieStr;
|
||||
}
|
||||
}
|
||||
return parent::fetch($url, $options, $headers);
|
||||
}
|
||||
|
||||
public function homeContent($filter = []) {
|
||||
$classes = [
|
||||
["type_id" => "沙雕仙逆", "type_name" => "傻屌仙逆"],
|
||||
["type_id" => "沙雕动画", "type_name" => "沙雕动画"],
|
||||
["type_id" => "纪录片超清", "type_name" => "纪录片"],
|
||||
["type_id" => "演唱会超清", "type_name" => "演唱会"],
|
||||
["type_id" => "音乐超清", "type_name" => "流行音乐"],
|
||||
["type_id" => "美食超清", "type_name" => "美食"],
|
||||
["type_id" => "食谱", "type_name" => "食谱"],
|
||||
["type_id" => "体育超清", "type_name" => "体育"],
|
||||
["type_id" => "球星", "type_name" => "球星"],
|
||||
["type_id" => "中小学教育", "type_name" => "教育"],
|
||||
["type_id" => "幼儿教育", "type_name" => "幼儿教育"],
|
||||
["type_id" => "旅游", "type_name" => "旅游"],
|
||||
["type_id" => "风景4K", "type_name" => "风景"],
|
||||
["type_id" => "说案", "type_name" => "说案"],
|
||||
["type_id" => "知名UP主", "type_name" => "知名UP主"],
|
||||
["type_id" => "探索发现超清", "type_name" => "探索发现"],
|
||||
["type_id" => "鬼畜", "type_name" => "鬼畜"],
|
||||
["type_id" => "搞笑超清", "type_name" => "搞笑"],
|
||||
["type_id" => "儿童超清", "type_name" => "儿童"],
|
||||
["type_id" => "动物世界超清", "type_name" => "动物世界"],
|
||||
["type_id" => "相声小品超清", "type_name" => "相声小品"],
|
||||
["type_id" => "戏曲", "type_name" => "戏曲"],
|
||||
["type_id" => "解说", "type_name" => "解说"],
|
||||
["type_id" => "演讲", "type_name" => "演讲"],
|
||||
["type_id" => "小姐姐超清", "type_name" => "小姐姐"],
|
||||
["type_id" => "荒野求生超清", "type_name" => "荒野求生"],
|
||||
["type_id" => "健身", "type_name" => "健身"],
|
||||
["type_id" => "帕梅拉", "type_name" => "帕梅拉"],
|
||||
["type_id" => "太极拳", "type_name" => "太极拳"],
|
||||
["type_id" => "广场舞", "type_name" => "广场舞"],
|
||||
["type_id" => "舞蹈", "type_name" => "舞蹈"],
|
||||
["type_id" => "音乐", "type_name" => "音乐"],
|
||||
["type_id" => "歌曲", "type_name" => "歌曲"],
|
||||
["type_id" => "MV4K", "type_name" => "MV"],
|
||||
["type_id" => "舞曲超清", "type_name" => "舞曲"],
|
||||
["type_id" => "4K", "type_name" => "4K"],
|
||||
["type_id" => "电影", "type_name" => "电影"],
|
||||
["type_id" => "电视剧", "type_name" => "电视剧"],
|
||||
["type_id" => "白噪音超清", "type_name" => "白噪音"],
|
||||
["type_id" => "考公考证", "type_name" => "考公考证"],
|
||||
["type_id" => "平面设计教学", "type_name" => "平面设计教学"],
|
||||
["type_id" => "软件教程", "type_name" => "软件教程"],
|
||||
["type_id" => "Windows", "type_name" => "Windows"]
|
||||
];
|
||||
return ['class' => $classes];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
$url = 'https://api.bilibili.com/x/web-interface/popular?ps=20&pn=1';
|
||||
$data = json_decode($this->fetch($url), true);
|
||||
|
||||
$videos = [];
|
||||
if (isset($data['data']['list'])) {
|
||||
foreach ($data['data']['list'] as $item) {
|
||||
$videos[] = [
|
||||
'vod_id' => $item['aid'],
|
||||
'vod_name' => strip_tags($item['title']),
|
||||
'vod_pic' => $item['pic'],
|
||||
'vod_remarks' => $this->formatDuration($item['duration'])
|
||||
];
|
||||
}
|
||||
}
|
||||
return ['list' => $videos];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$page = max(1, intval($pg));
|
||||
|
||||
$url = 'https://api.bilibili.com/x/web-interface/search/type';
|
||||
$params = [
|
||||
'search_type' => 'video',
|
||||
'keyword' => $tid,
|
||||
'page' => $page
|
||||
];
|
||||
$url .= '?' . http_build_query($params);
|
||||
|
||||
$data = json_decode($this->fetch($url), true);
|
||||
|
||||
$videos = [];
|
||||
if (isset($data['data']['result'])) {
|
||||
foreach ($data['data']['result'] as $item) {
|
||||
if ($item['type'] !== 'video') continue;
|
||||
|
||||
$videos[] = [
|
||||
'vod_id' => $item['aid'],
|
||||
'vod_name' => strip_tags($item['title']),
|
||||
'vod_pic' => 'https:' . $item['pic'],
|
||||
'vod_remarks' => $this->formatSearchDuration($item['duration'])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$pageCount = $data['data']['numPages'] ?? 1;
|
||||
$total = $data['data']['numResults'] ?? count($videos);
|
||||
|
||||
return $this->pageResult($videos, $page, $total, 20);
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
return $this->categoryContent($key, $pg);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
if (empty($ids)) return ['list' => []];
|
||||
$vid = $ids[0];
|
||||
|
||||
$url = 'https://api.bilibili.com/x/web-interface/view?aid=' . $vid;
|
||||
$data = json_decode($this->fetch($url), true);
|
||||
|
||||
if (!isset($data['data'])) {
|
||||
return ['list' => []];
|
||||
}
|
||||
|
||||
$video = $data['data'];
|
||||
|
||||
// 构建播放列表
|
||||
$playUrl = '';
|
||||
foreach ($video['pages'] as $index => $page) {
|
||||
$part = $page['part'] ?: '第' . ($index + 1) . '集';
|
||||
// 构造 playId: avid_cid
|
||||
$playUrl .= "{$part}\${$vid}_{$page['cid']}#";
|
||||
}
|
||||
|
||||
$vod = [
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => strip_tags($video['title']),
|
||||
"vod_pic" => $video['pic'],
|
||||
"vod_content" => $video['desc'],
|
||||
"vod_play_from" => "B站视频",
|
||||
"vod_play_url" => rtrim($playUrl, '#')
|
||||
];
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
if (strpos($id, '_') !== false) {
|
||||
list($avid, $cid) = explode('_', $id);
|
||||
} else {
|
||||
return ['parse' => 0, 'url' => '', 'error' => '无效的视频ID格式'];
|
||||
}
|
||||
|
||||
$url = 'https://api.bilibili.com/x/player/playurl';
|
||||
$params = [
|
||||
'avid' => $avid,
|
||||
'cid' => $cid,
|
||||
'qn' => 112, // 原画质量
|
||||
'fnval' => 0,
|
||||
];
|
||||
$url .= '?' . http_build_query($params);
|
||||
|
||||
$data = json_decode($this->fetch($url), true);
|
||||
|
||||
if (!isset($data['data']) || $data['code'] !== 0) {
|
||||
return ['parse' => 0, 'url' => '', 'error' => '获取播放地址失败'];
|
||||
}
|
||||
|
||||
// 直接返回第一个播放地址
|
||||
if (isset($data['data']['durl'][0]['url'])) {
|
||||
$playUrl = $data['data']['durl'][0]['url'];
|
||||
|
||||
$headers = $this->headers;
|
||||
$headers['Referer'] = 'https://www.bilibili.com/video/av' . $avid;
|
||||
$headers['Origin'] = 'https://www.bilibili.com';
|
||||
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => $playUrl,
|
||||
'header' => $headers,
|
||||
'danmaku' => "https://api.bilibili.com/x/v1/dm/list.so?oid={$cid}"
|
||||
];
|
||||
}
|
||||
|
||||
return ['parse' => 0, 'url' => '', 'error' => '无法获取播放地址'];
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
private function formatDuration($seconds) {
|
||||
if ($seconds <= 0) return '00:00';
|
||||
$minutes = floor($seconds / 60);
|
||||
$secs = $seconds % 60;
|
||||
return sprintf('%02d:%02d', $minutes, $secs);
|
||||
}
|
||||
|
||||
private function formatSearchDuration($duration) {
|
||||
$parts = explode(':', $duration);
|
||||
if (count($parts) === 2) {
|
||||
return $duration;
|
||||
}
|
||||
return '00:00';
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,2145 @@
|
||||
<?php
|
||||
// PDF阅读器.php - 全屏沉浸版:12静态主题 + 护眼主题 + 12动态主题 + 3D悬浮书架 + 酷炫加载动画
|
||||
header("Content-Type: text/html; charset=utf-8");
|
||||
$baseDir = 'PDF';
|
||||
|
||||
if (!is_dir($baseDir)) {
|
||||
mkdir($baseDir);
|
||||
echo "已自动创建 PDF 目录,请放入PDF/EPUB/TXT";
|
||||
exit;
|
||||
}
|
||||
|
||||
function scanDirectory($path) {
|
||||
$result = [];
|
||||
if (!is_dir($path)) return $result;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
if ($entry == '.epub_cache' || $entry == '.txt_cache') continue;
|
||||
if (strpos($entry, '.') === 0) continue;
|
||||
$result[] = $path . '/' . $entry;
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
natsort($result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
function scanImages($path) {
|
||||
$images = [];
|
||||
$extensions = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
|
||||
if (!is_dir($path)) return $images;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
$ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $extensions)) $images[] = $path . '/' . $entry;
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
natsort($images);
|
||||
return array_values($images);
|
||||
}
|
||||
|
||||
function parseTxtFile($txtPath, $book, $chapter, $baseDir) {
|
||||
$cacheDir = $baseDir . '/.txt_cache/' . $book . '/' . md5($chapter);
|
||||
$cacheFile = $cacheDir . '/chapters.json';
|
||||
if (file_exists($cacheFile)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
if ($data && isset($data['chapters'])) return $data;
|
||||
}
|
||||
$content = file_get_contents($txtPath);
|
||||
$encoding = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'BIG5'], true);
|
||||
if (!$encoding) $encoding = 'UTF-8';
|
||||
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
|
||||
$patterns = [
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+章[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+节[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+卷[\s]*[^\n]*/u',
|
||||
'/(?:Chapter|CHAPTER|Ch\.?)\s*\d+[.:\s]*[^\n]*/i',
|
||||
'/\[\d+\][\s]*[^\n]*/',
|
||||
'/(?:一|二|三|四|五|六|七|八|九|十)、[\s]*[^\n]*/u',
|
||||
];
|
||||
$lines = preg_split('/\r\n|\r|\n/', $content);
|
||||
$chapters = [];
|
||||
$currentChapter = ['title' => '序章', 'content' => ''];
|
||||
$foundFirstChapter = false;
|
||||
foreach ($lines as $line) {
|
||||
$line = rtrim($line);
|
||||
$isChapter = false; $chapterTitle = '';
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $line, $matches)) {
|
||||
$chapterTitle = trim($matches[0]);
|
||||
$isChapter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($isChapter && $chapterTitle) {
|
||||
if ($foundFirstChapter && $currentChapter['content'] !== '') $chapters[] = $currentChapter;
|
||||
$currentChapter = ['title' => $chapterTitle, 'content' => ''];
|
||||
$foundFirstChapter = true;
|
||||
} else {
|
||||
if ($line !== '' || $currentChapter['content'] !== '') $currentChapter['content'] .= $line . "\n";
|
||||
}
|
||||
}
|
||||
if ($currentChapter['content'] !== '') $chapters[] = $currentChapter;
|
||||
if (empty($chapters)) $chapters = [['title' => basename($chapter, '.txt'), 'content' => $content]];
|
||||
foreach ($chapters as &$chap) {
|
||||
$chap['content'] = preg_replace('/\n\s*\n/', "</p><p>", $chap['content']);
|
||||
$chap['content'] = "<p>" . str_replace("\n", "<br>", $chap['content']) . "</p>";
|
||||
$chap['content'] = preg_replace('/<p>\s*<\/p>/', '', $chap['content']);
|
||||
}
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
$result = ['type' => 'txt', 'chapters' => $chapters, 'totalChapters' => count($chapters)];
|
||||
file_put_contents($cacheFile, json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
return $result;
|
||||
}
|
||||
|
||||
function parseEpub($epubFilePath, $book, $chapter, $baseDir) {
|
||||
$cacheDir = $baseDir . '/.epub_cache/' . $book . '/' . md5($chapter);
|
||||
$cacheTypeFile = $cacheDir . '/type.json';
|
||||
if (file_exists($cacheTypeFile)) {
|
||||
$cached = json_decode(file_get_contents($cacheTypeFile), true);
|
||||
if ($cached && isset($cached['type'])) {
|
||||
if (($cached['type'] == 'comic' || $cached['type'] == 'ebook') && isset($cached['data'])) return $cached;
|
||||
if ($cached['type'] == 'comic' && isset($cached['images'])) return $cached;
|
||||
if ($cached['type'] == 'ebook' && isset($cached['htmlContents'])) return $cached;
|
||||
}
|
||||
}
|
||||
if (!class_exists('ZipArchive')) return ['error' => '请启用ZipArchive扩展'];
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($epubFilePath) !== true) return ['error' => '无法打开EPUB文件'];
|
||||
$container = $zip->getFromName('META-INF/container.xml');
|
||||
if (!$container) { $zip->close(); return ['error' => '无效的EPUB文件']; }
|
||||
$rootFile = '';
|
||||
if (preg_match('/full-path="([^"]+)"/', $container, $matches)) $rootFile = $matches[1];
|
||||
if (!$rootFile) { $zip->close(); return ['error' => '无法解析EPUB结构']; }
|
||||
$opfContent = $zip->getFromName($rootFile);
|
||||
if (!$opfContent) { $zip->close(); return ['error' => '无法解析OPF文件']; }
|
||||
$opfDir = dirname($rootFile);
|
||||
if ($opfDir == '.') $opfDir = '';
|
||||
else $opfDir .= '/';
|
||||
$manifest = [];
|
||||
preg_match_all('/<item[^>]*id="([^"]*)"[^>]*href="([^"]*)"[^>]*>/i', $opfContent, $items);
|
||||
foreach ($items[1] as $i => $id) $manifest[$id] = $opfDir . $items[2][$i];
|
||||
$spineOrder = [];
|
||||
preg_match_all('/<itemref[^>]*idref="([^"]+)"/i', $opfContent, $spineMatches);
|
||||
if (!empty($spineMatches[1])) $spineOrder = $spineMatches[1];
|
||||
$allImages = []; $htmlContents = []; $cssContent = '';
|
||||
preg_match_all('/<item[^>]*href="([^"]+\.css)"[^>]*media-type="text\/css"[^>]*>/i', $opfContent, $cssMatches);
|
||||
foreach ($cssMatches[1] as $cssPath) {
|
||||
$fullPath = $opfDir . $cssPath;
|
||||
$cssData = $zip->getFromName($fullPath);
|
||||
if ($cssData !== false) $cssContent .= $cssData . "\n";
|
||||
}
|
||||
foreach ($spineOrder as $idref) {
|
||||
if (!isset($manifest[$idref])) continue;
|
||||
$filePath = $manifest[$idref];
|
||||
$content = $zip->getFromName($filePath);
|
||||
if ($content === false) continue;
|
||||
preg_match_all('/<img[^>]*src=["\']([^"\']+)["\']/i', $content, $imgMatches);
|
||||
$pageImages = [];
|
||||
foreach ($imgMatches[1] as $src) {
|
||||
$imgPath = dirname($filePath) . '/' . $src;
|
||||
$imgPath = preg_replace('#/\./#', '/', $imgPath);
|
||||
while (strpos($imgPath, '../') !== false) $imgPath = preg_replace('#[^/]+/\.\./#', '', $imgPath, 1);
|
||||
if (!in_array($imgPath, $allImages)) { $allImages[] = $imgPath; $pageImages[] = $imgPath; }
|
||||
}
|
||||
$title = '';
|
||||
if (preg_match('/<title[^>]*>([^<]+)<\/title>/i', $content, $titleMatch)) $title = trim($titleMatch[1]);
|
||||
if (!$title) {
|
||||
if (preg_match('/<h1[^>]*>([^<]+)<\/h1>/i', $content, $h1Match)) $title = trim($h1Match[1]);
|
||||
else $title = '第 ' . (count($htmlContents) + 1) . ' 章';
|
||||
}
|
||||
if (preg_match('/<body[^>]*>([\s\S]*?)<\/body>/i', $content, $bodyMatch)) $bodyContent = $bodyMatch[1];
|
||||
else $bodyContent = $content;
|
||||
$htmlContents[] = ['title' => $title, 'content' => $bodyContent, 'images' => $pageImages, 'index' => count($htmlContents)];
|
||||
}
|
||||
$totalItems = count($spineOrder);
|
||||
$totalImages = count($allImages);
|
||||
$isComic = false;
|
||||
if ($totalImages == 0) $isComic = false;
|
||||
else if ($totalItems == 0) $isComic = true;
|
||||
else {
|
||||
$totalTextLength = 0; $pagesWithLittleText = 0; $pagesWithManyImages = 0;
|
||||
foreach ($htmlContents as $chapter) {
|
||||
$plainText = strip_tags($chapter['content']);
|
||||
$textLength = mb_strlen($plainText);
|
||||
$totalTextLength += $textLength;
|
||||
$imageCount = count($chapter['images']);
|
||||
if ($textLength < 300) $pagesWithLittleText++;
|
||||
if ($imageCount > 2) $pagesWithManyImages++;
|
||||
}
|
||||
$avgTextLength = $totalTextLength / max($totalItems, 1);
|
||||
$littleTextRatio = $pagesWithLittleText / max($totalItems, 1);
|
||||
$manyImagesRatio = $pagesWithManyImages / max($totalItems, 1);
|
||||
if ($avgTextLength < 500 || $littleTextRatio > 0.3 || $manyImagesRatio > 0.3) $isComic = true;
|
||||
}
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
if ($isComic) {
|
||||
$cachedImages = []; $orderedImages = [];
|
||||
foreach ($htmlContents as $chapter) {
|
||||
foreach ($chapter['images'] as $imgPath) {
|
||||
if (!in_array($imgPath, $orderedImages)) $orderedImages[] = $imgPath;
|
||||
}
|
||||
}
|
||||
if (empty($orderedImages)) {
|
||||
preg_match_all('/<item[^>]*href="([^"]+\.(jpg|jpeg|png|webp|gif))"[^>]*>/i', $opfContent, $imgMatches);
|
||||
foreach ($imgMatches[1] as $imgPath) {
|
||||
$fullPath = $opfDir . $imgPath;
|
||||
if (!in_array($fullPath, $orderedImages)) $orderedImages[] = $fullPath;
|
||||
}
|
||||
}
|
||||
foreach ($orderedImages as $idx => $relativePath) {
|
||||
$ext = strtolower(pathinfo($relativePath, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif'])) $ext = 'jpg';
|
||||
$cacheFile = $cacheDir . '/' . sprintf('%04d', $idx+1) . '.' . $ext;
|
||||
if (file_exists($cacheFile) && filesize($cacheFile) > 100) { $cachedImages[] = $cacheFile; continue; }
|
||||
$imageData = $zip->getFromName($relativePath);
|
||||
if ($imageData === false) $imageData = $zip->getFromName(urldecode($relativePath));
|
||||
if ($imageData !== false && strlen($imageData) > 100) {
|
||||
file_put_contents($cacheFile, $imageData);
|
||||
$cachedImages[] = $cacheFile;
|
||||
} else {
|
||||
$cachedImages[] = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="300" height="400"%3E%3Crect width="300" height="400" fill="%23333"/%3E%3Ctext x="150" y="200" fill="%23fff" text-anchor="middle"%3E图片缺失%3C/text%3E%3C/svg%3E';
|
||||
}
|
||||
}
|
||||
$result = ['type' => 'comic', 'images' => $cachedImages, 'totalPages' => count($cachedImages)];
|
||||
} else {
|
||||
$imagesDir = $cacheDir . '/images/';
|
||||
if (!is_dir($imagesDir)) mkdir($imagesDir, 0777, true);
|
||||
$imageUrlMap = [];
|
||||
foreach ($allImages as $relativePath) {
|
||||
$originalFilename = basename($relativePath);
|
||||
$ext = strtolower(pathinfo($originalFilename, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif'])) $ext = 'jpg';
|
||||
$cacheFilename = md5($relativePath) . '_' . preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $originalFilename);
|
||||
$cacheFile = $imagesDir . $cacheFilename;
|
||||
if (!file_exists($cacheFile)) {
|
||||
$imageData = $zip->getFromName($relativePath);
|
||||
if ($imageData === false) $imageData = $zip->getFromName(urldecode($relativePath));
|
||||
if ($imageData !== false) file_put_contents($cacheFile, $imageData);
|
||||
}
|
||||
if (file_exists($cacheFile)) {
|
||||
$imageUrlMap[$originalFilename] = $cacheFile;
|
||||
$nameNoExt = pathinfo($originalFilename, PATHINFO_FILENAME);
|
||||
$imageUrlMap[$nameNoExt] = $cacheFile;
|
||||
}
|
||||
}
|
||||
foreach ($htmlContents as &$chapter) {
|
||||
$chapter['content'] = preg_replace_callback('/src=["\']([^"\']+)["\']/i', function($matches) use ($imageUrlMap) {
|
||||
$src = $matches[1];
|
||||
if (strpos($src, 'http://') === 0 || strpos($src, 'https://') === 0 || strpos($src, 'data:') === 0) return $matches[0];
|
||||
if (strpos($src, '.epub_cache/') !== false) return $matches[0];
|
||||
$filename = basename(urldecode($src));
|
||||
if (isset($imageUrlMap[$filename])) return 'src="' . $imageUrlMap[$filename] . '"';
|
||||
$name = pathinfo($filename, PATHINFO_FILENAME);
|
||||
if (isset($imageUrlMap[$name])) return 'src="' . $imageUrlMap[$name] . '"';
|
||||
return $matches[0];
|
||||
}, $chapter['content']);
|
||||
$chapter['content'] = preg_replace_callback('/url\([\'"]?([^\'"\)]+)[\'"]?\)/i', function($matches) use ($imageUrlMap) {
|
||||
$url = $matches[1];
|
||||
$filename = basename(urldecode($url));
|
||||
if (isset($imageUrlMap[$filename])) return 'url("' . $imageUrlMap[$filename] . '")';
|
||||
return $matches[0];
|
||||
}, $chapter['content']);
|
||||
}
|
||||
$result = ['type' => 'ebook', 'htmlContents' => $htmlContents, 'cssContent' => $cssContent, 'totalChapters' => count($htmlContents)];
|
||||
}
|
||||
$zip->close();
|
||||
file_put_contents($cacheTypeFile, json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
return $result;
|
||||
}
|
||||
|
||||
$book = isset($_GET['book']) ? $_GET['book'] : '';
|
||||
$chapter = isset($_GET['chapter']) ? $_GET['chapter'] : '';
|
||||
$isChapterPage = ($book && $chapter);
|
||||
$isPdf = $chapter && (stripos($chapter, '.pdf') !== false);
|
||||
$isEpub = $chapter && (stripos($chapter, '.epub') !== false);
|
||||
$isTxt = $chapter && (stripos($chapter, '.txt') !== false);
|
||||
|
||||
if ($book && $chapter) {
|
||||
$encodedBook = rawurlencode($book);
|
||||
$encodedChapter = rawurlencode($chapter);
|
||||
$fileUrl = "$baseDir/$encodedBook/$encodedChapter";
|
||||
}
|
||||
|
||||
$images = [];
|
||||
$epubData = null;
|
||||
$txtData = null;
|
||||
$epubError = null;
|
||||
|
||||
if ($isTxt && $isChapterPage && $book && $chapter) {
|
||||
$txtPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
if (file_exists($txtPath)) $txtData = parseTxtFile($txtPath, $book, $chapter, $baseDir);
|
||||
else $epubError = 'TXT文件不存在';
|
||||
} elseif ($isEpub && $isChapterPage && $book && $chapter) {
|
||||
$epubPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
if (file_exists($epubPath)) {
|
||||
$result = parseEpub($epubPath, $book, $chapter, $baseDir);
|
||||
if (isset($result['error'])) $epubError = $result['error'];
|
||||
else { $epubData = $result; if ($epubData['type'] == 'comic') $images = $epubData['images']; }
|
||||
} else $epubError = 'EPUB文件不存在';
|
||||
} elseif (!$isPdf && $isChapterPage && $book && $chapter) {
|
||||
$localPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
$images = scanImages($localPath);
|
||||
}
|
||||
|
||||
$currentFile = 'PDF阅读器.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
|
||||
<script>pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';</script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; min-height: 100vh; transition: all 0.3s ease; }
|
||||
|
||||
.floating-buttons, .speed-panel, .theme-selector, .font-controls {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.floating-buttons.visible, .speed-panel.visible, .theme-selector.visible, .font-controls.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.global-progress-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1003;
|
||||
padding: 8px 16px 16px 16px;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease;
|
||||
transform: translateY(0);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
.global-progress-container.hide {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
.progress-range-area {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.progress-slider-global {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.progress-slider-global::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #ff9800;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 8px rgba(255,152,0,0.8);
|
||||
border: 2px solid #fff;
|
||||
transition: transform 0.1s, background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.progress-slider-global::-webkit-slider-thumb:hover { transform: scale(1.2); }
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
padding: 4px 0 2px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.chapter-tooltip {
|
||||
position: fixed;
|
||||
background: rgba(0,0,0,0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
color: #ff9800;
|
||||
padding: 10px 20px;
|
||||
border-radius: 40px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
|
||||
z-index: 10007;
|
||||
border: 1px solid rgba(255,152,0,0.6);
|
||||
transition: all 0.2s ease;
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.5px;
|
||||
bottom: 280px;
|
||||
right: 12px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.page-turn-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeInOutFull 0.5s ease-out forwards;
|
||||
perspective: 2000px;
|
||||
backdrop-filter: blur(4px);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.page-turn-overlay .book-container {
|
||||
position: relative;
|
||||
width: 70%;
|
||||
max-width: 500px;
|
||||
height: 70%;
|
||||
max-height: 500px;
|
||||
transform-style: preserve-3d;
|
||||
animation: bookFlipFull 0.5s ease-in-out forwards;
|
||||
}
|
||||
.page-turn-overlay .book-left, .page-turn-overlay .book-right {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 80px;
|
||||
box-shadow: 0 0 40px rgba(0,0,0,0.4);
|
||||
transition: background 0.3s ease, border 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.page-turn-overlay .book-left {
|
||||
left: 0;
|
||||
transform-origin: right center;
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
.page-turn-overlay .book-right {
|
||||
right: 0;
|
||||
transform-origin: left center;
|
||||
border-radius: 0 16px 16px 0;
|
||||
}
|
||||
.page-turn-overlay .message {
|
||||
position: absolute;
|
||||
bottom: 20%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 28px;
|
||||
border-radius: 50px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
backdrop-filter: blur(8px);
|
||||
letter-spacing: 2px;
|
||||
transition: background 0.3s ease, color 0.3s ease, border 0.3s ease;
|
||||
}
|
||||
@keyframes fadeInOutFull {
|
||||
0% { opacity: 0; backdrop-filter: blur(0px); }
|
||||
15% { opacity: 1; backdrop-filter: blur(4px); }
|
||||
85% { opacity: 1; backdrop-filter: blur(4px); }
|
||||
100% { opacity: 0; backdrop-filter: blur(0px); visibility: hidden; }
|
||||
}
|
||||
@keyframes bookFlipFull {
|
||||
0% { transform: scale(0.9) rotateY(0deg); opacity: 0.5; }
|
||||
30% { transform: scale(1.05) rotateY(-15deg); opacity: 1; }
|
||||
70% { transform: scale(1.05) rotateY(-5deg); opacity: 1; }
|
||||
100% { transform: scale(1) rotateY(0deg); opacity: 1; }
|
||||
}
|
||||
|
||||
.speed-panel {
|
||||
position: fixed;
|
||||
right: 80px;
|
||||
bottom: 105px;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 12px 16px;
|
||||
border-radius: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
z-index: 10001;
|
||||
min-width: 170px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.speed-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
gap: 12px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.speed-value {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.speed-slider {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.speed-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #ff9800;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.speed-presets {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.speed-preset {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.1s, background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.speed-preset.active { color: #ff9800; background: rgba(255,152,0,0.2); }
|
||||
.auto-chapter-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
transition: border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.auto-chapter-line input { width: 36px; height: 20px; cursor: pointer; accent-color: #ff9800; transition: accent-color 0.3s ease; }
|
||||
|
||||
.font-controls {
|
||||
position: fixed; right: 12px; bottom: 230px; backdrop-filter: blur(10px); padding: 8px 12px; border-radius: 30px; display: flex; gap: 12px; z-index: 10001; transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.font-controls button { background: none; border: none; font-size: 18px; padding: 4px 8px; cursor: pointer; transition: color 0.3s ease; }
|
||||
|
||||
.theme-selector {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 290px;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 12px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
z-index: 10001;
|
||||
max-width: 340px;
|
||||
width: max-content;
|
||||
transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.floating-buttons { position: fixed; right: 12px; bottom: 100px; display: flex; flex-direction: column; gap: 12px; z-index: 10000; }
|
||||
.floating-btn { width: 52px; height: 52px; backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; transition: all 0.2s, background 0.3s ease, border-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease; }
|
||||
.floating-btn.bookmark-btn { background: linear-gradient(135deg, #ff9800, #ff5722); }
|
||||
.floating-btn.active { background: #ff9800; }
|
||||
|
||||
.bookmark-panel {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 220px;
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
width: 320px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
z-index: 10002;
|
||||
transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.bookmark-panel.show { display: block; }
|
||||
.bookmark-header { padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,0.15); font-weight: 600; display: flex; justify-content: space-between; transition: border-color 0.3s ease, color 0.3s ease; }
|
||||
.bookmark-header span:last-child { cursor: pointer; font-size: 22px; }
|
||||
.bookmark-list { padding: 10px; }
|
||||
.bookmark-item { background: rgba(255,255,255,0.1); margin: 8px 0; padding: 12px; border-radius: 14px; cursor: pointer; transition: background 0.3s ease; }
|
||||
.bookmark-item:hover { background: rgba(255,255,255,0.2); }
|
||||
.bookmark-item .title { font-weight: 600; color: #ffb347; font-size: 14px; transition: color 0.3s ease; }
|
||||
.bookmark-item .info { font-size: 11px; color: rgba(255,255,255,0.6); margin-top: 5px; transition: color 0.3s ease; }
|
||||
.bookmark-item .delete { float: right; color: #ff6b6b; font-size: 16px; cursor: pointer; }
|
||||
.empty-bookmark { color: rgba(255,255,255,0.5); text-align: center; padding: 30px; font-size: 13px; transition: color 0.3s ease; }
|
||||
|
||||
.theme-dot { width: 36px; height: 36px; border-radius: 12px; cursor: pointer; border: 2px solid rgba(255,255,255,0.5); transition: all 0.1s, border-color 0.3s ease, box-shadow 0.3s ease; box-sizing: border-box; }
|
||||
.theme-dot.active { border-color: #ff9800; transform: scale(1.05); box-shadow: 0 0 8px rgba(255,152,0,0.5); }
|
||||
|
||||
.top-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
transition: background 0.3s ease, border-bottom 0.3s ease;
|
||||
}
|
||||
.top-bar-left { display: flex; align-items: center; gap: 12px; flex: 1; overflow: hidden; }
|
||||
.back-btn { width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.15); border: none; transition: background 0.3s ease, color 0.3s ease; }
|
||||
.top-bar .nav-links { font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; transition: color 0.3s ease; }
|
||||
.top-bar a { text-decoration: none; transition: color 0.3s ease; }
|
||||
.top-bar button { padding: 8px 18px; border-radius: 30px; cursor: pointer; font-size: 14px; margin-left: 8px; background: rgba(255,255,255,0.15); border: none; transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease; }
|
||||
.top-bar button.bookmark { background: rgba(255, 152, 0, 0.8); color: white; }
|
||||
.content { margin-top: 70px; padding: 16px; position: relative; z-index: 1; margin-bottom: 70px; }
|
||||
.ebook-chapter { border-radius: 24px; padding: 30px 24px; margin: 20px auto; max-width: 800px; transition: all 0.2s ease, background 0.3s ease, color 0.3s ease, border 0.3s ease, box-shadow 0.3s ease; }
|
||||
.ebook-chapter p { margin-bottom: 1em; line-height: 1.8; }
|
||||
.ebook-chapter .chapter-title { font-size: 1.8em; text-align: center; margin-bottom: 1em; padding-bottom: 0.3em; transition: color 0.3s ease, border-bottom-color 0.3s ease; }
|
||||
.ebook-nav { display: flex; justify-content: space-between; gap: 12px; margin: 20px auto; max-width: 800px; }
|
||||
.ebook-nav button { border: none; padding: 12px 24px; border-radius: 40px; cursor: pointer; font-size: 16px; flex: 1; background: linear-gradient(135deg, #667eea, #764ba2); color: white; transition: background 0.3s ease, opacity 0.3s ease; }
|
||||
.ebook-nav button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.chapter-indicator { text-align: center; margin: 10px auto; font-size: 14px; transition: color 0.3s ease; }
|
||||
.toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); backdrop-filter: blur(20px); color: white; padding: 10px 20px; border-radius: 50px; font-size: 14px; z-index: 2000; pointer-events: none; white-space: nowrap; transition: background 0.3s ease, color 0.3s ease, border 0.3s ease; }
|
||||
|
||||
/* ==================== 3D悬浮书架 - 增强玻璃立体效果(已去除左侧光影扫光特效) ==================== */
|
||||
.shelf-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 28px;
|
||||
padding: 24px;
|
||||
perspective: 1800px;
|
||||
perspective-origin: center 40px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.shelf-grid { grid-template-columns: repeat(2, 1fr); gap: 18px; padding: 16px; }
|
||||
}
|
||||
|
||||
.shelf-item {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
backdrop-filter: blur(18px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(180%);
|
||||
border-radius: 32px;
|
||||
padding: 28px 12px 24px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
transition: all 0.5s cubic-bezier(0.2, 0.9, 0.4, 1.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
overflow: visible;
|
||||
/* 强化立体玻璃阴影:外部深色阴影 + 内部高光描边 */
|
||||
box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.35) inset,
|
||||
0 1px 0 rgba(255, 255, 255, 0.25) inset,
|
||||
0 -1px 0 rgba(0, 0, 0, 0.05) inset;
|
||||
transform-style: preserve-3d;
|
||||
transform: translateZ(0) rotateX(0deg) rotateY(0deg);
|
||||
opacity: 0;
|
||||
animation: fadeInUpGlide 0.6s cubic-bezier(0.2, 0.9, 0.3, 1.1) forwards;
|
||||
border: 1px solid rgba(255,255,240,0.4);
|
||||
}
|
||||
/* 入场动画延迟 */
|
||||
.shelf-item:nth-child(1) { animation-delay: 0.03s; } .shelf-item:nth-child(2) { animation-delay: 0.08s; }
|
||||
.shelf-item:nth-child(3) { animation-delay: 0.13s; } .shelf-item:nth-child(4) { animation-delay: 0.18s; }
|
||||
.shelf-item:nth-child(5) { animation-delay: 0.23s; } .shelf-item:nth-child(6) { animation-delay: 0.28s; }
|
||||
.shelf-item:nth-child(7) { animation-delay: 0.33s; } .shelf-item:nth-child(8) { animation-delay: 0.38s; }
|
||||
.shelf-item:nth-child(9) { animation-delay: 0.43s; } .shelf-item:nth-child(10){ animation-delay: 0.48s; }
|
||||
.shelf-item:nth-child(11){ animation-delay: 0.53s; } .shelf-item:nth-child(12){ animation-delay: 0.58s; }
|
||||
|
||||
@keyframes fadeInUpGlide {
|
||||
0% { opacity: 0; transform: translateY(40px) rotateX(-6deg) translateZ(-20px); }
|
||||
100% { opacity: 1; transform: translateY(0) rotateX(0deg) translateZ(0); }
|
||||
}
|
||||
|
||||
/* 悬停3D悬浮效果 - 无扫光特效 */
|
||||
.shelf-item:hover {
|
||||
transform: translateY(-16px) translateZ(28px) rotateX(5deg) rotateY(-2deg) scale(1.02);
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
border-color: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 35px 45px -18px rgba(0, 0, 0, 0.6),
|
||||
0 0 0 2px rgba(255, 255, 255, 0.5) inset,
|
||||
0 0 25px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.shelf-item .emoji {
|
||||
font-size: 52px;
|
||||
display: block;
|
||||
margin-bottom: 14px;
|
||||
transition: all 0.4s cubic-bezier(0.2, 0.9, 0.4, 1.1);
|
||||
transform-style: preserve-3d;
|
||||
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
.shelf-item:hover .emoji {
|
||||
transform: scale(1.15) rotateY(12deg) rotateX(6deg) translateZ(12px);
|
||||
filter: drop-shadow(0 15px 20px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
.shelf-item div:last-child {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
.shelf-item:hover div:last-child {
|
||||
letter-spacing: 1.2px;
|
||||
text-shadow: 0 0 12px rgba(255,255,255,0.6);
|
||||
transform: translateZ(10px);
|
||||
}
|
||||
/* 顶部高光渐变(保留,增加玻璃质感) */
|
||||
.shelf-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
height: 35%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
border-radius: 32px 32px 0 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.shelf-item:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
/* 注意:原本的 .shelf-item::before 左侧扫光特效已完全移除 */
|
||||
|
||||
.book-chapter-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; padding: 16px; }
|
||||
.book-chapter-item { position: relative; background: rgba(255,255,255,0.12); backdrop-filter: blur(12px); border-radius: 18px; border: 1px solid rgba(255,255,255,0.3); text-align: center; transition: all 0.3s; cursor: pointer; transform-style: preserve-3d; box-shadow: 0 6px 15px rgba(0,0,0,0.15); opacity: 0; animation: fadeInUp 0.4s ease forwards; }
|
||||
.book-chapter-item:nth-child(1) { animation-delay: 0.03s; } .book-chapter-item:nth-child(2) { animation-delay: 0.06s; }
|
||||
.book-chapter-item:hover { transform: translateY(-6px) translateZ(10px) scale(1.01); background: rgba(255,255,255,0.22); border-color: rgba(255,255,255,0.55); }
|
||||
.book-chapter-item a { text-decoration: none; display: block; padding: 16px 12px; font-weight: 500; color: inherit; }
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.page-transition { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(12px); z-index: 10000; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: opacity 0.4s ease, visibility 0.4s ease, background 0.3s ease; }
|
||||
.page-transition.active { opacity: 1; visibility: visible; }
|
||||
.page-transition .book-loader { position: relative; width: 80px; height: 100px; perspective: 1000px; margin-bottom: 30px; }
|
||||
.page-transition .book-page { position: absolute; width: 100%; height: 100%; background: linear-gradient(135deg, #ff9800, #ff5722); border-radius: 4px 8px 8px 4px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); transform-origin: left center; animation: bookFlip 1.2s ease-in-out infinite; transition: background 0.3s ease; }
|
||||
.page-transition .book-page:nth-child(1) { animation-delay: 0s; background: linear-gradient(135deg, #ff9800, #f57c00); }
|
||||
.page-transition .book-page:nth-child(2) { animation-delay: 0.15s; background: linear-gradient(135deg, #ffb74d, #ff9800); }
|
||||
.page-transition .book-page:nth-child(3) { animation-delay: 0.3s; background: linear-gradient(135deg, #ffcc80, #ffb74d); }
|
||||
.page-transition .book-page:nth-child(4) { animation-delay: 0.45s; background: linear-gradient(135deg, #ffe0b2, #ffcc80); }
|
||||
@keyframes bookFlip { 0% { transform: rotateY(0deg); opacity: 1; } 50% { transform: rotateY(-90deg); opacity: 0.5; } 100% { transform: rotateY(-180deg); opacity: 0; } }
|
||||
.page-transition .loading-text { color: white; font-size: 18px; letter-spacing: 4px; font-weight: 300; margin-top: 20px; animation: textPulse 1s ease-in-out infinite; transition: color 0.3s ease; }
|
||||
@keyframes textPulse { 0%, 100% { opacity: 0.5; letter-spacing: 4px; } 50% { opacity: 1; letter-spacing: 8px; text-shadow: 0 0 10px #ff9800; } }
|
||||
.page-transition .loading-dots { display: flex; gap: 8px; margin-top: 15px; }
|
||||
.page-transition .loading-dots span { width: 10px; height: 10px; background: #ff9800; border-radius: 50%; animation: dotBounce 0.6s ease-in-out infinite; transition: background 0.3s ease; }
|
||||
@keyframes dotBounce { 0%, 100% { transform: translateY(0); opacity: 0.5; } 50% { transform: translateY(-10px); opacity: 1; } }
|
||||
.ripple { position: absolute; border-radius: 50%; background: rgba(255, 255, 255, 0.5); transform: scale(0); animation: rippleAnim 0.6s linear forwards; pointer-events: none; }
|
||||
@keyframes rippleAnim { to { transform: scale(4); opacity: 0; } }
|
||||
.page-title { font-size: 26px; font-weight: 600; color: white; padding: 16px; margin: 0; text-shadow: 1px 1px 2px rgba(0,0,0,0.3); transition: color 0.3s ease, text-shadow 0.3s ease; }
|
||||
.progress-bar { position: fixed; top: 60px; left: 0; width: 100%; height: 2px; background: rgba(255,255,255,0.2); z-index: 1002; transition: background 0.3s ease; }
|
||||
.progress-fill { width: 0%; height: 100%; background: #ff9800; transition: width 0.3s, background 0.3s ease; }
|
||||
|
||||
/* ==================== 12款静态主题 ==================== */
|
||||
body.theme-deep-space { background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); }
|
||||
body.theme-deep-space .top-bar { background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-deep-space .top-bar, body.theme-deep-space .top-bar a, body.theme-deep-space .top-bar button { color: #fff; }
|
||||
body.theme-deep-space .ebook-chapter { background: rgba(30, 30, 50, 0.95); color: #e0e0e0; }
|
||||
body.theme-deep-space .ebook-chapter .chapter-title { color: #9b59b6; }
|
||||
body.theme-deep-space .speed-panel, body.theme-deep-space .font-controls, body.theme-deep-space .theme-selector, body.theme-deep-space .bookmark-panel { background: rgba(15, 12, 41, 0.95); color: #e0e0e0; }
|
||||
body.theme-deep-space .floating-btn { background: rgba(15, 12, 41, 0.9); color: #fff; }
|
||||
body.theme-deep-space .global-progress-container { background: rgba(15, 12, 41, 0.92); }
|
||||
body.theme-deep-space .page-turn-overlay { background: rgba(15, 12, 41, 0.92) !important; }
|
||||
body.theme-deep-space .page-turn-overlay .book-left,
|
||||
body.theme-deep-space .page-turn-overlay .book-right { background: rgba(48, 43, 99, 0.95) !important; border: 2px solid rgba(155, 89, 182, 0.6) !important; color: #bb86fc !important; }
|
||||
body.theme-deep-space .page-turn-overlay .message { background: rgba(48, 43, 99, 0.95) !important; color: #bb86fc !important; border: 1px solid rgba(155, 89, 182, 0.5) !important; }
|
||||
body.theme-deep-space .chapter-tooltip { background: rgba(15, 12, 41, 0.95) !important; border-color: #9b59b6 !important; color: #bb86fc !important; }
|
||||
body.theme-deep-space .shelf-item { background: rgba(15, 12, 41, 0.6) !important; border-color: rgba(155, 89, 182, 0.4) !important; }
|
||||
body.theme-deep-space .shelf-item:hover { background: rgba(48, 43, 99, 0.8) !important; border-color: #9b59b6 !important; }
|
||||
|
||||
body.theme-ocean { background: linear-gradient(135deg, #1a2980 0%, #26d0ce 100%); }
|
||||
body.theme-ocean .top-bar { background: rgba(0, 40, 60, 0.85); }
|
||||
body.theme-ocean .top-bar, body.theme-ocean .top-bar a, body.theme-ocean .top-bar button { color: #e0f7fa; }
|
||||
body.theme-ocean .ebook-chapter { background: rgba(255, 255, 255, 0.95); color: #2c3e50; }
|
||||
body.theme-ocean .ebook-chapter .chapter-title { color: #1a2980; }
|
||||
body.theme-ocean .speed-panel, body.theme-ocean .font-controls, body.theme-ocean .theme-selector, body.theme-ocean .bookmark-panel { background: rgba(26, 41, 128, 0.95); color: #e0f7fa; }
|
||||
body.theme-ocean .floating-btn { background: rgba(38, 208, 206, 0.85); color: #e0f7fa; }
|
||||
body.theme-ocean .global-progress-container { background: rgba(26, 41, 128, 0.92); }
|
||||
body.theme-ocean .page-turn-overlay { background: rgba(26, 41, 128, 0.92) !important; }
|
||||
body.theme-ocean .page-turn-overlay .book-left,
|
||||
body.theme-ocean .page-turn-overlay .book-right { background: rgba(38, 208, 206, 0.9) !important; border: 2px solid rgba(255,255,255,0.4) !important; color: #e0f7fa !important; }
|
||||
body.theme-ocean .page-turn-overlay .message { background: rgba(26, 41, 128, 0.95) !important; color: #e0f7fa !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-ocean .chapter-tooltip { background: rgba(26, 41, 128, 0.95) !important; border-color: #26d0ce !important; color: #e0f7fa !important; }
|
||||
|
||||
body.theme-cherry { background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%); }
|
||||
body.theme-cherry .top-bar { background: rgba(219, 112, 147, 0.85); }
|
||||
body.theme-cherry .top-bar, body.theme-cherry .top-bar a, body.theme-cherry .top-bar button { color: #5a2e3e; }
|
||||
body.theme-cherry .ebook-chapter { background: rgba(255, 245, 245, 0.95); color: #5a3a3a; }
|
||||
body.theme-cherry .ebook-chapter .chapter-title { color: #db7093; }
|
||||
body.theme-cherry .speed-panel, body.theme-cherry .font-controls, body.theme-cherry .theme-selector, body.theme-cherry .bookmark-panel { background: rgba(255, 245, 245, 0.95); color: #5a2e3e; }
|
||||
body.theme-cherry .floating-btn { background: rgba(219, 112, 147, 0.85); color: #5a2e3e; }
|
||||
body.theme-cherry .global-progress-container { background: rgba(255, 245, 245, 0.92); }
|
||||
body.theme-cherry .page-turn-overlay { background: rgba(255, 154, 158, 0.92) !important; }
|
||||
body.theme-cherry .page-turn-overlay .book-left,
|
||||
body.theme-cherry .page-turn-overlay .book-right { background: rgba(254, 207, 239, 0.95) !important; border: 2px solid rgba(219, 112, 147, 0.6) !important; color: #5a2e3e !important; }
|
||||
body.theme-cherry .page-turn-overlay .message { background: rgba(219, 112, 147, 0.95) !important; color: #5a2e3e !important; border: 1px solid rgba(219, 112, 147, 0.4) !important; }
|
||||
body.theme-cherry .chapter-tooltip { background: rgba(219, 112, 147, 0.95) !important; border-color: #ff9a9e !important; color: #5a2e3e !important; }
|
||||
|
||||
body.theme-night { background: #0a0a0a; }
|
||||
body.theme-night .top-bar { background: rgba(10, 10, 10, 0.95); }
|
||||
body.theme-night .top-bar, body.theme-night .top-bar a, body.theme-night .top-bar button { color: #aaa; }
|
||||
body.theme-night .ebook-chapter { background: #1a1a1a; color: #b0b0b0; border: 1px solid #333; }
|
||||
body.theme-night .ebook-chapter .chapter-title { color: #888; }
|
||||
body.theme-night .speed-panel, body.theme-night .font-controls, body.theme-night .theme-selector, body.theme-night .bookmark-panel { background: rgba(10, 10, 10, 0.95); color: #aaa; }
|
||||
body.theme-night .floating-btn { background: rgba(30, 30, 30, 0.95); color: #aaa; }
|
||||
body.theme-night .global-progress-container { background: rgba(10, 10, 10, 0.92); }
|
||||
body.theme-night .page-turn-overlay { background: rgba(10, 10, 10, 0.95) !important; }
|
||||
body.theme-night .page-turn-overlay .book-left,
|
||||
body.theme-night .page-turn-overlay .book-right { background: rgba(30, 30, 30, 0.98) !important; border: 2px solid #555 !important; color: #aaa !important; }
|
||||
body.theme-night .page-turn-overlay .message { background: rgba(30, 30, 30, 0.98) !important; color: #aaa !important; border: 1px solid #555 !important; }
|
||||
body.theme-night .chapter-tooltip { background: rgba(30, 30, 30, 0.98) !important; border-color: #666 !important; color: #ccc !important; }
|
||||
|
||||
body.theme-forest { background: linear-gradient(135deg, #134e5e 0%, #71b280 100%); }
|
||||
body.theme-forest .top-bar { background: rgba(20, 60, 40, 0.85); }
|
||||
body.theme-forest .top-bar, body.theme-forest .top-bar a, body.theme-forest .top-bar button { color: #e8f5e9; }
|
||||
body.theme-forest .ebook-chapter { background: rgba(255, 255, 245, 0.95); color: #2d5a3b; }
|
||||
body.theme-forest .ebook-chapter .chapter-title { color: #2e7d32; }
|
||||
body.theme-forest .speed-panel, body.theme-forest .font-controls, body.theme-forest .theme-selector, body.theme-forest .bookmark-panel { background: rgba(19, 78, 94, 0.95); color: #e8f5e9; }
|
||||
body.theme-forest .floating-btn { background: rgba(113, 178, 128, 0.85); color: #e8f5e9; }
|
||||
body.theme-forest .global-progress-container { background: rgba(19, 78, 94, 0.92); }
|
||||
body.theme-forest .page-turn-overlay { background: rgba(19, 78, 94, 0.92) !important; }
|
||||
body.theme-forest .page-turn-overlay .book-left,
|
||||
body.theme-forest .page-turn-overlay .book-right { background: rgba(113, 178, 128, 0.9) !important; border: 2px solid rgba(255,255,255,0.4) !important; color: #e8f5e9 !important; }
|
||||
body.theme-forest .page-turn-overlay .message { background: rgba(19, 78, 94, 0.95) !important; color: #e8f5e9 !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-forest .chapter-tooltip { background: rgba(19, 78, 94, 0.95) !important; border-color: #71b280 !important; color: #e8f5e9 !important; }
|
||||
|
||||
body.theme-sunset { background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 100%); }
|
||||
body.theme-sunset .top-bar { background: rgba(180, 70, 40, 0.85); }
|
||||
body.theme-sunset .top-bar, body.theme-sunset .top-bar a, body.theme-sunset .top-bar button { color: #fff3e0; }
|
||||
body.theme-sunset .ebook-chapter { background: rgba(255, 248, 240, 0.96); color: #6b3e1f; }
|
||||
body.theme-sunset .ebook-chapter .chapter-title { color: #d84315; }
|
||||
body.theme-sunset .speed-panel, body.theme-sunset .font-controls, body.theme-sunset .theme-selector, body.theme-sunset .bookmark-panel { background: rgba(255, 126, 95, 0.95); color: #fff3e0; }
|
||||
body.theme-sunset .floating-btn { background: rgba(254, 180, 123, 0.85); color: #fff3e0; }
|
||||
body.theme-sunset .global-progress-container { background: rgba(255, 126, 95, 0.92); }
|
||||
body.theme-sunset .page-turn-overlay { background: rgba(255, 126, 95, 0.92) !important; }
|
||||
body.theme-sunset .page-turn-overlay .book-left,
|
||||
body.theme-sunset .page-turn-overlay .book-right { background: rgba(254, 180, 123, 0.95) !important; border: 2px solid rgba(255,255,255,0.4) !important; color: #fff3e0 !important; }
|
||||
body.theme-sunset .page-turn-overlay .message { background: rgba(255, 126, 95, 0.95) !important; color: #fff3e0 !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-sunset .chapter-tooltip { background: rgba(180, 70, 40, 0.95) !important; border-color: #feb47b !important; color: #fff3e0 !important; }
|
||||
|
||||
body.theme-lavender { background: linear-gradient(135deg, #8e9ecc 0%, #e0bbff 100%); }
|
||||
body.theme-lavender .top-bar { background: rgba(100, 80, 140, 0.85); }
|
||||
body.theme-lavender .top-bar, body.theme-lavender .top-bar a, body.theme-lavender .top-bar button { color: #f3e5f5; }
|
||||
body.theme-lavender .ebook-chapter { background: rgba(245, 235, 255, 0.96); color: #4a3a6e; }
|
||||
body.theme-lavender .ebook-chapter .chapter-title { color: #7b1fa2; }
|
||||
body.theme-lavender .speed-panel, body.theme-lavender .font-controls, body.theme-lavender .theme-selector, body.theme-lavender .bookmark-panel { background: rgba(142, 158, 204, 0.95); color: #4a3a6e; }
|
||||
body.theme-lavender .floating-btn { background: rgba(224, 187, 255, 0.85); color: #4a3a6e; }
|
||||
body.theme-lavender .global-progress-container { background: rgba(142, 158, 204, 0.92); }
|
||||
body.theme-lavender .page-turn-overlay { background: rgba(142, 158, 204, 0.92) !important; }
|
||||
body.theme-lavender .page-turn-overlay .book-left,
|
||||
body.theme-lavender .page-turn-overlay .book-right { background: rgba(224, 187, 255, 0.95) !important; border: 2px solid rgba(100, 80, 140, 0.6) !important; color: #4a3a6e !important; }
|
||||
body.theme-lavender .page-turn-overlay .message { background: rgba(142, 158, 204, 0.95) !important; color: #f3e5f5 !important; border: 1px solid rgba(100, 80, 140, 0.4) !important; }
|
||||
body.theme-lavender .chapter-tooltip { background: rgba(100, 80, 140, 0.95) !important; border-color: #e0bbff !important; color: #f3e5f5 !important; }
|
||||
|
||||
body.theme-blueberry { background: linear-gradient(135deg, #2c3e66 0%, #4a69bd 100%); }
|
||||
body.theme-blueberry .top-bar { background: rgba(30, 50, 80, 0.85); }
|
||||
body.theme-blueberry .top-bar, body.theme-blueberry .top-bar a, body.theme-blueberry .top-bar button { color: #dfe6e9; }
|
||||
body.theme-blueberry .ebook-chapter { background: rgba(240, 245, 255, 0.96); color: #2c3e66; }
|
||||
body.theme-blueberry .ebook-chapter .chapter-title { color: #3b82f6; }
|
||||
body.theme-blueberry .speed-panel, body.theme-blueberry .font-controls, body.theme-blueberry .theme-selector, body.theme-blueberry .bookmark-panel { background: rgba(44, 62, 102, 0.95); color: #dfe6e9; }
|
||||
body.theme-blueberry .floating-btn { background: rgba(74, 105, 189, 0.85); color: #dfe6e9; }
|
||||
body.theme-blueberry .global-progress-container { background: rgba(44, 62, 102, 0.92); }
|
||||
body.theme-blueberry .page-turn-overlay { background: rgba(44, 62, 102, 0.92) !important; }
|
||||
body.theme-blueberry .page-turn-overlay .book-left,
|
||||
body.theme-blueberry .page-turn-overlay .book-right { background: rgba(74, 105, 189, 0.9) !important; border: 2px solid rgba(255,255,255,0.3) !important; color: #dfe6e9 !important; }
|
||||
body.theme-blueberry .page-turn-overlay .message { background: rgba(44, 62, 102, 0.95) !important; color: #dfe6e9 !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-blueberry .chapter-tooltip { background: rgba(44, 62, 102, 0.95) !important; border-color: #4a69bd !important; color: #dfe6e9 !important; }
|
||||
|
||||
body.theme-amber { background: linear-gradient(135deg, #ffb347 0%, #ffcc33 100%); }
|
||||
body.theme-amber .top-bar { background: rgba(160, 90, 30, 0.85); }
|
||||
body.theme-amber .top-bar, body.theme-amber .top-bar a, body.theme-amber .top-bar button { color: #3e2723; }
|
||||
body.theme-amber .ebook-chapter { background: rgba(255, 250, 230, 0.96); color: #5d4037; }
|
||||
body.theme-amber .ebook-chapter .chapter-title { color: #f57c00; }
|
||||
body.theme-amber .speed-panel, body.theme-amber .font-controls, body.theme-amber .theme-selector, body.theme-amber .bookmark-panel { background: rgba(255, 179, 71, 0.95); color: #3e2723; }
|
||||
body.theme-amber .floating-btn { background: rgba(255, 204, 51, 0.85); color: #3e2723; }
|
||||
body.theme-amber .global-progress-container { background: rgba(255, 179, 71, 0.92); }
|
||||
body.theme-amber .page-turn-overlay { background: rgba(255, 179, 71, 0.92) !important; }
|
||||
body.theme-amber .page-turn-overlay .book-left,
|
||||
body.theme-amber .page-turn-overlay .book-right { background: rgba(255, 204, 51, 0.95) !important; border: 2px solid rgba(160, 90, 30, 0.6) !important; color: #3e2723 !important; }
|
||||
body.theme-amber .page-turn-overlay .message { background: rgba(255, 179, 71, 0.95) !important; color: #3e2723 !important; border: 1px solid rgba(160, 90, 30, 0.4) !important; }
|
||||
body.theme-amber .chapter-tooltip { background: rgba(160, 90, 30, 0.95) !important; border-color: #ffcc33 !important; color: #fff8e1 !important; }
|
||||
|
||||
body.theme-coral { background: linear-gradient(135deg, #ff6b6b 0%, #ffb8b8 100%); }
|
||||
body.theme-coral .top-bar { background: rgba(200, 80, 80, 0.85); }
|
||||
body.theme-coral .top-bar, body.theme-coral .top-bar a, body.theme-coral .top-bar button { color: #fff; }
|
||||
body.theme-coral .ebook-chapter { background: rgba(255, 240, 240, 0.95); color: #5a3a3a; }
|
||||
body.theme-coral .ebook-chapter .chapter-title { color: #ff6b6b; }
|
||||
body.theme-coral .speed-panel, body.theme-coral .font-controls, body.theme-coral .theme-selector, body.theme-coral .bookmark-panel { background: rgba(200, 80, 80, 0.95); color: #fff; }
|
||||
body.theme-coral .floating-btn { background: rgba(200, 80, 80, 0.9); color: #fff; }
|
||||
body.theme-coral .global-progress-container { background: rgba(200, 80, 80, 0.92); }
|
||||
body.theme-coral .page-turn-overlay { background: rgba(200, 80, 80, 0.92) !important; }
|
||||
body.theme-coral .page-turn-overlay .book-left,
|
||||
body.theme-coral .page-turn-overlay .book-right { background: rgba(255, 184, 184, 0.95) !important; border: 2px solid rgba(200, 80, 80, 0.6) !important; color: #fff !important; }
|
||||
body.theme-coral .page-turn-overlay .message { background: rgba(200, 80, 80, 0.95) !important; color: #fff !important; border: 1px solid rgba(200, 80, 80, 0.4) !important; }
|
||||
body.theme-coral .chapter-tooltip { background: rgba(200, 80, 80, 0.95) !important; border-color: #ffb8b8 !important; color: #fff !important; }
|
||||
|
||||
body.theme-mint { background: linear-gradient(135deg, #a8e6cf 0%, #80deea 100%); }
|
||||
body.theme-mint .top-bar { background: rgba(60, 120, 100, 0.85); }
|
||||
body.theme-mint .top-bar, body.theme-mint .top-bar a, body.theme-mint .top-bar button { color: #2d5a3b; }
|
||||
body.theme-mint .ebook-chapter { background: rgba(255, 255, 250, 0.95); color: #2d5a3b; }
|
||||
body.theme-mint .ebook-chapter .chapter-title { color: #2ecc71; }
|
||||
body.theme-mint .speed-panel, body.theme-mint .font-controls, body.theme-mint .theme-selector, body.theme-mint .bookmark-panel { background: rgba(60, 120, 100, 0.95); color: #fff; }
|
||||
body.theme-mint .floating-btn { background: rgba(60, 120, 100, 0.9); color: #fff; }
|
||||
body.theme-mint .global-progress-container { background: rgba(60, 120, 100, 0.92); }
|
||||
body.theme-mint .page-turn-overlay { background: rgba(60, 120, 100, 0.92) !important; }
|
||||
body.theme-mint .page-turn-overlay .book-left,
|
||||
body.theme-mint .page-turn-overlay .book-right { background: rgba(168, 230, 207, 0.9) !important; border: 2px solid rgba(60, 120, 100, 0.6) !important; color: #2d5a3b !important; }
|
||||
body.theme-mint .page-turn-overlay .message { background: rgba(60, 120, 100, 0.95) !important; color: #fff !important; border: 1px solid rgba(60, 120, 100, 0.4) !important; }
|
||||
body.theme-mint .chapter-tooltip { background: rgba(60, 120, 100, 0.95) !important; border-color: #80deea !important; color: #fff !important; }
|
||||
|
||||
body.theme-rosegold { background: linear-gradient(135deg, #e8b4b8 0%, #ffd9e2 100%); }
|
||||
body.theme-rosegold .top-bar { background: rgba(160, 100, 110, 0.85); }
|
||||
body.theme-rosegold .top-bar, body.theme-rosegold .top-bar a, body.theme-rosegold .top-bar button { color: #5a3a3e; }
|
||||
body.theme-rosegold .ebook-chapter { background: rgba(255, 248, 250, 0.95); color: #5a3a3e; }
|
||||
body.theme-rosegold .ebook-chapter .chapter-title { color: #e8b4b8; }
|
||||
body.theme-rosegold .speed-panel, body.theme-rosegold .font-controls, body.theme-rosegold .theme-selector, body.theme-rosegold .bookmark-panel { background: rgba(160, 100, 110, 0.95); color: #fff; }
|
||||
body.theme-rosegold .floating-btn { background: rgba(160, 100, 110, 0.9); color: #fff; }
|
||||
body.theme-rosegold .global-progress-container { background: rgba(160, 100, 110, 0.92); }
|
||||
body.theme-rosegold .page-turn-overlay { background: rgba(160, 100, 110, 0.92) !important; }
|
||||
body.theme-rosegold .page-turn-overlay .book-left,
|
||||
body.theme-rosegold .page-turn-overlay .book-right { background: rgba(255, 217, 226, 0.95) !important; border: 2px solid rgba(160, 100, 110, 0.6) !important; color: #5a3a3e !important; }
|
||||
body.theme-rosegold .page-turn-overlay .message { background: rgba(160, 100, 110, 0.95) !important; color: #fff !important; border: 1px solid rgba(160, 100, 110, 0.4) !important; }
|
||||
body.theme-rosegold .chapter-tooltip { background: rgba(160, 100, 110, 0.95) !important; border-color: #ffd9e2 !important; color: #fff5f5 !important; }
|
||||
|
||||
/* ==================== 护眼主题 ==================== */
|
||||
body.theme-eyecare { background: #c7edcc !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .top-bar { background: rgba(199, 237, 204, 0.92) !important; backdrop-filter: blur(20px) !important; border-bottom: 1px solid rgba(100, 100, 80, 0.2) !important; }
|
||||
body.theme-eyecare .top-bar, body.theme-eyecare .top-bar a, body.theme-eyecare .top-bar button { color: #2d2d2d !important; }
|
||||
body.theme-eyecare .ebook-chapter { background: rgba(215, 245, 210, 0.95) !important; color: #2d2d2d !important; box-shadow: 0 8px 32px rgba(0,0,0,0.08) !important; }
|
||||
body.theme-eyecare .ebook-chapter .chapter-title { color: #5a6b3a !important; border-bottom-color: #a0b880 !important; }
|
||||
body.theme-eyecare .speed-panel, body.theme-eyecare .font-controls, body.theme-eyecare .theme-selector, body.theme-eyecare .bookmark-panel { background: rgba(215, 245, 210, 0.95) !important; color: #2d2d2d !important; border: 1px solid rgba(100, 100, 80, 0.2) !important; }
|
||||
body.theme-eyecare .floating-btn { background: rgba(199, 237, 204, 0.9) !important; color: #2d2d2d !important; border: 1px solid rgba(100, 100, 80, 0.3) !important; }
|
||||
body.theme-eyecare .global-progress-container { background: rgba(199, 237, 204, 0.92) !important; }
|
||||
body.theme-eyecare .shelf-item { background: rgba(215, 245, 210, 0.8) !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .shelf-item:hover { background: rgba(199, 237, 204, 0.9) !important; }
|
||||
body.theme-eyecare .progress-slider-global::-webkit-slider-thumb { background: #8b9a6e !important; }
|
||||
body.theme-eyecare .page-turn-overlay { background: rgba(199, 237, 204, 0.92) !important; }
|
||||
body.theme-eyecare .page-turn-overlay .book-left,
|
||||
body.theme-eyecare .page-turn-overlay .book-right { background: rgba(215, 245, 210, 0.95) !important; border: 2px solid rgba(139, 154, 110, 0.5) !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .page-turn-overlay .message { background: rgba(215, 245, 210, 0.95) !important; color: #2d2d2d !important; border: 1px solid rgba(139, 154, 110, 0.4) !important; }
|
||||
body.theme-eyecare .chapter-tooltip { background: rgba(215, 245, 210, 0.98) !important; border-color: #8b9a6e !important; color: #2d2d2d !important; }
|
||||
|
||||
/* ==================== 12款动态主题 ==================== */
|
||||
/* 1. 极光幻彩 */
|
||||
body.theme-aurora-dynamic { background: linear-gradient(270deg, #1a0b2e, #2d1b69, #1a4d8c, #0f5c6b); background-size: 400% 400%; animation: auroraFlow 12s ease infinite; color: #f0f0f0 !important; }
|
||||
@keyframes auroraFlow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
||||
body.theme-aurora-dynamic .top-bar { background: rgba(0, 0, 0, 0.5) !important; backdrop-filter: blur(20px) !important; border-bottom: 1px solid rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .top-bar, body.theme-aurora-dynamic .top-bar a, body.theme-aurora-dynamic .top-bar button { color: #7cffd0 !important; text-shadow: 0 0 5px rgba(124,255,208,0.3); }
|
||||
body.theme-aurora-dynamic .back-btn { background: rgba(124, 255, 208, 0.15) !important; }
|
||||
body.theme-aurora-dynamic .page-title { color: #7cffd0 !important; text-shadow: 0 0 10px rgba(124,255,208,0.4); }
|
||||
body.theme-aurora-dynamic .ebook-chapter { background: rgba(0, 0, 0, 0.4) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .ebook-chapter .chapter-title { color: #7cffd0 !important; border-bottom-color: rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .floating-btn, body.theme-aurora-dynamic .speed-panel, body.theme-aurora-dynamic .font-controls, body.theme-aurora-dynamic .theme-selector, body.theme-aurora-dynamic .bookmark-panel { background: rgba(0, 0, 0, 0.5) !important; border: 1px solid rgba(124, 255, 208, 0.3) !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .floating-btn { background: rgba(0, 0, 0, 0.4) !important; color: #7cffd0 !important; border: 1px solid rgba(124, 255, 208, 0.4) !important; }
|
||||
body.theme-aurora-dynamic .floating-btn.bookmark-btn { background: rgba(124, 255, 208, 0.2) !important; border: 1px solid #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .floating-btn.bookmark-btn.active { background: #7cffd0 !important; color: #1a0b2e !important; }
|
||||
body.theme-aurora-dynamic .speed-slider::-webkit-slider-thumb { background: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .progress-slider-global::-webkit-slider-thumb { background: #7cffd0 !important; box-shadow: 0 0 8px rgba(124,255,208,0.8) !important; }
|
||||
body.theme-aurora-dynamic .progress-fill { background: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .speed-preset.active { color: #7cffd0 !important; background: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .auto-chapter-line { border-top-color: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .auto-chapter-line input { accent-color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .global-progress-container { background: rgba(0, 0, 0, 0.5) !important; border-top: 1px solid rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .progress-info { color: rgba(124, 255, 208, 0.8) !important; }
|
||||
body.theme-aurora-dynamic .shelf-item { background: rgba(124, 255, 208, 0.1) !important; border-color: rgba(124, 255, 208, 0.3) !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .shelf-item:hover { background: rgba(124, 255, 208, 0.2) !important; border-color: rgba(124, 255, 208, 0.6) !important; }
|
||||
body.theme-aurora-dynamic .book-chapter-item { background: rgba(124, 255, 208, 0.1) !important; border-color: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .book-chapter-item a { color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .book-chapter-item:hover { background: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .bookmark-item .title { color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .bookmark-header { border-bottom-color: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .chapter-tooltip { background: rgba(0, 0, 0, 0.75) !important; border-color: #7cffd0 !important; color: #7cffd0 !important; box-shadow: 0 0 15px rgba(124,255,208,0.3) !important; }
|
||||
body.theme-aurora-dynamic .page-turn-overlay { background: rgba(0, 0, 0, 0.6) !important; }
|
||||
body.theme-aurora-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-aurora-dynamic .page-turn-overlay .book-right { background: rgba(0, 0, 0, 0.5) !important; border: 2px solid rgba(124, 255, 208, 0.4) !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .page-turn-overlay .message { background: rgba(0, 0, 0, 0.7) !important; color: #7cffd0 !important; border: 1px solid rgba(124, 255, 208, 0.5) !important; }
|
||||
body.theme-aurora-dynamic .page-transition { background: rgba(0, 0, 0, 0.6) !important; }
|
||||
body.theme-aurora-dynamic .page-transition .book-page { background: linear-gradient(135deg, #7cffd0, #2d1b69) !important; }
|
||||
body.theme-aurora-dynamic .page-transition .loading-text { color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .page-transition .loading-dots span { background: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .top-bar button.bookmark { background: rgba(124, 255, 208, 0.2) !important; border: 1px solid #7cffd0 !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .toast { background: rgba(0, 0, 0, 0.8) !important; color: #7cffd0 !important; border: 1px solid rgba(124, 255, 208, 0.3) !important; }
|
||||
|
||||
/* 2. 霓虹脉冲 */
|
||||
body.theme-neon-dynamic { background: #0a0a0a !important; animation: neonBgPulse 2s ease-in-out infinite; color: #fff !important; }
|
||||
@keyframes neonBgPulse { 0% { background: #0a0a0a; } 30% { background: #0d1a1a; } 100% { background: #0a0a0a; } }
|
||||
body.theme-neon-dynamic .top-bar { background: rgba(0, 0, 0, 0.7) !important; border-bottom: 1px solid rgba(0, 255, 255, 0.4) !important; animation: neonBorderFlash 1.5s ease-in-out infinite !important; }
|
||||
@keyframes neonBorderFlash { 0% { border-bottom-color: rgba(0, 255, 255, 0.2); } 50% { border-bottom-color: rgba(0, 255, 255, 0.8); } 100% { border-bottom-color: rgba(0, 255, 255, 0.2); } }
|
||||
body.theme-neon-dynamic .top-bar, body.theme-neon-dynamic .top-bar a, body.theme-neon-dynamic .top-bar button { color: #0ff !important; text-shadow: 0 0 5px rgba(0,255,255,0.5); }
|
||||
body.theme-neon-dynamic .back-btn { background: rgba(0, 255, 255, 0.1) !important; }
|
||||
body.theme-neon-dynamic .page-title { color: #0ff !important; text-shadow: 0 0 10px rgba(0,255,255,0.5); animation: neonTitlePulse 1.5s ease-in-out infinite; }
|
||||
@keyframes neonTitlePulse { 0% { text-shadow: 0 0 5px rgba(0,255,255,0.3); } 50% { text-shadow: 0 0 20px rgba(0,255,255,0.8); } 100% { text-shadow: 0 0 5px rgba(0,255,255,0.3); } }
|
||||
body.theme-neon-dynamic .ebook-chapter { background: rgba(0, 0, 0, 0.7) !important; border: 1px solid rgba(0, 255, 255, 0.2) !important; animation: neonBoxGlow 2s ease-in-out infinite !important; }
|
||||
@keyframes neonBoxGlow { 0% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.1); } 50% { box-shadow: 0 0 25px rgba(0, 255, 255, 0.4); } 100% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.1); } }
|
||||
body.theme-neon-dynamic .ebook-chapter .chapter-title { color: #0ff !important; border-bottom-color: rgba(0, 255, 255, 0.3) !important; }
|
||||
body.theme-neon-dynamic .floating-btn, body.theme-neon-dynamic .speed-panel, body.theme-neon-dynamic .font-controls, body.theme-neon-dynamic .theme-selector, body.theme-neon-dynamic .bookmark-panel { background: rgba(0, 0, 0, 0.7) !important; color: #0ff !important; border: 1px solid rgba(0, 255, 255, 0.3) !important; animation: neonPanelGlow 1.5s ease-in-out infinite !important; }
|
||||
@keyframes neonPanelGlow { 0% { border-color: rgba(0, 255, 255, 0.2); } 50% { border-color: rgba(0, 255, 255, 0.6); } 100% { border-color: rgba(0, 255, 255, 0.2); } }
|
||||
body.theme-neon-dynamic .floating-btn { background: rgba(0, 0, 0, 0.6) !important; color: #0ff !important; border: 1px solid #0ff !important; animation: neonBtnPulse 1.5s ease-in-out infinite !important; }
|
||||
@keyframes neonBtnPulse { 0% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.3); } 50% { box-shadow: 0 0 15px rgba(0, 255, 255, 0.8); } 100% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.3); } }
|
||||
body.theme-neon-dynamic .floating-btn.bookmark-btn { background: rgba(0, 255, 255, 0.15) !important; }
|
||||
body.theme-neon-dynamic .floating-btn.bookmark-btn.active { background: #0ff !important; color: #0a0a0a !important; }
|
||||
body.theme-neon-dynamic .speed-slider::-webkit-slider-thumb { background: #0ff !important; }
|
||||
body.theme-neon-dynamic .progress-slider-global::-webkit-slider-thumb { background: #0ff !important; box-shadow: 0 0 8px rgba(0,255,255,0.8) !important; }
|
||||
body.theme-neon-dynamic .progress-fill { background: #0ff !important; animation: neonFillPulse 1.5s ease-in-out infinite; }
|
||||
@keyframes neonFillPulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
|
||||
body.theme-neon-dynamic .speed-preset.active { color: #0ff !important; background: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .auto-chapter-line { border-top-color: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .auto-chapter-line input { accent-color: #0ff !important; }
|
||||
body.theme-neon-dynamic .global-progress-container { background: rgba(0, 0, 0, 0.7) !important; border-top: 1px solid rgba(0, 255, 255, 0.3) !important; }
|
||||
body.theme-neon-dynamic .progress-info { color: rgba(0, 255, 255, 0.8) !important; }
|
||||
body.theme-neon-dynamic .shelf-item { background: rgba(0, 255, 255, 0.08) !important; border-color: rgba(0, 255, 255, 0.3) !important; color: #0ff !important; }
|
||||
body.theme-neon-dynamic .shelf-item:hover { background: rgba(0, 255, 255, 0.18) !important; border-color: #0ff !important; box-shadow: 0 0 20px rgba(0,255,255,0.3) !important; }
|
||||
body.theme-neon-dynamic .book-chapter-item { background: rgba(0, 255, 255, 0.08) !important; border-color: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .book-chapter-item a { color: #0ff !important; }
|
||||
body.theme-neon-dynamic .book-chapter-item:hover { background: rgba(0, 255, 255, 0.18) !important; box-shadow: 0 0 15px rgba(0,255,255,0.2) !important; }
|
||||
body.theme-neon-dynamic .bookmark-item .title { color: #0ff !important; }
|
||||
body.theme-neon-dynamic .bookmark-header { border-bottom-color: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .chapter-tooltip { background: rgba(0, 0, 0, 0.9) !important; border-color: #0ff !important; color: #0ff !important; box-shadow: 0 0 15px rgba(0,255,255,0.4) !important; text-shadow: 0 0 3px #0ff !important; }
|
||||
body.theme-neon-dynamic .page-turn-overlay { background: rgba(0, 0, 0, 0.8) !important; }
|
||||
body.theme-neon-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-neon-dynamic .page-turn-overlay .book-right { background: rgba(0, 0, 0, 0.7) !important; border: 2px solid #0ff !important; color: #0ff !important; }
|
||||
body.theme-neon-dynamic .page-turn-overlay .message { background: rgba(0, 0, 0, 0.9) !important; color: #0ff !important; border: 1px solid #0ff !important; }
|
||||
body.theme-neon-dynamic .page-transition { background: rgba(0, 0, 0, 0.7) !important; }
|
||||
body.theme-neon-dynamic .page-transition .book-page { background: linear-gradient(135deg, #0ff, #0a0a0a) !important; }
|
||||
body.theme-neon-dynamic .page-transition .loading-text { color: #0ff !important; text-shadow: 0 0 10px rgba(0,255,255,0.5); }
|
||||
body.theme-neon-dynamic .page-transition .loading-dots span { background: #0ff !important; }
|
||||
body.theme-neon-dynamic .top-bar button.bookmark { background: rgba(0, 255, 255, 0.15) !important; border: 1px solid #0ff !important; color: #0ff !important; animation: neonBtnPulse 1.5s ease-in-out infinite !important; }
|
||||
body.theme-neon-dynamic .toast { background: rgba(0, 0, 0, 0.85) !important; color: #0ff !important; border: 1px solid #0ff !important; }
|
||||
|
||||
/* 3. 暮色晚霞 */
|
||||
body.theme-sunset-dynamic { background: linear-gradient(270deg, #1a0a2e, #5c2a4a, #c45c3a, #e8a04a); background-size: 400% 400%; animation: sunsetFlow 15s ease infinite; color: #f5e6d3 !important; }
|
||||
@keyframes sunsetFlow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
||||
body.theme-sunset-dynamic .top-bar { background: rgba(0, 0, 0, 0.4) !important; border-bottom: 1px solid rgba(255, 184, 107, 0.3) !important; }
|
||||
body.theme-sunset-dynamic .top-bar, body.theme-sunset-dynamic .top-bar a, body.theme-sunset-dynamic .top-bar button { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .back-btn { background: rgba(255, 184, 107, 0.15) !important; }
|
||||
body.theme-sunset-dynamic .page-title { color: #ffb86b !important; text-shadow: 0 0 8px rgba(255,184,107,0.3); }
|
||||
body.theme-sunset-dynamic .ebook-chapter { background: rgba(0, 0, 0, 0.4) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .ebook-chapter .chapter-title { color: #ffb86b !important; border-bottom-color: rgba(255, 184, 107, 0.3) !important; }
|
||||
body.theme-sunset-dynamic .floating-btn, body.theme-sunset-dynamic .speed-panel, body.theme-sunset-dynamic .font-controls, body.theme-sunset-dynamic .theme-selector, body.theme-sunset-dynamic .bookmark-panel { background: rgba(0, 0, 0, 0.45) !important; border: 1px solid rgba(255, 184, 107, 0.3) !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .floating-btn { background: rgba(0, 0, 0, 0.35) !important; color: #ffb86b !important; border: 1px solid rgba(255, 184, 107, 0.4) !important; }
|
||||
body.theme-sunset-dynamic .floating-btn.bookmark-btn { background: rgba(255, 184, 107, 0.15) !important; border: 1px solid #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .floating-btn.bookmark-btn.active { background: #ffb86b !important; color: #1a0a2e !important; }
|
||||
body.theme-sunset-dynamic .speed-slider::-webkit-slider-thumb { background: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .progress-slider-global::-webkit-slider-thumb { background: #ffb86b !important; box-shadow: 0 0 8px rgba(255,184,107,0.8) !important; }
|
||||
body.theme-sunset-dynamic .progress-fill { background: linear-gradient(90deg, #ffb86b, #ff6b6b) !important; }
|
||||
body.theme-sunset-dynamic .speed-preset.active { color: #ffb86b !important; background: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .auto-chapter-line { border-top-color: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .auto-chapter-line input { accent-color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .global-progress-container { background: rgba(0, 0, 0, 0.45) !important; border-top: 1px solid rgba(255, 184, 107, 0.3) !important; }
|
||||
body.theme-sunset-dynamic .progress-info { color: rgba(255, 184, 107, 0.85) !important; }
|
||||
body.theme-sunset-dynamic .shelf-item { background: rgba(255, 184, 107, 0.1) !important; border-color: rgba(255, 184, 107, 0.3) !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .shelf-item:hover { background: rgba(255, 184, 107, 0.2) !important; border-color: rgba(255, 184, 107, 0.6) !important; }
|
||||
body.theme-sunset-dynamic .book-chapter-item { background: rgba(255, 184, 107, 0.1) !important; border-color: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .book-chapter-item a { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .book-chapter-item:hover { background: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .bookmark-item .title { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .bookmark-header { border-bottom-color: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .chapter-tooltip { background: rgba(30, 20, 30, 0.85) !important; border-color: #ffb86b !important; color: #ffb86b !important; box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; }
|
||||
body.theme-sunset-dynamic .page-turn-overlay { background: rgba(0, 0, 0, 0.5) !important; }
|
||||
body.theme-sunset-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-sunset-dynamic .page-turn-overlay .book-right { background: rgba(30, 20, 30, 0.6) !important; border: 2px solid rgba(255, 184, 107, 0.4) !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .page-turn-overlay .message { background: rgba(30, 20, 30, 0.8) !important; color: #ffb86b !important; border: 1px solid rgba(255, 184, 107, 0.5) !important; }
|
||||
body.theme-sunset-dynamic .page-transition { background: rgba(0, 0, 0, 0.5) !important; }
|
||||
body.theme-sunset-dynamic .page-transition .book-page { background: linear-gradient(135deg, #ffb86b, #c45c3a) !important; }
|
||||
body.theme-sunset-dynamic .page-transition .loading-text { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .page-transition .loading-dots span { background: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .top-bar button.bookmark { background: rgba(255, 184, 107, 0.2) !important; border: 1px solid #ffb86b !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .toast { background: rgba(0, 0, 0, 0.7) !important; color: #ffb86b !important; border: 1px solid rgba(255, 184, 107, 0.4) !important; }
|
||||
|
||||
/* 4. 深海波动 */
|
||||
body.theme-wave-dynamic { background: linear-gradient(135deg, #0b2b44, #0d3b5e, #0a2a40, #0d3b5e, #0b2b44); background-size: 300% 300%; animation: waveMoveEnhanced 6s ease infinite; color: #c8e7f5 !important; }
|
||||
@keyframes waveMoveEnhanced { 0% { background-position: 0% 0%; } 25% { background-position: 100% 50%; } 50% { background-position: 50% 100%; } 75% { background-position: 0% 50%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-wave-dynamic .top-bar { background: rgba(0, 20, 30, 0.6) !important; border-bottom: 1px solid rgba(91, 192, 255, 0.3) !important; }
|
||||
body.theme-wave-dynamic .top-bar, body.theme-wave-dynamic .top-bar a, body.theme-wave-dynamic .top-bar button { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .back-btn { background: rgba(91, 192, 255, 0.15) !important; }
|
||||
body.theme-wave-dynamic .page-title { color: #5bc0ff !important; text-shadow: 0 0 8px rgba(91,192,255,0.3); }
|
||||
body.theme-wave-dynamic .ebook-chapter { background: rgba(0, 20, 30, 0.5) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .ebook-chapter .chapter-title { color: #5bc0ff !important; border-bottom-color: rgba(91, 192, 255, 0.3) !important; }
|
||||
body.theme-wave-dynamic .floating-btn, body.theme-wave-dynamic .speed-panel, body.theme-wave-dynamic .font-controls, body.theme-wave-dynamic .theme-selector, body.theme-wave-dynamic .bookmark-panel { background: rgba(0, 20, 30, 0.65) !important; border: 1px solid rgba(91, 192, 255, 0.3) !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .floating-btn { background: rgba(0, 20, 30, 0.5) !important; color: #5bc0ff !important; border: 1px solid rgba(91, 192, 255, 0.4) !important; animation: waveBtnFloat 3s ease-in-out infinite !important; }
|
||||
@keyframes waveBtnFloat { 0% { transform: translateY(0px); } 50% { transform: translateY(-3px); } 100% { transform: translateY(0px); } }
|
||||
body.theme-wave-dynamic .floating-btn.bookmark-btn { background: rgba(91, 192, 255, 0.15) !important; }
|
||||
body.theme-wave-dynamic .floating-btn.bookmark-btn.active { background: #5bc0ff !important; color: #0b2b44 !important; }
|
||||
body.theme-wave-dynamic .speed-slider::-webkit-slider-thumb { background: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .progress-slider-global::-webkit-slider-thumb { background: #5bc0ff !important; box-shadow: 0 0 8px rgba(91,192,255,0.8) !important; }
|
||||
body.theme-wave-dynamic .progress-fill { background: linear-gradient(90deg, #5bc0ff, #2d9cdb) !important; }
|
||||
body.theme-wave-dynamic .speed-preset.active { color: #5bc0ff !important; background: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .auto-chapter-line { border-top-color: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .auto-chapter-line input { accent-color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .global-progress-container { background: rgba(0, 20, 30, 0.65) !important; border-top: 1px solid rgba(91, 192, 255, 0.3) !important; }
|
||||
body.theme-wave-dynamic .progress-info { color: rgba(91, 192, 255, 0.85) !important; }
|
||||
body.theme-wave-dynamic .shelf-item { background: rgba(91, 192, 255, 0.1) !important; border-color: rgba(91, 192, 255, 0.3) !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .shelf-item:hover { background: rgba(91, 192, 255, 0.2) !important; border-color: rgba(91, 192, 255, 0.6) !important; }
|
||||
body.theme-wave-dynamic .book-chapter-item { background: rgba(91, 192, 255, 0.1) !important; border-color: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .book-chapter-item a { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .book-chapter-item:hover { background: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .bookmark-item .title { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .bookmark-header { border-bottom-color: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .chapter-tooltip { background: rgba(0, 20, 30, 0.9) !important; border-color: #5bc0ff !important; color: #5bc0ff !important; box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; }
|
||||
body.theme-wave-dynamic .page-turn-overlay { background: rgba(10, 40, 60, 0.7) !important; }
|
||||
body.theme-wave-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-wave-dynamic .page-turn-overlay .book-right { background: rgba(10, 40, 60, 0.6) !important; border: 2px solid rgba(91, 192, 255, 0.4) !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .page-turn-overlay .message { background: rgba(10, 40, 60, 0.8) !important; color: #5bc0ff !important; border: 1px solid rgba(91, 192, 255, 0.5) !important; }
|
||||
body.theme-wave-dynamic .page-transition { background: rgba(0, 20, 30, 0.7) !important; }
|
||||
body.theme-wave-dynamic .page-transition .book-page { background: linear-gradient(135deg, #5bc0ff, #0d3b5e) !important; }
|
||||
body.theme-wave-dynamic .page-transition .loading-text { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .page-transition .loading-dots span { background: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .top-bar button.bookmark { background: rgba(91, 192, 255, 0.2) !important; border: 1px solid #5bc0ff !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .toast { background: rgba(0, 20, 30, 0.85) !important; color: #5bc0ff !important; border: 1px solid rgba(91, 192, 255, 0.4) !important; }
|
||||
|
||||
/* 5. 火焰之心 */
|
||||
body.theme-fire-dynamic { background: linear-gradient(180deg, #4a0a0a, #8b2a1a, #d45a2a); background-size: 100% 200%; animation: firePulse 2s ease infinite alternate; color: #ffe0c0 !important; }
|
||||
@keyframes firePulse { 0% { background-position: 0% 0%; } 100% { background-position: 0% 100%; } }
|
||||
body.theme-fire-dynamic .top-bar { background: rgba(60, 10, 10, 0.6) !important; border-bottom: 1px solid rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .top-bar, body.theme-fire-dynamic .top-bar a, body.theme-fire-dynamic .top-bar button { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .back-btn { background: rgba(255, 140, 66, 0.15) !important; }
|
||||
body.theme-fire-dynamic .page-title { color: #ff8c42 !important; text-shadow: 0 0 8px rgba(255,140,66,0.4); }
|
||||
body.theme-fire-dynamic .ebook-chapter { background: rgba(60, 10, 10, 0.5) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 140, 66, 0.3) !important; }
|
||||
body.theme-fire-dynamic .ebook-chapter .chapter-title { color: #ff8c42 !important; border-bottom-color: rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .floating-btn, body.theme-fire-dynamic .speed-panel, body.theme-fire-dynamic .font-controls, body.theme-fire-dynamic .theme-selector, body.theme-fire-dynamic .bookmark-panel { background: rgba(60, 10, 10, 0.7) !important; border: 1px solid rgba(255, 140, 66, 0.4) !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .floating-btn { background: rgba(60, 10, 10, 0.6) !important; color: #ff8c42 !important; border: 1px solid rgba(255, 140, 66, 0.5) !important; }
|
||||
body.theme-fire-dynamic .floating-btn.bookmark-btn { background: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .floating-btn.bookmark-btn.active { background: #ff8c42 !important; color: #4a0a0a !important; }
|
||||
body.theme-fire-dynamic .speed-slider::-webkit-slider-thumb { background: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .progress-slider-global::-webkit-slider-thumb { background: #ff8c42 !important; box-shadow: 0 0 8px rgba(255,140,66,0.8) !important; }
|
||||
body.theme-fire-dynamic .progress-fill { background: linear-gradient(90deg, #ff8c42, #ff5722) !important; }
|
||||
body.theme-fire-dynamic .speed-preset.active { color: #ff8c42 !important; background: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .auto-chapter-line { border-top-color: rgba(255, 140, 66, 0.3) !important; }
|
||||
body.theme-fire-dynamic .auto-chapter-line input { accent-color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .global-progress-container { background: rgba(60, 10, 10, 0.7) !important; border-top: 1px solid rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .progress-info { color: rgba(255, 140, 66, 0.85) !important; }
|
||||
body.theme-fire-dynamic .shelf-item { background: rgba(255, 140, 66, 0.1) !important; border-color: rgba(255, 140, 66, 0.3) !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .shelf-item:hover { background: rgba(255, 140, 66, 0.2) !important; border-color: rgba(255, 140, 66, 0.6) !important; }
|
||||
body.theme-fire-dynamic .book-chapter-item { background: rgba(255, 140, 66, 0.1) !important; border-color: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .book-chapter-item a { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .book-chapter-item:hover { background: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .bookmark-item .title { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .bookmark-header { border-bottom-color: rgba(255, 140, 66, 0.3) !important; }
|
||||
body.theme-fire-dynamic .chapter-tooltip { background: rgba(60, 10, 10, 0.92) !important; border-color: #ff8c42 !important; color: #ff8c42 !important; box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; }
|
||||
body.theme-fire-dynamic .page-turn-overlay { background: rgba(60, 10, 10, 0.7) !important; }
|
||||
body.theme-fire-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-fire-dynamic .page-turn-overlay .book-right { background: rgba(60, 10, 10, 0.6) !important; border: 2px solid rgba(255, 140, 66, 0.4) !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .page-turn-overlay .message { background: rgba(60, 10, 10, 0.8) !important; color: #ff8c42 !important; border: 1px solid rgba(255, 140, 66, 0.5) !important; }
|
||||
body.theme-fire-dynamic .page-transition { background: rgba(60, 10, 10, 0.7) !important; }
|
||||
body.theme-fire-dynamic .page-transition .book-page { background: linear-gradient(135deg, #ff8c42, #d45a2a) !important; }
|
||||
body.theme-fire-dynamic .page-transition .loading-text { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .page-transition .loading-dots span { background: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .top-bar button.bookmark { background: rgba(255, 140, 66, 0.2) !important; border: 1px solid #ff8c42 !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .toast { background: rgba(60, 10, 10, 0.9) !important; color: #ff8c42 !important; border: 1px solid rgba(255, 140, 66, 0.4) !important; }
|
||||
|
||||
/* 6. 樱花飘舞 */
|
||||
body.theme-sakura-dynamic { background: linear-gradient(135deg, #ffeef8 0%, #ffd9e8 50%, #ffb7c5 100%); background-size: 200% 200%; animation: sakuraFlow 8s ease infinite; color: #6b3e4a !important; }
|
||||
@keyframes sakuraFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-sakura-dynamic .top-bar { background: rgba(255, 240, 245, 0.85) !important; backdrop-filter: blur(20px) !important; border-bottom: 1px solid rgba(255, 160, 180, 0.4) !important; }
|
||||
body.theme-sakura-dynamic .top-bar, body.theme-sakura-dynamic .top-bar a, body.theme-sakura-dynamic .top-bar button { color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .back-btn { background: rgba(184, 59, 94, 0.12) !important; }
|
||||
body.theme-sakura-dynamic .page-title { color: #b83b5e !important; text-shadow: 0 0 8px rgba(184,59,94,0.2); }
|
||||
body.theme-sakura-dynamic .ebook-chapter { background: rgba(255, 255, 255, 0.7) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 160, 180, 0.3) !important; color: #6b3e4a !important; }
|
||||
body.theme-sakura-dynamic .ebook-chapter .chapter-title { color: #e86f8f !important; border-bottom-color: rgba(232, 111, 143, 0.3) !important; }
|
||||
body.theme-sakura-dynamic .floating-btn, body.theme-sakura-dynamic .speed-panel, body.theme-sakura-dynamic .font-controls, body.theme-sakura-dynamic .theme-selector, body.theme-sakura-dynamic .bookmark-panel { background: rgba(255, 240, 245, 0.9) !important; border: 1px solid rgba(232, 111, 143, 0.3) !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .floating-btn { background: rgba(255, 240, 245, 0.85) !important; color: #e86f8f !important; border: 1px solid rgba(232, 111, 143, 0.4) !important; }
|
||||
body.theme-sakura-dynamic .floating-btn.bookmark-btn { background: rgba(232, 111, 143, 0.2) !important; border: 1px solid #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .speed-slider::-webkit-slider-thumb { background: #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .progress-slider-global::-webkit-slider-thumb { background: #e86f8f !important; box-shadow: 0 0 8px rgba(232,111,143,0.6) !important; }
|
||||
body.theme-sakura-dynamic .progress-fill { background: linear-gradient(90deg, #e86f8f, #b83b5e) !important; }
|
||||
body.theme-sakura-dynamic .speed-preset.active { color: #e86f8f !important; background: rgba(232, 111, 143, 0.15) !important; }
|
||||
body.theme-sakura-dynamic .global-progress-container { background: rgba(255, 240, 245, 0.9) !important; border-top: 1px solid rgba(232, 111, 143, 0.3) !important; }
|
||||
body.theme-sakura-dynamic .shelf-item { background: rgba(232, 111, 143, 0.1) !important; border-color: rgba(232, 111, 143, 0.3) !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .shelf-item:hover { background: rgba(232, 111, 143, 0.2) !important; }
|
||||
body.theme-sakura-dynamic .book-chapter-item { background: rgba(232, 111, 143, 0.1) !important; }
|
||||
body.theme-sakura-dynamic .book-chapter-item a { color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .chapter-tooltip { background: rgba(255, 240, 245, 0.95) !important; border-color: #e86f8f !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .page-turn-overlay { background: rgba(255, 240, 245, 0.85) !important; }
|
||||
body.theme-sakura-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-sakura-dynamic .page-turn-overlay .book-right { background: rgba(255, 245, 250, 0.9) !important; border: 2px solid rgba(232, 111, 143, 0.5) !important; color: #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .page-turn-overlay .message { background: rgba(255, 240, 245, 0.95) !important; color: #b83b5e !important; border: 1px solid #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .page-transition .book-page { background: linear-gradient(135deg, #e86f8f, #b83b5e) !important; }
|
||||
body.theme-sakura-dynamic .page-transition .loading-text { color: #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .toast { background: rgba(255, 240, 245, 0.95) !important; color: #b83b5e !important; border: 1px solid #e86f8f !important; }
|
||||
|
||||
/* 7. 薄荷冰霜 */
|
||||
body.theme-mintfrost-dynamic { background: linear-gradient(135deg, #c8e8e9 0%, #a8d8ea 50%, #88c8e8 100%); background-size: 200% 200%; animation: mintFlow 6s ease infinite; color: #2c5a5a !important; }
|
||||
@keyframes mintFlow { 0% { background-position: 0% 0%; } 100% { background-position: 100% 100%; } }
|
||||
body.theme-mintfrost-dynamic .top-bar { background: rgba(200, 232, 233, 0.85) !important; border-bottom: 1px solid rgba(100, 180, 200, 0.4) !important; }
|
||||
body.theme-mintfrost-dynamic .top-bar, body.theme-mintfrost-dynamic .top-bar a, body.theme-mintfrost-dynamic .top-bar button { color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .ebook-chapter { background: rgba(255, 255, 250, 0.75) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(100, 180, 200, 0.3) !important; color: #2c5a5a !important; }
|
||||
body.theme-mintfrost-dynamic .ebook-chapter .chapter-title { color: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .floating-btn, body.theme-mintfrost-dynamic .speed-panel, body.theme-mintfrost-dynamic .font-controls, body.theme-mintfrost-dynamic .theme-selector, body.theme-mintfrost-dynamic .bookmark-panel { background: rgba(200, 232, 233, 0.9) !important; border: 1px solid rgba(100, 180, 200, 0.3) !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .floating-btn { background: rgba(200, 232, 233, 0.85) !important; color: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .progress-slider-global::-webkit-slider-thumb { background: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .progress-fill { background: linear-gradient(90deg, #3a9a9a, #2a7a7a) !important; }
|
||||
body.theme-mintfrost-dynamic .chapter-tooltip { background: rgba(200, 232, 233, 0.95) !important; border-color: #3a9a9a !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay { background: rgba(200, 232, 233, 0.85) !important; }
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay .book-right { background: rgba(220, 245, 245, 0.9) !important; border: 2px solid rgba(58, 154, 154, 0.5) !important; color: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay .message { background: rgba(200, 232, 233, 0.95) !important; color: #2a7a7a !important; border: 1px solid #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .toast { background: rgba(200, 232, 233, 0.95) !important; color: #2a7a7a !important; border: 1px solid #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .shelf-item { background: rgba(100, 180, 200, 0.15) !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .page-transition .book-page { background: linear-gradient(135deg, #3a9a9a, #2a7a7a) !important; }
|
||||
|
||||
/* 8. 薰衣草庄园 */
|
||||
body.theme-lavenderfield-dynamic { background: linear-gradient(145deg, #d8cce8 0%, #b9a8d4 50%, #9b88c2 100%); background-size: 200% 200%; animation: lavenderFlow 10s ease infinite; color: #3a2a5a !important; }
|
||||
@keyframes lavenderFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-lavenderfield-dynamic .top-bar { background: rgba(216, 204, 232, 0.85) !important; border-bottom: 1px solid rgba(155, 136, 194, 0.4) !important; }
|
||||
body.theme-lavenderfield-dynamic .top-bar, body.theme-lavenderfield-dynamic .top-bar a, body.theme-lavenderfield-dynamic .top-bar button { color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .ebook-chapter { background: rgba(255, 250, 255, 0.75) !important; backdrop-filter: blur(10px) !important; color: #3a2a5a !important; }
|
||||
body.theme-lavenderfield-dynamic .ebook-chapter .chapter-title { color: #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .floating-btn, body.theme-lavenderfield-dynamic .speed-panel, body.theme-lavenderfield-dynamic .font-controls, body.theme-lavenderfield-dynamic .theme-selector, body.theme-lavenderfield-dynamic .bookmark-panel { background: rgba(216, 204, 232, 0.9) !important; border: 1px solid rgba(155, 136, 194, 0.3) !important; color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .progress-slider-global::-webkit-slider-thumb { background: #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .chapter-tooltip { background: rgba(216, 204, 232, 0.95) !important; border-color: #8b6bbf !important; color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay { background: rgba(216, 204, 232, 0.85) !important; }
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay .book-right { background: rgba(230, 220, 245, 0.9) !important; border: 2px solid rgba(139, 107, 191, 0.5) !important; color: #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay .message { background: rgba(216, 204, 232, 0.95) !important; color: #5a4a8a !important; border: 1px solid #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .toast { background: rgba(216, 204, 232, 0.95) !important; color: #5a4a8a !important; border: 1px solid #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .page-transition .book-page { background: linear-gradient(135deg, #8b6bbf, #6b4a9f) !important; }
|
||||
body.theme-lavenderfield-dynamic .shelf-item { background: rgba(139, 107, 191, 0.15) !important; color: #5a4a8a !important; }
|
||||
|
||||
/* 9. 金色麦田 */
|
||||
body.theme-golden-dynamic { background: linear-gradient(135deg, #f5e6b8 0%, #e8d498 50%, #d4b86a 100%); background-size: 200% 200%; animation: goldenFlow 8s ease infinite; color: #5a4a2a !important; }
|
||||
@keyframes goldenFlow { 0% { background-position: 0% 0%; } 100% { background-position: 100% 100%; } }
|
||||
body.theme-golden-dynamic .top-bar { background: rgba(245, 230, 184, 0.85) !important; border-bottom: 1px solid rgba(212, 184, 106, 0.4) !important; }
|
||||
body.theme-golden-dynamic .top-bar, body.theme-golden-dynamic .top-bar a, body.theme-golden-dynamic .top-bar button { color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .ebook-chapter { background: rgba(255, 255, 240, 0.8) !important; backdrop-filter: blur(10px) !important; color: #5a4a2a !important; }
|
||||
body.theme-golden-dynamic .ebook-chapter .chapter-title { color: #c4a030 !important; }
|
||||
body.theme-golden-dynamic .floating-btn, body.theme-golden-dynamic .speed-panel, body.theme-golden-dynamic .font-controls, body.theme-golden-dynamic .theme-selector, body.theme-golden-dynamic .bookmark-panel { background: rgba(245, 230, 184, 0.9) !important; border: 1px solid rgba(212, 184, 106, 0.3) !important; color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .progress-slider-global::-webkit-slider-thumb { background: #d4a030 !important; }
|
||||
body.theme-golden-dynamic .chapter-tooltip { background: rgba(245, 230, 184, 0.95) !important; border-color: #d4a030 !important; color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .page-turn-overlay { background: rgba(245, 230, 184, 0.85) !important; }
|
||||
body.theme-golden-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-golden-dynamic .page-turn-overlay .book-right { background: rgba(255, 250, 220, 0.9) !important; border: 2px solid rgba(212, 160, 48, 0.5) !important; color: #d4a030 !important; }
|
||||
body.theme-golden-dynamic .page-turn-overlay .message { background: rgba(245, 230, 184, 0.95) !important; color: #8a6a2a !important; border: 1px solid #d4a030 !important; }
|
||||
body.theme-golden-dynamic .toast { background: rgba(245, 230, 184, 0.95) !important; color: #8a6a2a !important; border: 1px solid #d4a030 !important; }
|
||||
body.theme-golden-dynamic .page-transition .book-page { background: linear-gradient(135deg, #d4a030, #b08020) !important; }
|
||||
|
||||
/* 10. 珊瑚海洋 */
|
||||
body.theme-coralreef-dynamic { background: linear-gradient(125deg, #ffaa88 0%, #ff8866 50%, #ff6644 100%); background-size: 200% 200%; animation: coralFlow 7s ease infinite; color: #4a2a1a !important; }
|
||||
@keyframes coralFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-coralreef-dynamic .top-bar { background: rgba(255, 170, 136, 0.85) !important; border-bottom: 1px solid rgba(255, 100, 70, 0.4) !important; }
|
||||
body.theme-coralreef-dynamic .top-bar, body.theme-coralreef-dynamic .top-bar a, body.theme-coralreef-dynamic .top-bar button { color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .ebook-chapter { background: rgba(255, 250, 245, 0.8) !important; color: #4a2a1a !important; }
|
||||
body.theme-coralreef-dynamic .ebook-chapter .chapter-title { color: #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .floating-btn, body.theme-coralreef-dynamic .speed-panel, body.theme-coralreef-dynamic .font-controls, body.theme-coralreef-dynamic .theme-selector, body.theme-coralreef-dynamic .bookmark-panel { background: rgba(255, 170, 136, 0.9) !important; color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .progress-slider-global::-webkit-slider-thumb { background: #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .chapter-tooltip { background: rgba(255, 170, 136, 0.95) !important; border-color: #ff6644 !important; color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .page-turn-overlay { background: rgba(255, 170, 136, 0.85) !important; }
|
||||
body.theme-coralreef-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-coralreef-dynamic .page-turn-overlay .book-right { background: rgba(255, 200, 180, 0.9) !important; border: 2px solid rgba(255, 102, 68, 0.5) !important; color: #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .page-turn-overlay .message { background: rgba(255, 170, 136, 0.95) !important; color: #8a3010 !important; border: 1px solid #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .toast { background: rgba(255, 170, 136, 0.95) !important; color: #8a3010 !important; border: 1px solid #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .page-transition .book-page { background: linear-gradient(135deg, #ff8866, #ff6644) !important; }
|
||||
|
||||
/* 11. 星空银河 */
|
||||
body.theme-galaxy-dynamic { background: radial-gradient(ellipse at center, #0a0a2a 0%, #1a1a4a 50%, #2a2a5a 100%); background-size: 200% 200%; animation: galaxyTwinkle 15s ease infinite; color: #c8d0ff !important; }
|
||||
@keyframes galaxyTwinkle { 0% { background-size: 100% 100%; opacity: 1; } 50% { background-size: 120% 120%; opacity: 0.95; } 100% { background-size: 100% 100%; opacity: 1; } }
|
||||
body.theme-galaxy-dynamic .top-bar { background: rgba(10, 10, 42, 0.85) !important; border-bottom: 1px solid rgba(200, 200, 255, 0.3) !important; }
|
||||
body.theme-galaxy-dynamic .top-bar, body.theme-galaxy-dynamic .top-bar a, body.theme-galaxy-dynamic .top-bar button { color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .ebook-chapter { background: rgba(30, 30, 70, 0.8) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(170, 172, 255, 0.2) !important; color: #c8d0ff !important; }
|
||||
body.theme-galaxy-dynamic .ebook-chapter .chapter-title { color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .floating-btn, body.theme-galaxy-dynamic .speed-panel, body.theme-galaxy-dynamic .font-controls, body.theme-galaxy-dynamic .theme-selector, body.theme-galaxy-dynamic .bookmark-panel { background: rgba(10, 10, 42, 0.9) !important; border: 1px solid rgba(170, 172, 255, 0.3) !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .progress-slider-global::-webkit-slider-thumb { background: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .chapter-tooltip { background: rgba(10, 10, 42, 0.95) !important; border-color: #aaacff !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .page-turn-overlay { background: rgba(10, 10, 42, 0.85) !important; }
|
||||
body.theme-galaxy-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-galaxy-dynamic .page-turn-overlay .book-right { background: rgba(30, 30, 70, 0.9) !important; border: 2px solid rgba(170, 172, 255, 0.5) !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .page-turn-overlay .message { background: rgba(10, 10, 42, 0.95) !important; color: #aaacff !important; border: 1px solid #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .toast { background: rgba(10, 10, 42, 0.95) !important; color: #aaacff !important; border: 1px solid #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .page-transition .book-page { background: linear-gradient(135deg, #aaacff, #6a6acf) !important; }
|
||||
body.theme-galaxy-dynamic .shelf-item { background: rgba(170, 172, 255, 0.1) !important; color: #aaacff !important; }
|
||||
|
||||
/* 12. 玫瑰花园 */
|
||||
body.theme-rosegarden-dynamic { background: linear-gradient(145deg, #f5c8d8 0%, #e8a8c0 50%, #d888a8 100%); background-size: 200% 200%; animation: roseFlow 9s ease infinite; color: #5a2a3a !important; }
|
||||
@keyframes roseFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-rosegarden-dynamic .top-bar { background: rgba(245, 200, 216, 0.85) !important; border-bottom: 1px solid rgba(216, 136, 168, 0.4) !important; }
|
||||
body.theme-rosegarden-dynamic .top-bar, body.theme-rosegarden-dynamic .top-bar a, body.theme-rosegarden-dynamic .top-bar button { color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .ebook-chapter { background: rgba(255, 245, 250, 0.8) !important; color: #5a2a3a !important; }
|
||||
body.theme-rosegarden-dynamic .ebook-chapter .chapter-title { color: #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .floating-btn, body.theme-rosegarden-dynamic .speed-panel, body.theme-rosegarden-dynamic .font-controls, body.theme-rosegarden-dynamic .theme-selector, body.theme-rosegarden-dynamic .bookmark-panel { background: rgba(245, 200, 216, 0.9) !important; border: 1px solid rgba(216, 136, 168, 0.3) !important; color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .progress-slider-global::-webkit-slider-thumb { background: #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .chapter-tooltip { background: rgba(245, 200, 216, 0.95) !important; border-color: #d888a8 !important; color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay { background: rgba(245, 200, 216, 0.85) !important; }
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay .book-right { background: rgba(255, 230, 240, 0.9) !important; border: 2px solid rgba(216, 136, 168, 0.5) !important; color: #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay .message { background: rgba(245, 200, 216, 0.95) !important; color: #a03050 !important; border: 1px solid #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .toast { background: rgba(245, 200, 216, 0.95) !important; color: #a03050 !important; border: 1px solid #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .page-transition .book-page { background: linear-gradient(135deg, #d888a8, #c06888) !important; }
|
||||
|
||||
/* 主题色块样式 */
|
||||
.theme-dot[data-theme="deep-space"] { background: linear-gradient(135deg, #0f0c29, #302b63); }
|
||||
.theme-dot[data-theme="ocean"] { background: linear-gradient(135deg, #1a2980, #26d0ce); }
|
||||
.theme-dot[data-theme="cherry"] { background: linear-gradient(135deg, #ff9a9e, #fecfef); }
|
||||
.theme-dot[data-theme="night"] { background: #1a1a1a; }
|
||||
.theme-dot[data-theme="forest"] { background: linear-gradient(135deg, #134e5e, #71b280); }
|
||||
.theme-dot[data-theme="sunset"] { background: linear-gradient(135deg, #ff7e5f, #feb47b); }
|
||||
.theme-dot[data-theme="lavender"] { background: linear-gradient(135deg, #8e9ecc, #e0bbff); }
|
||||
.theme-dot[data-theme="blueberry"] { background: linear-gradient(135deg, #2c3e66, #4a69bd); }
|
||||
.theme-dot[data-theme="amber"] { background: linear-gradient(135deg, #ffb347, #ffcc33); }
|
||||
.theme-dot[data-theme="coral"] { background: linear-gradient(135deg, #ff6b6b, #ffb8b8); }
|
||||
.theme-dot[data-theme="mint"] { background: linear-gradient(135deg, #a8e6cf, #80deea); }
|
||||
.theme-dot[data-theme="rosegold"] { background: linear-gradient(135deg, #e8b4b8, #ffd9e2); }
|
||||
.theme-dot[data-theme="eyecare"] { background: #c7edcc; border: 2px solid #8b9a6e; }
|
||||
.theme-dot[data-theme="aurora-dynamic"] { background: linear-gradient(270deg, #1a0b2e, #2d1b69, #1a4d8c, #0f5c6b); animation: none; }
|
||||
.theme-dot[data-theme="neon-dynamic"] { background: #0a0a0a; border: 2px solid #0ff; box-shadow: 0 0 5px #0ff; }
|
||||
.theme-dot[data-theme="sunset-dynamic"] { background: linear-gradient(135deg, #5c2a4a, #e8a04a); }
|
||||
.theme-dot[data-theme="wave-dynamic"] { background: linear-gradient(135deg, #0b2b44, #0d3b5e); }
|
||||
.theme-dot[data-theme="fire-dynamic"] { background: linear-gradient(180deg, #4a0a0a, #d45a2a); }
|
||||
.theme-dot[data-theme="sakura-dynamic"] { background: linear-gradient(135deg, #ffeef8, #ffb7c5); }
|
||||
.theme-dot[data-theme="mintfrost-dynamic"] { background: linear-gradient(135deg, #c8e8e9, #88c8e8); }
|
||||
.theme-dot[data-theme="lavenderfield-dynamic"] { background: linear-gradient(145deg, #d8cce8, #9b88c2); }
|
||||
.theme-dot[data-theme="golden-dynamic"] { background: linear-gradient(135deg, #f5e6b8, #d4b86a); }
|
||||
.theme-dot[data-theme="coralreef-dynamic"] { background: linear-gradient(125deg, #ffaa88, #ff6644); }
|
||||
.theme-dot[data-theme="galaxy-dynamic"] { background: radial-gradient(ellipse at center, #0a0a2a, #2a2a5a); }
|
||||
.theme-dot[data-theme="rosegarden-dynamic"] { background: linear-gradient(145deg, #f5c8d8, #d888a8); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="theme-deep-space">
|
||||
|
||||
<div class="floating-buttons" id="floatingButtons">
|
||||
<div class="floating-btn bookmark-btn" id="bookmarkFloatBtn">📋</div>
|
||||
<div class="floating-btn scroll-down" id="scrollToggleBtn">▼</div>
|
||||
<div class="floating-btn" id="themeFloatBtn">🎨</div>
|
||||
</div>
|
||||
|
||||
<div class="speed-panel" id="speedPanel">
|
||||
<div class="speed-label"><span>⚡ 滚动速度</span><span class="speed-value" id="speedValue">6 px/帧</span></div>
|
||||
<input type="range" class="speed-slider" id="speedSlider" min="1" max="30" value="6" step="1">
|
||||
<div class="speed-presets">
|
||||
<div class="speed-preset" data-speed="3">🐢 慢</div>
|
||||
<div class="speed-preset" data-speed="6">⚡ 中</div>
|
||||
<div class="speed-preset" data-speed="10">🚀 快</div>
|
||||
<div class="speed-preset" data-speed="18">💨 极快</div>
|
||||
</div>
|
||||
<div class="auto-chapter-line"><span>📖 自动翻章</span><input type="checkbox" id="autoChapterCheckbox" checked></div>
|
||||
</div>
|
||||
|
||||
<div class="font-controls" id="fontControls">
|
||||
<button id="fontMinus">A-</button>
|
||||
<button id="fontPlus">A+</button>
|
||||
</div>
|
||||
<div class="theme-selector" id="themeSelector">
|
||||
<!-- 12静态主题 -->
|
||||
<div class="theme-dot" data-theme="deep-space" title="深邃星空"></div>
|
||||
<div class="theme-dot" data-theme="ocean" title="深海宁静"></div>
|
||||
<div class="theme-dot" data-theme="cherry" title="樱花"></div>
|
||||
<div class="theme-dot" data-theme="night" title="黑夜模式"></div>
|
||||
<div class="theme-dot" data-theme="forest" title="森林绿意"></div>
|
||||
<div class="theme-dot" data-theme="sunset" title="日落橙"></div>
|
||||
<div class="theme-dot" data-theme="lavender" title="薰衣草"></div>
|
||||
<div class="theme-dot" data-theme="blueberry" title="蓝莓"></div>
|
||||
<div class="theme-dot" data-theme="amber" title="琥珀"></div>
|
||||
<div class="theme-dot" data-theme="coral" title="珊瑚粉"></div>
|
||||
<div class="theme-dot" data-theme="mint" title="薄荷绿"></div>
|
||||
<div class="theme-dot" data-theme="rosegold" title="玫瑰金"></div>
|
||||
<div class="theme-dot" data-theme="eyecare" title="护眼模式"></div>
|
||||
<div style="width:100%; height:1px; background:rgba(255,255,255,0.2); margin:5px 0;"></div>
|
||||
<!-- 12动态主题 -->
|
||||
<div class="theme-dot" data-theme="aurora-dynamic" title="极光幻彩(动态)"></div>
|
||||
<div class="theme-dot" data-theme="neon-dynamic" title="霓虹脉冲(动态)"></div>
|
||||
<div class="theme-dot" data-theme="sunset-dynamic" title="暮色晚霞(动态)"></div>
|
||||
<div class="theme-dot" data-theme="wave-dynamic" title="深海波动(动态)"></div>
|
||||
<div class="theme-dot" data-theme="fire-dynamic" title="火焰之心(动态)"></div>
|
||||
<div class="theme-dot" data-theme="sakura-dynamic" title="樱花飘舞(动态)"></div>
|
||||
<div class="theme-dot" data-theme="mintfrost-dynamic" title="薄荷冰霜(动态)"></div>
|
||||
<div class="theme-dot" data-theme="lavenderfield-dynamic" title="薰衣草庄园(动态)"></div>
|
||||
<div class="theme-dot" data-theme="golden-dynamic" title="金色麦田(动态)"></div>
|
||||
<div class="theme-dot" data-theme="coralreef-dynamic" title="珊瑚海洋(动态)"></div>
|
||||
<div class="theme-dot" data-theme="galaxy-dynamic" title="星空银河(动态)"></div>
|
||||
<div class="theme-dot" data-theme="rosegarden-dynamic" title="玫瑰花园(动态)"></div>
|
||||
</div>
|
||||
<div class="bookmark-panel" id="bookmarkPanel">
|
||||
<div class="bookmark-header"><span>📖 我的书签</span><span id="closePanelBtn">✕</span></div>
|
||||
<div class="bookmark-list" id="bookmarkList"><div class="empty-bookmark">📭 暂无书签<br>点击 ⭐ 添加</div></div>
|
||||
</div>
|
||||
|
||||
<div id="globalProgressPlaceholder"></div>
|
||||
|
||||
<!-- 页面切换转场动画 -->
|
||||
<div id="pageTransition" class="page-transition">
|
||||
<div class="book-loader">
|
||||
<div class="book-page"></div>
|
||||
<div class="book-page"></div>
|
||||
<div class="book-page"></div>
|
||||
<div class="book-page"></div>
|
||||
</div>
|
||||
<div class="loading-text">加载中</div>
|
||||
<div class="loading-dots">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ==================== 主题适配的 Toast 提示框 ====================
|
||||
function showToast(msg) {
|
||||
let t = document.querySelector('.toast');
|
||||
if (!t) {
|
||||
t = document.createElement('div');
|
||||
t.className = 'toast';
|
||||
document.body.appendChild(t);
|
||||
}
|
||||
t.textContent = msg;
|
||||
t.style.display = 'block';
|
||||
|
||||
const bodyClass = document.body.className;
|
||||
if (bodyClass.includes('aurora')) {
|
||||
t.style.background = 'rgba(0, 0, 0, 0.8)'; t.style.color = '#7cffd0'; t.style.border = '1px solid rgba(124, 255, 208, 0.3)';
|
||||
} else if (bodyClass.includes('neon')) {
|
||||
t.style.background = 'rgba(0, 0, 0, 0.85)'; t.style.color = '#0ff'; t.style.border = '1px solid #0ff';
|
||||
} else if (bodyClass.includes('sunset-dynamic')) {
|
||||
t.style.background = 'rgba(0, 0, 0, 0.7)'; t.style.color = '#ffb86b'; t.style.border = '1px solid rgba(255, 184, 107, 0.4)';
|
||||
} else if (bodyClass.includes('wave')) {
|
||||
t.style.background = 'rgba(0, 20, 30, 0.85)'; t.style.color = '#5bc0ff'; t.style.border = '1px solid rgba(91, 192, 255, 0.4)';
|
||||
} else if (bodyClass.includes('fire')) {
|
||||
t.style.background = 'rgba(60, 10, 10, 0.9)'; t.style.color = '#ff8c42'; t.style.border = '1px solid rgba(255, 140, 66, 0.4)';
|
||||
} else if (bodyClass.includes('sakura')) {
|
||||
t.style.background = 'rgba(255, 240, 245, 0.95)'; t.style.color = '#b83b5e'; t.style.border = '1px solid #e86f8f';
|
||||
} else if (bodyClass.includes('mintfrost')) {
|
||||
t.style.background = 'rgba(200, 232, 233, 0.95)'; t.style.color = '#2a7a7a'; t.style.border = '1px solid #3a9a9a';
|
||||
} else if (bodyClass.includes('lavenderfield')) {
|
||||
t.style.background = 'rgba(216, 204, 232, 0.95)'; t.style.color = '#5a4a8a'; t.style.border = '1px solid #8b6bbf';
|
||||
} else if (bodyClass.includes('golden')) {
|
||||
t.style.background = 'rgba(245, 230, 184, 0.95)'; t.style.color = '#8a6a2a'; t.style.border = '1px solid #d4a030';
|
||||
} else if (bodyClass.includes('coralreef')) {
|
||||
t.style.background = 'rgba(255, 170, 136, 0.95)'; t.style.color = '#8a3010'; t.style.border = '1px solid #ff6644';
|
||||
} else if (bodyClass.includes('galaxy')) {
|
||||
t.style.background = 'rgba(10, 10, 42, 0.95)'; t.style.color = '#aaacff'; t.style.border = '1px solid #aaacff';
|
||||
} else if (bodyClass.includes('rosegarden')) {
|
||||
t.style.background = 'rgba(245, 200, 216, 0.95)'; t.style.color = '#a03050'; t.style.border = '1px solid #d888a8';
|
||||
} else if (bodyClass.includes('eyecare')) {
|
||||
t.style.background = 'rgba(199, 237, 204, 0.95)'; t.style.color = '#2d2d2d'; t.style.border = '1px solid rgba(139, 154, 110, 0.4)';
|
||||
} else {
|
||||
t.style.background = 'rgba(0,0,0,0.85)'; t.style.color = '#fff'; t.style.border = 'none';
|
||||
}
|
||||
setTimeout(() => t.style.display = 'none', 1500);
|
||||
}
|
||||
window.showToast = showToast;
|
||||
|
||||
const floatingBtns = document.getElementById('floatingButtons');
|
||||
const speedPanel = document.getElementById('speedPanel');
|
||||
const fontControls = document.getElementById('fontControls');
|
||||
const themeSelector = document.getElementById('themeSelector');
|
||||
const bookmarkPanel = document.getElementById('bookmarkPanel');
|
||||
|
||||
let hideTimer = null;
|
||||
let globalProgressBar = null;
|
||||
|
||||
function showControls() {
|
||||
floatingBtns.classList.add('visible');
|
||||
speedPanel.classList.add('visible');
|
||||
if (globalProgressBar) globalProgressBar.classList.remove('hide');
|
||||
resetHideTimer();
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
floatingBtns.classList.remove('visible');
|
||||
speedPanel.classList.remove('visible');
|
||||
fontControls.classList.remove('visible');
|
||||
themeSelector.classList.remove('visible');
|
||||
if (globalProgressBar) globalProgressBar.classList.add('hide');
|
||||
}
|
||||
|
||||
function resetHideTimer() {
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(() => {
|
||||
if (!bookmarkPanel.classList.contains('show') && !themeSelector.classList.contains('visible') && !fontControls.classList.contains('visible') && !speedPanel.classList.contains('visible')) {
|
||||
hideControls();
|
||||
} else {
|
||||
resetHideTimer();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
let lastTap = 0;
|
||||
document.body.addEventListener('click', (e) => {
|
||||
const now = Date.now();
|
||||
const timeDiff = now - lastTap;
|
||||
const isControlElement = e.target.closest('.floating-btn') || e.target.closest('.bookmark-panel') ||
|
||||
e.target.closest('.theme-selector') || e.target.closest('.font-controls') ||
|
||||
e.target.closest('.speed-panel') || e.target.closest('.global-progress-container');
|
||||
if (!isControlElement && timeDiff < 300 && timeDiff > 0) {
|
||||
e.preventDefault();
|
||||
if (floatingBtns.classList.contains('visible')) {
|
||||
hideControls();
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
} else {
|
||||
showControls();
|
||||
}
|
||||
}
|
||||
lastTap = now;
|
||||
});
|
||||
|
||||
const bookmarkFloatBtn = document.getElementById('bookmarkFloatBtn');
|
||||
const closePanelBtn = document.getElementById('closePanelBtn');
|
||||
if (bookmarkFloatBtn) {
|
||||
bookmarkFloatBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
bookmarkPanel.classList.toggle('show');
|
||||
if (bookmarkPanel.classList.contains('show')) {
|
||||
showControls();
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
} else {
|
||||
resetHideTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (closePanelBtn) {
|
||||
closePanelBtn.addEventListener('click', () => {
|
||||
bookmarkPanel.classList.remove('show');
|
||||
resetHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
const themeFloatBtn = document.getElementById('themeFloatBtn');
|
||||
if (themeFloatBtn) {
|
||||
themeFloatBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (themeSelector.classList.contains('visible')) {
|
||||
themeSelector.classList.remove('visible');
|
||||
} else {
|
||||
themeSelector.classList.add('visible');
|
||||
resetHideTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllThemeStyles() {
|
||||
const bodyClass = document.body.className;
|
||||
const scrollBtn = document.getElementById('scrollToggleBtn');
|
||||
const bookmarkBtn = document.getElementById('bookmarkFloatBtn');
|
||||
const topBarBookmarkBtn = document.querySelector('.top-bar button.bookmark');
|
||||
|
||||
if (bodyClass.includes('aurora')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#7cffd0';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(124, 255, 208, 0.2)'; bookmarkBtn.style.border = '1px solid #7cffd0'; bookmarkBtn.style.color = '#7cffd0'; }
|
||||
} else if (bodyClass.includes('neon')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#0ff';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(0, 255, 255, 0.15)'; bookmarkBtn.style.border = '1px solid #0ff'; bookmarkBtn.style.color = '#0ff'; }
|
||||
} else if (bodyClass.includes('sunset-dynamic')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#ffb86b';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(255, 184, 107, 0.2)'; bookmarkBtn.style.border = '1px solid #ffb86b'; bookmarkBtn.style.color = '#ffb86b'; }
|
||||
} else if (bodyClass.includes('wave')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#5bc0ff';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(91, 192, 255, 0.2)'; bookmarkBtn.style.border = '1px solid #5bc0ff'; bookmarkBtn.style.color = '#5bc0ff'; }
|
||||
} else if (bodyClass.includes('fire')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#ff8c42';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(255, 140, 66, 0.2)'; bookmarkBtn.style.border = '1px solid #ff8c42'; bookmarkBtn.style.color = '#ff8c42'; }
|
||||
} else if (bodyClass.includes('sakura')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#e86f8f';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(232, 111, 143, 0.2)'; bookmarkBtn.style.border = '1px solid #e86f8f'; bookmarkBtn.style.color = '#b83b5e'; }
|
||||
} else if (bodyClass.includes('mintfrost')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#3a9a9a';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(58, 154, 154, 0.2)'; bookmarkBtn.style.border = '1px solid #3a9a9a'; bookmarkBtn.style.color = '#2a7a7a'; }
|
||||
} else if (bodyClass.includes('lavenderfield')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#8b6bbf';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(139, 107, 191, 0.2)'; bookmarkBtn.style.border = '1px solid #8b6bbf'; bookmarkBtn.style.color = '#5a4a8a'; }
|
||||
} else if (bodyClass.includes('golden')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#d4a030';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(212, 160, 48, 0.2)'; bookmarkBtn.style.border = '1px solid #d4a030'; bookmarkBtn.style.color = '#8a6a2a'; }
|
||||
} else if (bodyClass.includes('coralreef')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#ff6644';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(255, 102, 68, 0.2)'; bookmarkBtn.style.border = '1px solid #ff6644'; bookmarkBtn.style.color = '#8a3010'; }
|
||||
} else if (bodyClass.includes('galaxy')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#aaacff';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(170, 172, 255, 0.2)'; bookmarkBtn.style.border = '1px solid #aaacff'; bookmarkBtn.style.color = '#aaacff'; }
|
||||
} else if (bodyClass.includes('rosegarden')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#d888a8';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(216, 136, 168, 0.2)'; bookmarkBtn.style.border = '1px solid #d888a8'; bookmarkBtn.style.color = '#a03050'; }
|
||||
} else if (bodyClass.includes('eyecare')) {
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(139, 154, 110, 0.2)'; bookmarkBtn.style.border = '1px solid #8b9a6e'; bookmarkBtn.style.color = '#2d2d2d'; }
|
||||
} else {
|
||||
if (scrollBtn) scrollBtn.style.color = '';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = ''; bookmarkBtn.style.border = ''; bookmarkBtn.style.color = ''; }
|
||||
}
|
||||
if (topBarBookmarkBtn) {
|
||||
if (bodyClass.includes('aurora')) { topBarBookmarkBtn.style.background = 'rgba(124, 255, 208, 0.2)'; topBarBookmarkBtn.style.border = '1px solid #7cffd0'; topBarBookmarkBtn.style.color = '#7cffd0'; }
|
||||
else if (bodyClass.includes('neon')) { topBarBookmarkBtn.style.background = 'rgba(0, 255, 255, 0.2)'; topBarBookmarkBtn.style.border = '1px solid #0ff'; topBarBookmarkBtn.style.color = '#0ff'; }
|
||||
else if (bodyClass.includes('eyecare')) { topBarBookmarkBtn.style.background = '#8b9a6e'; topBarBookmarkBtn.style.border = 'none'; topBarBookmarkBtn.style.color = '#2d2d2d'; }
|
||||
else { topBarBookmarkBtn.style.background = ''; topBarBookmarkBtn.style.border = ''; topBarBookmarkBtn.style.color = ''; }
|
||||
}
|
||||
}
|
||||
|
||||
const THEMES = [
|
||||
'deep-space', 'ocean', 'cherry', 'night', 'forest', 'sunset',
|
||||
'lavender', 'blueberry', 'amber', 'coral', 'mint', 'rosegold',
|
||||
'eyecare',
|
||||
'aurora-dynamic', 'neon-dynamic', 'sunset-dynamic', 'wave-dynamic', 'fire-dynamic',
|
||||
'sakura-dynamic', 'mintfrost-dynamic', 'lavenderfield-dynamic', 'golden-dynamic',
|
||||
'coralreef-dynamic', 'galaxy-dynamic', 'rosegarden-dynamic'
|
||||
];
|
||||
function setTheme(themeName) {
|
||||
document.body.className = 'theme-' + themeName;
|
||||
localStorage.setItem('reader_theme', themeName);
|
||||
document.querySelectorAll('.theme-dot').forEach(dot => {
|
||||
if (dot.dataset.theme === themeName) dot.classList.add('active');
|
||||
else dot.classList.remove('active');
|
||||
});
|
||||
if (window.updateAllTooltipColors) window.updateAllTooltipColors();
|
||||
updateAllThemeStyles();
|
||||
setTimeout(() => { if (typeof updateGlobalProgress === 'function') updateGlobalProgress(); }, 50);
|
||||
}
|
||||
const savedTheme = localStorage.getItem('reader_theme');
|
||||
if (savedTheme && THEMES.includes(savedTheme)) setTheme(savedTheme);
|
||||
else setTheme('deep-space');
|
||||
|
||||
document.querySelectorAll('.theme-dot').forEach(dot => {
|
||||
dot.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
setTheme(dot.dataset.theme);
|
||||
themeSelector.classList.remove('visible');
|
||||
showToast('🎨 主题已切换');
|
||||
resetHideTimer();
|
||||
});
|
||||
});
|
||||
|
||||
// 滚动速度相关逻辑
|
||||
const scrollBtn = document.getElementById('scrollToggleBtn');
|
||||
if (scrollBtn) {
|
||||
scrollBtn.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
if (fontControls.classList.contains('visible')) fontControls.classList.remove('visible');
|
||||
else fontControls.classList.add('visible');
|
||||
resetHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
const speedSlider = document.getElementById('speedSlider');
|
||||
const speedValue = document.getElementById('speedValue');
|
||||
const speedPresets = document.querySelectorAll('.speed-preset');
|
||||
let currentSpeed = 6;
|
||||
let autoScrollInterval = null;
|
||||
let isAutoScrolling = false;
|
||||
const savedSpeed = localStorage.getItem('scroll_speed');
|
||||
if (savedSpeed) {
|
||||
currentSpeed = parseInt(savedSpeed);
|
||||
if (speedSlider) speedSlider.value = currentSpeed;
|
||||
if (speedValue) speedValue.innerText = currentSpeed + ' px/帧';
|
||||
speedPresets.forEach(preset => {
|
||||
if (parseInt(preset.dataset.speed) === currentSpeed) preset.classList.add('active');
|
||||
else preset.classList.remove('active');
|
||||
});
|
||||
}
|
||||
function updateSpeed(newSpeed) {
|
||||
currentSpeed = Math.min(30, Math.max(1, newSpeed));
|
||||
if (speedSlider) speedSlider.value = currentSpeed;
|
||||
if (speedValue) speedValue.innerText = currentSpeed + ' px/帧';
|
||||
localStorage.setItem('scroll_speed', currentSpeed);
|
||||
speedPresets.forEach(preset => {
|
||||
if (parseInt(preset.dataset.speed) === currentSpeed) preset.classList.add('active');
|
||||
else preset.classList.remove('active');
|
||||
});
|
||||
if (isAutoScrolling) { stopAutoScroll(); startAutoScroll(); }
|
||||
}
|
||||
if (speedSlider) {
|
||||
speedSlider.oninput = (e) => { updateSpeed(parseInt(e.target.value)); showToast(`⚡ 速度 ${currentSpeed} px/帧`); resetHideTimer(); };
|
||||
}
|
||||
speedPresets.forEach(preset => {
|
||||
preset.onclick = () => { updateSpeed(parseInt(preset.dataset.speed)); showToast(`⚡ ${preset.innerText} ${currentSpeed} px/帧`); resetHideTimer(); };
|
||||
});
|
||||
function startAutoScroll() {
|
||||
if (autoScrollInterval) clearInterval(autoScrollInterval);
|
||||
autoScrollInterval = setInterval(() => window.scrollBy(0, currentSpeed), 25);
|
||||
isAutoScrolling = true;
|
||||
if (scrollBtn) { scrollBtn.classList.add('active'); scrollBtn.innerHTML = "⏸"; }
|
||||
showToast(`▶ 滚动中 (${currentSpeed}px/帧)`);
|
||||
}
|
||||
function stopAutoScroll() {
|
||||
if (autoScrollInterval) { clearInterval(autoScrollInterval); autoScrollInterval = null; }
|
||||
isAutoScrolling = false;
|
||||
if (scrollBtn) { scrollBtn.classList.remove('active'); scrollBtn.innerHTML = "▼"; }
|
||||
showToast('⏹ 已停止');
|
||||
}
|
||||
if (scrollBtn) {
|
||||
scrollBtn.onclick = (e) => { e.stopPropagation(); if (isAutoScrolling) stopAutoScroll(); else startAutoScroll(); resetHideTimer(); };
|
||||
}
|
||||
|
||||
// 书签功能
|
||||
const STORAGE_KEY = "bookmarks_v6";
|
||||
function getBookmarks(){
|
||||
try{ return JSON.parse(localStorage.getItem(STORAGE_KEY)||'[]'); }catch(e){ return []; }
|
||||
}
|
||||
function saveBookmarks(list){
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
||||
refreshBookmarkList();
|
||||
}
|
||||
function refreshBookmarkList(){
|
||||
let list = getBookmarks();
|
||||
let container = document.getElementById('bookmarkList');
|
||||
if(!container) return;
|
||||
if(list.length===0){
|
||||
container.innerHTML='<div class="empty-bookmark">📭 暂无书签<br>点击 ⭐ 添加</div>';
|
||||
return;
|
||||
}
|
||||
list.sort((a,b)=>b.time-a.time);
|
||||
let html='';
|
||||
for(let b of list){
|
||||
let pageLabel = b.type==='txt'?'第'+b.page+'章':(b.type==='ebook'?'第'+b.page+'章':'第'+b.page+'页');
|
||||
html+=`<div class="bookmark-item" data-book="${escapeHtml(b.book)}" data-chapter="${escapeHtml(b.chapter)}" data-page="${b.page}">
|
||||
<span class="delete" data-book="${escapeHtml(b.book)}" data-chapter="${escapeHtml(b.chapter)}">🗑</span>
|
||||
<div class="title">📖 ${escapeHtml(b.book.length>18?b.book.substring(0,18)+'...':b.book)}</div>
|
||||
<div class="info">📄 ${escapeHtml(b.chapterName||b.chapter.substring(0,25))} | 📍 ${pageLabel}</div>
|
||||
</div>`;
|
||||
}
|
||||
container.innerHTML=html;
|
||||
document.querySelectorAll('#bookmarkList .bookmark-item').forEach(item=>{
|
||||
let book=item.getAttribute('data-book'), chapter=item.getAttribute('data-chapter'), page=parseInt(item.getAttribute('data-page'))||1;
|
||||
item.onclick=(e)=>{ if(e.target.classList.contains('delete')) return; jumpToBookmark(book,chapter,page); };
|
||||
let db=item.querySelector('.delete');
|
||||
if(db) db.onclick=(e)=>{ e.stopPropagation(); removeBookmark(db.getAttribute('data-book'), db.getAttribute('data-chapter')); };
|
||||
});
|
||||
}
|
||||
function removeBookmark(book,chapter){
|
||||
let list=getBookmarks();
|
||||
list=list.filter(b=>!(b.book===book&&b.chapter===chapter));
|
||||
saveBookmarks(list);
|
||||
showToast('🗑 删除书签');
|
||||
}
|
||||
function escapeHtml(s){ return (s||'').replace(/[&<>]/g,m=>({'&':'&','<':'<','>':'>'}[m])); }
|
||||
let CURRENT_BOOK = '', CURRENT_CHAPTER = '';
|
||||
function jumpToBookmark(book,chapter,page){
|
||||
if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){
|
||||
if (typeof jumpToPage === 'function') jumpToPage(page);
|
||||
else if (typeof renderChapter === 'function') renderChapter(Math.min(Math.max(1,page), (typeof chapters !== 'undefined' ? chapters.length : 1))-1);
|
||||
}else{
|
||||
sessionStorage.setItem('jump_target', JSON.stringify({book,chapter,page}));
|
||||
location.href = '<?php echo $currentFile; ?>?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);
|
||||
}
|
||||
}
|
||||
|
||||
// 自动翻章功能
|
||||
let autoChapterEnabled = true;
|
||||
let isTurningPage = false;
|
||||
let turnTimer = null;
|
||||
let lastScrollTop = 0;
|
||||
let scrollDirection = 'down';
|
||||
const autoChapterCheckbox = document.getElementById('autoChapterCheckbox');
|
||||
|
||||
function getThemeColorsForAnimation() {
|
||||
const bodyClass = document.body.className;
|
||||
let overlayBg = 'rgba(15, 12, 41, 0.92)', leftBg = 'rgba(48, 43, 99, 0.95)', rightBg = 'rgba(48, 43, 99, 0.95)';
|
||||
let leftBorder = '2px solid rgba(155, 89, 182, 0.6)', rightBorder = '2px solid rgba(155, 89, 182, 0.6)';
|
||||
let msgBg = 'rgba(48, 43, 99, 0.95)', msgColor = '#bb86fc', msgBorder = '1px solid rgba(155, 89, 182, 0.5)';
|
||||
|
||||
if (bodyClass.includes('aurora')) {
|
||||
overlayBg = 'rgba(0, 0, 0, 0.6)'; leftBg = rightBg = 'rgba(0, 0, 0, 0.5)';
|
||||
leftBorder = rightBorder = '2px solid rgba(124, 255, 208, 0.4)';
|
||||
msgBg = 'rgba(0, 0, 0, 0.7)'; msgColor = '#7cffd0'; msgBorder = '1px solid rgba(124, 255, 208, 0.5)';
|
||||
} else if (bodyClass.includes('neon')) {
|
||||
overlayBg = 'rgba(0, 0, 0, 0.8)'; leftBg = rightBg = 'rgba(0, 0, 0, 0.7)';
|
||||
leftBorder = rightBorder = '2px solid #0ff'; msgBg = 'rgba(0, 0, 0, 0.9)'; msgColor = '#0ff'; msgBorder = '1px solid #0ff';
|
||||
} else if (bodyClass.includes('sunset-dynamic')) {
|
||||
overlayBg = 'rgba(0, 0, 0, 0.5)'; leftBg = rightBg = 'rgba(30, 20, 30, 0.6)';
|
||||
leftBorder = rightBorder = '2px solid rgba(255, 184, 107, 0.4)'; msgBg = 'rgba(30, 20, 30, 0.8)'; msgColor = '#ffb86b'; msgBorder = '1px solid rgba(255, 184, 107, 0.5)';
|
||||
} else if (bodyClass.includes('wave')) {
|
||||
overlayBg = 'rgba(10, 40, 60, 0.7)'; leftBg = rightBg = 'rgba(10, 40, 60, 0.6)';
|
||||
leftBorder = rightBorder = '2px solid rgba(91, 192, 255, 0.4)'; msgBg = 'rgba(10, 40, 60, 0.8)'; msgColor = '#5bc0ff'; msgBorder = '1px solid rgba(91, 192, 255, 0.5)';
|
||||
} else if (bodyClass.includes('fire')) {
|
||||
overlayBg = 'rgba(60, 10, 10, 0.7)'; leftBg = rightBg = 'rgba(60, 10, 10, 0.6)';
|
||||
leftBorder = rightBorder = '2px solid rgba(255, 140, 66, 0.4)'; msgBg = 'rgba(60, 10, 10, 0.8)'; msgColor = '#ff8c42'; msgBorder = '1px solid rgba(255, 140, 66, 0.5)';
|
||||
} else if (bodyClass.includes('sakura')) {
|
||||
overlayBg = 'rgba(255, 240, 245, 0.85)'; leftBg = rightBg = 'rgba(255, 245, 250, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(232, 111, 143, 0.5)'; msgBg = 'rgba(255, 240, 245, 0.95)'; msgColor = '#b83b5e'; msgBorder = '1px solid #e86f8f';
|
||||
} else if (bodyClass.includes('mintfrost')) {
|
||||
overlayBg = 'rgba(200, 232, 233, 0.85)'; leftBg = rightBg = 'rgba(220, 245, 245, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(58, 154, 154, 0.5)'; msgBg = 'rgba(200, 232, 233, 0.95)'; msgColor = '#2a7a7a'; msgBorder = '1px solid #3a9a9a';
|
||||
} else if (bodyClass.includes('lavenderfield')) {
|
||||
overlayBg = 'rgba(216, 204, 232, 0.85)'; leftBg = rightBg = 'rgba(230, 220, 245, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(139, 107, 191, 0.5)'; msgBg = 'rgba(216, 204, 232, 0.95)'; msgColor = '#5a4a8a'; msgBorder = '1px solid #8b6bbf';
|
||||
} else if (bodyClass.includes('golden')) {
|
||||
overlayBg = 'rgba(245, 230, 184, 0.85)'; leftBg = rightBg = 'rgba(255, 250, 220, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(212, 160, 48, 0.5)'; msgBg = 'rgba(245, 230, 184, 0.95)'; msgColor = '#8a6a2a'; msgBorder = '1px solid #d4a030';
|
||||
} else if (bodyClass.includes('coralreef')) {
|
||||
overlayBg = 'rgba(255, 170, 136, 0.85)'; leftBg = rightBg = 'rgba(255, 200, 180, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(255, 102, 68, 0.5)'; msgBg = 'rgba(255, 170, 136, 0.95)'; msgColor = '#8a3010'; msgBorder = '1px solid #ff6644';
|
||||
} else if (bodyClass.includes('galaxy')) {
|
||||
overlayBg = 'rgba(10, 10, 42, 0.85)'; leftBg = rightBg = 'rgba(30, 30, 70, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(170, 172, 255, 0.5)'; msgBg = 'rgba(10, 10, 42, 0.95)'; msgColor = '#aaacff'; msgBorder = '1px solid #aaacff';
|
||||
} else if (bodyClass.includes('rosegarden')) {
|
||||
overlayBg = 'rgba(245, 200, 216, 0.85)'; leftBg = rightBg = 'rgba(255, 230, 240, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(216, 136, 168, 0.5)'; msgBg = 'rgba(245, 200, 216, 0.95)'; msgColor = '#a03050'; msgBorder = '1px solid #d888a8';
|
||||
} else if (bodyClass.includes('eyecare')) {
|
||||
overlayBg = 'rgba(199, 237, 204, 0.92)'; leftBg = rightBg = 'rgba(215, 245, 210, 0.95)';
|
||||
leftBorder = rightBorder = '2px solid rgba(139, 154, 110, 0.5)'; msgBg = 'rgba(215, 245, 210, 0.95)'; msgColor = '#2d2d2d'; msgBorder = '1px solid rgba(139, 154, 110, 0.4)';
|
||||
}
|
||||
return { overlayBg, leftBg, rightBg, leftBorder, rightBorder, msgBg, msgColor, msgBorder };
|
||||
}
|
||||
|
||||
function showBookTurnAnimation(callback) {
|
||||
if (isTurningPage) { if (callback) callback(); return; }
|
||||
isTurningPage = true;
|
||||
const colors = getThemeColorsForAnimation();
|
||||
let overlay = document.createElement('div');
|
||||
overlay.className = 'page-turn-overlay';
|
||||
overlay.style.background = colors.overlayBg;
|
||||
overlay.innerHTML = `<div class="book-container"><div class="book-left">📖</div><div class="book-right">📖</div><div class="message">✨ 正在翻开新篇章 ✨</div></div>`;
|
||||
const bookLeft = overlay.querySelector('.book-left'), bookRight = overlay.querySelector('.book-right'), message = overlay.querySelector('.message');
|
||||
if (bookLeft) { bookLeft.style.background = colors.leftBg; bookLeft.style.border = colors.leftBorder; bookLeft.style.color = colors.msgColor; }
|
||||
if (bookRight) { bookRight.style.background = colors.rightBg; bookRight.style.border = colors.rightBorder; bookRight.style.color = colors.msgColor; }
|
||||
if (message) { message.style.background = colors.msgBg; message.style.color = colors.msgColor; message.style.border = colors.msgBorder; }
|
||||
document.body.appendChild(overlay);
|
||||
setTimeout(() => { if (callback) callback(); setTimeout(() => { if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); isTurningPage = false; }, 150); }, 450);
|
||||
}
|
||||
|
||||
if (localStorage.getItem('autoChapterEnabled') !== null) {
|
||||
autoChapterEnabled = localStorage.getItem('autoChapterEnabled') === 'true';
|
||||
if (autoChapterCheckbox) autoChapterCheckbox.checked = autoChapterEnabled;
|
||||
} else { autoChapterEnabled = true; if (autoChapterCheckbox) autoChapterCheckbox.checked = true; }
|
||||
if (autoChapterCheckbox) {
|
||||
autoChapterCheckbox.onchange = function(e) {
|
||||
autoChapterEnabled = this.checked;
|
||||
localStorage.setItem('autoChapterEnabled', autoChapterEnabled);
|
||||
showToast(autoChapterEnabled ? '✅ 自动翻章已开启' : '⏹ 自动翻章已关闭');
|
||||
resetHideTimer();
|
||||
};
|
||||
}
|
||||
|
||||
let scrollTimer = null;
|
||||
function checkScrollBottom() {
|
||||
if (!autoChapterEnabled || isTurningPage) return;
|
||||
let totalHeight = document.body.scrollHeight, windowHeight = window.innerHeight, scrollTop = window.scrollY;
|
||||
if (scrollTop > lastScrollTop) scrollDirection = 'down';
|
||||
else if (scrollTop < lastScrollTop) { scrollDirection = 'up'; if (turnTimer) { clearTimeout(turnTimer); turnTimer = null; } }
|
||||
lastScrollTop = scrollTop;
|
||||
let isAtBottom = (scrollTop + windowHeight + 15) >= totalHeight;
|
||||
if (isAtBottom && scrollDirection === 'down' && !turnTimer) {
|
||||
let nextBtn = document.getElementById('nextChapterBtn');
|
||||
if (nextBtn && !nextBtn.disabled) {
|
||||
showToast('📖 3秒后自动翻到下一章...');
|
||||
turnTimer = setTimeout(() => {
|
||||
if (!autoChapterEnabled || isTurningPage) { turnTimer = null; return; }
|
||||
if ((window.scrollY + windowHeight + 15) >= document.body.scrollHeight) {
|
||||
showBookTurnAnimation(() => { if (nextBtn) nextBtn.click(); });
|
||||
}
|
||||
turnTimer = null;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', function() { if (scrollTimer) clearTimeout(scrollTimer); scrollTimer = setTimeout(checkScrollBottom, 100); });
|
||||
function onChapterChange() { if (turnTimer) { clearTimeout(turnTimer); turnTimer = null; } isTurningPage = false; lastScrollTop = 0; scrollDirection = 'down'; window.scrollTo(0, 0); if (typeof updateGlobalProgress === 'function') updateGlobalProgress(); }
|
||||
|
||||
// 页面转场动画
|
||||
const pageTransition = {
|
||||
element: document.getElementById('pageTransition'),
|
||||
show() { if (!this.element) return; this.element.classList.add('active'); if (this.timeout) clearTimeout(this.timeout); this.timeout = setTimeout(() => { if (this.element) this.element.classList.remove('active'); }, 3000); },
|
||||
hide() { if (!this.element) return; this.element.classList.remove('active'); if (this.timeout) clearTimeout(this.timeout); }
|
||||
};
|
||||
function rippleHandler(e) {
|
||||
const ripple = document.createElement('span'); ripple.classList.add('ripple');
|
||||
const rect = this.getBoundingClientRect(); const size = Math.max(rect.width, rect.height);
|
||||
const x = e.clientX - rect.left - size / 2, y = e.clientY - rect.top - size / 2;
|
||||
ripple.style.width = ripple.style.height = size + 'px'; ripple.style.left = x + 'px'; ripple.style.top = y + 'px';
|
||||
this.style.position = 'relative'; this.style.overflow = 'hidden'; this.appendChild(ripple);
|
||||
setTimeout(() => ripple.remove(), 500);
|
||||
if (this.tagName === 'A' && this.getAttribute('href') && !this.hasAttribute('data-no-transition')) {
|
||||
e.preventDefault(); const targetUrl = this.getAttribute('href');
|
||||
if (targetUrl && targetUrl !== '#') { pageTransition.show(); setTimeout(() => { window.location.href = targetUrl; }, 280); }
|
||||
}
|
||||
}
|
||||
function enhanceBookCards() { document.querySelectorAll('.shelf-item, .book-chapter-item a').forEach(card => { card.removeEventListener('click', rippleHandler); card.addEventListener('click', rippleHandler); }); }
|
||||
function initPageLoadAnimation() { pageTransition.hide(); enhanceBookCards(); }
|
||||
document.addEventListener('DOMContentLoaded', () => { initPageLoadAnimation(); enhanceBookCards(); updateAllThemeStyles(); });
|
||||
window.addEventListener('pageshow', () => { pageTransition.hide(); });
|
||||
window.addEventListener('beforeunload', () => { pageTransition.hide(); });
|
||||
|
||||
// 全局进度条
|
||||
let globalChapters = [], globalTotalChapters = 0, globalCurrentIndex = 0, lastChapterIndex = -1, tooltipHideTimer = null;
|
||||
function getThemeTooltipColors() {
|
||||
const bodyClass = document.body.className;
|
||||
if (bodyClass.includes('aurora')) return { bgColor: 'rgba(0, 0, 0, 0.75)', textColor: '#7cffd0', borderColor: '#7cffd0' };
|
||||
if (bodyClass.includes('neon')) return { bgColor: 'rgba(0, 0, 0, 0.9)', textColor: '#0ff', borderColor: '#0ff' };
|
||||
if (bodyClass.includes('sunset-dynamic')) return { bgColor: 'rgba(30, 20, 30, 0.85)', textColor: '#ffb86b', borderColor: '#ffb86b' };
|
||||
if (bodyClass.includes('wave')) return { bgColor: 'rgba(0, 20, 30, 0.9)', textColor: '#5bc0ff', borderColor: '#5bc0ff' };
|
||||
if (bodyClass.includes('fire')) return { bgColor: 'rgba(60, 10, 10, 0.92)', textColor: '#ff8c42', borderColor: '#ff8c42' };
|
||||
if (bodyClass.includes('sakura')) return { bgColor: 'rgba(255, 240, 245, 0.95)', textColor: '#b83b5e', borderColor: '#e86f8f' };
|
||||
if (bodyClass.includes('mintfrost')) return { bgColor: 'rgba(200, 232, 233, 0.95)', textColor: '#2a7a7a', borderColor: '#3a9a9a' };
|
||||
if (bodyClass.includes('lavenderfield')) return { bgColor: 'rgba(216, 204, 232, 0.95)', textColor: '#5a4a8a', borderColor: '#8b6bbf' };
|
||||
if (bodyClass.includes('golden')) return { bgColor: 'rgba(245, 230, 184, 0.95)', textColor: '#8a6a2a', borderColor: '#d4a030' };
|
||||
if (bodyClass.includes('coralreef')) return { bgColor: 'rgba(255, 170, 136, 0.95)', textColor: '#8a3010', borderColor: '#ff6644' };
|
||||
if (bodyClass.includes('galaxy')) return { bgColor: 'rgba(10, 10, 42, 0.95)', textColor: '#aaacff', borderColor: '#aaacff' };
|
||||
if (bodyClass.includes('rosegarden')) return { bgColor: 'rgba(245, 200, 216, 0.95)', textColor: '#a03050', borderColor: '#d888a8' };
|
||||
if (bodyClass.includes('eyecare')) return { bgColor: 'rgba(215, 245, 210, 0.98)', textColor: '#2d2d2d', borderColor: '#8b9a6e' };
|
||||
return { bgColor: 'rgba(0,0,0,0.95)', textColor: '#ff9800', borderColor: 'rgba(255,152,0,0.6)' };
|
||||
}
|
||||
function updateTooltipStyle(tooltip) { if (!tooltip) return; const c = getThemeTooltipColors(); tooltip.style.backgroundColor = c.bgColor; tooltip.style.color = c.textColor; tooltip.style.border = `1px solid ${c.borderColor}`; }
|
||||
window.updateAllTooltipColors = function() { const tooltip = document.getElementById('chapterTooltip'); if (tooltip) updateTooltipStyle(tooltip); updateAllThemeStyles(); };
|
||||
function showChapterTooltip(chapterIndex, chapterTitle) {
|
||||
let tooltip = document.getElementById('chapterTooltip');
|
||||
if (!tooltip) return;
|
||||
if (tooltipHideTimer) clearTimeout(tooltipHideTimer);
|
||||
let displayText = `📖 第 ${chapterIndex+1} 章 · ${chapterTitle.substring(0, 32)}${chapterTitle.length > 32 ? '...' : ''}`;
|
||||
tooltip.textContent = displayText;
|
||||
updateTooltipStyle(tooltip);
|
||||
tooltip.style.display = 'block';
|
||||
tooltipHideTimer = setTimeout(() => { if (tooltip) tooltip.style.display = 'none'; }, 2000);
|
||||
}
|
||||
function createGlobalProgressBar(total, currentIdx, chaptersList) {
|
||||
let container = document.getElementById('globalProgressPlaceholder');
|
||||
if (!container) return;
|
||||
container.innerHTML = `<div class="global-progress-container" id="globalProgressBar"><div class="progress-range-area"><input type="range" class="progress-slider-global" id="globalProgressSlider" min="0" max="${total-1}" value="${currentIdx}" step="1"><div class="chapter-tooltip" id="chapterTooltip" style="display: none;">📖 ${escapeHtml(chaptersList[currentIdx]?.title || '章节')}</div></div><div class="progress-info"><div class="progress-label"><span>📖 第 ${currentIdx+1} / ${total} 章</span></div><div>${escapeHtml(chaptersList[currentIdx]?.title || '')}</div></div></div>`;
|
||||
globalProgressBar = document.getElementById('globalProgressBar');
|
||||
let slider = document.getElementById('globalProgressSlider');
|
||||
if (slider) {
|
||||
slider.addEventListener('input', (e) => { let idx = parseInt(e.target.value); if (idx !== lastChapterIndex) { lastChapterIndex = idx; showChapterTooltip(idx, chaptersList[idx]?.title || '章节'); } });
|
||||
slider.addEventListener('change', (e) => { let idx = parseInt(e.target.value); if (typeof renderChapter === 'function') { renderChapter(idx); showToast(`📖 跳转到第 ${idx+1} 章`); } resetHideTimer(); });
|
||||
}
|
||||
if (globalProgressBar) globalProgressBar.classList.add('hide');
|
||||
}
|
||||
function updateGlobalProgress() {
|
||||
let container = document.getElementById('globalProgressBar');
|
||||
if (!container) return;
|
||||
let slider = document.getElementById('globalProgressSlider');
|
||||
let infoLabel = container.querySelector('.progress-label span');
|
||||
let infoTitle = container.querySelector('.progress-info > div:last-child');
|
||||
if (slider && globalTotalChapters > 0) {
|
||||
slider.value = globalCurrentIndex;
|
||||
if (infoLabel) infoLabel.innerText = `📖 第 ${globalCurrentIndex+1} / ${globalTotalChapters} 章`;
|
||||
if (infoTitle && globalChapters[globalCurrentIndex]) infoTitle.innerText = globalChapters[globalCurrentIndex].title || '';
|
||||
lastChapterIndex = globalCurrentIndex;
|
||||
}
|
||||
}
|
||||
window.updateGlobalProgress = updateGlobalProgress;
|
||||
window.updateAllTooltipColors = updateAllTooltipColors;
|
||||
window.updateAllThemeStyles = updateAllThemeStyles;
|
||||
</script>
|
||||
|
||||
<?php if ($isChapterPage && $isPdf): ?>
|
||||
<!-- PDF阅读页 -->
|
||||
<div class="top-bar">
|
||||
<div class="top-bar-left">
|
||||
<button class="back-btn" id="backBtn">←</button>
|
||||
<div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div>
|
||||
</div>
|
||||
<div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div>
|
||||
</div>
|
||||
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
|
||||
<div class="content" id="reader"><div class="loading-msg" id="loadingMsg">⏳ 正在加载 PDF...<br><?php echo htmlspecialchars($chapter); ?></div></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const PDF_URL = "<?php echo $fileUrl; ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
|
||||
let pdfDoc=null,totalPages=0,renderedPages=new Set(),targetPage=null,scale=1.5;
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
function getCurrentPage(){let cs=document.querySelectorAll('.canvas-container');for(let i=0;i<cs.length;i++){let r=cs[i].getBoundingClientRect();if(r.top<=150&&r.bottom>=100){let p=parseInt(cs[i].getAttribute('data-page'));if(!isNaN(p))return p;}}return 1;}
|
||||
function addBookmark(){let p=getCurrentPage(),l=getBookmarks(),i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:p,time:Date.now()};if(i>=0)l[i]=n;else l.push(n);saveBookmarks(l);showToast('✅ 第 '+p+' 页');}
|
||||
function jumpToPage(p){p=Math.min(Math.max(1,p),totalPages);let t=document.querySelector(`.canvas-container[data-page="${p}"]`);if(t){t.scrollIntoView({behavior:'smooth',block:'start'});showToast('✨ 第 '+p+' 页');}else{showToast('📖 加载中...');(async()=>{let s=Math.max(1,p-3),e=Math.min(totalPages,p+3);for(let i=s;i<=e;i++)if(!renderedPages.has(i))await renderPage(i);setTimeout(()=>{let c=document.querySelector(`.canvas-container[data-page="${p}"]`);if(c){c.scrollIntoView({behavior:'smooth',block:'start'});showToast('✨ 第 '+p+' 页');}},300);})();}}
|
||||
window.jumpToBookmark = function(book,chapter,p){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER)jumpToPage(p);else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page:p}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
async function renderPage(n){if(!pdfDoc||renderedPages.has(n))return;renderedPages.add(n);let div=document.createElement('div');div.className='canvas-container';div.setAttribute('data-page',n);let p=document.createElement('div');p.className='loading-placeholder';p.innerText=`⏳ 第 ${n} 页...`;div.appendChild(p);let ins=false,ex=document.querySelectorAll('.canvas-container');for(let i=0;i<ex.length;i++){let ep=parseInt(ex[i].getAttribute('data-page'));if(ep>n){ex[i].before(div);ins=true;break;}}if(!ins)document.getElementById('reader').appendChild(div);try{let page=await pdfDoc.getPage(n),vp=page.getViewport({scale:scale}),cv=document.createElement('canvas');cv.width=vp.width;cv.height=vp.height;cv.style.width='100%';cv.style.height='auto';await page.render({canvasContext:cv.getContext('2d'),viewport:vp}).promise;div.innerHTML='';div.appendChild(cv);let pf=document.getElementById('progressFill');if(pf)pf.style.width=(renderedPages.size/totalPages)*100+'%';}catch(e){p.innerText=`❌ 第 ${n} 页失败`;}}
|
||||
let st;function onScrollLoad(){if(st)clearTimeout(st);st=setTimeout(()=>{if(!pdfDoc)return;let cs=document.querySelectorAll('.canvas-container'),need=new Set();cs.forEach(c=>{let r=c.getBoundingClientRect();if(r.top-600<window.innerHeight&&r.bottom+600>0){let p=parseInt(c.getAttribute('data-page'));if(!isNaN(p))need.add(p);}});let toRender=[];need.forEach(p=>{for(let i=-2;i<=2;i++){let np=p+i;if(np>=1&&np<=totalPages&&!renderedPages.has(np))toRender.push(np);}});toRender.sort((a,b)=>a-b).forEach(p=>renderPage(p));},200);}
|
||||
async function loadPDF(){try{let lm=document.getElementById('loadingMsg');lm.style.display='block';pdfDoc=await pdfjsLib.getDocument(PDF_URL).promise;totalPages=pdfDoc.numPages;lm.innerText=`📄 共 ${totalPages} 页,加载中...`;let jump=sessionStorage.getItem('jump_target');if(jump){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jump);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)targetPage=t.page;}catch(e){}}else{let bks=getBookmarks(),ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);if(ex&&ex.page)targetPage=ex.page;}for(let i=1;i<=Math.min(5,totalPages);i++)await renderPage(i);lm.style.display='none';if(targetPage){let s=Math.max(1,targetPage-2),e=Math.min(totalPages,targetPage+2);for(let i=s;i<=e;i++)if(!renderedPages.has(i))await renderPage(i);setTimeout(()=>{let c=document.querySelector(`.canvas-container[data-page="${targetPage}"]`);if(c){c.scrollIntoView({behavior:'smooth',block:'start'});showToast('📖 第 '+targetPage+' 页');}targetPage=null;},500);}window.addEventListener('scroll',onScrollLoad);}catch(e){document.getElementById('loadingMsg').innerHTML=`❌ 加载失败<br>${e.message}`;}}
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
loadPDF(); refreshBookmarkList();
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
|
||||
<?php elseif ($isChapterPage && $isTxt && $txtData): ?>
|
||||
<!-- TXT小说阅读页 -->
|
||||
<div class="top-bar"><div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div></div><div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div></div>
|
||||
<div class="content" id="reader"><div id="txtContent"></div><div class="ebook-nav"><button id="prevChapterBtn" disabled>◀ 上一章</button><button id="nextChapterBtn" disabled>下一章 ▶</button></div><div class="chapter-indicator" id="chapterIndicator"></div></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
const TXT_DATA = <?php echo json_encode($txtData); ?>;
|
||||
let curIdx=0,chapters=TXT_DATA.chapters||[],fontSize=18;
|
||||
globalChapters = chapters;
|
||||
globalTotalChapters = chapters.length;
|
||||
globalCurrentIndex = 0;
|
||||
function applyStyles(){let s=document.getElementById('txt-style');if(!s){s=document.createElement('style');s.id='txt-style';document.head.appendChild(s);}s.textContent=`.ebook-chapter{font-size:${fontSize}px}.ebook-chapter p{margin-bottom:1em;text-indent:2em}`;}
|
||||
function renderChapter(i){if(!chapters||i<0||i>=chapters.length)return;onChapterChange();curIdx=i;globalCurrentIndex=i;document.getElementById('txtContent').innerHTML=`<div class="ebook-chapter"><div class="chapter-title">${escapeHtml(chapters[i].title)}</div>${chapters[i].content}</div>`;document.getElementById('prevChapterBtn').disabled=(i<=0);document.getElementById('nextChapterBtn').disabled=(i>=chapters.length-1);document.getElementById('chapterIndicator').innerText=`第 ${i+1}/${chapters.length} 章 · ${chapters[i].title}`;saveProgress(i);if(typeof updateGlobalProgress==='function')updateGlobalProgress();}
|
||||
function saveProgress(i){let l=getBookmarks(),idx=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:i+1,time:Date.now(),type:'txt'};if(idx>=0)l[idx]=n;else l.push(n);saveBookmarks(l);}
|
||||
window.jumpToBookmark = function(book,chapter,page){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){renderChapter(Math.min(Math.max(1,page),chapters.length)-1);showToast('✨ 第 '+page+' 章');}else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'txt'}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
function addBookmark(){let n=curIdx+1,l=getBookmarks(),i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),ni={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:n,time:Date.now(),type:'txt'};if(i>=0)l[i]=ni;else l.push(ni);saveBookmarks(l);showToast('✅ 第 '+n+' 章');}
|
||||
document.getElementById('fontPlus').onclick=()=>{fontSize=Math.min(fontSize+2,32);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('fontMinus').onclick=()=>{fontSize=Math.max(fontSize-2,12);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
document.getElementById('prevChapterBtn').onclick=()=>{if(curIdx>0)renderChapter(curIdx-1);resetHideTimer();};
|
||||
document.getElementById('nextChapterBtn').onclick=()=>{if(curIdx<chapters.length-1)renderChapter(curIdx+1);resetHideTimer();};
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
applyStyles();if(chapters.length>0){let saved=0,jump=sessionStorage.getItem('jump_target');if(jump){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jump);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)saved=Math.min(Math.max(1,t.page),chapters.length)-1;}catch(e){}}else{let bks=getBookmarks(),ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);if(ex&&ex.page)saved=Math.min(Math.max(1,ex.page),chapters.length)-1;}renderChapter(saved);}
|
||||
refreshBookmarkList();
|
||||
createGlobalProgressBar(globalTotalChapters, globalCurrentIndex, globalChapters);
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
|
||||
<?php elseif ($isChapterPage && $isEpub && $epubData): ?>
|
||||
<!-- EPUB阅读页 -->
|
||||
<div class="top-bar"><div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div></div><div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div></div>
|
||||
<div class="content" id="reader">
|
||||
<?php if ($epubData['type'] == 'comic'): ?>
|
||||
<?php $comicImages = $epubData['images']; ?>
|
||||
<div id="comicViewer">
|
||||
<?php foreach($comicImages as $idx => $imgPath): ?>
|
||||
<div class="canvas-container" data-page="<?php echo $idx+1; ?>" style="margin-bottom: 20px;">
|
||||
<img src="<?php echo $imgPath; ?>" loading="lazy" style="max-width:100%; height:auto; border-radius:8px; display:block; margin:0 auto;"
|
||||
onerror="this.onerror=null; this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22300%22 height=%22400%22%3E%3Crect width=%22300%22 height=%22400%22 fill=%22%23333%22/%3E%3Ctext x=%22150%22 y=%22200%22 fill=%22%23fff%22 text-anchor=%22middle%22%3E图片加载失败%3C/text%3E%3C/svg%3E';">
|
||||
<div class="comic-page-info" style="text-align:center; padding:8px; font-size:12px; color:rgba(255,255,255,0.6);">第 <?php echo $idx+1; ?> / <?php echo count($comicImages); ?> 页</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
const TOTAL_PAGES = <?php echo count($comicImages); ?>;
|
||||
globalChapters = [{title: CHAPTER_NAME}];
|
||||
globalTotalChapters = 1;
|
||||
globalCurrentIndex = 0;
|
||||
function getCurrentPage(){let cs=document.querySelectorAll('.canvas-container'),bestPage=1,bestDistance=Infinity,viewportHeight=window.innerHeight;for(let i=0;i<cs.length;i++){let rect=cs[i].getBoundingClientRect(),center=rect.top+rect.height/2,distance=Math.abs(center-viewportHeight/2);if(distance<bestDistance){bestDistance=distance;bestPage=i+1;}}return bestPage;}
|
||||
function updateGlobalProgressForComic(){let c=document.getElementById('globalProgressBar');if(!c)return;let s=document.getElementById('globalProgressSlider'),l=c.querySelector('.progress-label span'),t=c.querySelector('.progress-info > div:last-child');if(s&&TOTAL_PAGES>0){let p=getCurrentPage();s.value=p;if(l)l.innerText=`📖 第 ${p} / ${TOTAL_PAGES} 页`;if(t)t.innerText=`第 ${p} 页 / 共 ${TOTAL_PAGES} 页`;}}
|
||||
window.updateGlobalProgress = updateGlobalProgressForComic;
|
||||
function saveComicProgress(page) {
|
||||
let list = getBookmarks();
|
||||
let idx = list.findIndex(b => b.book === CURRENT_BOOK && b.chapter === CURRENT_CHAPTER);
|
||||
let bookmark = {
|
||||
book: CURRENT_BOOK,
|
||||
chapter: CURRENT_CHAPTER,
|
||||
chapterName: CHAPTER_NAME.length > 35 ? CHAPTER_NAME.substring(0,32)+'...' : CHAPTER_NAME,
|
||||
page: page,
|
||||
time: Date.now(),
|
||||
type: 'comic'
|
||||
};
|
||||
if (idx >= 0) list[idx] = bookmark;
|
||||
else list.push(bookmark);
|
||||
saveBookmarks(list);
|
||||
}
|
||||
function addBookmark(){let p=getCurrentPage();saveComicProgress(p);showToast('✅ 第 '+p+' 页');}
|
||||
function jumpToPage(p){p=Math.min(Math.max(1,p),TOTAL_PAGES);let t=document.querySelector(`.canvas-container[data-page="${p}"]`);if(t){t.scrollIntoView({behavior:'smooth',block:'start'});showToast('✨ 第 '+p+' 页');saveComicProgress(p);setTimeout(()=>{if(typeof updateGlobalProgress==='function')updateGlobalProgress();},300);}else{showToast('📖 页面加载中...');}}
|
||||
window.jumpToBookmark = function(book,chapter,page){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER)jumpToPage(page);else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'comic'}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
document.getElementById('fontControls').style.display='none';
|
||||
refreshBookmarkList();
|
||||
function createComicProgressBar(){let c=document.getElementById('globalProgressPlaceholder');if(!c)return;c.innerHTML=`<div class="global-progress-container" id="globalProgressBar"><div class="progress-range-area"><input type="range" class="progress-slider-global" id="globalProgressSlider" min="1" max="${TOTAL_PAGES}" value="1" step="1"><div class="chapter-tooltip" id="chapterTooltip" style="display: none;">📖 第 1 / ${TOTAL_PAGES} 页</div></div><div class="progress-info"><div class="progress-label"><span>📖 第 1 / ${TOTAL_PAGES} 页</span></div><div>第 1 页 / 共 ${TOTAL_PAGES} 页</div></div></div>`;globalProgressBar=document.getElementById('globalProgressBar');let s=document.getElementById('globalProgressSlider'),tip=document.getElementById('chapterTooltip'),tipTimer=null;function st(page){if(tipTimer)clearTimeout(tipTimer);tip.textContent=`📖 第 ${page} / ${TOTAL_PAGES} 页`;tip.style.display='block';tipTimer=setTimeout(()=>{tip.style.display='none';},2000);}if(s){s.addEventListener('input',(e)=>{let p=parseInt(e.target.value),l=c.querySelector('.progress-label span'),t=c.querySelector('.progress-info > div:last-child');if(l)l.innerText=`📖 第 ${p} / ${TOTAL_PAGES} 页`;if(t)t.innerText=`第 ${p} 页 / 共 ${TOTAL_PAGES} 页`;st(p);});s.addEventListener('change',(e)=>{jumpToPage(parseInt(e.target.value));resetHideTimer();});}if(globalProgressBar)globalProgressBar.classList.add('hide');let stt=null;window.addEventListener('scroll',function(){if(stt)clearTimeout(stt);stt=setTimeout(()=>{if(typeof updateGlobalProgress==='function')updateGlobalProgress();let p=getCurrentPage();saveComicProgress(p);},200);});}
|
||||
createComicProgressBar();
|
||||
let jumpTarget=sessionStorage.getItem('jump_target');if(jumpTarget){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jumpTarget);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)setTimeout(()=>jumpToPage(t.page),500);}catch(e){}} else {
|
||||
let bookmarks = getBookmarks();
|
||||
let lastProgress = bookmarks.find(b => b.book === CURRENT_BOOK && b.chapter === CURRENT_CHAPTER);
|
||||
if(lastProgress && lastProgress.page) setTimeout(()=>jumpToPage(lastProgress.page), 500);
|
||||
}
|
||||
setTimeout(()=>{if(typeof updateGlobalProgress==='function')updateGlobalProgress();},500);
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
<?php else: ?>
|
||||
<div id="epubContent"></div>
|
||||
<div class="ebook-nav"><button id="prevChapterBtn" disabled>◀ 上一章</button><button id="nextChapterBtn" disabled>下一章 ▶</button></div>
|
||||
<div class="chapter-indicator" id="chapterIndicator"></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
const EPUB_DATA = <?php echo json_encode($epubData); ?>;
|
||||
let curIdx=0,chapters=EPUB_DATA.htmlContents||[],css=EPUB_DATA.cssContent||'',fontSize=18;
|
||||
globalChapters = chapters; globalTotalChapters = chapters.length; globalCurrentIndex = 0;
|
||||
function applyStyles(){let s=document.getElementById('epub-style');if(!s){s=document.createElement('style');s.id='epub-style';document.head.appendChild(s);}s.textContent=`.ebook-chapter{font-size:${fontSize}px}.ebook-chapter img{max-width:100%;height:auto;display:block;margin:1em auto;border-radius:12px}.ebook-chapter p{margin-bottom:1em}${css}`;}
|
||||
function renderChapter(i){if(!chapters||i<0||i>=chapters.length)return;onChapterChange();curIdx=i;globalCurrentIndex=i;let c=chapters[i];document.getElementById('epubContent').innerHTML=`<div class="ebook-chapter"><div class="chapter-title">${escapeHtml(c.title)}</div>${c.content}</div>`;document.getElementById('prevChapterBtn').disabled=(i<=0);document.getElementById('nextChapterBtn').disabled=(i>=chapters.length-1);document.getElementById('chapterIndicator').innerText=`第 ${i+1}/${chapters.length} 章 · ${c.title}`;saveProgress(i);if(typeof updateGlobalProgress==='function')updateGlobalProgress();}
|
||||
function saveProgress(i){let l=getBookmarks(),idx=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:i+1,time:Date.now(),type:'ebook'};if(idx>=0)l[idx]=n;else l.push(n);saveBookmarks(l);}
|
||||
window.jumpToBookmark = function(book,chapter,page){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){renderChapter(Math.min(Math.max(1,page),chapters.length)-1);showToast('✨ 第 '+page+' 章');}else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'ebook'}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
function addBookmark(){let n=curIdx+1,l=getBookmarks(),i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),ni={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:n,time:Date.now(),type:'ebook'};if(i>=0)l[i]=ni;else l.push(ni);saveBookmarks(l);showToast('✅ 第 '+n+' 章');}
|
||||
document.getElementById('fontPlus').onclick=()=>{fontSize=Math.min(fontSize+2,32);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('fontMinus').onclick=()=>{fontSize=Math.max(fontSize-2,12);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
document.getElementById('prevChapterBtn').onclick=()=>{if(curIdx>0)renderChapter(curIdx-1);resetHideTimer();};
|
||||
document.getElementById('nextChapterBtn').onclick=()=>{if(curIdx<chapters.length-1)renderChapter(curIdx+1);resetHideTimer();};
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
applyStyles();if(chapters.length>0){let saved=0,jump=sessionStorage.getItem('jump_target');if(jump){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jump);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)saved=Math.min(Math.max(1,t.page),chapters.length)-1;}catch(e){}}else{let bks=getBookmarks(),ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);if(ex&&ex.page)saved=Math.min(Math.max(1,ex.page),chapters.length)-1;}renderChapter(saved);}
|
||||
refreshBookmarkList();
|
||||
createGlobalProgressBar(globalTotalChapters, globalCurrentIndex, globalChapters);
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif ($book): ?>
|
||||
<!-- 书籍章节列表页 -->
|
||||
<div class="content">
|
||||
<div class="top-bar" style="position:relative; margin-top:-70px; margin-bottom:20px;">
|
||||
<div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a></div></div>
|
||||
</div>
|
||||
<h2 class="page-title">📖 <?php echo htmlspecialchars($book); ?></h2>
|
||||
<div class="shelf-grid">
|
||||
<?php
|
||||
$files = scanDirectory($baseDir . '/' . $book);
|
||||
if ($files) {
|
||||
foreach ($files as $f) {
|
||||
$name = basename($f);
|
||||
$url = $currentFile . "?book=" . rawurlencode($book) . "&chapter=" . rawurlencode($name);
|
||||
if (stripos($name, '.txt') !== false) $icon = '📖';
|
||||
elseif (stripos($name, '.epub') !== false) $icon = '📘';
|
||||
else $icon = (stripos($name, '.pdf') !== false ? '📕' : '📁');
|
||||
echo '<a href="' . $url . '" class="shelf-item"><div class="emoji">' . $icon . '</div><div>' . htmlspecialchars($name) . '</div></a>';
|
||||
}
|
||||
} else {
|
||||
echo '<div style="grid-column:1/-1; text-align:center; padding:50px; color:rgba(255,255,255,0.6);">📭 没有章节</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
refreshBookmarkList();
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer?.includes(window.location.host))history.back();else location.href='<?php echo $currentFile; ?>';};
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- 书架首页 -->
|
||||
<div class="content">
|
||||
<h1 class="page-title">📚 我的书架</h1>
|
||||
<div class="shelf-grid">
|
||||
<?php
|
||||
$books = scanDirectory($baseDir);
|
||||
if ($books) {
|
||||
foreach ($books as $b) {
|
||||
if (is_dir($b)) {
|
||||
$name = basename($b);
|
||||
echo '<a href="' . $currentFile . '?book=' . rawurlencode($name) . '" class="shelf-item"><div class="emoji">📖</div><div>' . htmlspecialchars($name) . '</div></a>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo '<div style="grid-column:1/-1; text-align:center; padding:50px; color:rgba(255,255,255,0.6);">📭 请在 PDF 文件夹里放入书籍文件夹</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
refreshBookmarkList();
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2400 @@
|
||||
<?php
|
||||
// PDF阅读器.php - 全屏沉浸版:12静态主题 + 护眼主题 + 12动态主题 + 3D悬浮书架 + 酷炫加载动画 + 滑动切换章节
|
||||
header("Content-Type: text/html; charset=utf-8");
|
||||
$baseDir = 'PDF';
|
||||
|
||||
if (!is_dir($baseDir)) {
|
||||
mkdir($baseDir);
|
||||
echo "已自动创建 PDF 目录,请放入PDF/EPUB/TXT";
|
||||
exit;
|
||||
}
|
||||
|
||||
function scanDirectory($path) {
|
||||
$result = [];
|
||||
if (!is_dir($path)) return $result;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
if ($entry == '.epub_cache' || $entry == '.txt_cache') continue;
|
||||
if (strpos($entry, '.') === 0) continue;
|
||||
$result[] = $path . '/' . $entry;
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
natsort($result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
function scanImages($path) {
|
||||
$images = [];
|
||||
$extensions = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
|
||||
if (!is_dir($path)) return $images;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
$ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $extensions)) $images[] = $path . '/' . $entry;
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
natsort($images);
|
||||
return array_values($images);
|
||||
}
|
||||
|
||||
function parseTxtFile($txtPath, $book, $chapter, $baseDir) {
|
||||
$cacheDir = $baseDir . '/.txt_cache/' . $book . '/' . md5($chapter);
|
||||
$cacheFile = $cacheDir . '/chapters.json';
|
||||
if (file_exists($cacheFile)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
if ($data && isset($data['chapters'])) return $data;
|
||||
}
|
||||
$content = file_get_contents($txtPath);
|
||||
$encoding = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'BIG5'], true);
|
||||
if (!$encoding) $encoding = 'UTF-8';
|
||||
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
|
||||
$patterns = [
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+章[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+节[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+卷[\s]*[^\n]*/u',
|
||||
'/(?:Chapter|CHAPTER|Ch\.?)\s*\d+[.:\s]*[^\n]*/i',
|
||||
'/\[\d+\][\s]*[^\n]*/',
|
||||
'/(?:一|二|三|四|五|六|七|八|九|十)、[\s]*[^\n]*/u',
|
||||
];
|
||||
$lines = preg_split('/\r\n|\r|\n/', $content);
|
||||
$chapters = [];
|
||||
$currentChapter = ['title' => '序章', 'content' => ''];
|
||||
$foundFirstChapter = false;
|
||||
foreach ($lines as $line) {
|
||||
$line = rtrim($line);
|
||||
$isChapter = false; $chapterTitle = '';
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $line, $matches)) {
|
||||
$chapterTitle = trim($matches[0]);
|
||||
$isChapter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($isChapter && $chapterTitle) {
|
||||
if ($foundFirstChapter && $currentChapter['content'] !== '') $chapters[] = $currentChapter;
|
||||
$currentChapter = ['title' => $chapterTitle, 'content' => ''];
|
||||
$foundFirstChapter = true;
|
||||
} else {
|
||||
if ($line !== '' || $currentChapter['content'] !== '') $currentChapter['content'] .= $line . "\n";
|
||||
}
|
||||
}
|
||||
if ($currentChapter['content'] !== '') $chapters[] = $currentChapter;
|
||||
if (empty($chapters)) $chapters = [['title' => basename($chapter, '.txt'), 'content' => $content]];
|
||||
foreach ($chapters as &$chap) {
|
||||
$chap['content'] = preg_replace('/\n\s*\n/', "</p><p>", $chap['content']);
|
||||
$chap['content'] = "<p>" . str_replace("\n", "<br>", $chap['content']) . "</p>";
|
||||
$chap['content'] = preg_replace('/<p>\s*<\/p>/', '', $chap['content']);
|
||||
}
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
$result = ['type' => 'txt', 'chapters' => $chapters, 'totalChapters' => count($chapters)];
|
||||
file_put_contents($cacheFile, json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
return $result;
|
||||
}
|
||||
|
||||
function parseEpub($epubFilePath, $book, $chapter, $baseDir) {
|
||||
$cacheDir = $baseDir . '/.epub_cache/' . $book . '/' . md5($chapter);
|
||||
$cacheTypeFile = $cacheDir . '/type.json';
|
||||
if (file_exists($cacheTypeFile)) {
|
||||
$cached = json_decode(file_get_contents($cacheTypeFile), true);
|
||||
if ($cached && isset($cached['type'])) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
if (!class_exists('ZipArchive')) return ['error' => '请启用ZipArchive扩展'];
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($epubFilePath) !== true) return ['error' => '无法打开EPUB文件'];
|
||||
$container = $zip->getFromName('META-INF/container.xml');
|
||||
if (!$container) { $zip->close(); return ['error' => '无效的EPUB文件']; }
|
||||
$rootFile = '';
|
||||
if (preg_match('/full-path="([^"]+)"/', $container, $matches)) $rootFile = $matches[1];
|
||||
if (!$rootFile) { $zip->close(); return ['error' => '无法解析EPUB结构']; }
|
||||
$opfContent = $zip->getFromName($rootFile);
|
||||
if (!$opfContent) { $zip->close(); return ['error' => '无法解析OPF文件']; }
|
||||
$opfDir = dirname($rootFile);
|
||||
if ($opfDir == '.') $opfDir = '';
|
||||
else $opfDir .= '/';
|
||||
$manifest = [];
|
||||
preg_match_all('/<item[^>]*id="([^"]*)"[^>]*href="([^"]*)"[^>]*>/i', $opfContent, $items);
|
||||
foreach ($items[1] as $i => $id) $manifest[$id] = $opfDir . $items[2][$i];
|
||||
$spineOrder = [];
|
||||
preg_match_all('/<itemref[^>]*idref="([^"]+)"/i', $opfContent, $spineMatches);
|
||||
if (!empty($spineMatches[1])) $spineOrder = $spineMatches[1];
|
||||
$allImages = []; $htmlContents = []; $cssContent = '';
|
||||
preg_match_all('/<item[^>]*href="([^"]+\.css)"[^>]*media-type="text\/css"[^>]*>/i', $opfContent, $cssMatches);
|
||||
foreach ($cssMatches[1] as $cssPath) {
|
||||
$fullPath = $opfDir . $cssPath;
|
||||
$cssData = $zip->getFromName($fullPath);
|
||||
if ($cssData !== false) $cssContent .= $cssData . "\n";
|
||||
}
|
||||
foreach ($spineOrder as $idref) {
|
||||
if (!isset($manifest[$idref])) continue;
|
||||
$filePath = $manifest[$idref];
|
||||
$content = $zip->getFromName($filePath);
|
||||
if ($content === false) continue;
|
||||
preg_match_all('/<img[^>]*src=["\']([^"\']+)["\']/i', $content, $imgMatches);
|
||||
$pageImages = [];
|
||||
foreach ($imgMatches[1] as $src) {
|
||||
$imgPath = dirname($filePath) . '/' . $src;
|
||||
$imgPath = preg_replace('#/\./#', '/', $imgPath);
|
||||
while (strpos($imgPath, '../') !== false) $imgPath = preg_replace('#[^/]+/\.\./#', '', $imgPath, 1);
|
||||
if (!in_array($imgPath, $allImages)) { $allImages[] = $imgPath; $pageImages[] = $imgPath; }
|
||||
}
|
||||
$title = '';
|
||||
if (preg_match('/<title[^>]*>([^<]+)<\/title>/i', $content, $titleMatch)) $title = trim($titleMatch[1]);
|
||||
if (!$title) {
|
||||
if (preg_match('/<h1[^>]*>([^<]+)<\/h1>/i', $content, $h1Match)) $title = trim($h1Match[1]);
|
||||
else $title = '第 ' . (count($htmlContents) + 1) . ' 章';
|
||||
}
|
||||
if (preg_match('/<body[^>]*>([\s\S]*?)<\/body>/i', $content, $bodyMatch)) $bodyContent = $bodyMatch[1];
|
||||
else $bodyContent = $content;
|
||||
$htmlContents[] = ['title' => $title, 'content' => $bodyContent, 'images' => $pageImages, 'index' => count($htmlContents)];
|
||||
}
|
||||
$totalItems = count($spineOrder);
|
||||
$totalImages = count($allImages);
|
||||
|
||||
// 智能判断文档类型:
|
||||
// - comic: 纯图片漫画(几乎没有文字)
|
||||
// - mixed: 图文混排(既有图片又有文字,如小说插图)
|
||||
// - ebook: 纯文本电子书(很少或没有图片)
|
||||
$type = 'ebook';
|
||||
|
||||
if ($totalImages == 0) {
|
||||
$type = 'ebook';
|
||||
} else if ($totalItems == 0) {
|
||||
$type = 'comic';
|
||||
} else {
|
||||
$totalTextLength = 0;
|
||||
$pagesWithLittleText = 0;
|
||||
$pagesWithManyImages = 0;
|
||||
$pagesWithAnyImage = 0;
|
||||
$pagesWithSubstantialText = 0; // 有实质文字的页面(>200字符)
|
||||
|
||||
foreach ($htmlContents as $chapter) {
|
||||
$plainText = strip_tags($chapter['content']);
|
||||
$plainTextClean = preg_replace('/\s+/', '', $plainText);
|
||||
$textLength = mb_strlen($plainTextClean);
|
||||
$totalTextLength += $textLength;
|
||||
$imageCount = count($chapter['images']);
|
||||
|
||||
if ($imageCount > 0) $pagesWithAnyImage++;
|
||||
if ($textLength < 100) $pagesWithLittleText++;
|
||||
if ($imageCount >= 2) $pagesWithManyImages++;
|
||||
if ($textLength > 200) $pagesWithSubstantialText++;
|
||||
}
|
||||
|
||||
$avgTextLength = $totalTextLength / max($totalItems, 1);
|
||||
$littleTextRatio = $pagesWithLittleText / max($totalItems, 1);
|
||||
$hasImagesRatio = $pagesWithAnyImage / max($totalItems, 1);
|
||||
$substantialTextRatio = $pagesWithSubstantialText / max($totalItems, 1);
|
||||
|
||||
// 漫画判定:平均文字量极少(小于50) 或 超过80%的页面文字很少(小于100)
|
||||
if ($avgTextLength < 50 || $littleTextRatio > 0.8) {
|
||||
$type = 'comic';
|
||||
}
|
||||
// 图文混排判定:有图片且超过30%的页面有实质文字,或者平均文字量在100-3000之间
|
||||
else if ($hasImagesRatio > 0.1 && ($substantialTextRatio > 0.3 || ($avgTextLength >= 100 && $avgTextLength <= 3000))) {
|
||||
$type = 'mixed';
|
||||
}
|
||||
// 如果图片很少或者几乎没有文字,还是按ebook处理
|
||||
else {
|
||||
$type = 'ebook';
|
||||
}
|
||||
|
||||
// 特殊处理:如果存在任何章节有超过500字符的文字,且也有图片,优先使用mixed模式
|
||||
foreach ($htmlContents as $chapter) {
|
||||
$plainText = strip_tags($chapter['content']);
|
||||
if (mb_strlen(preg_replace('/\s+/', '', $plainText)) > 500 && count($chapter['images']) > 0) {
|
||||
$type = 'mixed';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
|
||||
// 漫画模式:只提取图片
|
||||
if ($type == 'comic') {
|
||||
$cachedImages = []; $orderedImages = [];
|
||||
foreach ($htmlContents as $chapter) {
|
||||
foreach ($chapter['images'] as $imgPath) {
|
||||
if (!in_array($imgPath, $orderedImages)) $orderedImages[] = $imgPath;
|
||||
}
|
||||
}
|
||||
if (empty($orderedImages)) {
|
||||
preg_match_all('/<item[^>]*href="([^"]+\.(jpg|jpeg|png|webp|gif))"[^>]*>/i', $opfContent, $imgMatches);
|
||||
foreach ($imgMatches[1] as $imgPath) {
|
||||
$fullPath = $opfDir . $imgPath;
|
||||
if (!in_array($fullPath, $orderedImages)) $orderedImages[] = $fullPath;
|
||||
}
|
||||
}
|
||||
foreach ($orderedImages as $idx => $relativePath) {
|
||||
$ext = strtolower(pathinfo($relativePath, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif'])) $ext = 'jpg';
|
||||
$cacheFile = $cacheDir . '/' . sprintf('%04d', $idx+1) . '.' . $ext;
|
||||
if (file_exists($cacheFile) && filesize($cacheFile) > 100) { $cachedImages[] = $cacheFile; continue; }
|
||||
$imageData = $zip->getFromName($relativePath);
|
||||
if ($imageData === false) $imageData = $zip->getFromName(urldecode($relativePath));
|
||||
if ($imageData !== false && strlen($imageData) > 100) {
|
||||
file_put_contents($cacheFile, $imageData);
|
||||
$cachedImages[] = $cacheFile;
|
||||
} else {
|
||||
$cachedImages[] = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="300" height="400"%3E%3Crect width="300" height="400" fill="%23333"/%3E%3Ctext x="150" y="200" fill="%23fff" text-anchor="middle"%3E图片缺失%3C/text%3E%3C/svg%3E';
|
||||
}
|
||||
}
|
||||
$result = ['type' => 'comic', 'images' => $cachedImages, 'totalPages' => count($cachedImages)];
|
||||
}
|
||||
// 图文混排模式或纯文本模式:保留完整HTML结构
|
||||
else {
|
||||
$imagesDir = $cacheDir . '/images/';
|
||||
if (!is_dir($imagesDir)) mkdir($imagesDir, 0777, true);
|
||||
$imageUrlMap = [];
|
||||
foreach ($allImages as $relativePath) {
|
||||
$originalFilename = basename($relativePath);
|
||||
$ext = strtolower(pathinfo($originalFilename, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif'])) $ext = 'jpg';
|
||||
$cacheFilename = md5($relativePath) . '_' . preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $originalFilename);
|
||||
$cacheFile = $imagesDir . $cacheFilename;
|
||||
if (!file_exists($cacheFile)) {
|
||||
$imageData = $zip->getFromName($relativePath);
|
||||
if ($imageData === false) $imageData = $zip->getFromName(urldecode($relativePath));
|
||||
if ($imageData !== false) file_put_contents($cacheFile, $imageData);
|
||||
}
|
||||
if (file_exists($cacheFile)) {
|
||||
$imageUrlMap[$originalFilename] = $cacheFile;
|
||||
$nameNoExt = pathinfo($originalFilename, PATHINFO_FILENAME);
|
||||
$imageUrlMap[$nameNoExt] = $cacheFile;
|
||||
}
|
||||
}
|
||||
foreach ($htmlContents as &$chapter) {
|
||||
$chapter['content'] = preg_replace_callback('/src=["\']([^"\']+)["\']/i', function($matches) use ($imageUrlMap, $zip, $opfDir, $imagesDir) {
|
||||
$src = $matches[1];
|
||||
if (strpos($src, 'http://') === 0 || strpos($src, 'https://') === 0 || strpos($src, 'data:') === 0) return $matches[0];
|
||||
if (strpos($src, '.epub_cache/') !== false) return $matches[0];
|
||||
$filename = basename(urldecode($src));
|
||||
if (isset($imageUrlMap[$filename])) return 'src="' . $imageUrlMap[$filename] . '"';
|
||||
$name = pathinfo($filename, PATHINFO_FILENAME);
|
||||
if (isset($imageUrlMap[$name])) return 'src="' . $imageUrlMap[$name] . '"';
|
||||
$fullPath = $opfDir . $src;
|
||||
$fullPath = preg_replace('#/\./#', '/', $fullPath);
|
||||
while (strpos($fullPath, '../') !== false) $fullPath = preg_replace('#[^/]+/\.\./#', '', $fullPath, 1);
|
||||
$imageData = $zip->getFromName($fullPath);
|
||||
if ($imageData !== false && strlen($imageData) > 100) {
|
||||
$cacheFilename = md5($fullPath) . '_' . preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename);
|
||||
$cacheFile = $imagesDir . $cacheFilename;
|
||||
if (!file_exists($cacheFile)) file_put_contents($cacheFile, $imageData);
|
||||
return 'src="' . $cacheFile . '"';
|
||||
}
|
||||
return $matches[0];
|
||||
}, $chapter['content']);
|
||||
|
||||
$chapter['content'] = preg_replace_callback('/url\([\'"]?([^\'"\)]+)[\'"]?\)/i', function($matches) use ($imageUrlMap) {
|
||||
$url = $matches[1];
|
||||
$filename = basename(urldecode($url));
|
||||
if (isset($imageUrlMap[$filename])) return 'url("' . $imageUrlMap[$filename] . '")';
|
||||
return $matches[0];
|
||||
}, $chapter['content']);
|
||||
|
||||
// 为图片添加样式,使其在移动端自适应,同时保持良好的显示效果
|
||||
$chapter['content'] = preg_replace('/<img /i', '<img style="max-width:100%; height:auto; display:block; margin:1em auto; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,0.15);" ', $chapter['content']);
|
||||
}
|
||||
|
||||
$result = [
|
||||
'type' => $type, // 'mixed' 或 'ebook'
|
||||
'htmlContents' => $htmlContents,
|
||||
'cssContent' => $cssContent,
|
||||
'totalChapters' => count($htmlContents),
|
||||
'hasImages' => count($allImages) > 0
|
||||
];
|
||||
}
|
||||
$zip->close();
|
||||
file_put_contents($cacheTypeFile, json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
return $result;
|
||||
}
|
||||
|
||||
$book = isset($_GET['book']) ? $_GET['book'] : '';
|
||||
$chapter = isset($_GET['chapter']) ? $_GET['chapter'] : '';
|
||||
$isChapterPage = ($book && $chapter);
|
||||
$isPdf = $chapter && (stripos($chapter, '.pdf') !== false);
|
||||
$isEpub = $chapter && (stripos($chapter, '.epub') !== false);
|
||||
$isTxt = $chapter && (stripos($chapter, '.txt') !== false);
|
||||
|
||||
if ($book && $chapter) {
|
||||
$encodedBook = rawurlencode($book);
|
||||
$encodedChapter = rawurlencode($chapter);
|
||||
$fileUrl = "$baseDir/$encodedBook/$encodedChapter";
|
||||
}
|
||||
|
||||
$images = [];
|
||||
$epubData = null;
|
||||
$txtData = null;
|
||||
$epubError = null;
|
||||
|
||||
if ($isTxt && $isChapterPage && $book && $chapter) {
|
||||
$txtPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
if (file_exists($txtPath)) $txtData = parseTxtFile($txtPath, $book, $chapter, $baseDir);
|
||||
else $epubError = 'TXT文件不存在';
|
||||
} elseif ($isEpub && $isChapterPage && $book && $chapter) {
|
||||
$epubPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
if (file_exists($epubPath)) {
|
||||
$result = parseEpub($epubPath, $book, $chapter, $baseDir);
|
||||
if (isset($result['error'])) $epubError = $result['error'];
|
||||
else { $epubData = $result; if ($epubData['type'] == 'comic') $images = $epubData['images']; }
|
||||
} else $epubError = 'EPUB文件不存在';
|
||||
} elseif (!$isPdf && $isChapterPage && $book && $chapter) {
|
||||
$localPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
$images = scanImages($localPath);
|
||||
}
|
||||
|
||||
$currentFile = 'PDF阅读器.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
|
||||
<script>pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';</script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; min-height: 100vh; transition: all 0.3s ease; }
|
||||
|
||||
.floating-buttons, .speed-panel, .theme-selector, .font-controls {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.floating-buttons.visible, .speed-panel.visible, .theme-selector.visible, .font-controls.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.global-progress-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1003;
|
||||
padding: 8px 16px 16px 16px;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease;
|
||||
transform: translateY(0);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
.global-progress-container.hide {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
.progress-range-area {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.progress-slider-global {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.progress-slider-global::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #ff9800;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 8px rgba(255,152,0,0.8);
|
||||
border: 2px solid #fff;
|
||||
transition: transform 0.1s, background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.progress-slider-global::-webkit-slider-thumb:hover { transform: scale(1.2); }
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
padding: 4px 0 2px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.chapter-tooltip {
|
||||
position: fixed;
|
||||
background: rgba(0,0,0,0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
color: #ff9800;
|
||||
padding: 10px 20px;
|
||||
border-radius: 40px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
|
||||
z-index: 10007;
|
||||
border: 1px solid rgba(255,152,0,0.6);
|
||||
transition: all 0.2s ease;
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.5px;
|
||||
bottom: 280px;
|
||||
right: 12px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.page-turn-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeInOutFull 0.5s ease-out forwards;
|
||||
perspective: 2000px;
|
||||
backdrop-filter: blur(4px);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.page-turn-overlay .book-container {
|
||||
position: relative;
|
||||
width: 70%;
|
||||
max-width: 500px;
|
||||
height: 70%;
|
||||
max-height: 500px;
|
||||
transform-style: preserve-3d;
|
||||
animation: bookFlipFull 0.5s ease-in-out forwards;
|
||||
}
|
||||
.page-turn-overlay .book-left, .page-turn-overlay .book-right {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 80px;
|
||||
box-shadow: 0 0 40px rgba(0,0,0,0.4);
|
||||
transition: background 0.3s ease, border 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.page-turn-overlay .book-left {
|
||||
left: 0;
|
||||
transform-origin: right center;
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
.page-turn-overlay .book-right {
|
||||
right: 0;
|
||||
transform-origin: left center;
|
||||
border-radius: 0 16px 16px 0;
|
||||
}
|
||||
.page-turn-overlay .message {
|
||||
position: absolute;
|
||||
bottom: 20%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 28px;
|
||||
border-radius: 50px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
backdrop-filter: blur(8px);
|
||||
letter-spacing: 2px;
|
||||
transition: background 0.3s ease, color 0.3s ease, border 0.3s ease;
|
||||
}
|
||||
@keyframes fadeInOutFull {
|
||||
0% { opacity: 0; backdrop-filter: blur(0px); }
|
||||
15% { opacity: 1; backdrop-filter: blur(4px); }
|
||||
85% { opacity: 1; backdrop-filter: blur(4px); }
|
||||
100% { opacity: 0; backdrop-filter: blur(0px); visibility: hidden; }
|
||||
}
|
||||
@keyframes bookFlipFull {
|
||||
0% { transform: scale(0.9) rotateY(0deg); opacity: 0.5; }
|
||||
30% { transform: scale(1.05) rotateY(-15deg); opacity: 1; }
|
||||
70% { transform: scale(1.05) rotateY(-5deg); opacity: 1; }
|
||||
100% { transform: scale(1) rotateY(0deg); opacity: 1; }
|
||||
}
|
||||
|
||||
.speed-panel {
|
||||
position: fixed;
|
||||
right: 80px;
|
||||
bottom: 105px;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 12px 16px;
|
||||
border-radius: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
z-index: 10001;
|
||||
min-width: 170px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.speed-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
gap: 12px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.speed-value {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.speed-slider {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.speed-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #ff9800;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.speed-presets {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.speed-preset {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.1s, background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.speed-preset.active { color: #ff9800; background: rgba(255,152,0,0.2); }
|
||||
.auto-chapter-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
transition: border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.auto-chapter-line input { width: 36px; height: 20px; cursor: pointer; accent-color: #ff9800; transition: accent-color 0.3s ease; }
|
||||
|
||||
.font-controls {
|
||||
position: fixed; right: 12px; bottom: 230px; backdrop-filter: blur(10px); padding: 8px 12px; border-radius: 30px; display: flex; gap: 12px; z-index: 10001; transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.font-controls button { background: none; border: none; font-size: 18px; padding: 4px 8px; cursor: pointer; transition: color 0.3s ease; }
|
||||
|
||||
.theme-selector {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 290px;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 12px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
z-index: 10001;
|
||||
max-width: 340px;
|
||||
width: max-content;
|
||||
transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.floating-buttons { position: fixed; right: 12px; bottom: 100px; display: flex; flex-direction: column; gap: 12px; z-index: 10000; }
|
||||
.floating-btn { width: 52px; height: 52px; backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; transition: all 0.2s, background 0.3s ease, border-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease; }
|
||||
.floating-btn.bookmark-btn { background: linear-gradient(135deg, #ff9800, #ff5722); }
|
||||
.floating-btn.active { background: #ff9800; }
|
||||
|
||||
.bookmark-panel {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 220px;
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
width: 320px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
z-index: 10002;
|
||||
transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.bookmark-panel.show { display: block; }
|
||||
.bookmark-header { padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,0.15); font-weight: 600; display: flex; justify-content: space-between; transition: border-color 0.3s ease, color 0.3s ease; }
|
||||
.bookmark-header span:last-child { cursor: pointer; font-size: 22px; }
|
||||
.bookmark-list { padding: 10px; }
|
||||
.bookmark-item { background: rgba(255,255,255,0.1); margin: 8px 0; padding: 12px; border-radius: 14px; cursor: pointer; transition: background 0.3s ease; }
|
||||
.bookmark-item:hover { background: rgba(255,255,255,0.2); }
|
||||
.bookmark-item .title { font-weight: 600; color: #ffb347; font-size: 14px; transition: color 0.3s ease; }
|
||||
.bookmark-item .info { font-size: 11px; color: rgba(255,255,255,0.6); margin-top: 5px; transition: color 0.3s ease; }
|
||||
.bookmark-item .delete { float: right; color: #ff6b6b; font-size: 16px; cursor: pointer; }
|
||||
.empty-bookmark { color: rgba(255,255,255,0.5); text-align: center; padding: 30px; font-size: 13px; transition: color 0.3s ease; }
|
||||
|
||||
.theme-dot { width: 36px; height: 36px; border-radius: 12px; cursor: pointer; border: 2px solid rgba(255,255,255,0.5); transition: all 0.1s, border-color 0.3s ease, box-shadow 0.3s ease; box-sizing: border-box; }
|
||||
.theme-dot.active { border-color: #ff9800; transform: scale(1.05); box-shadow: 0 0 8px rgba(255,152,0,0.5); }
|
||||
|
||||
.top-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
transition: background 0.3s ease, border-bottom 0.3s ease;
|
||||
}
|
||||
.top-bar-left { display: flex; align-items: center; gap: 12px; flex: 1; overflow: hidden; }
|
||||
.back-btn { width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.15); border: none; transition: background 0.3s ease, color 0.3s ease; }
|
||||
.top-bar .nav-links { font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; transition: color 0.3s ease; }
|
||||
.top-bar a { text-decoration: none; transition: color 0.3s ease; }
|
||||
.top-bar button { padding: 8px 18px; border-radius: 30px; cursor: pointer; font-size: 14px; margin-left: 8px; background: rgba(255,255,255,0.15); border: none; transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease; }
|
||||
.top-bar button.bookmark { background: rgba(255, 152, 0, 0.8); color: white; }
|
||||
.content { margin-top: 70px; padding: 16px; position: relative; z-index: 1; margin-bottom: 70px; }
|
||||
.ebook-chapter { border-radius: 24px; padding: 30px 24px; margin: 20px auto; max-width: 800px; transition: all 0.2s ease, background 0.3s ease, color 0.3s ease, border 0.3s ease, box-shadow 0.3s ease; }
|
||||
.ebook-chapter p { margin-bottom: 1em; line-height: 1.8; }
|
||||
.ebook-chapter .chapter-title { font-size: 1.8em; text-align: center; margin-bottom: 1em; padding-bottom: 0.3em; transition: color 0.3s ease, border-bottom-color 0.3s ease; }
|
||||
.ebook-nav { display: flex; justify-content: space-between; gap: 12px; margin: 20px auto; max-width: 800px; }
|
||||
.ebook-nav button { border: none; padding: 12px 24px; border-radius: 40px; cursor: pointer; font-size: 16px; flex: 1; background: linear-gradient(135deg, #667eea, #764ba2); color: white; transition: background 0.3s ease, opacity 0.3s ease; }
|
||||
.ebook-nav button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.chapter-indicator { text-align: center; margin: 10px auto; font-size: 14px; transition: color 0.3s ease; }
|
||||
.toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); backdrop-filter: blur(20px); color: white; padding: 10px 20px; border-radius: 50px; font-size: 14px; z-index: 2000; pointer-events: none; white-space: nowrap; transition: background 0.3s ease, color 0.3s ease, border 0.3s ease; }
|
||||
|
||||
/* 滑动切换动画 */
|
||||
.swipe-transition {
|
||||
transition: transform 0.25s cubic-bezier(0.2, 0.9, 0.4, 1.1), opacity 0.2s ease;
|
||||
}
|
||||
.swipe-slide-left {
|
||||
transform: translateX(-40px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.swipe-slide-right {
|
||||
transform: translateX(40px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.swipe-indicator {
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.65);
|
||||
backdrop-filter: blur(12px);
|
||||
color: #ff9800;
|
||||
padding: 8px 20px;
|
||||
border-radius: 40px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: 10005;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
font-family: monospace;
|
||||
letter-spacing: 1px;
|
||||
border: 1px solid rgba(255,152,0,0.4);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
transition: opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
.swipe-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.swipe-arrow-left, .swipe-arrow-right {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
color: #ff9800;
|
||||
z-index: 10004;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
opacity: 0;
|
||||
border: 1px solid rgba(255,152,0,0.3);
|
||||
}
|
||||
.swipe-arrow-left { left: 15px; }
|
||||
.swipe-arrow-right { right: 15px; }
|
||||
.swipe-arrow-left.show, .swipe-arrow-right.show {
|
||||
opacity: 0.7;
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
}
|
||||
|
||||
/* ==================== 3D悬浮书架 - 增强玻璃立体效果 ==================== */
|
||||
.shelf-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 28px;
|
||||
padding: 24px;
|
||||
perspective: 1800px;
|
||||
perspective-origin: center 40px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.shelf-grid { grid-template-columns: repeat(2, 1fr); gap: 18px; padding: 16px; }
|
||||
}
|
||||
|
||||
.shelf-item {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
backdrop-filter: blur(18px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(180%);
|
||||
border-radius: 32px;
|
||||
padding: 28px 12px 24px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
transition: all 0.5s cubic-bezier(0.2, 0.9, 0.4, 1.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
overflow: visible;
|
||||
box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.35) inset,
|
||||
0 1px 0 rgba(255, 255, 255, 0.25) inset,
|
||||
0 -1px 0 rgba(0, 0, 0, 0.05) inset;
|
||||
transform-style: preserve-3d;
|
||||
transform: translateZ(0) rotateX(0deg) rotateY(0deg);
|
||||
opacity: 0;
|
||||
animation: fadeInUpGlide 0.6s cubic-bezier(0.2, 0.9, 0.3, 1.1) forwards;
|
||||
border: 1px solid rgba(255,255,240,0.4);
|
||||
}
|
||||
.shelf-item:nth-child(1) { animation-delay: 0.03s; } .shelf-item:nth-child(2) { animation-delay: 0.08s; }
|
||||
.shelf-item:nth-child(3) { animation-delay: 0.13s; } .shelf-item:nth-child(4) { animation-delay: 0.18s; }
|
||||
.shelf-item:nth-child(5) { animation-delay: 0.23s; } .shelf-item:nth-child(6) { animation-delay: 0.28s; }
|
||||
.shelf-item:nth-child(7) { animation-delay: 0.33s; } .shelf-item:nth-child(8) { animation-delay: 0.38s; }
|
||||
.shelf-item:nth-child(9) { animation-delay: 0.43s; } .shelf-item:nth-child(10){ animation-delay: 0.48s; }
|
||||
.shelf-item:nth-child(11){ animation-delay: 0.53s; } .shelf-item:nth-child(12){ animation-delay: 0.58s; }
|
||||
|
||||
@keyframes fadeInUpGlide {
|
||||
0% { opacity: 0; transform: translateY(40px) rotateX(-6deg) translateZ(-20px); }
|
||||
100% { opacity: 1; transform: translateY(0) rotateX(0deg) translateZ(0); }
|
||||
}
|
||||
|
||||
.shelf-item:hover {
|
||||
transform: translateY(-16px) translateZ(28px) rotateX(5deg) rotateY(-2deg) scale(1.02);
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
border-color: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 35px 45px -18px rgba(0, 0, 0, 0.6),
|
||||
0 0 0 2px rgba(255, 255, 255, 0.5) inset,
|
||||
0 0 25px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.shelf-item .emoji {
|
||||
font-size: 52px;
|
||||
display: block;
|
||||
margin-bottom: 14px;
|
||||
transition: all 0.4s cubic-bezier(0.2, 0.9, 0.4, 1.1);
|
||||
transform-style: preserve-3d;
|
||||
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
.shelf-item:hover .emoji {
|
||||
transform: scale(1.15) rotateY(12deg) rotateX(6deg) translateZ(12px);
|
||||
filter: drop-shadow(0 15px 20px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
.shelf-item div:last-child {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
.shelf-item:hover div:last-child {
|
||||
letter-spacing: 1.2px;
|
||||
text-shadow: 0 0 12px rgba(255,255,255,0.6);
|
||||
transform: translateZ(10px);
|
||||
}
|
||||
.shelf-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
height: 35%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
border-radius: 32px 32px 0 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.shelf-item:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.book-chapter-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; padding: 16px; }
|
||||
.book-chapter-item { position: relative; background: rgba(255,255,255,0.12); backdrop-filter: blur(12px); border-radius: 18px; border: 1px solid rgba(255,255,255,0.3); text-align: center; transition: all 0.3s; cursor: pointer; transform-style: preserve-3d; box-shadow: 0 6px 15px rgba(0,0,0,0.15); opacity: 0; animation: fadeInUp 0.4s ease forwards; }
|
||||
.book-chapter-item:nth-child(1) { animation-delay: 0.03s; } .book-chapter-item:nth-child(2) { animation-delay: 0.06s; }
|
||||
.book-chapter-item:hover { transform: translateY(-6px) translateZ(10px) scale(1.01); background: rgba(255,255,255,0.22); border-color: rgba(255,255,255,0.55); }
|
||||
.book-chapter-item a { text-decoration: none; display: block; padding: 16px 12px; font-weight: 500; color: inherit; }
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.page-transition { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(12px); z-index: 10000; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: opacity 0.4s ease, visibility 0.4s ease, background 0.3s ease; }
|
||||
.page-transition.active { opacity: 1; visibility: visible; }
|
||||
.page-transition .book-loader { position: relative; width: 80px; height: 100px; perspective: 1000px; margin-bottom: 30px; }
|
||||
.page-transition .book-page { position: absolute; width: 100%; height: 100%; background: linear-gradient(135deg, #ff9800, #ff5722); border-radius: 4px 8px 8px 4px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); transform-origin: left center; animation: bookFlip 1.2s ease-in-out infinite; transition: background 0.3s ease; }
|
||||
.page-transition .book-page:nth-child(1) { animation-delay: 0s; background: linear-gradient(135deg, #ff9800, #f57c00); }
|
||||
.page-transition .book-page:nth-child(2) { animation-delay: 0.15s; background: linear-gradient(135deg, #ffb74d, #ff9800); }
|
||||
.page-transition .book-page:nth-child(3) { animation-delay: 0.3s; background: linear-gradient(135deg, #ffcc80, #ffb74d); }
|
||||
.page-transition .book-page:nth-child(4) { animation-delay: 0.45s; background: linear-gradient(135deg, #ffe0b2, #ffcc80); }
|
||||
@keyframes bookFlip { 0% { transform: rotateY(0deg); opacity: 1; } 50% { transform: rotateY(-90deg); opacity: 0.5; } 100% { transform: rotateY(-180deg); opacity: 0; } }
|
||||
.page-transition .loading-text { color: white; font-size: 18px; letter-spacing: 4px; font-weight: 300; margin-top: 20px; animation: textPulse 1s ease-in-out infinite; transition: color 0.3s ease; }
|
||||
@keyframes textPulse { 0%, 100% { opacity: 0.5; letter-spacing: 4px; } 50% { opacity: 1; letter-spacing: 8px; text-shadow: 0 0 10px #ff9800; } }
|
||||
.page-transition .loading-dots { display: flex; gap: 8px; margin-top: 15px; }
|
||||
.page-transition .loading-dots span { width: 10px; height: 10px; background: #ff9800; border-radius: 50%; animation: dotBounce 0.6s ease-in-out infinite; transition: background 0.3s ease; }
|
||||
@keyframes dotBounce { 0%, 100% { transform: translateY(0); opacity: 0.5; } 50% { transform: translateY(-10px); opacity: 1; } }
|
||||
.ripple { position: absolute; border-radius: 50%; background: rgba(255, 255, 255, 0.5); transform: scale(0); animation: rippleAnim 0.6s linear forwards; pointer-events: none; }
|
||||
@keyframes rippleAnim { to { transform: scale(4); opacity: 0; } }
|
||||
.page-title { font-size: 26px; font-weight: 600; color: white; padding: 16px; margin: 0; text-shadow: 1px 1px 2px rgba(0,0,0,0.3); transition: color 0.3s ease, text-shadow 0.3s ease; }
|
||||
.progress-bar { position: fixed; top: 60px; left: 0; width: 100%; height: 2px; background: rgba(255,255,255,0.2); z-index: 1002; transition: background 0.3s ease; }
|
||||
.progress-fill { width: 0%; height: 100%; background: #ff9800; transition: width 0.3s, background 0.3s ease; }
|
||||
|
||||
/* ==================== 12款静态主题 ==================== */
|
||||
body.theme-deep-space { background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); }
|
||||
body.theme-deep-space .top-bar { background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-deep-space .top-bar, body.theme-deep-space .top-bar a, body.theme-deep-space .top-bar button { color: #fff; }
|
||||
body.theme-deep-space .ebook-chapter { background: rgba(30, 30, 50, 0.95); color: #e0e0e0; }
|
||||
body.theme-deep-space .ebook-chapter .chapter-title { color: #9b59b6; }
|
||||
body.theme-deep-space .speed-panel, body.theme-deep-space .font-controls, body.theme-deep-space .theme-selector, body.theme-deep-space .bookmark-panel { background: rgba(15, 12, 41, 0.95); color: #e0e0e0; }
|
||||
body.theme-deep-space .floating-btn { background: rgba(15, 12, 41, 0.9); color: #fff; }
|
||||
body.theme-deep-space .global-progress-container { background: rgba(15, 12, 41, 0.92); }
|
||||
body.theme-deep-space .page-turn-overlay { background: rgba(15, 12, 41, 0.92) !important; }
|
||||
body.theme-deep-space .page-turn-overlay .book-left,
|
||||
body.theme-deep-space .page-turn-overlay .book-right { background: rgba(48, 43, 99, 0.95) !important; border: 2px solid rgba(155, 89, 182, 0.6) !important; color: #bb86fc !important; }
|
||||
body.theme-deep-space .page-turn-overlay .message { background: rgba(48, 43, 99, 0.95) !important; color: #bb86fc !important; border: 1px solid rgba(155, 89, 182, 0.5) !important; }
|
||||
body.theme-deep-space .chapter-tooltip { background: rgba(15, 12, 41, 0.95) !important; border-color: #9b59b6 !important; color: #bb86fc !important; }
|
||||
body.theme-deep-space .shelf-item { background: rgba(15, 12, 41, 0.6) !important; border-color: rgba(155, 89, 182, 0.4) !important; }
|
||||
body.theme-deep-space .shelf-item:hover { background: rgba(48, 43, 99, 0.8) !important; border-color: #9b59b6 !important; }
|
||||
|
||||
body.theme-ocean { background: linear-gradient(135deg, #1a2980 0%, #26d0ce 100%); }
|
||||
body.theme-ocean .top-bar { background: rgba(0, 40, 60, 0.85); }
|
||||
body.theme-ocean .top-bar, body.theme-ocean .top-bar a, body.theme-ocean .top-bar button { color: #e0f7fa; }
|
||||
body.theme-ocean .ebook-chapter { background: rgba(255, 255, 255, 0.95); color: #2c3e50; }
|
||||
body.theme-ocean .ebook-chapter .chapter-title { color: #1a2980; }
|
||||
body.theme-ocean .speed-panel, body.theme-ocean .font-controls, body.theme-ocean .theme-selector, body.theme-ocean .bookmark-panel { background: rgba(26, 41, 128, 0.95); color: #e0f7fa; }
|
||||
body.theme-ocean .floating-btn { background: rgba(38, 208, 206, 0.85); color: #e0f7fa; }
|
||||
body.theme-ocean .global-progress-container { background: rgba(26, 41, 128, 0.92); }
|
||||
body.theme-ocean .page-turn-overlay { background: rgba(26, 41, 128, 0.92) !important; }
|
||||
body.theme-ocean .page-turn-overlay .book-left,
|
||||
body.theme-ocean .page-turn-overlay .book-right { background: rgba(38, 208, 206, 0.9) !important; border: 2px solid rgba(255,255,255,0.4) !important; color: #e0f7fa !important; }
|
||||
body.theme-ocean .page-turn-overlay .message { background: rgba(26, 41, 128, 0.95) !important; color: #e0f7fa !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-ocean .chapter-tooltip { background: rgba(26, 41, 128, 0.95) !important; border-color: #26d0ce !important; color: #e0f7fa !important; }
|
||||
|
||||
body.theme-cherry { background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%); }
|
||||
body.theme-cherry .top-bar { background: rgba(219, 112, 147, 0.85); }
|
||||
body.theme-cherry .top-bar, body.theme-cherry .top-bar a, body.theme-cherry .top-bar button { color: #5a2e3e; }
|
||||
body.theme-cherry .ebook-chapter { background: rgba(255, 245, 245, 0.95); color: #5a3a3a; }
|
||||
body.theme-cherry .ebook-chapter .chapter-title { color: #db7093; }
|
||||
body.theme-cherry .speed-panel, body.theme-cherry .font-controls, body.theme-cherry .theme-selector, body.theme-cherry .bookmark-panel { background: rgba(255, 245, 245, 0.95); color: #5a2e3e; }
|
||||
body.theme-cherry .floating-btn { background: rgba(219, 112, 147, 0.85); color: #5a2e3e; }
|
||||
body.theme-cherry .global-progress-container { background: rgba(255, 245, 245, 0.92); }
|
||||
body.theme-cherry .page-turn-overlay { background: rgba(255, 154, 158, 0.92) !important; }
|
||||
body.theme-cherry .page-turn-overlay .book-left,
|
||||
body.theme-cherry .page-turn-overlay .book-right { background: rgba(254, 207, 239, 0.95) !important; border: 2px solid rgba(219, 112, 147, 0.6) !important; color: #5a2e3e !important; }
|
||||
body.theme-cherry .page-turn-overlay .message { background: rgba(219, 112, 147, 0.95) !important; color: #5a2e3e !important; border: 1px solid rgba(219, 112, 147, 0.4) !important; }
|
||||
body.theme-cherry .chapter-tooltip { background: rgba(219, 112, 147, 0.95) !important; border-color: #ff9a9e !important; color: #5a2e3e !important; }
|
||||
|
||||
body.theme-night { background: #0a0a0a; }
|
||||
body.theme-night .top-bar { background: rgba(10, 10, 10, 0.95); }
|
||||
body.theme-night .top-bar, body.theme-night .top-bar a, body.theme-night .top-bar button { color: #aaa; }
|
||||
body.theme-night .ebook-chapter { background: #1a1a1a; color: #b0b0b0; border: 1px solid #333; }
|
||||
body.theme-night .ebook-chapter .chapter-title { color: #888; }
|
||||
body.theme-night .speed-panel, body.theme-night .font-controls, body.theme-night .theme-selector, body.theme-night .bookmark-panel { background: rgba(10, 10, 10, 0.95); color: #aaa; }
|
||||
body.theme-night .floating-btn { background: rgba(30, 30, 30, 0.95); color: #aaa; }
|
||||
body.theme-night .global-progress-container { background: rgba(10, 10, 10, 0.92); }
|
||||
body.theme-night .page-turn-overlay { background: rgba(10, 10, 10, 0.95) !important; }
|
||||
body.theme-night .page-turn-overlay .book-left,
|
||||
body.theme-night .page-turn-overlay .book-right { background: rgba(30, 30, 30, 0.98) !important; border: 2px solid #555 !important; color: #aaa !important; }
|
||||
body.theme-night .page-turn-overlay .message { background: rgba(30, 30, 30, 0.98) !important; color: #aaa !important; border: 1px solid #555 !important; }
|
||||
body.theme-night .chapter-tooltip { background: rgba(30, 30, 30, 0.98) !important; border-color: #666 !important; color: #ccc !important; }
|
||||
|
||||
body.theme-forest { background: linear-gradient(135deg, #134e5e 0%, #71b280 100%); }
|
||||
body.theme-forest .top-bar { background: rgba(20, 60, 40, 0.85); }
|
||||
body.theme-forest .top-bar, body.theme-forest .top-bar a, body.theme-forest .top-bar button { color: #e8f5e9; }
|
||||
body.theme-forest .ebook-chapter { background: rgba(255, 255, 245, 0.95); color: #2d5a3b; }
|
||||
body.theme-forest .ebook-chapter .chapter-title { color: #2e7d32; }
|
||||
body.theme-forest .speed-panel, body.theme-forest .font-controls, body.theme-forest .theme-selector, body.theme-forest .bookmark-panel { background: rgba(19, 78, 94, 0.95); color: #e8f5e9; }
|
||||
body.theme-forest .floating-btn { background: rgba(113, 178, 128, 0.85); color: #e8f5e9; }
|
||||
body.theme-forest .global-progress-container { background: rgba(19, 78, 94, 0.92); }
|
||||
body.theme-forest .page-turn-overlay { background: rgba(19, 78, 94, 0.92) !important; }
|
||||
body.theme-forest .page-turn-overlay .book-left,
|
||||
body.theme-forest .page-turn-overlay .book-right { background: rgba(113, 178, 128, 0.9) !important; border: 2px solid rgba(255,255,255,0.4) !important; color: #e8f5e9 !important; }
|
||||
body.theme-forest .page-turn-overlay .message { background: rgba(19, 78, 94, 0.95) !important; color: #e8f5e9 !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-forest .chapter-tooltip { background: rgba(19, 78, 94, 0.95) !important; border-color: #71b280 !important; color: #e8f5e9 !important; }
|
||||
|
||||
body.theme-sunset { background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 100%); }
|
||||
body.theme-sunset .top-bar { background: rgba(180, 70, 40, 0.85); }
|
||||
body.theme-sunset .top-bar, body.theme-sunset .top-bar a, body.theme-sunset .top-bar button { color: #fff3e0; }
|
||||
body.theme-sunset .ebook-chapter { background: rgba(255, 248, 240, 0.96); color: #6b3e1f; }
|
||||
body.theme-sunset .ebook-chapter .chapter-title { color: #d84315; }
|
||||
body.theme-sunset .speed-panel, body.theme-sunset .font-controls, body.theme-sunset .theme-selector, body.theme-sunset .bookmark-panel { background: rgba(255, 126, 95, 0.95); color: #fff3e0; }
|
||||
body.theme-sunset .floating-btn { background: rgba(254, 180, 123, 0.85); color: #fff3e0; }
|
||||
body.theme-sunset .global-progress-container { background: rgba(255, 126, 95, 0.92); }
|
||||
body.theme-sunset .page-turn-overlay { background: rgba(255, 126, 95, 0.92) !important; }
|
||||
body.theme-sunset .page-turn-overlay .book-left,
|
||||
body.theme-sunset .page-turn-overlay .book-right { background: rgba(254, 180, 123, 0.95) !important; border: 2px solid rgba(255,255,255,0.4) !important; color: #fff3e0 !important; }
|
||||
body.theme-sunset .page-turn-overlay .message { background: rgba(255, 126, 95, 0.95) !important; color: #fff3e0 !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-sunset .chapter-tooltip { background: rgba(180, 70, 40, 0.95) !important; border-color: #feb47b !important; color: #fff3e0 !important; }
|
||||
|
||||
body.theme-lavender { background: linear-gradient(135deg, #8e9ecc 0%, #e0bbff 100%); }
|
||||
body.theme-lavender .top-bar { background: rgba(100, 80, 140, 0.85); }
|
||||
body.theme-lavender .top-bar, body.theme-lavender .top-bar a, body.theme-lavender .top-bar button { color: #f3e5f5; }
|
||||
body.theme-lavender .ebook-chapter { background: rgba(245, 235, 255, 0.96); color: #4a3a6e; }
|
||||
body.theme-lavender .ebook-chapter .chapter-title { color: #7b1fa2; }
|
||||
body.theme-lavender .speed-panel, body.theme-lavender .font-controls, body.theme-lavender .theme-selector, body.theme-lavender .bookmark-panel { background: rgba(142, 158, 204, 0.95); color: #4a3a6e; }
|
||||
body.theme-lavender .floating-btn { background: rgba(224, 187, 255, 0.85); color: #4a3a6e; }
|
||||
body.theme-lavender .global-progress-container { background: rgba(142, 158, 204, 0.92); }
|
||||
body.theme-lavender .page-turn-overlay { background: rgba(142, 158, 204, 0.92) !important; }
|
||||
body.theme-lavender .page-turn-overlay .book-left,
|
||||
body.theme-lavender .page-turn-overlay .book-right { background: rgba(224, 187, 255, 0.95) !important; border: 2px solid rgba(100, 80, 140, 0.6) !important; color: #4a3a6e !important; }
|
||||
body.theme-lavender .page-turn-overlay .message { background: rgba(142, 158, 204, 0.95) !important; color: #f3e5f5 !important; border: 1px solid rgba(100, 80, 140, 0.4) !important; }
|
||||
body.theme-lavender .chapter-tooltip { background: rgba(100, 80, 140, 0.95) !important; border-color: #e0bbff !important; color: #f3e5f5 !important; }
|
||||
|
||||
body.theme-blueberry { background: linear-gradient(135deg, #2c3e66 0%, #4a69bd 100%); }
|
||||
body.theme-blueberry .top-bar { background: rgba(30, 50, 80, 0.85); }
|
||||
body.theme-blueberry .top-bar, body.theme-blueberry .top-bar a, body.theme-blueberry .top-bar button { color: #dfe6e9; }
|
||||
body.theme-blueberry .ebook-chapter { background: rgba(240, 245, 255, 0.96); color: #2c3e66; }
|
||||
body.theme-blueberry .ebook-chapter .chapter-title { color: #3b82f6; }
|
||||
body.theme-blueberry .speed-panel, body.theme-blueberry .font-controls, body.theme-blueberry .theme-selector, body.theme-blueberry .bookmark-panel { background: rgba(44, 62, 102, 0.95); color: #dfe6e9; }
|
||||
body.theme-blueberry .floating-btn { background: rgba(74, 105, 189, 0.85); color: #dfe6e9; }
|
||||
body.theme-blueberry .global-progress-container { background: rgba(44, 62, 102, 0.92); }
|
||||
body.theme-blueberry .page-turn-overlay { background: rgba(44, 62, 102, 0.92) !important; }
|
||||
body.theme-blueberry .page-turn-overlay .book-left,
|
||||
body.theme-blueberry .page-turn-overlay .book-right { background: rgba(74, 105, 189, 0.9) !important; border: 2px solid rgba(255,255,255,0.3) !important; color: #dfe6e9 !important; }
|
||||
body.theme-blueberry .page-turn-overlay .message { background: rgba(44, 62, 102, 0.95) !important; color: #dfe6e9 !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-blueberry .chapter-tooltip { background: rgba(44, 62, 102, 0.95) !important; border-color: #4a69bd !important; color: #dfe6e9 !important; }
|
||||
|
||||
body.theme-amber { background: linear-gradient(135deg, #ffb347 0%, #ffcc33 100%); }
|
||||
body.theme-amber .top-bar { background: rgba(160, 90, 30, 0.85); }
|
||||
body.theme-amber .top-bar, body.theme-amber .top-bar a, body.theme-amber .top-bar button { color: #3e2723; }
|
||||
body.theme-amber .ebook-chapter { background: rgba(255, 250, 230, 0.96); color: #5d4037; }
|
||||
body.theme-amber .ebook-chapter .chapter-title { color: #f57c00; }
|
||||
body.theme-amber .speed-panel, body.theme-amber .font-controls, body.theme-amber .theme-selector, body.theme-amber .bookmark-panel { background: rgba(255, 179, 71, 0.95); color: #3e2723; }
|
||||
body.theme-amber .floating-btn { background: rgba(255, 204, 51, 0.85); color: #3e2723; }
|
||||
body.theme-amber .global-progress-container { background: rgba(255, 179, 71, 0.92); }
|
||||
body.theme-amber .page-turn-overlay { background: rgba(255, 179, 71, 0.92) !important; }
|
||||
body.theme-amber .page-turn-overlay .book-left,
|
||||
body.theme-amber .page-turn-overlay .book-right { background: rgba(255, 204, 51, 0.95) !important; border: 2px solid rgba(160, 90, 30, 0.6) !important; color: #3e2723 !important; }
|
||||
body.theme-amber .page-turn-overlay .message { background: rgba(255, 179, 71, 0.95) !important; color: #3e2723 !important; border: 1px solid rgba(160, 90, 30, 0.4) !important; }
|
||||
body.theme-amber .chapter-tooltip { background: rgba(160, 90, 30, 0.95) !important; border-color: #ffcc33 !important; color: #fff8e1 !important; }
|
||||
|
||||
body.theme-coral { background: linear-gradient(135deg, #ff6b6b 0%, #ffb8b8 100%); }
|
||||
body.theme-coral .top-bar { background: rgba(200, 80, 80, 0.85); }
|
||||
body.theme-coral .top-bar, body.theme-coral .top-bar a, body.theme-coral .top-bar button { color: #fff; }
|
||||
body.theme-coral .ebook-chapter { background: rgba(255, 240, 240, 0.95); color: #5a3a3a; }
|
||||
body.theme-coral .ebook-chapter .chapter-title { color: #ff6b6b; }
|
||||
body.theme-coral .speed-panel, body.theme-coral .font-controls, body.theme-coral .theme-selector, body.theme-coral .bookmark-panel { background: rgba(200, 80, 80, 0.95); color: #fff; }
|
||||
body.theme-coral .floating-btn { background: rgba(200, 80, 80, 0.9); color: #fff; }
|
||||
body.theme-coral .global-progress-container { background: rgba(200, 80, 80, 0.92); }
|
||||
body.theme-coral .page-turn-overlay { background: rgba(200, 80, 80, 0.92) !important; }
|
||||
body.theme-coral .page-turn-overlay .book-left,
|
||||
body.theme-coral .page-turn-overlay .book-right { background: rgba(255, 184, 184, 0.95) !important; border: 2px solid rgba(200, 80, 80, 0.6) !important; color: #fff !important; }
|
||||
body.theme-coral .page-turn-overlay .message { background: rgba(200, 80, 80, 0.95) !important; color: #fff !important; border: 1px solid rgba(200, 80, 80, 0.4) !important; }
|
||||
body.theme-coral .chapter-tooltip { background: rgba(200, 80, 80, 0.95) !important; border-color: #ffb8b8 !important; color: #fff !important; }
|
||||
|
||||
body.theme-mint { background: linear-gradient(135deg, #a8e6cf 0%, #80deea 100%); }
|
||||
body.theme-mint .top-bar { background: rgba(60, 120, 100, 0.85); }
|
||||
body.theme-mint .top-bar, body.theme-mint .top-bar a, body.theme-mint .top-bar button { color: #2d5a3b; }
|
||||
body.theme-mint .ebook-chapter { background: rgba(255, 255, 250, 0.95); color: #2d5a3b; }
|
||||
body.theme-mint .ebook-chapter .chapter-title { color: #2ecc71; }
|
||||
body.theme-mint .speed-panel, body.theme-mint .font-controls, body.theme-mint .theme-selector, body.theme-mint .bookmark-panel { background: rgba(60, 120, 100, 0.95); color: #fff; }
|
||||
body.theme-mint .floating-btn { background: rgba(60, 120, 100, 0.9); color: #fff; }
|
||||
body.theme-mint .global-progress-container { background: rgba(60, 120, 100, 0.92); }
|
||||
body.theme-mint .page-turn-overlay { background: rgba(60, 120, 100, 0.92) !important; }
|
||||
body.theme-mint .page-turn-overlay .book-left,
|
||||
body.theme-mint .page-turn-overlay .book-right { background: rgba(168, 230, 207, 0.9) !important; border: 2px solid rgba(60, 120, 100, 0.6) !important; color: #2d5a3b !important; }
|
||||
body.theme-mint .page-turn-overlay .message { background: rgba(60, 120, 100, 0.95) !important; color: #fff !important; border: 1px solid rgba(60, 120, 100, 0.4) !important; }
|
||||
body.theme-mint .chapter-tooltip { background: rgba(60, 120, 100, 0.95) !important; border-color: #80deea !important; color: #fff !important; }
|
||||
|
||||
body.theme-rosegold { background: linear-gradient(135deg, #e8b4b8 0%, #ffd9e2 100%); }
|
||||
body.theme-rosegold .top-bar { background: rgba(160, 100, 110, 0.85); }
|
||||
body.theme-rosegold .top-bar, body.theme-rosegold .top-bar a, body.theme-rosegold .top-bar button { color: #5a3a3e; }
|
||||
body.theme-rosegold .ebook-chapter { background: rgba(255, 248, 250, 0.95); color: #5a3a3e; }
|
||||
body.theme-rosegold .ebook-chapter .chapter-title { color: #e8b4b8; }
|
||||
body.theme-rosegold .speed-panel, body.theme-rosegold .font-controls, body.theme-rosegold .theme-selector, body.theme-rosegold .bookmark-panel { background: rgba(160, 100, 110, 0.95); color: #fff; }
|
||||
body.theme-rosegold .floating-btn { background: rgba(160, 100, 110, 0.9); color: #fff; }
|
||||
body.theme-rosegold .global-progress-container { background: rgba(160, 100, 110, 0.92); }
|
||||
body.theme-rosegold .page-turn-overlay { background: rgba(160, 100, 110, 0.92) !important; }
|
||||
body.theme-rosegold .page-turn-overlay .book-left,
|
||||
body.theme-rosegold .page-turn-overlay .book-right { background: rgba(255, 217, 226, 0.95) !important; border: 2px solid rgba(160, 100, 110, 0.6) !important; color: #5a3a3e !important; }
|
||||
body.theme-rosegold .page-turn-overlay .message { background: rgba(160, 100, 110, 0.95) !important; color: #fff !important; border: 1px solid rgba(160, 100, 110, 0.4) !important; }
|
||||
body.theme-rosegold .chapter-tooltip { background: rgba(160, 100, 110, 0.95) !important; border-color: #ffd9e2 !important; color: #fff5f5 !important; }
|
||||
|
||||
/* ==================== 护眼主题 ==================== */
|
||||
body.theme-eyecare { background: #c7edcc !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .top-bar { background: rgba(199, 237, 204, 0.92) !important; backdrop-filter: blur(20px) !important; border-bottom: 1px solid rgba(100, 100, 80, 0.2) !important; }
|
||||
body.theme-eyecare .top-bar, body.theme-eyecare .top-bar a, body.theme-eyecare .top-bar button { color: #2d2d2d !important; }
|
||||
body.theme-eyecare .ebook-chapter { background: rgba(215, 245, 210, 0.95) !important; color: #2d2d2d !important; box-shadow: 0 8px 32px rgba(0,0,0,0.08) !important; }
|
||||
body.theme-eyecare .ebook-chapter .chapter-title { color: #5a6b3a !important; border-bottom-color: #a0b880 !important; }
|
||||
body.theme-eyecare .speed-panel, body.theme-eyecare .font-controls, body.theme-eyecare .theme-selector, body.theme-eyecare .bookmark-panel { background: rgba(215, 245, 210, 0.95) !important; color: #2d2d2d !important; border: 1px solid rgba(100, 100, 80, 0.2) !important; }
|
||||
body.theme-eyecare .floating-btn { background: rgba(199, 237, 204, 0.9) !important; color: #2d2d2d !important; border: 1px solid rgba(100, 100, 80, 0.3) !important; }
|
||||
body.theme-eyecare .global-progress-container { background: rgba(199, 237, 204, 0.92) !important; }
|
||||
body.theme-eyecare .shelf-item { background: rgba(215, 245, 210, 0.8) !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .shelf-item:hover { background: rgba(199, 237, 204, 0.9) !important; }
|
||||
body.theme-eyecare .progress-slider-global::-webkit-slider-thumb { background: #8b9a6e !important; }
|
||||
body.theme-eyecare .page-turn-overlay { background: rgba(199, 237, 204, 0.92) !important; }
|
||||
body.theme-eyecare .page-turn-overlay .book-left,
|
||||
body.theme-eyecare .page-turn-overlay .book-right { background: rgba(215, 245, 210, 0.95) !important; border: 2px solid rgba(139, 154, 110, 0.5) !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .page-turn-overlay .message { background: rgba(215, 245, 210, 0.95) !important; color: #2d2d2d !important; border: 1px solid rgba(139, 154, 110, 0.4) !important; }
|
||||
body.theme-eyecare .chapter-tooltip { background: rgba(215, 245, 210, 0.98) !important; border-color: #8b9a6e !important; color: #2d2d2d !important; }
|
||||
|
||||
/* ==================== 12款动态主题 ==================== */
|
||||
/* 1. 极光幻彩 */
|
||||
body.theme-aurora-dynamic { background: linear-gradient(270deg, #1a0b2e, #2d1b69, #1a4d8c, #0f5c6b); background-size: 400% 400%; animation: auroraFlow 12s ease infinite; color: #f0f0f0 !important; }
|
||||
@keyframes auroraFlow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
||||
body.theme-aurora-dynamic .top-bar { background: rgba(0, 0, 0, 0.5) !important; backdrop-filter: blur(20px) !important; border-bottom: 1px solid rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .top-bar, body.theme-aurora-dynamic .top-bar a, body.theme-aurora-dynamic .top-bar button { color: #7cffd0 !important; text-shadow: 0 0 5px rgba(124,255,208,0.3); }
|
||||
body.theme-aurora-dynamic .back-btn { background: rgba(124, 255, 208, 0.15) !important; }
|
||||
body.theme-aurora-dynamic .page-title { color: #7cffd0 !important; text-shadow: 0 0 10px rgba(124,255,208,0.4); }
|
||||
body.theme-aurora-dynamic .ebook-chapter { background: rgba(0, 0, 0, 0.4) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .ebook-chapter .chapter-title { color: #7cffd0 !important; border-bottom-color: rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .floating-btn, body.theme-aurora-dynamic .speed-panel, body.theme-aurora-dynamic .font-controls, body.theme-aurora-dynamic .theme-selector, body.theme-aurora-dynamic .bookmark-panel { background: rgba(0, 0, 0, 0.5) !important; border: 1px solid rgba(124, 255, 208, 0.3) !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .floating-btn { background: rgba(0, 0, 0, 0.4) !important; color: #7cffd0 !important; border: 1px solid rgba(124, 255, 208, 0.4) !important; }
|
||||
body.theme-aurora-dynamic .floating-btn.bookmark-btn { background: rgba(124, 255, 208, 0.2) !important; border: 1px solid #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .floating-btn.bookmark-btn.active { background: #7cffd0 !important; color: #1a0b2e !important; }
|
||||
body.theme-aurora-dynamic .speed-slider::-webkit-slider-thumb { background: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .progress-slider-global::-webkit-slider-thumb { background: #7cffd0 !important; box-shadow: 0 0 8px rgba(124,255,208,0.8) !important; }
|
||||
body.theme-aurora-dynamic .progress-fill { background: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .speed-preset.active { color: #7cffd0 !important; background: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .auto-chapter-line { border-top-color: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .auto-chapter-line input { accent-color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .global-progress-container { background: rgba(0, 0, 0, 0.5) !important; border-top: 1px solid rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .progress-info { color: rgba(124, 255, 208, 0.8) !important; }
|
||||
body.theme-aurora-dynamic .shelf-item { background: rgba(124, 255, 208, 0.1) !important; border-color: rgba(124, 255, 208, 0.3) !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .shelf-item:hover { background: rgba(124, 255, 208, 0.2) !important; border-color: rgba(124, 255, 208, 0.6) !important; }
|
||||
body.theme-aurora-dynamic .book-chapter-item { background: rgba(124, 255, 208, 0.1) !important; border-color: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .book-chapter-item a { color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .book-chapter-item:hover { background: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .bookmark-item .title { color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .bookmark-header { border-bottom-color: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .chapter-tooltip { background: rgba(0, 0, 0, 0.75) !important; border-color: #7cffd0 !important; color: #7cffd0 !important; box-shadow: 0 0 15px rgba(124,255,208,0.3) !important; }
|
||||
body.theme-aurora-dynamic .page-turn-overlay { background: rgba(0, 0, 0, 0.6) !important; }
|
||||
body.theme-aurora-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-aurora-dynamic .page-turn-overlay .book-right { background: rgba(0, 0, 0, 0.5) !important; border: 2px solid rgba(124, 255, 208, 0.4) !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .page-turn-overlay .message { background: rgba(0, 0, 0, 0.7) !important; color: #7cffd0 !important; border: 1px solid rgba(124, 255, 208, 0.5) !important; }
|
||||
body.theme-aurora-dynamic .page-transition { background: rgba(0, 0, 0, 0.6) !important; }
|
||||
body.theme-aurora-dynamic .page-transition .book-page { background: linear-gradient(135deg, #7cffd0, #2d1b69) !important; }
|
||||
body.theme-aurora-dynamic .page-transition .loading-text { color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .page-transition .loading-dots span { background: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .top-bar button.bookmark { background: rgba(124, 255, 208, 0.2) !important; border: 1px solid #7cffd0 !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .toast { background: rgba(0, 0, 0, 0.8) !important; color: #7cffd0 !important; border: 1px solid rgba(124, 255, 208, 0.3) !important; }
|
||||
|
||||
/* 2. 霓虹脉冲 */
|
||||
body.theme-neon-dynamic { background: #0a0a0a !important; animation: neonBgPulse 2s ease-in-out infinite; color: #fff !important; }
|
||||
@keyframes neonBgPulse { 0% { background: #0a0a0a; } 30% { background: #0d1a1a; } 100% { background: #0a0a0a; } }
|
||||
body.theme-neon-dynamic .top-bar { background: rgba(0, 0, 0, 0.7) !important; border-bottom: 1px solid rgba(0, 255, 255, 0.4) !important; animation: neonBorderFlash 1.5s ease-in-out infinite !important; }
|
||||
@keyframes neonBorderFlash { 0% { border-bottom-color: rgba(0, 255, 255, 0.2); } 50% { border-bottom-color: rgba(0, 255, 255, 0.8); } 100% { border-bottom-color: rgba(0, 255, 255, 0.2); } }
|
||||
body.theme-neon-dynamic .top-bar, body.theme-neon-dynamic .top-bar a, body.theme-neon-dynamic .top-bar button { color: #0ff !important; text-shadow: 0 0 5px rgba(0,255,255,0.5); }
|
||||
body.theme-neon-dynamic .back-btn { background: rgba(0, 255, 255, 0.1) !important; }
|
||||
body.theme-neon-dynamic .page-title { color: #0ff !important; text-shadow: 0 0 10px rgba(0,255,255,0.5); animation: neonTitlePulse 1.5s ease-in-out infinite; }
|
||||
@keyframes neonTitlePulse { 0% { text-shadow: 0 0 5px rgba(0,255,255,0.3); } 50% { text-shadow: 0 0 20px rgba(0,255,255,0.8); } 100% { text-shadow: 0 0 5px rgba(0,255,255,0.3); } }
|
||||
body.theme-neon-dynamic .ebook-chapter { background: rgba(0, 0, 0, 0.7) !important; border: 1px solid rgba(0, 255, 255, 0.2) !important; animation: neonBoxGlow 2s ease-in-out infinite !important; }
|
||||
@keyframes neonBoxGlow { 0% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.1); } 50% { box-shadow: 0 0 25px rgba(0, 255, 255, 0.4); } 100% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.1); } }
|
||||
body.theme-neon-dynamic .ebook-chapter .chapter-title { color: #0ff !important; border-bottom-color: rgba(0, 255, 255, 0.3) !important; }
|
||||
body.theme-neon-dynamic .floating-btn, body.theme-neon-dynamic .speed-panel, body.theme-neon-dynamic .font-controls, body.theme-neon-dynamic .theme-selector, body.theme-neon-dynamic .bookmark-panel { background: rgba(0, 0, 0, 0.7) !important; color: #0ff !important; border: 1px solid rgba(0, 255, 255, 0.3) !important; animation: neonPanelGlow 1.5s ease-in-out infinite !important; }
|
||||
@keyframes neonPanelGlow { 0% { border-color: rgba(0, 255, 255, 0.2); } 50% { border-color: rgba(0, 255, 255, 0.6); } 100% { border-color: rgba(0, 255, 255, 0.2); } }
|
||||
body.theme-neon-dynamic .floating-btn { background: rgba(0, 0, 0, 0.6) !important; color: #0ff !important; border: 1px solid #0ff !important; animation: neonBtnPulse 1.5s ease-in-out infinite !important; }
|
||||
@keyframes neonBtnPulse { 0% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.3); } 50% { box-shadow: 0 0 15px rgba(0, 255, 255, 0.8); } 100% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.3); } }
|
||||
body.theme-neon-dynamic .floating-btn.bookmark-btn { background: rgba(0, 255, 255, 0.15) !important; }
|
||||
body.theme-neon-dynamic .floating-btn.bookmark-btn.active { background: #0ff !important; color: #0a0a0a !important; }
|
||||
body.theme-neon-dynamic .speed-slider::-webkit-slider-thumb { background: #0ff !important; }
|
||||
body.theme-neon-dynamic .progress-slider-global::-webkit-slider-thumb { background: #0ff !important; box-shadow: 0 0 8px rgba(0,255,255,0.8) !important; }
|
||||
body.theme-neon-dynamic .progress-fill { background: #0ff !important; animation: neonFillPulse 1.5s ease-in-out infinite; }
|
||||
@keyframes neonFillPulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
|
||||
body.theme-neon-dynamic .speed-preset.active { color: #0ff !important; background: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .auto-chapter-line { border-top-color: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .auto-chapter-line input { accent-color: #0ff !important; }
|
||||
body.theme-neon-dynamic .global-progress-container { background: rgba(0, 0, 0, 0.7) !important; border-top: 1px solid rgba(0, 255, 255, 0.3) !important; }
|
||||
body.theme-neon-dynamic .progress-info { color: rgba(0, 255, 255, 0.8) !important; }
|
||||
body.theme-neon-dynamic .shelf-item { background: rgba(0, 255, 255, 0.08) !important; border-color: rgba(0, 255, 255, 0.3) !important; color: #0ff !important; }
|
||||
body.theme-neon-dynamic .shelf-item:hover { background: rgba(0, 255, 255, 0.18) !important; border-color: #0ff !important; box-shadow: 0 0 20px rgba(0,255,255,0.3) !important; }
|
||||
body.theme-neon-dynamic .book-chapter-item { background: rgba(0, 255, 255, 0.08) !important; border-color: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .book-chapter-item a { color: #0ff !important; }
|
||||
body.theme-neon-dynamic .book-chapter-item:hover { background: rgba(0, 255, 255, 0.18) !important; box-shadow: 0 0 15px rgba(0,255,255,0.2) !important; }
|
||||
body.theme-neon-dynamic .bookmark-item .title { color: #0ff !important; }
|
||||
body.theme-neon-dynamic .bookmark-header { border-bottom-color: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .chapter-tooltip { background: rgba(0, 0, 0, 0.9) !important; border-color: #0ff !important; color: #0ff !important; box-shadow: 0 0 15px rgba(0,255,255,0.4) !important; text-shadow: 0 0 3px #0ff !important; }
|
||||
body.theme-neon-dynamic .page-turn-overlay { background: rgba(0, 0, 0, 0.8) !important; }
|
||||
body.theme-neon-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-neon-dynamic .page-turn-overlay .book-right { background: rgba(0, 0, 0, 0.7) !important; border: 2px solid #0ff !important; color: #0ff !important; }
|
||||
body.theme-neon-dynamic .page-turn-overlay .message { background: rgba(0, 0, 0, 0.9) !important; color: #0ff !important; border: 1px solid #0ff !important; }
|
||||
body.theme-neon-dynamic .page-transition { background: rgba(0, 0, 0, 0.7) !important; }
|
||||
body.theme-neon-dynamic .page-transition .book-page { background: linear-gradient(135deg, #0ff, #0a0a0a) !important; }
|
||||
body.theme-neon-dynamic .page-transition .loading-text { color: #0ff !important; text-shadow: 0 0 10px rgba(0,255,255,0.5); }
|
||||
body.theme-neon-dynamic .page-transition .loading-dots span { background: #0ff !important; }
|
||||
body.theme-neon-dynamic .top-bar button.bookmark { background: rgba(0, 255, 255, 0.15) !important; border: 1px solid #0ff !important; color: #0ff !important; animation: neonBtnPulse 1.5s ease-in-out infinite !important; }
|
||||
body.theme-neon-dynamic .toast { background: rgba(0, 0, 0, 0.85) !important; color: #0ff !important; border: 1px solid #0ff !important; }
|
||||
|
||||
/* 3. 暮色晚霞 */
|
||||
body.theme-sunset-dynamic { background: linear-gradient(270deg, #1a0a2e, #5c2a4a, #c45c3a, #e8a04a); background-size: 400% 400%; animation: sunsetFlow 15s ease infinite; color: #f5e6d3 !important; }
|
||||
@keyframes sunsetFlow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
||||
body.theme-sunset-dynamic .top-bar { background: rgba(0, 0, 0, 0.4) !important; border-bottom: 1px solid rgba(255, 184, 107, 0.3) !important; }
|
||||
body.theme-sunset-dynamic .top-bar, body.theme-sunset-dynamic .top-bar a, body.theme-sunset-dynamic .top-bar button { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .back-btn { background: rgba(255, 184, 107, 0.15) !important; }
|
||||
body.theme-sunset-dynamic .page-title { color: #ffb86b !important; text-shadow: 0 0 8px rgba(255,184,107,0.3); }
|
||||
body.theme-sunset-dynamic .ebook-chapter { background: rgba(0, 0, 0, 0.4) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .ebook-chapter .chapter-title { color: #ffb86b !important; border-bottom-color: rgba(255, 184, 107, 0.3) !important; }
|
||||
body.theme-sunset-dynamic .floating-btn, body.theme-sunset-dynamic .speed-panel, body.theme-sunset-dynamic .font-controls, body.theme-sunset-dynamic .theme-selector, body.theme-sunset-dynamic .bookmark-panel { background: rgba(0, 0, 0, 0.45) !important; border: 1px solid rgba(255, 184, 107, 0.3) !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .floating-btn { background: rgba(0, 0, 0, 0.35) !important; color: #ffb86b !important; border: 1px solid rgba(255, 184, 107, 0.4) !important; }
|
||||
body.theme-sunset-dynamic .floating-btn.bookmark-btn { background: rgba(255, 184, 107, 0.15) !important; border: 1px solid #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .floating-btn.bookmark-btn.active { background: #ffb86b !important; color: #1a0a2e !important; }
|
||||
body.theme-sunset-dynamic .speed-slider::-webkit-slider-thumb { background: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .progress-slider-global::-webkit-slider-thumb { background: #ffb86b !important; box-shadow: 0 0 8px rgba(255,184,107,0.8) !important; }
|
||||
body.theme-sunset-dynamic .progress-fill { background: linear-gradient(90deg, #ffb86b, #ff6b6b) !important; }
|
||||
body.theme-sunset-dynamic .speed-preset.active { color: #ffb86b !important; background: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .auto-chapter-line { border-top-color: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .auto-chapter-line input { accent-color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .global-progress-container { background: rgba(0, 0, 0, 0.45) !important; border-top: 1px solid rgba(255, 184, 107, 0.3) !important; }
|
||||
body.theme-sunset-dynamic .progress-info { color: rgba(255, 184, 107, 0.85) !important; }
|
||||
body.theme-sunset-dynamic .shelf-item { background: rgba(255, 184, 107, 0.1) !important; border-color: rgba(255, 184, 107, 0.3) !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .shelf-item:hover { background: rgba(255, 184, 107, 0.2) !important; border-color: rgba(255, 184, 107, 0.6) !important; }
|
||||
body.theme-sunset-dynamic .book-chapter-item { background: rgba(255, 184, 107, 0.1) !important; border-color: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .book-chapter-item a { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .book-chapter-item:hover { background: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .bookmark-item .title { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .bookmark-header { border-bottom-color: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .chapter-tooltip { background: rgba(30, 20, 30, 0.85) !important; border-color: #ffb86b !important; color: #ffb86b !important; box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; }
|
||||
body.theme-sunset-dynamic .page-turn-overlay { background: rgba(0, 0, 0, 0.5) !important; }
|
||||
body.theme-sunset-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-sunset-dynamic .page-turn-overlay .book-right { background: rgba(30, 20, 30, 0.6) !important; border: 2px solid rgba(255, 184, 107, 0.4) !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .page-turn-overlay .message { background: rgba(30, 20, 30, 0.8) !important; color: #ffb86b !important; border: 1px solid rgba(255, 184, 107, 0.5) !important; }
|
||||
body.theme-sunset-dynamic .page-transition { background: rgba(0, 0, 0, 0.5) !important; }
|
||||
body.theme-sunset-dynamic .page-transition .book-page { background: linear-gradient(135deg, #ffb86b, #c45c3a) !important; }
|
||||
body.theme-sunset-dynamic .page-transition .loading-text { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .page-transition .loading-dots span { background: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .top-bar button.bookmark { background: rgba(255, 184, 107, 0.2) !important; border: 1px solid #ffb86b !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .toast { background: rgba(0, 0, 0, 0.7) !important; color: #ffb86b !important; border: 1px solid rgba(255, 184, 107, 0.4) !important; }
|
||||
|
||||
/* 4. 深海波动 */
|
||||
body.theme-wave-dynamic { background: linear-gradient(135deg, #0b2b44, #0d3b5e, #0a2a40, #0d3b5e, #0b2b44); background-size: 300% 300%; animation: waveMoveEnhanced 6s ease infinite; color: #c8e7f5 !important; }
|
||||
@keyframes waveMoveEnhanced { 0% { background-position: 0% 0%; } 25% { background-position: 100% 50%; } 50% { background-position: 50% 100%; } 75% { background-position: 0% 50%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-wave-dynamic .top-bar { background: rgba(0, 20, 30, 0.6) !important; border-bottom: 1px solid rgba(91, 192, 255, 0.3) !important; }
|
||||
body.theme-wave-dynamic .top-bar, body.theme-wave-dynamic .top-bar a, body.theme-wave-dynamic .top-bar button { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .back-btn { background: rgba(91, 192, 255, 0.15) !important; }
|
||||
body.theme-wave-dynamic .page-title { color: #5bc0ff !important; text-shadow: 0 0 8px rgba(91,192,255,0.3); }
|
||||
body.theme-wave-dynamic .ebook-chapter { background: rgba(0, 20, 30, 0.5) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .ebook-chapter .chapter-title { color: #5bc0ff !important; border-bottom-color: rgba(91, 192, 255, 0.3) !important; }
|
||||
body.theme-wave-dynamic .floating-btn, body.theme-wave-dynamic .speed-panel, body.theme-wave-dynamic .font-controls, body.theme-wave-dynamic .theme-selector, body.theme-wave-dynamic .bookmark-panel { background: rgba(0, 20, 30, 0.65) !important; border: 1px solid rgba(91, 192, 255, 0.3) !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .floating-btn { background: rgba(0, 20, 30, 0.5) !important; color: #5bc0ff !important; border: 1px solid rgba(91, 192, 255, 0.4) !important; animation: waveBtnFloat 3s ease-in-out infinite !important; }
|
||||
@keyframes waveBtnFloat { 0% { transform: translateY(0px); } 50% { transform: translateY(-3px); } 100% { transform: translateY(0px); } }
|
||||
body.theme-wave-dynamic .floating-btn.bookmark-btn { background: rgba(91, 192, 255, 0.15) !important; }
|
||||
body.theme-wave-dynamic .floating-btn.bookmark-btn.active { background: #5bc0ff !important; color: #0b2b44 !important; }
|
||||
body.theme-wave-dynamic .speed-slider::-webkit-slider-thumb { background: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .progress-slider-global::-webkit-slider-thumb { background: #5bc0ff !important; box-shadow: 0 0 8px rgba(91,192,255,0.8) !important; }
|
||||
body.theme-wave-dynamic .progress-fill { background: linear-gradient(90deg, #5bc0ff, #2d9cdb) !important; }
|
||||
body.theme-wave-dynamic .speed-preset.active { color: #5bc0ff !important; background: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .auto-chapter-line { border-top-color: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .auto-chapter-line input { accent-color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .global-progress-container { background: rgba(0, 20, 30, 0.65) !important; border-top: 1px solid rgba(91, 192, 255, 0.3) !important; }
|
||||
body.theme-wave-dynamic .progress-info { color: rgba(91, 192, 255, 0.85) !important; }
|
||||
body.theme-wave-dynamic .shelf-item { background: rgba(91, 192, 255, 0.1) !important; border-color: rgba(91, 192, 255, 0.3) !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .shelf-item:hover { background: rgba(91, 192, 255, 0.2) !important; border-color: rgba(91, 192, 255, 0.6) !important; }
|
||||
body.theme-wave-dynamic .book-chapter-item { background: rgba(91, 192, 255, 0.1) !important; border-color: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .book-chapter-item a { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .book-chapter-item:hover { background: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .bookmark-item .title { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .bookmark-header { border-bottom-color: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .chapter-tooltip { background: rgba(0, 20, 30, 0.9) !important; border-color: #5bc0ff !important; color: #5bc0ff !important; box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; }
|
||||
body.theme-wave-dynamic .page-turn-overlay { background: rgba(10, 40, 60, 0.7) !important; }
|
||||
body.theme-wave-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-wave-dynamic .page-turn-overlay .book-right { background: rgba(10, 40, 60, 0.6) !important; border: 2px solid rgba(91, 192, 255, 0.4) !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .page-turn-overlay .message { background: rgba(10, 40, 60, 0.8) !important; color: #5bc0ff !important; border: 1px solid rgba(91, 192, 255, 0.5) !important; }
|
||||
body.theme-wave-dynamic .page-transition { background: rgba(0, 20, 30, 0.7) !important; }
|
||||
body.theme-wave-dynamic .page-transition .book-page { background: linear-gradient(135deg, #5bc0ff, #0d3b5e) !important; }
|
||||
body.theme-wave-dynamic .page-transition .loading-text { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .page-transition .loading-dots span { background: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .top-bar button.bookmark { background: rgba(91, 192, 255, 0.2) !important; border: 1px solid #5bc0ff !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .toast { background: rgba(0, 20, 30, 0.85) !important; color: #5bc0ff !important; border: 1px solid rgba(91, 192, 255, 0.4) !important; }
|
||||
|
||||
/* 5. 火焰之心 */
|
||||
body.theme-fire-dynamic { background: linear-gradient(180deg, #4a0a0a, #8b2a1a, #d45a2a); background-size: 100% 200%; animation: firePulse 2s ease infinite alternate; color: #ffe0c0 !important; }
|
||||
@keyframes firePulse { 0% { background-position: 0% 0%; } 100% { background-position: 0% 100%; } }
|
||||
body.theme-fire-dynamic .top-bar { background: rgba(60, 10, 10, 0.6) !important; border-bottom: 1px solid rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .top-bar, body.theme-fire-dynamic .top-bar a, body.theme-fire-dynamic .top-bar button { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .back-btn { background: rgba(255, 140, 66, 0.15) !important; }
|
||||
body.theme-fire-dynamic .page-title { color: #ff8c42 !important; text-shadow: 0 0 8px rgba(255,140,66,0.4); }
|
||||
body.theme-fire-dynamic .ebook-chapter { background: rgba(60, 10, 10, 0.5) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 140, 66, 0.3) !important; }
|
||||
body.theme-fire-dynamic .ebook-chapter .chapter-title { color: #ff8c42 !important; border-bottom-color: rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .floating-btn, body.theme-fire-dynamic .speed-panel, body.theme-fire-dynamic .font-controls, body.theme-fire-dynamic .theme-selector, body.theme-fire-dynamic .bookmark-panel { background: rgba(60, 10, 10, 0.7) !important; border: 1px solid rgba(255, 140, 66, 0.4) !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .floating-btn { background: rgba(60, 10, 10, 0.6) !important; color: #ff8c42 !important; border: 1px solid rgba(255, 140, 66, 0.5) !important; }
|
||||
body.theme-fire-dynamic .floating-btn.bookmark-btn { background: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .floating-btn.bookmark-btn.active { background: #ff8c42 !important; color: #4a0a0a !important; }
|
||||
body.theme-fire-dynamic .speed-slider::-webkit-slider-thumb { background: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .progress-slider-global::-webkit-slider-thumb { background: #ff8c42 !important; box-shadow: 0 0 8px rgba(255,140,66,0.8) !important; }
|
||||
body.theme-fire-dynamic .progress-fill { background: linear-gradient(90deg, #ff8c42, #ff5722) !important; }
|
||||
body.theme-fire-dynamic .speed-preset.active { color: #ff8c42 !important; background: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .auto-chapter-line { border-top-color: rgba(255, 140, 66, 0.3) !important; }
|
||||
body.theme-fire-dynamic .auto-chapter-line input { accent-color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .global-progress-container { background: rgba(60, 10, 10, 0.7) !important; border-top: 1px solid rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .progress-info { color: rgba(255, 140, 66, 0.85) !important; }
|
||||
body.theme-fire-dynamic .shelf-item { background: rgba(255, 140, 66, 0.1) !important; border-color: rgba(255, 140, 66, 0.3) !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .shelf-item:hover { background: rgba(255, 140, 66, 0.2) !important; border-color: rgba(255, 140, 66, 0.6) !important; }
|
||||
body.theme-fire-dynamic .book-chapter-item { background: rgba(255, 140, 66, 0.1) !important; border-color: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .book-chapter-item a { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .book-chapter-item:hover { background: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .bookmark-item .title { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .bookmark-header { border-bottom-color: rgba(255, 140, 66, 0.3) !important; }
|
||||
body.theme-fire-dynamic .chapter-tooltip { background: rgba(60, 10, 10, 0.92) !important; border-color: #ff8c42 !important; color: #ff8c42 !important; box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; }
|
||||
body.theme-fire-dynamic .page-turn-overlay { background: rgba(60, 10, 10, 0.7) !important; }
|
||||
body.theme-fire-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-fire-dynamic .page-turn-overlay .book-right { background: rgba(60, 10, 10, 0.6) !important; border: 2px solid rgba(255, 140, 66, 0.4) !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .page-turn-overlay .message { background: rgba(60, 10, 10, 0.8) !important; color: #ff8c42 !important; border: 1px solid rgba(255, 140, 66, 0.5) !important; }
|
||||
body.theme-fire-dynamic .page-transition { background: rgba(60, 10, 10, 0.7) !important; }
|
||||
body.theme-fire-dynamic .page-transition .book-page { background: linear-gradient(135deg, #ff8c42, #d45a2a) !important; }
|
||||
body.theme-fire-dynamic .page-transition .loading-text { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .page-transition .loading-dots span { background: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .top-bar button.bookmark { background: rgba(255, 140, 66, 0.2) !important; border: 1px solid #ff8c42 !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .toast { background: rgba(60, 10, 10, 0.9) !important; color: #ff8c42 !important; border: 1px solid rgba(255, 140, 66, 0.4) !important; }
|
||||
|
||||
/* 6. 樱花飘舞 */
|
||||
body.theme-sakura-dynamic { background: linear-gradient(135deg, #ffeef8 0%, #ffd9e8 50%, #ffb7c5 100%); background-size: 200% 200%; animation: sakuraFlow 8s ease infinite; color: #6b3e4a !important; }
|
||||
@keyframes sakuraFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-sakura-dynamic .top-bar { background: rgba(255, 240, 245, 0.85) !important; backdrop-filter: blur(20px) !important; border-bottom: 1px solid rgba(255, 160, 180, 0.4) !important; }
|
||||
body.theme-sakura-dynamic .top-bar, body.theme-sakura-dynamic .top-bar a, body.theme-sakura-dynamic .top-bar button { color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .back-btn { background: rgba(184, 59, 94, 0.12) !important; }
|
||||
body.theme-sakura-dynamic .page-title { color: #b83b5e !important; text-shadow: 0 0 8px rgba(184,59,94,0.2); }
|
||||
body.theme-sakura-dynamic .ebook-chapter { background: rgba(255, 255, 255, 0.7) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 160, 180, 0.3) !important; color: #6b3e4a !important; }
|
||||
body.theme-sakura-dynamic .ebook-chapter .chapter-title { color: #e86f8f !important; border-bottom-color: rgba(232, 111, 143, 0.3) !important; }
|
||||
body.theme-sakura-dynamic .floating-btn, body.theme-sakura-dynamic .speed-panel, body.theme-sakura-dynamic .font-controls, body.theme-sakura-dynamic .theme-selector, body.theme-sakura-dynamic .bookmark-panel { background: rgba(255, 240, 245, 0.9) !important; border: 1px solid rgba(232, 111, 143, 0.3) !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .floating-btn { background: rgba(255, 240, 245, 0.85) !important; color: #e86f8f !important; border: 1px solid rgba(232, 111, 143, 0.4) !important; }
|
||||
body.theme-sakura-dynamic .floating-btn.bookmark-btn { background: rgba(232, 111, 143, 0.2) !important; border: 1px solid #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .speed-slider::-webkit-slider-thumb { background: #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .progress-slider-global::-webkit-slider-thumb { background: #e86f8f !important; box-shadow: 0 0 8px rgba(232,111,143,0.6) !important; }
|
||||
body.theme-sakura-dynamic .progress-fill { background: linear-gradient(90deg, #e86f8f, #b83b5e) !important; }
|
||||
body.theme-sakura-dynamic .speed-preset.active { color: #e86f8f !important; background: rgba(232, 111, 143, 0.15) !important; }
|
||||
body.theme-sakura-dynamic .global-progress-container { background: rgba(255, 240, 245, 0.9) !important; border-top: 1px solid rgba(232, 111, 143, 0.3) !important; }
|
||||
body.theme-sakura-dynamic .shelf-item { background: rgba(232, 111, 143, 0.1) !important; border-color: rgba(232, 111, 143, 0.3) !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .shelf-item:hover { background: rgba(232, 111, 143, 0.2) !important; }
|
||||
body.theme-sakura-dynamic .book-chapter-item { background: rgba(232, 111, 143, 0.1) !important; }
|
||||
body.theme-sakura-dynamic .book-chapter-item a { color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .chapter-tooltip { background: rgba(255, 240, 245, 0.95) !important; border-color: #e86f8f !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .page-turn-overlay { background: rgba(255, 240, 245, 0.85) !important; }
|
||||
body.theme-sakura-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-sakura-dynamic .page-turn-overlay .book-right { background: rgba(255, 245, 250, 0.9) !important; border: 2px solid rgba(232, 111, 143, 0.5) !important; color: #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .page-turn-overlay .message { background: rgba(255, 240, 245, 0.95) !important; color: #b83b5e !important; border: 1px solid #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .page-transition .book-page { background: linear-gradient(135deg, #e86f8f, #b83b5e) !important; }
|
||||
body.theme-sakura-dynamic .page-transition .loading-text { color: #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .toast { background: rgba(255, 240, 245, 0.95) !important; color: #b83b5e !important; border: 1px solid #e86f8f !important; }
|
||||
|
||||
/* 7. 薄荷冰霜 */
|
||||
body.theme-mintfrost-dynamic { background: linear-gradient(135deg, #c8e8e9 0%, #a8d8ea 50%, #88c8e8 100%); background-size: 200% 200%; animation: mintFlow 6s ease infinite; color: #2c5a5a !important; }
|
||||
@keyframes mintFlow { 0% { background-position: 0% 0%; } 100% { background-position: 100% 100%; } }
|
||||
body.theme-mintfrost-dynamic .top-bar { background: rgba(200, 232, 233, 0.85) !important; border-bottom: 1px solid rgba(100, 180, 200, 0.4) !important; }
|
||||
body.theme-mintfrost-dynamic .top-bar, body.theme-mintfrost-dynamic .top-bar a, body.theme-mintfrost-dynamic .top-bar button { color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .ebook-chapter { background: rgba(255, 255, 250, 0.75) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(100, 180, 200, 0.3) !important; color: #2c5a5a !important; }
|
||||
body.theme-mintfrost-dynamic .ebook-chapter .chapter-title { color: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .floating-btn, body.theme-mintfrost-dynamic .speed-panel, body.theme-mintfrost-dynamic .font-controls, body.theme-mintfrost-dynamic .theme-selector, body.theme-mintfrost-dynamic .bookmark-panel { background: rgba(200, 232, 233, 0.9) !important; border: 1px solid rgba(100, 180, 200, 0.3) !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .floating-btn { background: rgba(200, 232, 233, 0.85) !important; color: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .progress-slider-global::-webkit-slider-thumb { background: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .progress-fill { background: linear-gradient(90deg, #3a9a9a, #2a7a7a) !important; }
|
||||
body.theme-mintfrost-dynamic .chapter-tooltip { background: rgba(200, 232, 233, 0.95) !important; border-color: #3a9a9a !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay { background: rgba(200, 232, 233, 0.85) !important; }
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay .book-right { background: rgba(220, 245, 245, 0.9) !important; border: 2px solid rgba(58, 154, 154, 0.5) !important; color: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay .message { background: rgba(200, 232, 233, 0.95) !important; color: #2a7a7a !important; border: 1px solid #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .toast { background: rgba(200, 232, 233, 0.95) !important; color: #2a7a7a !important; border: 1px solid #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .shelf-item { background: rgba(100, 180, 200, 0.15) !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .page-transition .book-page { background: linear-gradient(135deg, #3a9a9a, #2a7a7a) !important; }
|
||||
|
||||
/* 8. 薰衣草庄园 */
|
||||
body.theme-lavenderfield-dynamic { background: linear-gradient(145deg, #d8cce8 0%, #b9a8d4 50%, #9b88c2 100%); background-size: 200% 200%; animation: lavenderFlow 10s ease infinite; color: #3a2a5a !important; }
|
||||
@keyframes lavenderFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-lavenderfield-dynamic .top-bar { background: rgba(216, 204, 232, 0.85) !important; border-bottom: 1px solid rgba(155, 136, 194, 0.4) !important; }
|
||||
body.theme-lavenderfield-dynamic .top-bar, body.theme-lavenderfield-dynamic .top-bar a, body.theme-lavenderfield-dynamic .top-bar button { color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .ebook-chapter { background: rgba(255, 250, 255, 0.75) !important; backdrop-filter: blur(10px) !important; color: #3a2a5a !important; }
|
||||
body.theme-lavenderfield-dynamic .ebook-chapter .chapter-title { color: #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .floating-btn, body.theme-lavenderfield-dynamic .speed-panel, body.theme-lavenderfield-dynamic .font-controls, body.theme-lavenderfield-dynamic .theme-selector, body.theme-lavenderfield-dynamic .bookmark-panel { background: rgba(216, 204, 232, 0.9) !important; border: 1px solid rgba(155, 136, 194, 0.3) !important; color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .progress-slider-global::-webkit-slider-thumb { background: #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .chapter-tooltip { background: rgba(216, 204, 232, 0.95) !important; border-color: #8b6bbf !important; color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay { background: rgba(216, 204, 232, 0.85) !important; }
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay .book-right { background: rgba(230, 220, 245, 0.9) !important; border: 2px solid rgba(139, 107, 191, 0.5) !important; color: #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay .message { background: rgba(216, 204, 232, 0.95) !important; color: #5a4a8a !important; border: 1px solid #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .toast { background: rgba(216, 204, 232, 0.95) !important; color: #5a4a8a !important; border: 1px solid #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .page-transition .book-page { background: linear-gradient(135deg, #8b6bbf, #6b4a9f) !important; }
|
||||
body.theme-lavenderfield-dynamic .shelf-item { background: rgba(139, 107, 191, 0.15) !important; color: #5a4a8a !important; }
|
||||
|
||||
/* 9. 金色麦田 */
|
||||
body.theme-golden-dynamic { background: linear-gradient(135deg, #f5e6b8 0%, #e8d498 50%, #d4b86a 100%); background-size: 200% 200%; animation: goldenFlow 8s ease infinite; color: #5a4a2a !important; }
|
||||
@keyframes goldenFlow { 0% { background-position: 0% 0%; } 100% { background-position: 100% 100%; } }
|
||||
body.theme-golden-dynamic .top-bar { background: rgba(245, 230, 184, 0.85) !important; border-bottom: 1px solid rgba(212, 184, 106, 0.4) !important; }
|
||||
body.theme-golden-dynamic .top-bar, body.theme-golden-dynamic .top-bar a, body.theme-golden-dynamic .top-bar button { color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .ebook-chapter { background: rgba(255, 255, 240, 0.8) !important; backdrop-filter: blur(10px) !important; color: #5a4a2a !important; }
|
||||
body.theme-golden-dynamic .ebook-chapter .chapter-title { color: #c4a030 !important; }
|
||||
body.theme-golden-dynamic .floating-btn, body.theme-golden-dynamic .speed-panel, body.theme-golden-dynamic .font-controls, body.theme-golden-dynamic .theme-selector, body.theme-golden-dynamic .bookmark-panel { background: rgba(245, 230, 184, 0.9) !important; border: 1px solid rgba(212, 184, 106, 0.3) !important; color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .progress-slider-global::-webkit-slider-thumb { background: #d4a030 !important; }
|
||||
body.theme-golden-dynamic .chapter-tooltip { background: rgba(245, 230, 184, 0.95) !important; border-color: #d4a030 !important; color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .page-turn-overlay { background: rgba(245, 230, 184, 0.85) !important; }
|
||||
body.theme-golden-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-golden-dynamic .page-turn-overlay .book-right { background: rgba(255, 250, 220, 0.9) !important; border: 2px solid rgba(212, 160, 48, 0.5) !important; color: #d4a030 !important; }
|
||||
body.theme-golden-dynamic .page-turn-overlay .message { background: rgba(245, 230, 184, 0.95) !important; color: #8a6a2a !important; border: 1px solid #d4a030 !important; }
|
||||
body.theme-golden-dynamic .toast { background: rgba(245, 230, 184, 0.95) !important; color: #8a6a2a !important; border: 1px solid #d4a030 !important; }
|
||||
body.theme-golden-dynamic .page-transition .book-page { background: linear-gradient(135deg, #d4a030, #b08020) !important; }
|
||||
|
||||
/* 10. 珊瑚海洋 */
|
||||
body.theme-coralreef-dynamic { background: linear-gradient(125deg, #ffaa88 0%, #ff8866 50%, #ff6644 100%); background-size: 200% 200%; animation: coralFlow 7s ease infinite; color: #4a2a1a !important; }
|
||||
@keyframes coralFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-coralreef-dynamic .top-bar { background: rgba(255, 170, 136, 0.85) !important; border-bottom: 1px solid rgba(255, 100, 70, 0.4) !important; }
|
||||
body.theme-coralreef-dynamic .top-bar, body.theme-coralreef-dynamic .top-bar a, body.theme-coralreef-dynamic .top-bar button { color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .ebook-chapter { background: rgba(255, 250, 245, 0.8) !important; color: #4a2a1a !important; }
|
||||
body.theme-coralreef-dynamic .ebook-chapter .chapter-title { color: #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .floating-btn, body.theme-coralreef-dynamic .speed-panel, body.theme-coralreef-dynamic .font-controls, body.theme-coralreef-dynamic .theme-selector, body.theme-coralreef-dynamic .bookmark-panel { background: rgba(255, 170, 136, 0.9) !important; color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .progress-slider-global::-webkit-slider-thumb { background: #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .chapter-tooltip { background: rgba(255, 170, 136, 0.95) !important; border-color: #ff6644 !important; color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .page-turn-overlay { background: rgba(255, 170, 136, 0.85) !important; }
|
||||
body.theme-coralreef-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-coralreef-dynamic .page-turn-overlay .book-right { background: rgba(255, 200, 180, 0.9) !important; border: 2px solid rgba(255, 102, 68, 0.5) !important; color: #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .page-turn-overlay .message { background: rgba(255, 170, 136, 0.95) !important; color: #8a3010 !important; border: 1px solid #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .toast { background: rgba(255, 170, 136, 0.95) !important; color: #8a3010 !important; border: 1px solid #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .page-transition .book-page { background: linear-gradient(135deg, #ff8866, #ff6644) !important; }
|
||||
|
||||
/* 11. 星空银河 */
|
||||
body.theme-galaxy-dynamic { background: radial-gradient(ellipse at center, #0a0a2a 0%, #1a1a4a 50%, #2a2a5a 100%); background-size: 200% 200%; animation: galaxyTwinkle 15s ease infinite; color: #c8d0ff !important; }
|
||||
@keyframes galaxyTwinkle { 0% { background-size: 100% 100%; opacity: 1; } 50% { background-size: 120% 120%; opacity: 0.95; } 100% { background-size: 100% 100%; opacity: 1; } }
|
||||
body.theme-galaxy-dynamic .top-bar { background: rgba(10, 10, 42, 0.85) !important; border-bottom: 1px solid rgba(200, 200, 255, 0.3) !important; }
|
||||
body.theme-galaxy-dynamic .top-bar, body.theme-galaxy-dynamic .top-bar a, body.theme-galaxy-dynamic .top-bar button { color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .ebook-chapter { background: rgba(30, 30, 70, 0.8) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(170, 172, 255, 0.2) !important; color: #c8d0ff !important; }
|
||||
body.theme-galaxy-dynamic .ebook-chapter .chapter-title { color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .floating-btn, body.theme-galaxy-dynamic .speed-panel, body.theme-galaxy-dynamic .font-controls, body.theme-galaxy-dynamic .theme-selector, body.theme-galaxy-dynamic .bookmark-panel { background: rgba(10, 10, 42, 0.9) !important; border: 1px solid rgba(170, 172, 255, 0.3) !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .progress-slider-global::-webkit-slider-thumb { background: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .chapter-tooltip { background: rgba(10, 10, 42, 0.95) !important; border-color: #aaacff !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .page-turn-overlay { background: rgba(10, 10, 42, 0.85) !important; }
|
||||
body.theme-galaxy-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-galaxy-dynamic .page-turn-overlay .book-right { background: rgba(30, 30, 70, 0.9) !important; border: 2px solid rgba(170, 172, 255, 0.5) !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .page-turn-overlay .message { background: rgba(10, 10, 42, 0.95) !important; color: #aaacff !important; border: 1px solid #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .toast { background: rgba(10, 10, 42, 0.95) !important; color: #aaacff !important; border: 1px solid #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .page-transition .book-page { background: linear-gradient(135deg, #aaacff, #6a6acf) !important; }
|
||||
body.theme-galaxy-dynamic .shelf-item { background: rgba(170, 172, 255, 0.1) !important; color: #aaacff !important; }
|
||||
|
||||
/* 12. 玫瑰花园 */
|
||||
body.theme-rosegarden-dynamic { background: linear-gradient(145deg, #f5c8d8 0%, #e8a8c0 50%, #d888a8 100%); background-size: 200% 200%; animation: roseFlow 9s ease infinite; color: #5a2a3a !important; }
|
||||
@keyframes roseFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-rosegarden-dynamic .top-bar { background: rgba(245, 200, 216, 0.85) !important; border-bottom: 1px solid rgba(216, 136, 168, 0.4) !important; }
|
||||
body.theme-rosegarden-dynamic .top-bar, body.theme-rosegarden-dynamic .top-bar a, body.theme-rosegarden-dynamic .top-bar button { color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .ebook-chapter { background: rgba(255, 245, 250, 0.8) !important; color: #5a2a3a !important; }
|
||||
body.theme-rosegarden-dynamic .ebook-chapter .chapter-title { color: #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .floating-btn, body.theme-rosegarden-dynamic .speed-panel, body.theme-rosegarden-dynamic .font-controls, body.theme-rosegarden-dynamic .theme-selector, body.theme-rosegarden-dynamic .bookmark-panel { background: rgba(245, 200, 216, 0.9) !important; border: 1px solid rgba(216, 136, 168, 0.3) !important; color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .progress-slider-global::-webkit-slider-thumb { background: #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .chapter-tooltip { background: rgba(245, 200, 216, 0.95) !important; border-color: #d888a8 !important; color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay { background: rgba(245, 200, 216, 0.85) !important; }
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay .book-left,
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay .book-right { background: rgba(255, 230, 240, 0.9) !important; border: 2px solid rgba(216, 136, 168, 0.5) !important; color: #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay .message { background: rgba(245, 200, 216, 0.95) !important; color: #a03050 !important; border: 1px solid #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .toast { background: rgba(245, 200, 216, 0.95) !important; color: #a03050 !important; border: 1px solid #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .page-transition .book-page { background: linear-gradient(135deg, #d888a8, #c06888) !important; }
|
||||
|
||||
/* 主题色块样式 */
|
||||
.theme-dot[data-theme="deep-space"] { background: linear-gradient(135deg, #0f0c29, #302b63); }
|
||||
.theme-dot[data-theme="ocean"] { background: linear-gradient(135deg, #1a2980, #26d0ce); }
|
||||
.theme-dot[data-theme="cherry"] { background: linear-gradient(135deg, #ff9a9e, #fecfef); }
|
||||
.theme-dot[data-theme="night"] { background: #1a1a1a; }
|
||||
.theme-dot[data-theme="forest"] { background: linear-gradient(135deg, #134e5e, #71b280); }
|
||||
.theme-dot[data-theme="sunset"] { background: linear-gradient(135deg, #ff7e5f, #feb47b); }
|
||||
.theme-dot[data-theme="lavender"] { background: linear-gradient(135deg, #8e9ecc, #e0bbff); }
|
||||
.theme-dot[data-theme="blueberry"] { background: linear-gradient(135deg, #2c3e66, #4a69bd); }
|
||||
.theme-dot[data-theme="amber"] { background: linear-gradient(135deg, #ffb347, #ffcc33); }
|
||||
.theme-dot[data-theme="coral"] { background: linear-gradient(135deg, #ff6b6b, #ffb8b8); }
|
||||
.theme-dot[data-theme="mint"] { background: linear-gradient(135deg, #a8e6cf, #80deea); }
|
||||
.theme-dot[data-theme="rosegold"] { background: linear-gradient(135deg, #e8b4b8, #ffd9e2); }
|
||||
.theme-dot[data-theme="eyecare"] { background: #c7edcc; border: 2px solid #8b9a6e; }
|
||||
.theme-dot[data-theme="aurora-dynamic"] { background: linear-gradient(270deg, #1a0b2e, #2d1b69, #1a4d8c, #0f5c6b); animation: none; }
|
||||
.theme-dot[data-theme="neon-dynamic"] { background: #0a0a0a; border: 2px solid #0ff; box-shadow: 0 0 5px #0ff; }
|
||||
.theme-dot[data-theme="sunset-dynamic"] { background: linear-gradient(135deg, #5c2a4a, #e8a04a); }
|
||||
.theme-dot[data-theme="wave-dynamic"] { background: linear-gradient(135deg, #0b2b44, #0d3b5e); }
|
||||
.theme-dot[data-theme="fire-dynamic"] { background: linear-gradient(180deg, #4a0a0a, #d45a2a); }
|
||||
.theme-dot[data-theme="sakura-dynamic"] { background: linear-gradient(135deg, #ffeef8, #ffb7c5); }
|
||||
.theme-dot[data-theme="mintfrost-dynamic"] { background: linear-gradient(135deg, #c8e8e9, #88c8e8); }
|
||||
.theme-dot[data-theme="lavenderfield-dynamic"] { background: linear-gradient(145deg, #d8cce8, #9b88c2); }
|
||||
.theme-dot[data-theme="golden-dynamic"] { background: linear-gradient(135deg, #f5e6b8, #d4b86a); }
|
||||
.theme-dot[data-theme="coralreef-dynamic"] { background: linear-gradient(125deg, #ffaa88, #ff6644); }
|
||||
.theme-dot[data-theme="galaxy-dynamic"] { background: radial-gradient(ellipse at center, #0a0a2a, #2a2a5a); }
|
||||
.theme-dot[data-theme="rosegarden-dynamic"] { background: linear-gradient(145deg, #f5c8d8, #d888a8); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="theme-deep-space">
|
||||
|
||||
<div class="floating-buttons" id="floatingButtons">
|
||||
<div class="floating-btn bookmark-btn" id="bookmarkFloatBtn">📋</div>
|
||||
<div class="floating-btn scroll-down" id="scrollToggleBtn">▼</div>
|
||||
<div class="floating-btn" id="themeFloatBtn">🎨</div>
|
||||
</div>
|
||||
|
||||
<div class="speed-panel" id="speedPanel">
|
||||
<div class="speed-label"><span>⚡ 滚动速度</span><span class="speed-value" id="speedValue">6 px/帧</span></div>
|
||||
<input type="range" class="speed-slider" id="speedSlider" min="1" max="30" value="6" step="1">
|
||||
<div class="speed-presets">
|
||||
<div class="speed-preset" data-speed="3">🐢 慢</div>
|
||||
<div class="speed-preset" data-speed="6">⚡ 中</div>
|
||||
<div class="speed-preset" data-speed="10">🚀 快</div>
|
||||
<div class="speed-preset" data-speed="18">💨 极快</div>
|
||||
</div>
|
||||
<div class="auto-chapter-line"><span>📖 自动翻章</span><input type="checkbox" id="autoChapterCheckbox" checked></div>
|
||||
</div>
|
||||
|
||||
<div class="font-controls" id="fontControls">
|
||||
<button id="fontMinus">A-</button>
|
||||
<button id="fontPlus">A+</button>
|
||||
</div>
|
||||
<div class="theme-selector" id="themeSelector">
|
||||
<!-- 12静态主题 -->
|
||||
<div class="theme-dot" data-theme="deep-space" title="深邃星空"></div>
|
||||
<div class="theme-dot" data-theme="ocean" title="深海宁静"></div>
|
||||
<div class="theme-dot" data-theme="cherry" title="樱花"></div>
|
||||
<div class="theme-dot" data-theme="night" title="黑夜模式"></div>
|
||||
<div class="theme-dot" data-theme="forest" title="森林绿意"></div>
|
||||
<div class="theme-dot" data-theme="sunset" title="日落橙"></div>
|
||||
<div class="theme-dot" data-theme="lavender" title="薰衣草"></div>
|
||||
<div class="theme-dot" data-theme="blueberry" title="蓝莓"></div>
|
||||
<div class="theme-dot" data-theme="amber" title="琥珀"></div>
|
||||
<div class="theme-dot" data-theme="coral" title="珊瑚粉"></div>
|
||||
<div class="theme-dot" data-theme="mint" title="薄荷绿"></div>
|
||||
<div class="theme-dot" data-theme="rosegold" title="玫瑰金"></div>
|
||||
<div class="theme-dot" data-theme="eyecare" title="护眼模式"></div>
|
||||
<div style="width:100%; height:1px; background:rgba(255,255,255,0.2); margin:5px 0;"></div>
|
||||
<!-- 12动态主题 -->
|
||||
<div class="theme-dot" data-theme="aurora-dynamic" title="极光幻彩(动态)"></div>
|
||||
<div class="theme-dot" data-theme="neon-dynamic" title="霓虹脉冲(动态)"></div>
|
||||
<div class="theme-dot" data-theme="sunset-dynamic" title="暮色晚霞(动态)"></div>
|
||||
<div class="theme-dot" data-theme="wave-dynamic" title="深海波动(动态)"></div>
|
||||
<div class="theme-dot" data-theme="fire-dynamic" title="火焰之心(动态)"></div>
|
||||
<div class="theme-dot" data-theme="sakura-dynamic" title="樱花飘舞(动态)"></div>
|
||||
<div class="theme-dot" data-theme="mintfrost-dynamic" title="薄荷冰霜(动态)"></div>
|
||||
<div class="theme-dot" data-theme="lavenderfield-dynamic" title="薰衣草庄园(动态)"></div>
|
||||
<div class="theme-dot" data-theme="golden-dynamic" title="金色麦田(动态)"></div>
|
||||
<div class="theme-dot" data-theme="coralreef-dynamic" title="珊瑚海洋(动态)"></div>
|
||||
<div class="theme-dot" data-theme="galaxy-dynamic" title="星空银河(动态)"></div>
|
||||
<div class="theme-dot" data-theme="rosegarden-dynamic" title="玫瑰花园(动态)"></div>
|
||||
</div>
|
||||
<div class="bookmark-panel" id="bookmarkPanel">
|
||||
<div class="bookmark-header"><span>📖 我的书签</span><span id="closePanelBtn">✕</span></div>
|
||||
<div class="bookmark-list" id="bookmarkList"><div class="empty-bookmark">📭 暂无书签<br>点击 ⭐ 添加</div></div>
|
||||
</div>
|
||||
|
||||
<div id="globalProgressPlaceholder"></div>
|
||||
|
||||
<!-- 页面切换转场动画 -->
|
||||
<div id="pageTransition" class="page-transition">
|
||||
<div class="book-loader">
|
||||
<div class="book-page"></div>
|
||||
<div class="book-page"></div>
|
||||
<div class="book-page"></div>
|
||||
<div class="book-page"></div>
|
||||
</div>
|
||||
<div class="loading-text">加载中</div>
|
||||
<div class="loading-dots">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ==================== 主题适配的 Toast 提示框 ====================
|
||||
function showToast(msg) {
|
||||
let t = document.querySelector('.toast');
|
||||
if (!t) {
|
||||
t = document.createElement('div');
|
||||
t.className = 'toast';
|
||||
document.body.appendChild(t);
|
||||
}
|
||||
t.textContent = msg;
|
||||
t.style.display = 'block';
|
||||
|
||||
const bodyClass = document.body.className;
|
||||
if (bodyClass.includes('aurora')) {
|
||||
t.style.background = 'rgba(0, 0, 0, 0.8)'; t.style.color = '#7cffd0'; t.style.border = '1px solid rgba(124, 255, 208, 0.3)';
|
||||
} else if (bodyClass.includes('neon')) {
|
||||
t.style.background = 'rgba(0, 0, 0, 0.85)'; t.style.color = '#0ff'; t.style.border = '1px solid #0ff';
|
||||
} else if (bodyClass.includes('sunset-dynamic')) {
|
||||
t.style.background = 'rgba(0, 0, 0, 0.7)'; t.style.color = '#ffb86b'; t.style.border = '1px solid rgba(255, 184, 107, 0.4)';
|
||||
} else if (bodyClass.includes('wave')) {
|
||||
t.style.background = 'rgba(0, 20, 30, 0.85)'; t.style.color = '#5bc0ff'; t.style.border = '1px solid rgba(91, 192, 255, 0.4)';
|
||||
} else if (bodyClass.includes('fire')) {
|
||||
t.style.background = 'rgba(60, 10, 10, 0.9)'; t.style.color = '#ff8c42'; t.style.border = '1px solid rgba(255, 140, 66, 0.4)';
|
||||
} else if (bodyClass.includes('sakura')) {
|
||||
t.style.background = 'rgba(255, 240, 245, 0.95)'; t.style.color = '#b83b5e'; t.style.border = '1px solid #e86f8f';
|
||||
} else if (bodyClass.includes('mintfrost')) {
|
||||
t.style.background = 'rgba(200, 232, 233, 0.95)'; t.style.color = '#2a7a7a'; t.style.border = '1px solid #3a9a9a';
|
||||
} else if (bodyClass.includes('lavenderfield')) {
|
||||
t.style.background = 'rgba(216, 204, 232, 0.95)'; t.style.color = '#5a4a8a'; t.style.border = '1px solid #8b6bbf';
|
||||
} else if (bodyClass.includes('golden')) {
|
||||
t.style.background = 'rgba(245, 230, 184, 0.95)'; t.style.color = '#8a6a2a'; t.style.border = '1px solid #d4a030';
|
||||
} else if (bodyClass.includes('coralreef')) {
|
||||
t.style.background = 'rgba(255, 170, 136, 0.95)'; t.style.color = '#8a3010'; t.style.border = '1px solid #ff6644';
|
||||
} else if (bodyClass.includes('galaxy')) {
|
||||
t.style.background = 'rgba(10, 10, 42, 0.95)'; t.style.color = '#aaacff'; t.style.border = '1px solid #aaacff';
|
||||
} else if (bodyClass.includes('rosegarden')) {
|
||||
t.style.background = 'rgba(245, 200, 216, 0.95)'; t.style.color = '#a03050'; t.style.border = '1px solid #d888a8';
|
||||
} else if (bodyClass.includes('eyecare')) {
|
||||
t.style.background = 'rgba(199, 237, 204, 0.95)'; t.style.color = '#2d2d2d'; t.style.border = '1px solid rgba(139, 154, 110, 0.4)';
|
||||
} else {
|
||||
t.style.background = 'rgba(0,0,0,0.85)'; t.style.color = '#fff'; t.style.border = 'none';
|
||||
}
|
||||
setTimeout(() => t.style.display = 'none', 1500);
|
||||
}
|
||||
window.showToast = showToast;
|
||||
|
||||
const floatingBtns = document.getElementById('floatingButtons');
|
||||
const speedPanel = document.getElementById('speedPanel');
|
||||
const fontControls = document.getElementById('fontControls');
|
||||
const themeSelector = document.getElementById('themeSelector');
|
||||
const bookmarkPanel = document.getElementById('bookmarkPanel');
|
||||
|
||||
let hideTimer = null;
|
||||
let globalProgressBar = null;
|
||||
|
||||
function showControls() {
|
||||
floatingBtns.classList.add('visible');
|
||||
speedPanel.classList.add('visible');
|
||||
if (globalProgressBar) globalProgressBar.classList.remove('hide');
|
||||
resetHideTimer();
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
floatingBtns.classList.remove('visible');
|
||||
speedPanel.classList.remove('visible');
|
||||
fontControls.classList.remove('visible');
|
||||
themeSelector.classList.remove('visible');
|
||||
if (globalProgressBar) globalProgressBar.classList.add('hide');
|
||||
}
|
||||
|
||||
function resetHideTimer() {
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(() => {
|
||||
if (!bookmarkPanel.classList.contains('show') && !themeSelector.classList.contains('visible') && !fontControls.classList.contains('visible') && !speedPanel.classList.contains('visible')) {
|
||||
hideControls();
|
||||
} else {
|
||||
resetHideTimer();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
let lastTap = 0;
|
||||
document.body.addEventListener('click', (e) => {
|
||||
const now = Date.now();
|
||||
const timeDiff = now - lastTap;
|
||||
const isControlElement = e.target.closest('.floating-btn') || e.target.closest('.bookmark-panel') ||
|
||||
e.target.closest('.theme-selector') || e.target.closest('.font-controls') ||
|
||||
e.target.closest('.speed-panel') || e.target.closest('.global-progress-container');
|
||||
if (!isControlElement && timeDiff < 300 && timeDiff > 0) {
|
||||
e.preventDefault();
|
||||
if (floatingBtns.classList.contains('visible')) {
|
||||
hideControls();
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
} else {
|
||||
showControls();
|
||||
}
|
||||
}
|
||||
lastTap = now;
|
||||
});
|
||||
|
||||
const bookmarkFloatBtn = document.getElementById('bookmarkFloatBtn');
|
||||
const closePanelBtn = document.getElementById('closePanelBtn');
|
||||
if (bookmarkFloatBtn) {
|
||||
bookmarkFloatBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
bookmarkPanel.classList.toggle('show');
|
||||
if (bookmarkPanel.classList.contains('show')) {
|
||||
showControls();
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
} else {
|
||||
resetHideTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (closePanelBtn) {
|
||||
closePanelBtn.addEventListener('click', () => {
|
||||
bookmarkPanel.classList.remove('show');
|
||||
resetHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
const themeFloatBtn = document.getElementById('themeFloatBtn');
|
||||
if (themeFloatBtn) {
|
||||
themeFloatBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (themeSelector.classList.contains('visible')) {
|
||||
themeSelector.classList.remove('visible');
|
||||
} else {
|
||||
themeSelector.classList.add('visible');
|
||||
resetHideTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllThemeStyles() {
|
||||
const bodyClass = document.body.className;
|
||||
const scrollBtn = document.getElementById('scrollToggleBtn');
|
||||
const bookmarkBtn = document.getElementById('bookmarkFloatBtn');
|
||||
const topBarBookmarkBtn = document.querySelector('.top-bar button.bookmark');
|
||||
|
||||
if (bodyClass.includes('aurora')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#7cffd0';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(124, 255, 208, 0.2)'; bookmarkBtn.style.border = '1px solid #7cffd0'; bookmarkBtn.style.color = '#7cffd0'; }
|
||||
} else if (bodyClass.includes('neon')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#0ff';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(0, 255, 255, 0.15)'; bookmarkBtn.style.border = '1px solid #0ff'; bookmarkBtn.style.color = '#0ff'; }
|
||||
} else if (bodyClass.includes('sunset-dynamic')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#ffb86b';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(255, 184, 107, 0.2)'; bookmarkBtn.style.border = '1px solid #ffb86b'; bookmarkBtn.style.color = '#ffb86b'; }
|
||||
} else if (bodyClass.includes('wave')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#5bc0ff';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(91, 192, 255, 0.2)'; bookmarkBtn.style.border = '1px solid #5bc0ff'; bookmarkBtn.style.color = '#5bc0ff'; }
|
||||
} else if (bodyClass.includes('fire')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#ff8c42';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(255, 140, 66, 0.2)'; bookmarkBtn.style.border = '1px solid #ff8c42'; bookmarkBtn.style.color = '#ff8c42'; }
|
||||
} else if (bodyClass.includes('sakura')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#e86f8f';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(232, 111, 143, 0.2)'; bookmarkBtn.style.border = '1px solid #e86f8f'; bookmarkBtn.style.color = '#b83b5e'; }
|
||||
} else if (bodyClass.includes('mintfrost')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#3a9a9a';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(58, 154, 154, 0.2)'; bookmarkBtn.style.border = '1px solid #3a9a9a'; bookmarkBtn.style.color = '#2a7a7a'; }
|
||||
} else if (bodyClass.includes('lavenderfield')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#8b6bbf';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(139, 107, 191, 0.2)'; bookmarkBtn.style.border = '1px solid #8b6bbf'; bookmarkBtn.style.color = '#5a4a8a'; }
|
||||
} else if (bodyClass.includes('golden')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#d4a030';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(212, 160, 48, 0.2)'; bookmarkBtn.style.border = '1px solid #d4a030'; bookmarkBtn.style.color = '#8a6a2a'; }
|
||||
} else if (bodyClass.includes('coralreef')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#ff6644';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(255, 102, 68, 0.2)'; bookmarkBtn.style.border = '1px solid #ff6644'; bookmarkBtn.style.color = '#8a3010'; }
|
||||
} else if (bodyClass.includes('galaxy')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#aaacff';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(170, 172, 255, 0.2)'; bookmarkBtn.style.border = '1px solid #aaacff'; bookmarkBtn.style.color = '#aaacff'; }
|
||||
} else if (bodyClass.includes('rosegarden')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#d888a8';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(216, 136, 168, 0.2)'; bookmarkBtn.style.border = '1px solid #d888a8'; bookmarkBtn.style.color = '#a03050'; }
|
||||
} else if (bodyClass.includes('eyecare')) {
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(139, 154, 110, 0.2)'; bookmarkBtn.style.border = '1px solid #8b9a6e'; bookmarkBtn.style.color = '#2d2d2d'; }
|
||||
} else {
|
||||
if (scrollBtn) scrollBtn.style.color = '';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = ''; bookmarkBtn.style.border = ''; bookmarkBtn.style.color = ''; }
|
||||
}
|
||||
if (topBarBookmarkBtn) {
|
||||
if (bodyClass.includes('aurora')) { topBarBookmarkBtn.style.background = 'rgba(124, 255, 208, 0.2)'; topBarBookmarkBtn.style.border = '1px solid #7cffd0'; topBarBookmarkBtn.style.color = '#7cffd0'; }
|
||||
else if (bodyClass.includes('neon')) { topBarBookmarkBtn.style.background = 'rgba(0, 255, 255, 0.2)'; topBarBookmarkBtn.style.border = '1px solid #0ff'; topBarBookmarkBtn.style.color = '#0ff'; }
|
||||
else if (bodyClass.includes('eyecare')) { topBarBookmarkBtn.style.background = '#8b9a6e'; topBarBookmarkBtn.style.border = 'none'; topBarBookmarkBtn.style.color = '#2d2d2d'; }
|
||||
else { topBarBookmarkBtn.style.background = ''; topBarBookmarkBtn.style.border = ''; topBarBookmarkBtn.style.color = ''; }
|
||||
}
|
||||
}
|
||||
|
||||
const THEMES = [
|
||||
'deep-space', 'ocean', 'cherry', 'night', 'forest', 'sunset',
|
||||
'lavender', 'blueberry', 'amber', 'coral', 'mint', 'rosegold',
|
||||
'eyecare',
|
||||
'aurora-dynamic', 'neon-dynamic', 'sunset-dynamic', 'wave-dynamic', 'fire-dynamic',
|
||||
'sakura-dynamic', 'mintfrost-dynamic', 'lavenderfield-dynamic', 'golden-dynamic',
|
||||
'coralreef-dynamic', 'galaxy-dynamic', 'rosegarden-dynamic'
|
||||
];
|
||||
function setTheme(themeName) {
|
||||
document.body.className = 'theme-' + themeName;
|
||||
localStorage.setItem('reader_theme', themeName);
|
||||
document.querySelectorAll('.theme-dot').forEach(dot => {
|
||||
if (dot.dataset.theme === themeName) dot.classList.add('active');
|
||||
else dot.classList.remove('active');
|
||||
});
|
||||
if (window.updateAllTooltipColors) window.updateAllTooltipColors();
|
||||
updateAllThemeStyles();
|
||||
setTimeout(() => { if (typeof updateGlobalProgress === 'function') updateGlobalProgress(); }, 50);
|
||||
}
|
||||
const savedTheme = localStorage.getItem('reader_theme');
|
||||
if (savedTheme && THEMES.includes(savedTheme)) setTheme(savedTheme);
|
||||
else setTheme('deep-space');
|
||||
|
||||
document.querySelectorAll('.theme-dot').forEach(dot => {
|
||||
dot.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
setTheme(dot.dataset.theme);
|
||||
themeSelector.classList.remove('visible');
|
||||
showToast('🎨 主题已切换');
|
||||
resetHideTimer();
|
||||
});
|
||||
});
|
||||
|
||||
// 滚动速度相关逻辑
|
||||
const scrollBtn = document.getElementById('scrollToggleBtn');
|
||||
if (scrollBtn) {
|
||||
scrollBtn.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
if (fontControls.classList.contains('visible')) fontControls.classList.remove('visible');
|
||||
else fontControls.classList.add('visible');
|
||||
resetHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
const speedSlider = document.getElementById('speedSlider');
|
||||
const speedValue = document.getElementById('speedValue');
|
||||
const speedPresets = document.querySelectorAll('.speed-preset');
|
||||
let currentSpeed = 6;
|
||||
let autoScrollInterval = null;
|
||||
let isAutoScrolling = false;
|
||||
const savedSpeed = localStorage.getItem('scroll_speed');
|
||||
if (savedSpeed) {
|
||||
currentSpeed = parseInt(savedSpeed);
|
||||
if (speedSlider) speedSlider.value = currentSpeed;
|
||||
if (speedValue) speedValue.innerText = currentSpeed + ' px/帧';
|
||||
speedPresets.forEach(preset => {
|
||||
if (parseInt(preset.dataset.speed) === currentSpeed) preset.classList.add('active');
|
||||
else preset.classList.remove('active');
|
||||
});
|
||||
}
|
||||
function updateSpeed(newSpeed) {
|
||||
currentSpeed = Math.min(30, Math.max(1, newSpeed));
|
||||
if (speedSlider) speedSlider.value = currentSpeed;
|
||||
if (speedValue) speedValue.innerText = currentSpeed + ' px/帧';
|
||||
localStorage.setItem('scroll_speed', currentSpeed);
|
||||
speedPresets.forEach(preset => {
|
||||
if (parseInt(preset.dataset.speed) === currentSpeed) preset.classList.add('active');
|
||||
else preset.classList.remove('active');
|
||||
});
|
||||
if (isAutoScrolling) { stopAutoScroll(); startAutoScroll(); }
|
||||
}
|
||||
if (speedSlider) {
|
||||
speedSlider.oninput = (e) => { updateSpeed(parseInt(e.target.value)); showToast(`⚡ 速度 ${currentSpeed} px/帧`); resetHideTimer(); };
|
||||
}
|
||||
speedPresets.forEach(preset => {
|
||||
preset.onclick = () => { updateSpeed(parseInt(preset.dataset.speed)); showToast(`⚡ ${preset.innerText} ${currentSpeed} px/帧`); resetHideTimer(); };
|
||||
});
|
||||
function startAutoScroll() {
|
||||
if (autoScrollInterval) clearInterval(autoScrollInterval);
|
||||
autoScrollInterval = setInterval(() => window.scrollBy(0, currentSpeed), 25);
|
||||
isAutoScrolling = true;
|
||||
if (scrollBtn) { scrollBtn.classList.add('active'); scrollBtn.innerHTML = "⏸"; }
|
||||
showToast(`▶ 滚动中 (${currentSpeed}px/帧)`);
|
||||
}
|
||||
function stopAutoScroll() {
|
||||
if (autoScrollInterval) { clearInterval(autoScrollInterval); autoScrollInterval = null; }
|
||||
isAutoScrolling = false;
|
||||
if (scrollBtn) { scrollBtn.classList.remove('active'); scrollBtn.innerHTML = "▼"; }
|
||||
showToast('⏹ 已停止');
|
||||
}
|
||||
if (scrollBtn) {
|
||||
scrollBtn.onclick = (e) => { e.stopPropagation(); if (isAutoScrolling) stopAutoScroll(); else startAutoScroll(); resetHideTimer(); };
|
||||
}
|
||||
|
||||
// 书签功能
|
||||
const STORAGE_KEY = "bookmarks_v6";
|
||||
function getBookmarks(){
|
||||
try{ return JSON.parse(localStorage.getItem(STORAGE_KEY)||'[]'); }catch(e){ return []; }
|
||||
}
|
||||
function saveBookmarks(list){
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
||||
refreshBookmarkList();
|
||||
}
|
||||
function refreshBookmarkList(){
|
||||
let list = getBookmarks();
|
||||
let container = document.getElementById('bookmarkList');
|
||||
if(!container) return;
|
||||
if(list.length===0){
|
||||
container.innerHTML='<div class="empty-bookmark">📭 暂无书签<br>点击 ⭐ 添加</div>';
|
||||
return;
|
||||
}
|
||||
list.sort((a,b)=>b.time-a.time);
|
||||
let html='';
|
||||
for(let b of list){
|
||||
let pageLabel = b.type==='txt'?'第'+b.page+'章':(b.type==='ebook'?'第'+b.page+'章':'第'+b.page+'页');
|
||||
html+=`<div class="bookmark-item" data-book="${escapeHtml(b.book)}" data-chapter="${escapeHtml(b.chapter)}" data-page="${b.page}">
|
||||
<span class="delete" data-book="${escapeHtml(b.book)}" data-chapter="${escapeHtml(b.chapter)}">🗑</span>
|
||||
<div class="title">📖 ${escapeHtml(b.book.length>18?b.book.substring(0,18)+'...':b.book)}</div>
|
||||
<div class="info">📄 ${escapeHtml(b.chapterName||b.chapter.substring(0,25))} | 📍 ${pageLabel}</div>
|
||||
</div>`;
|
||||
}
|
||||
container.innerHTML=html;
|
||||
document.querySelectorAll('#bookmarkList .bookmark-item').forEach(item=>{
|
||||
let book=item.getAttribute('data-book'), chapter=item.getAttribute('data-chapter'), page=parseInt(item.getAttribute('data-page'))||1;
|
||||
item.onclick=(e)=>{ if(e.target.classList.contains('delete')) return; jumpToBookmark(book,chapter,page); };
|
||||
let db=item.querySelector('.delete');
|
||||
if(db) db.onclick=(e)=>{ e.stopPropagation(); removeBookmark(db.getAttribute('data-book'), db.getAttribute('data-chapter')); };
|
||||
});
|
||||
}
|
||||
function removeBookmark(book,chapter){
|
||||
let list=getBookmarks();
|
||||
list=list.filter(b=>!(b.book===book&&b.chapter===chapter));
|
||||
saveBookmarks(list);
|
||||
showToast('🗑 删除书签');
|
||||
}
|
||||
function escapeHtml(s){ return (s||'').replace(/[&<>]/g,m=>({'&':'&','<':'<','>':'>'}[m])); }
|
||||
let CURRENT_BOOK = '', CURRENT_CHAPTER = '';
|
||||
function jumpToBookmark(book,chapter,page){
|
||||
if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){
|
||||
if (typeof jumpToPage === 'function') jumpToPage(page);
|
||||
else if (typeof renderChapter === 'function') renderChapter(Math.min(Math.max(1,page), (typeof chapters !== 'undefined' ? chapters.length : 1))-1);
|
||||
}else{
|
||||
sessionStorage.setItem('jump_target', JSON.stringify({book,chapter,page}));
|
||||
location.href = '<?php echo $currentFile; ?>?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);
|
||||
}
|
||||
}
|
||||
|
||||
// 自动翻章功能
|
||||
let autoChapterEnabled = true;
|
||||
let isTurningPage = false;
|
||||
let turnTimer = null;
|
||||
let lastScrollTop = 0;
|
||||
let scrollDirection = 'down';
|
||||
const autoChapterCheckbox = document.getElementById('autoChapterCheckbox');
|
||||
|
||||
function getThemeColorsForAnimation() {
|
||||
const bodyClass = document.body.className;
|
||||
let overlayBg = 'rgba(15, 12, 41, 0.92)', leftBg = 'rgba(48, 43, 99, 0.95)', rightBg = 'rgba(48, 43, 99, 0.95)';
|
||||
let leftBorder = '2px solid rgba(155, 89, 182, 0.6)', rightBorder = '2px solid rgba(155, 89, 182, 0.6)';
|
||||
let msgBg = 'rgba(48, 43, 99, 0.95)', msgColor = '#bb86fc', msgBorder = '1px solid rgba(155, 89, 182, 0.5)';
|
||||
|
||||
if (bodyClass.includes('aurora')) {
|
||||
overlayBg = 'rgba(0, 0, 0, 0.6)'; leftBg = rightBg = 'rgba(0, 0, 0, 0.5)';
|
||||
leftBorder = rightBorder = '2px solid rgba(124, 255, 208, 0.4)';
|
||||
msgBg = 'rgba(0, 0, 0, 0.7)'; msgColor = '#7cffd0'; msgBorder = '1px solid rgba(124, 255, 208, 0.5)';
|
||||
} else if (bodyClass.includes('neon')) {
|
||||
overlayBg = 'rgba(0, 0, 0, 0.8)'; leftBg = rightBg = 'rgba(0, 0, 0, 0.7)';
|
||||
leftBorder = rightBorder = '2px solid #0ff'; msgBg = 'rgba(0, 0, 0, 0.9)'; msgColor = '#0ff'; msgBorder = '1px solid #0ff';
|
||||
} else if (bodyClass.includes('sunset-dynamic')) {
|
||||
overlayBg = 'rgba(0, 0, 0, 0.5)'; leftBg = rightBg = 'rgba(30, 20, 30, 0.6)';
|
||||
leftBorder = rightBorder = '2px solid rgba(255, 184, 107, 0.4)'; msgBg = 'rgba(30, 20, 30, 0.8)'; msgColor = '#ffb86b'; msgBorder = '1px solid rgba(255, 184, 107, 0.5)';
|
||||
} else if (bodyClass.includes('wave')) {
|
||||
overlayBg = 'rgba(10, 40, 60, 0.7)'; leftBg = rightBg = 'rgba(10, 40, 60, 0.6)';
|
||||
leftBorder = rightBorder = '2px solid rgba(91, 192, 255, 0.4)'; msgBg = 'rgba(10, 40, 60, 0.8)'; msgColor = '#5bc0ff'; msgBorder = '1px solid rgba(91, 192, 255, 0.5)';
|
||||
} else if (bodyClass.includes('fire')) {
|
||||
overlayBg = 'rgba(60, 10, 10, 0.7)'; leftBg = rightBg = 'rgba(60, 10, 10, 0.6)';
|
||||
leftBorder = rightBorder = '2px solid rgba(255, 140, 66, 0.4)'; msgBg = 'rgba(60, 10, 10, 0.8)'; msgColor = '#ff8c42'; msgBorder = '1px solid rgba(255, 140, 66, 0.5)';
|
||||
} else if (bodyClass.includes('sakura')) {
|
||||
overlayBg = 'rgba(255, 240, 245, 0.85)'; leftBg = rightBg = 'rgba(255, 245, 250, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(232, 111, 143, 0.5)'; msgBg = 'rgba(255, 240, 245, 0.95)'; msgColor = '#b83b5e'; msgBorder = '1px solid #e86f8f';
|
||||
} else if (bodyClass.includes('mintfrost')) {
|
||||
overlayBg = 'rgba(200, 232, 233, 0.85)'; leftBg = rightBg = 'rgba(220, 245, 245, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(58, 154, 154, 0.5)'; msgBg = 'rgba(200, 232, 233, 0.95)'; msgColor = '#2a7a7a'; msgBorder = '1px solid #3a9a9a';
|
||||
} else if (bodyClass.includes('lavenderfield')) {
|
||||
overlayBg = 'rgba(216, 204, 232, 0.85)'; leftBg = rightBg = 'rgba(230, 220, 245, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(139, 107, 191, 0.5)'; msgBg = 'rgba(216, 204, 232, 0.95)'; msgColor = '#5a4a8a'; msgBorder = '1px solid #8b6bbf';
|
||||
} else if (bodyClass.includes('golden')) {
|
||||
overlayBg = 'rgba(245, 230, 184, 0.85)'; leftBg = rightBg = 'rgba(255, 250, 220, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(212, 160, 48, 0.5)'; msgBg = 'rgba(245, 230, 184, 0.95)'; msgColor = '#8a6a2a'; msgBorder = '1px solid #d4a030';
|
||||
} else if (bodyClass.includes('coralreef')) {
|
||||
overlayBg = 'rgba(255, 170, 136, 0.85)'; leftBg = rightBg = 'rgba(255, 200, 180, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(255, 102, 68, 0.5)'; msgBg = 'rgba(255, 170, 136, 0.95)'; msgColor = '#8a3010'; msgBorder = '1px solid #ff6644';
|
||||
} else if (bodyClass.includes('galaxy')) {
|
||||
overlayBg = 'rgba(10, 10, 42, 0.85)'; leftBg = rightBg = 'rgba(30, 30, 70, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(170, 172, 255, 0.5)'; msgBg = 'rgba(10, 10, 42, 0.95)'; msgColor = '#aaacff'; msgBorder = '1px solid #aaacff';
|
||||
} else if (bodyClass.includes('rosegarden')) {
|
||||
overlayBg = 'rgba(245, 200, 216, 0.85)'; leftBg = rightBg = 'rgba(255, 230, 240, 0.9)';
|
||||
leftBorder = rightBorder = '2px solid rgba(216, 136, 168, 0.5)'; msgBg = 'rgba(245, 200, 216, 0.95)'; msgColor = '#a03050'; msgBorder = '1px solid #d888a8';
|
||||
} else if (bodyClass.includes('eyecare')) {
|
||||
overlayBg = 'rgba(199, 237, 204, 0.92)'; leftBg = rightBg = 'rgba(215, 245, 210, 0.95)';
|
||||
leftBorder = rightBorder = '2px solid rgba(139, 154, 110, 0.5)'; msgBg = 'rgba(215, 245, 210, 0.95)'; msgColor = '#2d2d2d'; msgBorder = '1px solid rgba(139, 154, 110, 0.4)';
|
||||
}
|
||||
return { overlayBg, leftBg, rightBg, leftBorder, rightBorder, msgBg, msgColor, msgBorder };
|
||||
}
|
||||
|
||||
function showBookTurnAnimation(callback) {
|
||||
if (isTurningPage) { if (callback) callback(); return; }
|
||||
isTurningPage = true;
|
||||
const colors = getThemeColorsForAnimation();
|
||||
let overlay = document.createElement('div');
|
||||
overlay.className = 'page-turn-overlay';
|
||||
overlay.style.background = colors.overlayBg;
|
||||
overlay.innerHTML = `<div class="book-container"><div class="book-left">📖</div><div class="book-right">📖</div><div class="message">✨ 正在翻开新篇章 ✨</div></div>`;
|
||||
const bookLeft = overlay.querySelector('.book-left'), bookRight = overlay.querySelector('.book-right'), message = overlay.querySelector('.message');
|
||||
if (bookLeft) { bookLeft.style.background = colors.leftBg; bookLeft.style.border = colors.leftBorder; bookLeft.style.color = colors.msgColor; }
|
||||
if (bookRight) { bookRight.style.background = colors.rightBg; bookRight.style.border = colors.rightBorder; bookRight.style.color = colors.msgColor; }
|
||||
if (message) { message.style.background = colors.msgBg; message.style.color = colors.msgColor; message.style.border = colors.msgBorder; }
|
||||
document.body.appendChild(overlay);
|
||||
setTimeout(() => { if (callback) callback(); setTimeout(() => { if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); isTurningPage = false; }, 150); }, 450);
|
||||
}
|
||||
|
||||
if (localStorage.getItem('autoChapterEnabled') !== null) {
|
||||
autoChapterEnabled = localStorage.getItem('autoChapterEnabled') === 'true';
|
||||
if (autoChapterCheckbox) autoChapterCheckbox.checked = autoChapterEnabled;
|
||||
} else { autoChapterEnabled = true; if (autoChapterCheckbox) autoChapterCheckbox.checked = true; }
|
||||
if (autoChapterCheckbox) {
|
||||
autoChapterCheckbox.onchange = function(e) {
|
||||
autoChapterEnabled = this.checked;
|
||||
localStorage.setItem('autoChapterEnabled', autoChapterEnabled);
|
||||
showToast(autoChapterEnabled ? '✅ 自动翻章已开启' : '⏹ 自动翻章已关闭');
|
||||
resetHideTimer();
|
||||
};
|
||||
}
|
||||
|
||||
let scrollTimer = null;
|
||||
function checkScrollBottom() {
|
||||
if (!autoChapterEnabled || isTurningPage) return;
|
||||
let totalHeight = document.body.scrollHeight, windowHeight = window.innerHeight, scrollTop = window.scrollY;
|
||||
if (scrollTop > lastScrollTop) scrollDirection = 'down';
|
||||
else if (scrollTop < lastScrollTop) { scrollDirection = 'up'; if (turnTimer) { clearTimeout(turnTimer); turnTimer = null; } }
|
||||
lastScrollTop = scrollTop;
|
||||
let isAtBottom = (scrollTop + windowHeight + 15) >= totalHeight;
|
||||
if (isAtBottom && scrollDirection === 'down' && !turnTimer) {
|
||||
let nextBtn = document.getElementById('nextChapterBtn');
|
||||
if (nextBtn && !nextBtn.disabled) {
|
||||
showToast('📖 3秒后自动翻到下一章...');
|
||||
turnTimer = setTimeout(() => {
|
||||
if (!autoChapterEnabled || isTurningPage) { turnTimer = null; return; }
|
||||
if ((window.scrollY + windowHeight + 15) >= document.body.scrollHeight) {
|
||||
showBookTurnAnimation(() => { if (nextBtn) nextBtn.click(); });
|
||||
}
|
||||
turnTimer = null;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', function() { if (scrollTimer) clearTimeout(scrollTimer); scrollTimer = setTimeout(checkScrollBottom, 100); });
|
||||
function onChapterChange() { if (turnTimer) { clearTimeout(turnTimer); turnTimer = null; } isTurningPage = false; lastScrollTop = 0; scrollDirection = 'down'; window.scrollTo(0, 0); if (typeof updateGlobalProgress === 'function') updateGlobalProgress(); }
|
||||
|
||||
// 页面转场动画
|
||||
const pageTransition = {
|
||||
element: document.getElementById('pageTransition'),
|
||||
show() { if (!this.element) return; this.element.classList.add('active'); if (this.timeout) clearTimeout(this.timeout); this.timeout = setTimeout(() => { if (this.element) this.element.classList.remove('active'); }, 3000); },
|
||||
hide() { if (!this.element) return; this.element.classList.remove('active'); if (this.timeout) clearTimeout(this.timeout); }
|
||||
};
|
||||
function rippleHandler(e) {
|
||||
const ripple = document.createElement('span'); ripple.classList.add('ripple');
|
||||
const rect = this.getBoundingClientRect(); const size = Math.max(rect.width, rect.height);
|
||||
const x = e.clientX - rect.left - size / 2, y = e.clientY - rect.top - size / 2;
|
||||
ripple.style.width = ripple.style.height = size + 'px'; ripple.style.left = x + 'px'; ripple.style.top = y + 'px';
|
||||
this.style.position = 'relative'; this.style.overflow = 'hidden'; this.appendChild(ripple);
|
||||
setTimeout(() => ripple.remove(), 500);
|
||||
if (this.tagName === 'A' && this.getAttribute('href') && !this.hasAttribute('data-no-transition')) {
|
||||
e.preventDefault(); const targetUrl = this.getAttribute('href');
|
||||
if (targetUrl && targetUrl !== '#') { pageTransition.show(); setTimeout(() => { window.location.href = targetUrl; }, 280); }
|
||||
}
|
||||
}
|
||||
function enhanceBookCards() { document.querySelectorAll('.shelf-item, .book-chapter-item a').forEach(card => { card.removeEventListener('click', rippleHandler); card.addEventListener('click', rippleHandler); }); }
|
||||
function initPageLoadAnimation() { pageTransition.hide(); enhanceBookCards(); }
|
||||
document.addEventListener('DOMContentLoaded', () => { initPageLoadAnimation(); enhanceBookCards(); updateAllThemeStyles(); });
|
||||
window.addEventListener('pageshow', () => { pageTransition.hide(); });
|
||||
window.addEventListener('beforeunload', () => { pageTransition.hide(); });
|
||||
|
||||
// 全局进度条
|
||||
let globalChapters = [], globalTotalChapters = 0, globalCurrentIndex = 0, lastChapterIndex = -1, tooltipHideTimer = null;
|
||||
function getThemeTooltipColors() {
|
||||
const bodyClass = document.body.className;
|
||||
if (bodyClass.includes('aurora')) return { bgColor: 'rgba(0, 0, 0, 0.75)', textColor: '#7cffd0', borderColor: '#7cffd0' };
|
||||
if (bodyClass.includes('neon')) return { bgColor: 'rgba(0, 0, 0, 0.9)', textColor: '#0ff', borderColor: '#0ff' };
|
||||
if (bodyClass.includes('sunset-dynamic')) return { bgColor: 'rgba(30, 20, 30, 0.85)', textColor: '#ffb86b', borderColor: '#ffb86b' };
|
||||
if (bodyClass.includes('wave')) return { bgColor: 'rgba(0, 20, 30, 0.9)', textColor: '#5bc0ff', borderColor: '#5bc0ff' };
|
||||
if (bodyClass.includes('fire')) return { bgColor: 'rgba(60, 10, 10, 0.92)', textColor: '#ff8c42', borderColor: '#ff8c42' };
|
||||
if (bodyClass.includes('sakura')) return { bgColor: 'rgba(255, 240, 245, 0.95)', textColor: '#b83b5e', borderColor: '#e86f8f' };
|
||||
if (bodyClass.includes('mintfrost')) return { bgColor: 'rgba(200, 232, 233, 0.95)', textColor: '#2a7a7a', borderColor: '#3a9a9a' };
|
||||
if (bodyClass.includes('lavenderfield')) return { bgColor: 'rgba(216, 204, 232, 0.95)', textColor: '#5a4a8a', borderColor: '#8b6bbf' };
|
||||
if (bodyClass.includes('golden')) return { bgColor: 'rgba(245, 230, 184, 0.95)', textColor: '#8a6a2a', borderColor: '#d4a030' };
|
||||
if (bodyClass.includes('coralreef')) return { bgColor: 'rgba(255, 170, 136, 0.95)', textColor: '#8a3010', borderColor: '#ff6644' };
|
||||
if (bodyClass.includes('galaxy')) return { bgColor: 'rgba(10, 10, 42, 0.95)', textColor: '#aaacff', borderColor: '#aaacff' };
|
||||
if (bodyClass.includes('rosegarden')) return { bgColor: 'rgba(245, 200, 216, 0.95)', textColor: '#a03050', borderColor: '#d888a8' };
|
||||
if (bodyClass.includes('eyecare')) return { bgColor: 'rgba(215, 245, 210, 0.98)', textColor: '#2d2d2d', borderColor: '#8b9a6e' };
|
||||
return { bgColor: 'rgba(0,0,0,0.95)', textColor: '#ff9800', borderColor: 'rgba(255,152,0,0.6)' };
|
||||
}
|
||||
function updateTooltipStyle(tooltip) { if (!tooltip) return; const c = getThemeTooltipColors(); tooltip.style.backgroundColor = c.bgColor; tooltip.style.color = c.textColor; tooltip.style.border = `1px solid ${c.borderColor}`; }
|
||||
window.updateAllTooltipColors = function() { const tooltip = document.getElementById('chapterTooltip'); if (tooltip) updateTooltipStyle(tooltip); updateAllThemeStyles(); };
|
||||
function showChapterTooltip(chapterIndex, chapterTitle) {
|
||||
let tooltip = document.getElementById('chapterTooltip');
|
||||
if (!tooltip) return;
|
||||
if (tooltipHideTimer) clearTimeout(tooltipHideTimer);
|
||||
let displayText = `📖 第 ${chapterIndex+1} 章 · ${chapterTitle.substring(0, 32)}${chapterTitle.length > 32 ? '...' : ''}`;
|
||||
tooltip.textContent = displayText;
|
||||
updateTooltipStyle(tooltip);
|
||||
tooltip.style.display = 'block';
|
||||
tooltipHideTimer = setTimeout(() => { if (tooltip) tooltip.style.display = 'none'; }, 2000);
|
||||
}
|
||||
function createGlobalProgressBar(total, currentIdx, chaptersList) {
|
||||
let container = document.getElementById('globalProgressPlaceholder');
|
||||
if (!container) return;
|
||||
container.innerHTML = `<div class="global-progress-container" id="globalProgressBar"><div class="progress-range-area"><input type="range" class="progress-slider-global" id="globalProgressSlider" min="0" max="${total-1}" value="${currentIdx}" step="1"><div class="chapter-tooltip" id="chapterTooltip" style="display: none;">📖 ${escapeHtml(chaptersList[currentIdx]?.title || '章节')}</div></div><div class="progress-info"><div class="progress-label"><span>📖 第 ${currentIdx+1} / ${total} 章</span></div><div>${escapeHtml(chaptersList[currentIdx]?.title || '')}</div></div></div>`;
|
||||
globalProgressBar = document.getElementById('globalProgressBar');
|
||||
let slider = document.getElementById('globalProgressSlider');
|
||||
if (slider) {
|
||||
slider.addEventListener('input', (e) => { let idx = parseInt(e.target.value); if (idx !== lastChapterIndex) { lastChapterIndex = idx; showChapterTooltip(idx, chaptersList[idx]?.title || '章节'); } });
|
||||
slider.addEventListener('change', (e) => { let idx = parseInt(e.target.value); if (typeof renderChapter === 'function') { renderChapter(idx); showToast(`📖 跳转到第 ${idx+1} 章`); } resetHideTimer(); });
|
||||
}
|
||||
if (globalProgressBar) globalProgressBar.classList.add('hide');
|
||||
}
|
||||
function updateGlobalProgress() {
|
||||
let container = document.getElementById('globalProgressBar');
|
||||
if (!container) return;
|
||||
let slider = document.getElementById('globalProgressSlider');
|
||||
let infoLabel = container.querySelector('.progress-label span');
|
||||
let infoTitle = container.querySelector('.progress-info > div:last-child');
|
||||
if (slider && globalTotalChapters > 0) {
|
||||
slider.value = globalCurrentIndex;
|
||||
if (infoLabel) infoLabel.innerText = `📖 第 ${globalCurrentIndex+1} / ${globalTotalChapters} 章`;
|
||||
if (infoTitle && globalChapters[globalCurrentIndex]) infoTitle.innerText = globalChapters[globalCurrentIndex].title || '';
|
||||
lastChapterIndex = globalCurrentIndex;
|
||||
}
|
||||
}
|
||||
window.updateGlobalProgress = updateGlobalProgress;
|
||||
window.updateAllTooltipColors = updateAllTooltipColors;
|
||||
window.updateAllThemeStyles = updateAllThemeStyles;
|
||||
|
||||
// ==================== 左右滑动切换章节(手机端) ====================
|
||||
(function() {
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
let minSwipeDistance = 70;
|
||||
let isTextMode = false;
|
||||
let swipeIndicator = null;
|
||||
let leftArrow = null;
|
||||
let rightArrow = null;
|
||||
let swipeTimeout = null;
|
||||
|
||||
function detectTextMode() {
|
||||
const hasPrevBtn = document.getElementById('prevChapterBtn');
|
||||
const hasNextBtn = document.getElementById('nextChapterBtn');
|
||||
const isComic = document.getElementById('comicViewer') !== null;
|
||||
const isPdf = typeof pdfDoc !== 'undefined' && pdfDoc !== null;
|
||||
return (hasPrevBtn && hasNextBtn) && !isComic && !isPdf;
|
||||
}
|
||||
|
||||
function createSwipeUI() {
|
||||
if (swipeIndicator) return;
|
||||
swipeIndicator = document.createElement('div');
|
||||
swipeIndicator.className = 'swipe-indicator';
|
||||
swipeIndicator.innerHTML = '← 滑动切换章节 →';
|
||||
document.body.appendChild(swipeIndicator);
|
||||
|
||||
leftArrow = document.createElement('div');
|
||||
leftArrow.className = 'swipe-arrow-left';
|
||||
leftArrow.innerHTML = '←';
|
||||
document.body.appendChild(leftArrow);
|
||||
|
||||
rightArrow = document.createElement('div');
|
||||
rightArrow.className = 'swipe-arrow-right';
|
||||
rightArrow.innerHTML = '→';
|
||||
document.body.appendChild(rightArrow);
|
||||
|
||||
if (!localStorage.getItem('swipe_hint_shown')) {
|
||||
swipeIndicator.classList.add('show');
|
||||
setTimeout(() => {
|
||||
if (swipeIndicator) swipeIndicator.classList.remove('show');
|
||||
}, 3000);
|
||||
localStorage.setItem('swipe_hint_shown', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
function showSwipeArrows(direction) {
|
||||
if (leftArrow && rightArrow) {
|
||||
if (direction === 'left') {
|
||||
rightArrow.classList.add('show');
|
||||
setTimeout(() => rightArrow.classList.remove('show'), 200);
|
||||
} else if (direction === 'right') {
|
||||
leftArrow.classList.add('show');
|
||||
setTimeout(() => leftArrow.classList.remove('show'), 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function animateChapterTransition(direction, callback) {
|
||||
const reader = document.getElementById('reader');
|
||||
if (!reader) { if (callback) callback(); return; }
|
||||
reader.classList.add('swipe-transition');
|
||||
if (direction === 'next') {
|
||||
reader.classList.add('swipe-slide-left');
|
||||
} else {
|
||||
reader.classList.add('swipe-slide-right');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (callback) callback();
|
||||
setTimeout(() => {
|
||||
reader.classList.remove('swipe-slide-left', 'swipe-slide-right');
|
||||
setTimeout(() => {
|
||||
reader.classList.remove('swipe-transition');
|
||||
}, 50);
|
||||
}, 50);
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function handleTouchStart(e) {
|
||||
if (!detectTextMode()) return;
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
}
|
||||
|
||||
function handleTouchEnd(e) {
|
||||
if (!detectTextMode()) return;
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
const deltaX = touchEndX - touchStartX;
|
||||
|
||||
if (Math.abs(deltaX) < minSwipeDistance) return;
|
||||
|
||||
if (deltaX < -minSwipeDistance) {
|
||||
// 向左滑动 -> 下一章
|
||||
const nextBtn = document.getElementById('nextChapterBtn');
|
||||
if (nextBtn && !nextBtn.disabled) {
|
||||
if (window.navigator && window.navigator.vibrate) {
|
||||
window.navigator.vibrate(20);
|
||||
}
|
||||
showSwipeArrows('left');
|
||||
animateChapterTransition('next', () => {
|
||||
nextBtn.click();
|
||||
showToast('📖 下一章');
|
||||
});
|
||||
} else {
|
||||
showToast('📖 已经是最后一章了');
|
||||
}
|
||||
} else if (deltaX > minSwipeDistance) {
|
||||
// 向右滑动 -> 上一章
|
||||
const prevBtn = document.getElementById('prevChapterBtn');
|
||||
if (prevBtn && !prevBtn.disabled) {
|
||||
if (window.navigator && window.navigator.vibrate) {
|
||||
window.navigator.vibrate(20);
|
||||
}
|
||||
showSwipeArrows('right');
|
||||
animateChapterTransition('prev', () => {
|
||||
prevBtn.click();
|
||||
showToast('📖 上一章');
|
||||
});
|
||||
} else {
|
||||
showToast('📖 已经是第一章了');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
document.addEventListener('touchend', handleTouchEnd);
|
||||
|
||||
setTimeout(() => {
|
||||
if (detectTextMode()) {
|
||||
createSwipeUI();
|
||||
}
|
||||
}, 500);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php if ($isChapterPage && $isPdf): ?>
|
||||
<!-- PDF阅读页 -->
|
||||
<div class="top-bar">
|
||||
<div class="top-bar-left">
|
||||
<button class="back-btn" id="backBtn">←</button>
|
||||
<div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div>
|
||||
</div>
|
||||
<div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div>
|
||||
</div>
|
||||
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
|
||||
<div class="content" id="reader"><div class="loading-msg" id="loadingMsg">⏳ 正在加载 PDF...<br><?php echo htmlspecialchars($chapter); ?></div></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const PDF_URL = "<?php echo $fileUrl; ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
|
||||
let pdfDoc=null,totalPages=0,renderedPages=new Set(),targetPage=null,scale=1.5;
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
function getCurrentPage(){let cs=document.querySelectorAll('.canvas-container');for(let i=0;i<cs.length;i++){let r=cs[i].getBoundingClientRect();if(r.top<=150&&r.bottom>=100){let p=parseInt(cs[i].getAttribute('data-page'));if(!isNaN(p))return p;}}return 1;}
|
||||
function addBookmark(){let p=getCurrentPage(),l=getBookmarks(),i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:p,time:Date.now()};if(i>=0)l[i]=n;else l.push(n);saveBookmarks(l);showToast('✅ 第 '+p+' 页');}
|
||||
function jumpToPage(p){p=Math.min(Math.max(1,p),totalPages);let t=document.querySelector(`.canvas-container[data-page="${p}"]`);if(t){t.scrollIntoView({behavior:'smooth',block:'start'});showToast('✨ 第 '+p+' 页');}else{showToast('📖 加载中...');(async()=>{let s=Math.max(1,p-3),e=Math.min(totalPages,p+3);for(let i=s;i<=e;i++)if(!renderedPages.has(i))await renderPage(i);setTimeout(()=>{let c=document.querySelector(`.canvas-container[data-page="${p}"]`);if(c){c.scrollIntoView({behavior:'smooth',block:'start'});showToast('✨ 第 '+p+' 页');}},300);})();}}
|
||||
window.jumpToBookmark = function(book,chapter,p){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER)jumpToPage(p);else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page:p}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
async function renderPage(n){if(!pdfDoc||renderedPages.has(n))return;renderedPages.add(n);let div=document.createElement('div');div.className='canvas-container';div.setAttribute('data-page',n);let p=document.createElement('div');p.className='loading-placeholder';p.innerText=`⏳ 第 ${n} 页...`;div.appendChild(p);let ins=false,ex=document.querySelectorAll('.canvas-container');for(let i=0;i<ex.length;i++){let ep=parseInt(ex[i].getAttribute('data-page'));if(ep>n){ex[i].before(div);ins=true;break;}}if(!ins)document.getElementById('reader').appendChild(div);try{let page=await pdfDoc.getPage(n),vp=page.getViewport({scale:scale}),cv=document.createElement('canvas');cv.width=vp.width;cv.height=vp.height;cv.style.width='100%';cv.style.height='auto';await page.render({canvasContext:cv.getContext('2d'),viewport:vp}).promise;div.innerHTML='';div.appendChild(cv);let pf=document.getElementById('progressFill');if(pf)pf.style.width=(renderedPages.size/totalPages)*100+'%';}catch(e){p.innerText=`❌ 第 ${n} 页失败`;}}
|
||||
let st;function onScrollLoad(){if(st)clearTimeout(st);st=setTimeout(()=>{if(!pdfDoc)return;let cs=document.querySelectorAll('.canvas-container'),need=new Set();cs.forEach(c=>{let r=c.getBoundingClientRect();if(r.top-600<window.innerHeight&&r.bottom+600>0){let p=parseInt(c.getAttribute('data-page'));if(!isNaN(p))need.add(p);}});let toRender=[];need.forEach(p=>{for(let i=-2;i<=2;i++){let np=p+i;if(np>=1&&np<=totalPages&&!renderedPages.has(np))toRender.push(np);}});toRender.sort((a,b)=>a-b).forEach(p=>renderPage(p));},200);}
|
||||
async function loadPDF(){try{let lm=document.getElementById('loadingMsg');lm.style.display='block';pdfDoc=await pdfjsLib.getDocument(PDF_URL).promise;totalPages=pdfDoc.numPages;lm.innerText=`📄 共 ${totalPages} 页,加载中...`;let jump=sessionStorage.getItem('jump_target');if(jump){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jump);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)targetPage=t.page;}catch(e){}}else{let bks=getBookmarks(),ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);if(ex&&ex.page)targetPage=ex.page;}for(let i=1;i<=Math.min(5,totalPages);i++)await renderPage(i);lm.style.display='none';if(targetPage){let s=Math.max(1,targetPage-2),e=Math.min(totalPages,targetPage+2);for(let i=s;i<=e;i++)if(!renderedPages.has(i))await renderPage(i);setTimeout(()=>{let c=document.querySelector(`.canvas-container[data-page="${targetPage}"]`);if(c){c.scrollIntoView({behavior:'smooth',block:'start'});showToast('📖 第 '+targetPage+' 页');}targetPage=null;},500);}window.addEventListener('scroll',onScrollLoad);}catch(e){document.getElementById('loadingMsg').innerHTML=`❌ 加载失败<br>${e.message}`;}}
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
loadPDF(); refreshBookmarkList();
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
|
||||
<?php elseif ($isChapterPage && $isTxt && $txtData): ?>
|
||||
<!-- TXT小说阅读页 -->
|
||||
<div class="top-bar"><div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div></div><div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div></div>
|
||||
<div class="content" id="reader"><div id="txtContent"></div><div class="ebook-nav"><button id="prevChapterBtn" disabled>◀ 上一章</button><button id="nextChapterBtn" disabled>下一章 ▶</button></div><div class="chapter-indicator" id="chapterIndicator"></div></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
const TXT_DATA = <?php echo json_encode($txtData); ?>;
|
||||
let curIdx=0,chapters=TXT_DATA.chapters||[],fontSize=18;
|
||||
globalChapters = chapters;
|
||||
globalTotalChapters = chapters.length;
|
||||
globalCurrentIndex = 0;
|
||||
function applyStyles(){let s=document.getElementById('txt-style');if(!s){s=document.createElement('style');s.id='txt-style';document.head.appendChild(s);}s.textContent=`.ebook-chapter{font-size:${fontSize}px}.ebook-chapter p{margin-bottom:1em;text-indent:2em}`;}
|
||||
function renderChapter(i){if(!chapters||i<0||i>=chapters.length)return;onChapterChange();curIdx=i;globalCurrentIndex=i;document.getElementById('txtContent').innerHTML=`<div class="ebook-chapter"><div class="chapter-title">${escapeHtml(chapters[i].title)}</div>${chapters[i].content}</div>`;document.getElementById('prevChapterBtn').disabled=(i<=0);document.getElementById('nextChapterBtn').disabled=(i>=chapters.length-1);document.getElementById('chapterIndicator').innerText=`第 ${i+1}/${chapters.length} 章 · ${chapters[i].title}`;saveProgress(i);if(typeof updateGlobalProgress==='function')updateGlobalProgress();}
|
||||
function saveProgress(i){let l=getBookmarks(),idx=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:i+1,time:Date.now(),type:'txt'};if(idx>=0)l[idx]=n;else l.push(n);saveBookmarks(l);}
|
||||
window.jumpToBookmark = function(book,chapter,page){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){renderChapter(Math.min(Math.max(1,page),chapters.length)-1);showToast('✨ 第 '+page+' 章');}else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'txt'}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
function addBookmark(){let n=curIdx+1,l=getBookmarks(),i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),ni={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:n,time:Date.now(),type:'txt'};if(i>=0)l[i]=ni;else l.push(ni);saveBookmarks(l);showToast('✅ 第 '+n+' 章');}
|
||||
document.getElementById('fontPlus').onclick=()=>{fontSize=Math.min(fontSize+2,32);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('fontMinus').onclick=()=>{fontSize=Math.max(fontSize-2,12);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
document.getElementById('prevChapterBtn').onclick=()=>{if(curIdx>0)renderChapter(curIdx-1);resetHideTimer();};
|
||||
document.getElementById('nextChapterBtn').onclick=()=>{if(curIdx<chapters.length-1)renderChapter(curIdx+1);resetHideTimer();};
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
applyStyles();if(chapters.length>0){let saved=0,jump=sessionStorage.getItem('jump_target');if(jump){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jump);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)saved=Math.min(Math.max(1,t.page),chapters.length)-1;}catch(e){}}else{let bks=getBookmarks(),ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);if(ex&&ex.page)saved=Math.min(Math.max(1,ex.page),chapters.length)-1;}renderChapter(saved);}
|
||||
refreshBookmarkList();
|
||||
createGlobalProgressBar(globalTotalChapters, globalCurrentIndex, globalChapters);
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
|
||||
<?php elseif ($isChapterPage && $isEpub && $epubData): ?>
|
||||
<!-- EPUB阅读页 -->
|
||||
<div class="top-bar"><div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div></div><div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div></div>
|
||||
<div class="content" id="reader">
|
||||
<?php if ($epubData['type'] == 'comic'): ?>
|
||||
<?php $comicImages = $epubData['images']; ?>
|
||||
<div id="comicViewer">
|
||||
<?php foreach($comicImages as $idx => $imgPath): ?>
|
||||
<div class="canvas-container" data-page="<?php echo $idx+1; ?>" style="margin-bottom: 20px;">
|
||||
<img src="<?php echo $imgPath; ?>" loading="lazy" style="max-width:100%; height:auto; border-radius:8px; display:block; margin:0 auto; box-shadow: 0 4px 12px rgba(0,0,0,0.2);"
|
||||
onerror="this.onerror=null; this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22300%22 height=%22400%22%3E%3Crect width=%22300%22 height=%22400%22 fill=%22%23333%22/%3E%3Ctext x=%22150%22 y=%22200%22 fill=%22%23fff%22 text-anchor=%22middle%22%3E图片加载失败%3C/text%3E%3C/svg%3E';">
|
||||
<div class="comic-page-info" style="text-align:center; padding:8px; font-size:12px; color:rgba(255,255,255,0.6);">第 <?php echo $idx+1; ?> / <?php echo count($comicImages); ?> 页</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
const TOTAL_PAGES = <?php echo count($comicImages); ?>;
|
||||
globalChapters = [{title: CHAPTER_NAME}];
|
||||
globalTotalChapters = 1;
|
||||
globalCurrentIndex = 0;
|
||||
function getCurrentPage(){let cs=document.querySelectorAll('.canvas-container'),bestPage=1,bestDistance=Infinity,viewportHeight=window.innerHeight;for(let i=0;i<cs.length;i++){let rect=cs[i].getBoundingClientRect(),center=rect.top+rect.height/2,distance=Math.abs(center-viewportHeight/2);if(distance<bestDistance){bestDistance=distance;bestPage=i+1;}}return bestPage;}
|
||||
function updateGlobalProgressForComic(){let c=document.getElementById('globalProgressBar');if(!c)return;let s=document.getElementById('globalProgressSlider'),l=c.querySelector('.progress-label span'),t=c.querySelector('.progress-info > div:last-child');if(s&&TOTAL_PAGES>0){let p=getCurrentPage();s.value=p;if(l)l.innerText=`📖 第 ${p} / ${TOTAL_PAGES} 页`;if(t)t.innerText=`第 ${p} 页 / 共 ${TOTAL_PAGES} 页`;}}
|
||||
window.updateGlobalProgress = updateGlobalProgressForComic;
|
||||
function saveComicProgress(page) {
|
||||
let list = getBookmarks();
|
||||
let idx = list.findIndex(b => b.book === CURRENT_BOOK && b.chapter === CURRENT_CHAPTER);
|
||||
let bookmark = {
|
||||
book: CURRENT_BOOK,
|
||||
chapter: CURRENT_CHAPTER,
|
||||
chapterName: CHAPTER_NAME.length > 35 ? CHAPTER_NAME.substring(0,32)+'...' : CHAPTER_NAME,
|
||||
page: page,
|
||||
time: Date.now(),
|
||||
type: 'comic'
|
||||
};
|
||||
if (idx >= 0) list[idx] = bookmark;
|
||||
else list.push(bookmark);
|
||||
saveBookmarks(list);
|
||||
}
|
||||
function addBookmark(){let p=getCurrentPage();saveComicProgress(p);showToast('✅ 第 '+p+' 页');}
|
||||
function jumpToPage(p){p=Math.min(Math.max(1,p),TOTAL_PAGES);let t=document.querySelector(`.canvas-container[data-page="${p}"]`);if(t){t.scrollIntoView({behavior:'smooth',block:'start'});showToast('✨ 第 '+p+' 页');saveComicProgress(p);setTimeout(()=>{if(typeof updateGlobalProgress==='function')updateGlobalProgress();},300);}else{showToast('📖 页面加载中...');}}
|
||||
window.jumpToBookmark = function(book,chapter,page){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER)jumpToPage(page);else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'comic'}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
document.getElementById('fontControls').style.display='none';
|
||||
refreshBookmarkList();
|
||||
function createComicProgressBar(){let c=document.getElementById('globalProgressPlaceholder');if(!c)return;c.innerHTML=`<div class="global-progress-container" id="globalProgressBar"><div class="progress-range-area"><input type="range" class="progress-slider-global" id="globalProgressSlider" min="1" max="${TOTAL_PAGES}" value="1" step="1"><div class="chapter-tooltip" id="chapterTooltip" style="display: none;">📖 第 1 / ${TOTAL_PAGES} 页</div></div><div class="progress-info"><div class="progress-label"><span>📖 第 1 / ${TOTAL_PAGES} 页</span></div><div>第 1 页 / 共 ${TOTAL_PAGES} 页</div></div></div>`;globalProgressBar=document.getElementById('globalProgressBar');let s=document.getElementById('globalProgressSlider'),tip=document.getElementById('chapterTooltip'),tipTimer=null;function st(page){if(tipTimer)clearTimeout(tipTimer);tip.textContent=`📖 第 ${page} / ${TOTAL_PAGES} 页`;tip.style.display='block';tipTimer=setTimeout(()=>{tip.style.display='none';},2000);}if(s){s.addEventListener('input',(e)=>{let p=parseInt(e.target.value),l=c.querySelector('.progress-label span'),t=c.querySelector('.progress-info > div:last-child');if(l)l.innerText=`📖 第 ${p} / ${TOTAL_PAGES} 页`;if(t)t.innerText=`第 ${p} 页 / 共 ${TOTAL_PAGES} 页`;st(p);});s.addEventListener('change',(e)=>{jumpToPage(parseInt(e.target.value));resetHideTimer();});}if(globalProgressBar)globalProgressBar.classList.add('hide');let stt=null;window.addEventListener('scroll',function(){if(stt)clearTimeout(stt);stt=setTimeout(()=>{if(typeof updateGlobalProgress==='function')updateGlobalProgress();let p=getCurrentPage();saveComicProgress(p);},200);});}
|
||||
createComicProgressBar();
|
||||
let jumpTarget=sessionStorage.getItem('jump_target');if(jumpTarget){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jumpTarget);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)setTimeout(()=>jumpToPage(t.page),500);}catch(e){}} else {
|
||||
let bookmarks = getBookmarks();
|
||||
let lastProgress = bookmarks.find(b => b.book === CURRENT_BOOK && b.chapter === CURRENT_CHAPTER);
|
||||
if(lastProgress && lastProgress.page) setTimeout(()=>jumpToPage(lastProgress.page), 500);
|
||||
}
|
||||
setTimeout(()=>{if(typeof updateGlobalProgress==='function')updateGlobalProgress();},500);
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
<?php else: ?>
|
||||
<div id="epubContent"></div>
|
||||
<div class="ebook-nav"><button id="prevChapterBtn" disabled>◀ 上一章</button><button id="nextChapterBtn" disabled>下一章 ▶</button></div>
|
||||
<div class="chapter-indicator" id="chapterIndicator"></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
const EPUB_DATA = <?php echo json_encode($epubData); ?>;
|
||||
let curIdx=0,chapters=EPUB_DATA.htmlContents||[],css=EPUB_DATA.cssContent||'',fontSize=18;
|
||||
globalChapters = chapters; globalTotalChapters = chapters.length; globalCurrentIndex = 0;
|
||||
function applyStyles(){let s=document.getElementById('epub-style');if(!s){s=document.createElement('style');s.id='epub-style';document.head.appendChild(s);}s.textContent=`.ebook-chapter{font-size:${fontSize}px}.ebook-chapter img{max-width:100%;height:auto;display:block;margin:1em auto;border-radius:12px}.ebook-chapter p{margin-bottom:1em}${css}`;}
|
||||
function renderChapter(i){if(!chapters||i<0||i>=chapters.length)return;onChapterChange();curIdx=i;globalCurrentIndex=i;let c=chapters[i];document.getElementById('epubContent').innerHTML=`<div class="ebook-chapter"><div class="chapter-title">${escapeHtml(c.title)}</div>${c.content}</div>`;document.getElementById('prevChapterBtn').disabled=(i<=0);document.getElementById('nextChapterBtn').disabled=(i>=chapters.length-1);document.getElementById('chapterIndicator').innerText=`第 ${i+1}/${chapters.length} 章 · ${c.title}`;saveProgress(i);if(typeof updateGlobalProgress==='function')updateGlobalProgress();}
|
||||
function saveProgress(i){let l=getBookmarks(),idx=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:i+1,time:Date.now(),type:'ebook'};if(idx>=0)l[idx]=n;else l.push(n);saveBookmarks(l);}
|
||||
window.jumpToBookmark = function(book,chapter,page){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){renderChapter(Math.min(Math.max(1,page),chapters.length)-1);showToast('✨ 第 '+page+' 章');}else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'ebook'}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
function addBookmark(){let n=curIdx+1,l=getBookmarks(),i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),ni={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:n,time:Date.now(),type:'ebook'};if(i>=0)l[i]=ni;else l.push(ni);saveBookmarks(l);showToast('✅ 第 '+n+' 章');}
|
||||
document.getElementById('fontPlus').onclick=()=>{fontSize=Math.min(fontSize+2,32);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('fontMinus').onclick=()=>{fontSize=Math.max(fontSize-2,12);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
document.getElementById('prevChapterBtn').onclick=()=>{if(curIdx>0)renderChapter(curIdx-1);resetHideTimer();};
|
||||
document.getElementById('nextChapterBtn').onclick=()=>{if(curIdx<chapters.length-1)renderChapter(curIdx+1);resetHideTimer();};
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
applyStyles();if(chapters.length>0){let saved=0,jump=sessionStorage.getItem('jump_target');if(jump){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jump);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)saved=Math.min(Math.max(1,t.page),chapters.length)-1;}catch(e){}}else{let bks=getBookmarks(),ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);if(ex&&ex.page)saved=Math.min(Math.max(1,ex.page),chapters.length)-1;}renderChapter(saved);}
|
||||
refreshBookmarkList();
|
||||
createGlobalProgressBar(globalTotalChapters, globalCurrentIndex, globalChapters);
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif ($book): ?>
|
||||
<!-- 书籍章节列表页 -->
|
||||
<div class="content">
|
||||
<div class="top-bar" style="position:relative; margin-top:-70px; margin-bottom:20px;">
|
||||
<div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a></div></div>
|
||||
</div>
|
||||
<h2 class="page-title">📖 <?php echo htmlspecialchars($book); ?></h2>
|
||||
<div class="shelf-grid">
|
||||
<?php
|
||||
$files = scanDirectory($baseDir . '/' . $book);
|
||||
if ($files) {
|
||||
foreach ($files as $f) {
|
||||
$name = basename($f);
|
||||
$url = $currentFile . "?book=" . rawurlencode($book) . "&chapter=" . rawurlencode($name);
|
||||
if (stripos($name, '.txt') !== false) $icon = '📖';
|
||||
elseif (stripos($name, '.epub') !== false) $icon = '📘';
|
||||
else $icon = (stripos($name, '.pdf') !== false ? '📕' : '📁');
|
||||
echo '<a href="' . $url . '" class="shelf-item"><div class="emoji">' . $icon . '</div><div>' . htmlspecialchars($name) . '</div></a>';
|
||||
}
|
||||
} else {
|
||||
echo '<div style="grid-column:1/-1; text-align:center; padding:50px; color:rgba(255,255,255,0.6);">📭 没有章节</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
refreshBookmarkList();
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer?.includes(window.location.host))history.back();else location.href='<?php echo $currentFile; ?>';};
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- 书架首页 -->
|
||||
<div class="content">
|
||||
<h1 class="page-title">📚 我的书架</h1>
|
||||
<div class="shelf-grid">
|
||||
<?php
|
||||
$books = scanDirectory($baseDir);
|
||||
if ($books) {
|
||||
foreach ($books as $b) {
|
||||
if (is_dir($b)) {
|
||||
$name = basename($b);
|
||||
echo '<a href="' . $currentFile . '?book=' . rawurlencode($name) . '" class="shelf-item"><div class="emoji">📖</div><div>' . htmlspecialchars($name) . '</div></a>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo '<div style="grid-column:1/-1; text-align:center; padding:50px; color:rgba(255,255,255,0.6);">📭 请在 PDF 文件夹里放入书籍文件夹</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
refreshBookmarkList();
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2211 @@
|
||||
<?php
|
||||
// PDF阅读器.php - 完整版(24款主题 + 所有UI组件主题统一)
|
||||
header("Content-Type: text/html; charset=utf-8");
|
||||
$baseDir = 'PDF';
|
||||
|
||||
if (!is_dir($baseDir)) {
|
||||
mkdir($baseDir);
|
||||
echo "已自动创建 PDF 目录,请放入PDF/EPUB/TXT";
|
||||
exit;
|
||||
}
|
||||
|
||||
function scanDirectory($path) {
|
||||
$result = [];
|
||||
if (!is_dir($path)) return $result;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
if ($entry == '.epub_cache' || $entry == '.txt_cache') continue;
|
||||
if (strpos($entry, '.') === 0) continue;
|
||||
$result[] = $path . '/' . $entry;
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
natsort($result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
function scanImages($path) {
|
||||
$images = [];
|
||||
$extensions = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
|
||||
if (!is_dir($path)) return $images;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
$ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $extensions)) $images[] = $path . '/' . $entry;
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
natsort($images);
|
||||
return array_values($images);
|
||||
}
|
||||
|
||||
function parseTxtFile($txtPath, $book, $chapter, $baseDir) {
|
||||
$cacheDir = $baseDir . '/.txt_cache/' . $book . '/' . md5($chapter);
|
||||
$cacheFile = $cacheDir . '/chapters.json';
|
||||
if (file_exists($cacheFile)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
if ($data && isset($data['chapters'])) return $data;
|
||||
}
|
||||
$content = file_get_contents($txtPath);
|
||||
$encoding = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'BIG5'], true);
|
||||
if (!$encoding) $encoding = 'UTF-8';
|
||||
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
|
||||
|
||||
$patterns = [
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+章[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+节[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+卷[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+回[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+集[\s]*[^\n]*/u',
|
||||
'/(?:Chapter|CHAPTER|Ch\.?|CH\.?)\s*\d+[.:\s]*[^\n]*/i',
|
||||
'/\[\d+\][\s]*[^\n]*/',
|
||||
'/(?:一|二|三|四|五|六|七|八|九|十|十一|十二|十三|十四|十五|十六|十七|十八|十九|二十)、[\s]*[^\n]*/u',
|
||||
'/^\d+\.\s*[^\n]+/m',
|
||||
'/^\d+\、\s*[^\n]+/m',
|
||||
];
|
||||
|
||||
$lines = preg_split('/\r\n|\r|\n/', $content);
|
||||
$chapters = [];
|
||||
$currentChapter = ['title' => '序章', 'content' => ''];
|
||||
$foundFirstChapter = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = rtrim($line);
|
||||
$isChapter = false; $chapterTitle = '';
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $line, $matches)) {
|
||||
$chapterTitle = trim($matches[0]);
|
||||
$isChapter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$isChapter && preg_match('/^\s*\d+\s*$/', $line)) {
|
||||
$chapterTitle = "第 " . trim($line) . " 章";
|
||||
$isChapter = true;
|
||||
}
|
||||
if ($isChapter && $chapterTitle) {
|
||||
if ($foundFirstChapter && $currentChapter['content'] !== '') $chapters[] = $currentChapter;
|
||||
$currentChapter = ['title' => $chapterTitle, 'content' => ''];
|
||||
$foundFirstChapter = true;
|
||||
} else {
|
||||
if ($line !== '' || $currentChapter['content'] !== '') $currentChapter['content'] .= $line . "\n";
|
||||
}
|
||||
}
|
||||
if ($currentChapter['content'] !== '') $chapters[] = $currentChapter;
|
||||
if (empty($chapters)) $chapters = [['title' => basename($chapter, '.txt'), 'content' => $content]];
|
||||
|
||||
foreach ($chapters as &$chap) {
|
||||
$chap['content'] = preg_replace('/\n\s*\n/', "</p><p>", $chap['content']);
|
||||
$chap['content'] = "<p>" . str_replace("\n", "<br>", $chap['content']) . "</p>";
|
||||
$chap['content'] = preg_replace('/<p>\s*<\/p>/', '', $chap['content']);
|
||||
}
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
$result = ['type' => 'txt', 'chapters' => $chapters, 'totalChapters' => count($chapters)];
|
||||
file_put_contents($cacheFile, json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
return $result;
|
||||
}
|
||||
|
||||
function cachePathToUrl($absolutePath) {
|
||||
$docRoot = rtrim($_SERVER['DOCUMENT_ROOT'], '/\\');
|
||||
$relative = str_replace($docRoot, '', $absolutePath);
|
||||
$relative = ltrim($relative, '/\\');
|
||||
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
|
||||
if ($scriptDir != '/' && $scriptDir != '\\') {
|
||||
$relative = ltrim($scriptDir, '/') . '/' . $relative;
|
||||
}
|
||||
return '/' . $relative;
|
||||
}
|
||||
|
||||
function detectImageExt($data) {
|
||||
if (strlen($data) < 12) return 'jpg';
|
||||
if (substr($data, 0, 4) === 'RIFF' && substr($data, 8, 4) === 'WEBP') return 'webp';
|
||||
if (substr($data, 0, 8) === "\x89PNG\r\n\x1a\n") return 'png';
|
||||
if (substr($data, 0, 2) === "\xff\xd8") return 'jpg';
|
||||
if (substr($data, 0, 6) === 'GIF89a' || substr($data, 0, 6) === 'GIF87a') return 'gif';
|
||||
return 'jpg';
|
||||
}
|
||||
|
||||
function parseEpub($epubFilePath, $book, $chapter, $baseDir) {
|
||||
$cacheDir = $baseDir . '/.epub_cache/' . $book . '/' . md5($chapter);
|
||||
$cacheTypeFile = $cacheDir . '/type.json';
|
||||
|
||||
if (file_exists($cacheTypeFile)) {
|
||||
$cached = json_decode(file_get_contents($cacheTypeFile), true);
|
||||
if ($cached && isset($cached['type'])) {
|
||||
if ($cached['type'] == 'comic' && isset($cached['images'])) {
|
||||
return $cached;
|
||||
}
|
||||
if ($cached['type'] == 'ebook' && isset($cached['htmlContents'])) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('ZipArchive')) return ['error' => '请启用ZipArchive扩展'];
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($epubFilePath) !== true) return ['error' => '无法打开EPUB文件'];
|
||||
|
||||
$container = $zip->getFromName('META-INF/container.xml');
|
||||
if (!$container) { $zip->close(); return ['error' => '无效的EPUB文件']; }
|
||||
|
||||
$rootFile = '';
|
||||
if (preg_match('/full-path="([^"]+)"/', $container, $matches)) $rootFile = $matches[1];
|
||||
if (!$rootFile) { $zip->close(); return ['error' => '无法解析EPUB结构']; }
|
||||
|
||||
$opfContent = $zip->getFromName($rootFile);
|
||||
if (!$opfContent) { $zip->close(); return ['error' => '无法解析OPF文件']; }
|
||||
|
||||
$opfDir = dirname($rootFile);
|
||||
if ($opfDir == '.') $opfDir = '';
|
||||
else $opfDir .= '/';
|
||||
|
||||
$manifest = [];
|
||||
preg_match_all('/<item[^>]*id="([^"]*)"[^>]*href="([^"]*)"[^>]*>/i', $opfContent, $items);
|
||||
foreach ($items[1] as $i => $id) $manifest[$id] = $opfDir . $items[2][$i];
|
||||
|
||||
$spineOrder = [];
|
||||
preg_match_all('/<itemref[^>]*idref="([^"]+)"/i', $opfContent, $spineMatches);
|
||||
if (!empty($spineMatches[1])) $spineOrder = $spineMatches[1];
|
||||
|
||||
$coverImageIds = [];
|
||||
if (preg_match('/<meta[^>]*name="cover"[^>]*content="([^"]+)"/i', $opfContent, $coverMatch)) {
|
||||
$coverImageIds[] = $coverMatch[1];
|
||||
}
|
||||
if (preg_match('/<meta[^>]*name="cover-image"[^>]*content="([^"]+)"/i', $opfContent, $coverMatch)) {
|
||||
$coverImageIds[] = $coverMatch[1];
|
||||
}
|
||||
preg_match_all('/<item[^>]*properties="cover-image"[^>]*id="([^"]+)"/i', $opfContent, $coverPropMatches);
|
||||
foreach ($coverPropMatches[1] as $cid) $coverImageIds[] = $cid;
|
||||
foreach ($manifest as $id => $href) {
|
||||
if (stripos($href, 'cover') !== false && preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $href)) {
|
||||
$coverImageIds[] = $id;
|
||||
}
|
||||
}
|
||||
$coverImageIds = array_unique($coverImageIds);
|
||||
|
||||
$cssContent = '';
|
||||
preg_match_all('/<item[^>]*href="([^"]+\.css)"[^>]*media-type="text\/css"[^>]*>/i', $opfContent, $cssMatches);
|
||||
foreach ($cssMatches[1] as $cssPath) {
|
||||
$fullPath = $opfDir . $cssPath;
|
||||
$cssData = $zip->getFromName($fullPath);
|
||||
if ($cssData !== false) {
|
||||
$lines = explode("\n", $cssData);
|
||||
$scoped = '';
|
||||
foreach ($lines as $line) {
|
||||
if (strpos($line, '{') !== false && strpos(trim($line), '@') !== 0) {
|
||||
$scoped .= '.ebook-chapter ' . $line . "\n";
|
||||
} else {
|
||||
$scoped .= $line . "\n";
|
||||
}
|
||||
}
|
||||
$cssContent .= $scoped . "\n";
|
||||
}
|
||||
}
|
||||
$cssContent .= '.ebook-chapter { margin-top: 70px !important; margin-bottom: 80px !important; padding-top: 20px !important; padding-bottom: 20px !important; }' . "\n";
|
||||
|
||||
$allImagePaths = [];
|
||||
foreach ($manifest as $href) {
|
||||
if (preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $href)) {
|
||||
$allImagePaths[] = $href;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
|
||||
$imagesDir = $cacheDir . '/images/';
|
||||
if (!is_dir($imagesDir)) mkdir($imagesDir, 0777, true);
|
||||
|
||||
$imageUrlMap = [];
|
||||
foreach ($allImagePaths as $relativePath) {
|
||||
$originalFilename = basename($relativePath);
|
||||
$imageData = $zip->getFromName($relativePath);
|
||||
if ($imageData === false) $imageData = $zip->getFromName(urldecode($relativePath));
|
||||
if ($imageData === false) $imageData = $zip->getFromName(ltrim($relativePath, './'));
|
||||
if ($imageData === false) $imageData = $zip->getFromName($originalFilename);
|
||||
|
||||
if ($imageData !== false && strlen($imageData) > 100) {
|
||||
$ext = detectImageExt($imageData);
|
||||
$cacheFilename = md5($relativePath) . '_' . preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $originalFilename) . '.' . $ext;
|
||||
$cacheFile = $imagesDir . $cacheFilename;
|
||||
if (!file_exists($cacheFile)) file_put_contents($cacheFile, $imageData);
|
||||
$url = cachePathToUrl($cacheFile);
|
||||
$imageUrlMap[$originalFilename] = $url;
|
||||
$imageUrlMap[pathinfo($originalFilename, PATHINFO_FILENAME)] = $url;
|
||||
$imageUrlMap[$relativePath] = $url;
|
||||
$imageUrlMap[basename($relativePath)] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
$coverHtml = '';
|
||||
$usedCoverNames = [];
|
||||
foreach ($coverImageIds as $coverId) {
|
||||
if (isset($manifest[$coverId])) {
|
||||
$coverPath = $manifest[$coverId];
|
||||
$coverFilename = basename($coverPath);
|
||||
$coverUrl = $imageUrlMap[$coverFilename] ?? $imageUrlMap[pathinfo($coverFilename, PATHINFO_FILENAME)] ?? null;
|
||||
if ($coverUrl) {
|
||||
$coverHtml = '<div class="epub-cover" style="text-align:center; margin-bottom:20px;"><img src="' . $coverUrl . '" style="max-width:100%; border-radius:12px;" loading="lazy"></div>';
|
||||
$usedCoverNames[] = $coverFilename;
|
||||
$usedCoverNames[] = pathinfo($coverFilename, PATHINFO_FILENAME);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$htmlContents = [];
|
||||
foreach ($spineOrder as $idx => $idref) {
|
||||
if (!isset($manifest[$idref])) continue;
|
||||
$filePath = $manifest[$idref];
|
||||
$content = $zip->getFromName($filePath);
|
||||
if ($content === false) continue;
|
||||
|
||||
$title = '';
|
||||
if (preg_match('/<title[^>]*>([^<]+)<\/title>/i', $content, $titleMatch)) $title = trim($titleMatch[1]);
|
||||
if (!$title && preg_match('/<h1[^>]*>([^<]+)<\/h1>/i', $content, $h1Match)) $title = trim($h1Match[1]);
|
||||
if (!$title) $title = '第 ' . (count($htmlContents) + 1) . ' 章';
|
||||
|
||||
$bodyContent = '';
|
||||
if (preg_match('/<body[^>]*>([\s\S]*?)<\/body>/i', $content, $bodyMatch)) {
|
||||
$bodyContent = $bodyMatch[1];
|
||||
} elseif (preg_match('/<html[^>]*>([\s\S]*?)<\/html>/i', $content, $htmlMatch)) {
|
||||
if (preg_match('/<body[^>]*>([\s\S]*?)<\/body>/i', $htmlMatch[1], $bodyMatch2)) {
|
||||
$bodyContent = $bodyMatch2[1];
|
||||
} else {
|
||||
$bodyContent = $htmlMatch[1];
|
||||
}
|
||||
} else {
|
||||
$bodyContent = $content;
|
||||
}
|
||||
|
||||
$bodyContent = preg_replace_callback('/<(?:br|img)[^>]*src=["\']([^"\']+)["\'][^>]*>/i', function($matches) use ($imageUrlMap, $usedCoverNames) {
|
||||
$tag = $matches[0];
|
||||
$src = $matches[1];
|
||||
if (strpos($src, 'http') === 0 || strpos($src, 'data:') === 0 || strpos($src, '.epub_cache/') !== false) {
|
||||
return str_replace('<br', '<img', $tag);
|
||||
}
|
||||
$filename = basename(urldecode($src));
|
||||
$nameNoExt = pathinfo($filename, PATHINFO_FILENAME);
|
||||
foreach ($usedCoverNames as $coverName) {
|
||||
if (strpos($filename, $coverName) !== false || strpos($nameNoExt, $coverName) !== false) return '';
|
||||
}
|
||||
$newUrl = $imageUrlMap[$filename] ?? $imageUrlMap[$nameNoExt] ?? null;
|
||||
if ($newUrl) {
|
||||
$newTag = str_replace('<br', '<img', $tag);
|
||||
return str_replace($src, $newUrl, $newTag);
|
||||
}
|
||||
return str_replace('<br', '<img', $tag);
|
||||
}, $bodyContent);
|
||||
|
||||
if (stripos($title, 'cover') === false && stripos($title, '封面') === false) {
|
||||
$htmlContents[] = ['title' => $title, 'content' => $bodyContent, 'index' => count($htmlContents)];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($htmlContents) && !empty($coverHtml)) {
|
||||
$htmlContents[0]['content'] = $coverHtml . $htmlContents[0]['content'];
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
$totalImages = count($allImagePaths);
|
||||
$isComic = false;
|
||||
if ($totalImages > 0) {
|
||||
$totalTextLength = 0;
|
||||
foreach ($htmlContents as $ch) {
|
||||
$plainText = strip_tags($ch['content']);
|
||||
$totalTextLength += mb_strlen(preg_replace('/\s+/', '', $plainText));
|
||||
}
|
||||
$avgTextLength = $totalTextLength / max(count($htmlContents), 1);
|
||||
if ($totalImages >= count($htmlContents) * 1.2 && $avgTextLength < 150) {
|
||||
$isComic = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isComic) {
|
||||
$comicImages = [];
|
||||
foreach ($htmlContents as $chapter) {
|
||||
preg_match_all('/<img[^>]*src=["\']([^"\']+)["\'][^>]*>/i', $chapter['content'], $imgMatches);
|
||||
foreach ($imgMatches[1] as $imgSrc) {
|
||||
$comicImages[] = $imgSrc;
|
||||
}
|
||||
}
|
||||
$result = ['type' => 'comic', 'images' => $comicImages, 'totalPages' => count($comicImages)];
|
||||
file_put_contents($cacheTypeFile, json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result = [
|
||||
'type' => 'ebook',
|
||||
'htmlContents' => $htmlContents,
|
||||
'cssContent' => $cssContent,
|
||||
'totalChapters' => count($htmlContents)
|
||||
];
|
||||
file_put_contents($cacheTypeFile, json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
return $result;
|
||||
}
|
||||
|
||||
$book = isset($_GET['book']) ? $_GET['book'] : '';
|
||||
$chapter = isset($_GET['chapter']) ? $_GET['chapter'] : '';
|
||||
$isChapterPage = ($book && $chapter);
|
||||
$isPdf = $chapter && (stripos($chapter, '.pdf') !== false);
|
||||
$isEpub = $chapter && (stripos($chapter, '.epub') !== false);
|
||||
$isTxt = $chapter && (stripos($chapter, '.txt') !== false);
|
||||
|
||||
if ($book && $chapter) {
|
||||
$encodedBook = rawurlencode($book);
|
||||
$encodedChapter = rawurlencode($chapter);
|
||||
$fileUrl = "$baseDir/$encodedBook/$encodedChapter";
|
||||
}
|
||||
|
||||
$images = [];
|
||||
$epubData = null;
|
||||
$txtData = null;
|
||||
|
||||
if ($isTxt && $isChapterPage && $book && $chapter) {
|
||||
$txtPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
if (file_exists($txtPath)) $txtData = parseTxtFile($txtPath, $book, $chapter, $baseDir);
|
||||
} elseif ($isEpub && $isChapterPage && $book && $chapter) {
|
||||
$epubPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
if (file_exists($epubPath)) $epubData = parseEpub($epubPath, $book, $chapter, $baseDir);
|
||||
} elseif (!$isPdf && $isChapterPage && $book && $chapter) {
|
||||
$localPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
$images = scanImages($localPath);
|
||||
}
|
||||
|
||||
$currentFile = 'PDF阅读器.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
|
||||
<script>pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';</script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; min-height: 100vh; transition: all 0.3s ease; }
|
||||
|
||||
.floating-buttons, .speed-panel, .theme-selector, .font-controls, .stat-panel {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.floating-buttons.visible, .speed-panel.visible, .theme-selector.visible, .font-controls.visible, .stat-panel.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.global-progress-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1003;
|
||||
padding: 8px 16px 16px 16px;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease;
|
||||
transform: translateY(0);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
.global-progress-container.hide { transform: translateY(100%); }
|
||||
.progress-range-area {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.progress-slider-global {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.progress-slider-global::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #ff9800;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 8px rgba(255,152,0,0.8);
|
||||
border: 2px solid #fff;
|
||||
transition: transform 0.1s, background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.progress-slider-global::-webkit-slider-thumb:hover { transform: scale(1.2); }
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
padding: 4px 0 2px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.chapter-tooltip {
|
||||
position: fixed;
|
||||
background: rgba(0,0,0,0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
color: #ff9800;
|
||||
padding: 10px 20px;
|
||||
border-radius: 40px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
|
||||
z-index: 10007;
|
||||
border: 1px solid rgba(255,152,0,0.6);
|
||||
transition: all 0.2s ease;
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.5px;
|
||||
bottom: 280px;
|
||||
right: 12px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.page-turn-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeInOutFull 0.5s ease-out forwards;
|
||||
perspective: 2000px;
|
||||
backdrop-filter: blur(4px);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.page-turn-overlay .book-container {
|
||||
position: relative;
|
||||
width: 70%;
|
||||
max-width: 500px;
|
||||
height: 70%;
|
||||
max-height: 500px;
|
||||
transform-style: preserve-3d;
|
||||
animation: bookFlipFull 0.5s ease-in-out forwards;
|
||||
}
|
||||
.page-turn-overlay .book-left, .page-turn-overlay .book-right {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 80px;
|
||||
box-shadow: 0 0 40px rgba(0,0,0,0.4);
|
||||
transition: background 0.3s ease, border 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.page-turn-overlay .book-left {
|
||||
left: 0;
|
||||
transform-origin: right center;
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
.page-turn-overlay .book-right {
|
||||
right: 0;
|
||||
transform-origin: left center;
|
||||
border-radius: 0 16px 16px 0;
|
||||
}
|
||||
.page-turn-overlay .message {
|
||||
position: absolute;
|
||||
bottom: 20%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 28px;
|
||||
border-radius: 50px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
backdrop-filter: blur(8px);
|
||||
letter-spacing: 2px;
|
||||
transition: background 0.3s ease, color 0.3s ease, border 0.3s ease;
|
||||
}
|
||||
@keyframes fadeInOutFull {
|
||||
0% { opacity: 0; backdrop-filter: blur(0px); }
|
||||
15% { opacity: 1; backdrop-filter: blur(4px); }
|
||||
85% { opacity: 1; backdrop-filter: blur(4px); }
|
||||
100% { opacity: 0; backdrop-filter: blur(0px); visibility: hidden; }
|
||||
}
|
||||
@keyframes bookFlipFull {
|
||||
0% { transform: scale(0.9) rotateY(0deg); opacity: 0.5; }
|
||||
30% { transform: scale(1.05) rotateY(-15deg); opacity: 1; }
|
||||
70% { transform: scale(1.05) rotateY(-5deg); opacity: 1; }
|
||||
100% { transform: scale(1) rotateY(0deg); opacity: 1; }
|
||||
}
|
||||
|
||||
.speed-panel {
|
||||
position: fixed;
|
||||
right: 80px;
|
||||
bottom: 105px;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 12px 16px;
|
||||
border-radius: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
z-index: 10001;
|
||||
min-width: 170px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.speed-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
gap: 12px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.speed-value {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.speed-slider {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.speed-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #ff9800;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.speed-presets {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.speed-preset {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.1s, background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.speed-preset.active { color: #ff9800; background: rgba(255,152,0,0.2); }
|
||||
.auto-chapter-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
transition: border-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.auto-chapter-line input { width: 36px; height: 20px; cursor: pointer; accent-color: #ff9800; transition: accent-color 0.3s ease; }
|
||||
|
||||
.font-controls {
|
||||
position: fixed; right: 12px; bottom: 230px; backdrop-filter: blur(10px); padding: 8px 12px; border-radius: 30px; display: flex; gap: 12px; z-index: 10001; transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.font-controls button { background: none; border: none; font-size: 18px; padding: 4px 8px; cursor: pointer; transition: color 0.3s ease; }
|
||||
|
||||
.theme-selector {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 290px;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 12px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
z-index: 10001;
|
||||
max-width: 340px;
|
||||
width: max-content;
|
||||
transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-panel {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 350px;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 12px 16px;
|
||||
border-radius: 20px;
|
||||
min-width: 180px;
|
||||
z-index: 10001;
|
||||
font-size: 12px;
|
||||
transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
.stat-panel div {
|
||||
margin: 4px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.stat-panel .stat-label { opacity: 0.7; }
|
||||
.stat-panel .stat-value { font-weight: 600; color: #ff9800; transition: color 0.3s ease; }
|
||||
|
||||
.floating-buttons { position: fixed; right: 12px; bottom: 100px; display: flex; flex-direction: column; gap: 12px; z-index: 10000; }
|
||||
.floating-btn { width: 52px; height: 52px; backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; transition: all 0.2s, background 0.3s ease, border-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease; }
|
||||
.floating-btn.bookmark-btn { background: linear-gradient(135deg, #ff9800, #ff5722); }
|
||||
.floating-btn.active { background: #ff9800; }
|
||||
|
||||
.bookmark-panel {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 220px;
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
width: 320px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
z-index: 10002;
|
||||
transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.bookmark-panel.show { display: block; }
|
||||
.bookmark-header { padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,0.15); font-weight: 600; display: flex; justify-content: space-between; transition: border-color 0.3s ease, color 0.3s ease; }
|
||||
.bookmark-header span:last-child { cursor: pointer; font-size: 22px; }
|
||||
.bookmark-list { padding: 10px; }
|
||||
.bookmark-item { background: rgba(255,255,255,0.1); margin: 8px 0; padding: 12px; border-radius: 14px; cursor: pointer; transition: background 0.3s ease; }
|
||||
.bookmark-item:hover { background: rgba(255,255,255,0.2); }
|
||||
.bookmark-item .title { font-weight: 600; color: #ffb347; font-size: 14px; transition: color 0.3s ease; }
|
||||
.bookmark-item .info { font-size: 11px; color: rgba(255,255,255,0.6); margin-top: 5px; transition: color 0.3s ease; }
|
||||
.bookmark-item .delete { float: right; color: #ff6b6b; font-size: 16px; cursor: pointer; }
|
||||
.empty-bookmark { color: rgba(255,255,255,0.5); text-align: center; padding: 30px; font-size: 13px; transition: color 0.3s ease; }
|
||||
|
||||
.theme-dot { width: 36px; height: 36px; border-radius: 12px; cursor: pointer; border: 2px solid rgba(255,255,255,0.5); transition: all 0.1s, border-color 0.3s ease, box-shadow 0.3s ease; box-sizing: border-box; }
|
||||
.theme-dot.active { border-color: #ff9800; transform: scale(1.05); box-shadow: 0 0 8px rgba(255,152,0,0.5); }
|
||||
|
||||
.top-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
transition: background 0.3s ease, border-bottom 0.3s ease;
|
||||
}
|
||||
.top-bar-left { display: flex; align-items: center; gap: 12px; flex: 1; overflow: hidden; }
|
||||
.back-btn { width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.15); border: none; transition: background 0.3s ease, color 0.3s ease; }
|
||||
.top-bar .nav-links { font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; transition: color 0.3s ease; }
|
||||
.top-bar a { text-decoration: none; transition: color 0.3s ease; }
|
||||
.top-bar button { padding: 8px 18px; border-radius: 30px; cursor: pointer; font-size: 14px; margin-left: 8px; background: rgba(255,255,255,0.15); border: none; transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease; }
|
||||
.top-bar button.bookmark { background: rgba(255, 152, 0, 0.8); color: white; }
|
||||
.content { margin-top: 70px; padding: 16px; position: relative; z-index: 1; margin-bottom: 70px; }
|
||||
.ebook-chapter { border-radius: 24px; padding: 30px 24px; margin: 20px auto; max-width: 800px; transition: all 0.2s ease, background 0.3s ease, color 0.3s ease, border 0.3s ease, box-shadow 0.3s ease; }
|
||||
.ebook-chapter p { margin-bottom: 1em; line-height: 1.8; }
|
||||
.ebook-chapter .chapter-title { font-size: 1.8em; text-align: center; margin-bottom: 1em; padding-bottom: 0.3em; transition: color 0.3s ease, border-bottom-color 0.3s ease; }
|
||||
.ebook-nav { display: flex; justify-content: space-between; gap: 12px; margin: 20px auto; max-width: 800px; }
|
||||
.ebook-nav button { border: none; padding: 12px 24px; border-radius: 40px; cursor: pointer; font-size: 16px; flex: 1; background: linear-gradient(135deg, #667eea, #764ba2); color: white; transition: background 0.3s ease, opacity 0.3s ease; }
|
||||
.ebook-nav button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.chapter-indicator { text-align: center; margin: 10px auto; font-size: 14px; transition: color 0.3s ease; }
|
||||
.toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); backdrop-filter: blur(20px); color: white; padding: 10px 20px; border-radius: 50px; font-size: 14px; z-index: 2000; pointer-events: none; white-space: nowrap; transition: background 0.3s ease, color 0.3s ease, border 0.3s ease; }
|
||||
|
||||
.swipe-transition {
|
||||
transition: transform 0.25s cubic-bezier(0.2, 0.9, 0.4, 1.1), opacity 0.2s ease;
|
||||
}
|
||||
.swipe-slide-left {
|
||||
transform: translateX(-40px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.swipe-slide-right {
|
||||
transform: translateX(40px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.swipe-indicator {
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.65);
|
||||
backdrop-filter: blur(12px);
|
||||
color: #ff9800;
|
||||
padding: 8px 20px;
|
||||
border-radius: 40px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: 10005;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
font-family: monospace;
|
||||
letter-spacing: 1px;
|
||||
border: 1px solid rgba(255,152,0,0.4);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
transition: opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
.swipe-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.swipe-arrow-left, .swipe-arrow-right {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
color: #ff9800;
|
||||
z-index: 10004;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
opacity: 0;
|
||||
border: 1px solid rgba(255,152,0,0.3);
|
||||
}
|
||||
.swipe-arrow-left { left: 15px; }
|
||||
.swipe-arrow-right { right: 15px; }
|
||||
.swipe-arrow-left.show, .swipe-arrow-right.show {
|
||||
opacity: 0.7;
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
}
|
||||
|
||||
/* 3D悬浮书架 */
|
||||
.shelf-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 28px;
|
||||
padding: 24px;
|
||||
perspective: 1800px;
|
||||
perspective-origin: center 40px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.shelf-grid { grid-template-columns: repeat(2, 1fr); gap: 18px; padding: 16px; }
|
||||
}
|
||||
|
||||
.shelf-item {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
backdrop-filter: blur(18px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(180%);
|
||||
border-radius: 32px;
|
||||
padding: 28px 12px 24px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
transition: all 0.5s cubic-bezier(0.2, 0.9, 0.4, 1.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
overflow: visible;
|
||||
box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.35) inset,
|
||||
0 1px 0 rgba(255, 255, 255, 0.25) inset,
|
||||
0 -1px 0 rgba(0, 0, 0, 0.05) inset;
|
||||
transform-style: preserve-3d;
|
||||
transform: translateZ(0) rotateX(0deg) rotateY(0deg);
|
||||
opacity: 0;
|
||||
animation: fadeInUpGlide 0.6s cubic-bezier(0.2, 0.9, 0.3, 1.1) forwards;
|
||||
border: 1px solid rgba(255,255,240,0.4);
|
||||
}
|
||||
.shelf-item:nth-child(1) { animation-delay: 0.03s; } .shelf-item:nth-child(2) { animation-delay: 0.08s; }
|
||||
.shelf-item:nth-child(3) { animation-delay: 0.13s; } .shelf-item:nth-child(4) { animation-delay: 0.18s; }
|
||||
.shelf-item:nth-child(5) { animation-delay: 0.23s; } .shelf-item:nth-child(6) { animation-delay: 0.28s; }
|
||||
.shelf-item:nth-child(7) { animation-delay: 0.33s; } .shelf-item:nth-child(8) { animation-delay: 0.38s; }
|
||||
.shelf-item:nth-child(9) { animation-delay: 0.43s; } .shelf-item:nth-child(10){ animation-delay: 0.48s; }
|
||||
.shelf-item:nth-child(11){ animation-delay: 0.53s; } .shelf-item:nth-child(12){ animation-delay: 0.58s; }
|
||||
|
||||
@keyframes fadeInUpGlide {
|
||||
0% { opacity: 0; transform: translateY(40px) rotateX(-6deg) translateZ(-20px); }
|
||||
100% { opacity: 1; transform: translateY(0) rotateX(0deg) translateZ(0); }
|
||||
}
|
||||
|
||||
.shelf-item:hover {
|
||||
transform: translateY(-16px) translateZ(28px) rotateX(5deg) rotateY(-2deg) scale(1.02);
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
border-color: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 35px 45px -18px rgba(0, 0, 0, 0.6),
|
||||
0 0 0 2px rgba(255, 255, 255, 0.5) inset,
|
||||
0 0 25px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.shelf-item .emoji {
|
||||
font-size: 52px;
|
||||
display: block;
|
||||
margin-bottom: 14px;
|
||||
transition: all 0.4s cubic-bezier(0.2, 0.9, 0.4, 1.1);
|
||||
transform-style: preserve-3d;
|
||||
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
.shelf-item:hover .emoji {
|
||||
transform: scale(1.15) rotateY(12deg) rotateX(6deg) translateZ(12px);
|
||||
filter: drop-shadow(0 15px 20px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
.shelf-item div:last-child {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
.shelf-item:hover div:last-child {
|
||||
letter-spacing: 1.2px;
|
||||
text-shadow: 0 0 12px rgba(255,255,255,0.6);
|
||||
transform: translateZ(10px);
|
||||
}
|
||||
.shelf-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
height: 35%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
border-radius: 32px 32px 0 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.shelf-item:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.book-chapter-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; padding: 16px; }
|
||||
.book-chapter-item { position: relative; background: rgba(255,255,255,0.12); backdrop-filter: blur(12px); border-radius: 18px; border: 1px solid rgba(255,255,255,0.3); text-align: center; transition: all 0.3s; cursor: pointer; transform-style: preserve-3d; box-shadow: 0 6px 15px rgba(0,0,0,0.15); opacity: 0; animation: fadeInUp 0.4s ease forwards; }
|
||||
.book-chapter-item:nth-child(1) { animation-delay: 0.03s; } .book-chapter-item:nth-child(2) { animation-delay: 0.06s; }
|
||||
.book-chapter-item:hover { transform: translateY(-6px) translateZ(10px) scale(1.01); background: rgba(255,255,255,0.22); border-color: rgba(255,255,255,0.55); }
|
||||
.book-chapter-item a { text-decoration: none; display: block; padding: 16px 12px; font-weight: 500; color: inherit; }
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.page-transition { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(12px); z-index: 10000; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: opacity 0.4s ease, visibility 0.4s ease, background 0.3s ease; }
|
||||
.page-transition.active { opacity: 1; visibility: visible; }
|
||||
.page-transition .book-loader { position: relative; width: 80px; height: 100px; perspective: 1000px; margin-bottom: 30px; }
|
||||
.page-transition .book-page { position: absolute; width: 100%; height: 100%; background: linear-gradient(135deg, #ff9800, #ff5722); border-radius: 4px 8px 8px 4px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); transform-origin: left center; animation: bookFlip 1.2s ease-in-out infinite; transition: background 0.3s ease; }
|
||||
.page-transition .book-page:nth-child(1) { animation-delay: 0s; background: linear-gradient(135deg, #ff9800, #f57c00); }
|
||||
.page-transition .book-page:nth-child(2) { animation-delay: 0.15s; background: linear-gradient(135deg, #ffb74d, #ff9800); }
|
||||
.page-transition .book-page:nth-child(3) { animation-delay: 0.3s; background: linear-gradient(135deg, #ffcc80, #ffb74d); }
|
||||
.page-transition .book-page:nth-child(4) { animation-delay: 0.45s; background: linear-gradient(135deg, #ffe0b2, #ffcc80); }
|
||||
@keyframes bookFlip { 0% { transform: rotateY(0deg); opacity: 1; } 50% { transform: rotateY(-90deg); opacity: 0.5; } 100% { transform: rotateY(-180deg); opacity: 0; } }
|
||||
.page-transition .loading-text { color: white; font-size: 18px; letter-spacing: 4px; font-weight: 300; margin-top: 20px; animation: textPulse 1s ease-in-out infinite; transition: color 0.3s ease; }
|
||||
@keyframes textPulse { 0%, 100% { opacity: 0.5; letter-spacing: 4px; } 50% { opacity: 1; letter-spacing: 8px; text-shadow: 0 0 10px #ff9800; } }
|
||||
.page-transition .loading-dots { display: flex; gap: 8px; margin-top: 15px; }
|
||||
.page-transition .loading-dots span { width: 10px; height: 10px; background: #ff9800; border-radius: 50%; animation: dotBounce 0.6s ease-in-out infinite; transition: background 0.3s ease; }
|
||||
@keyframes dotBounce { 0%, 100% { transform: translateY(0); opacity: 0.5; } 50% { transform: translateY(-10px); opacity: 1; } }
|
||||
.ripple { position: absolute; border-radius: 50%; background: rgba(255, 255, 255, 0.5); transform: scale(0); animation: rippleAnim 0.6s linear forwards; pointer-events: none; }
|
||||
@keyframes rippleAnim { to { transform: scale(4); opacity: 0; } }
|
||||
.page-title { font-size: 26px; font-weight: 600; color: white; padding: 16px; margin: 0; text-shadow: 1px 1px 2px rgba(0,0,0,0.3); transition: color 0.3s ease, text-shadow 0.3s ease; }
|
||||
.progress-bar { position: fixed; top: 60px; left: 0; width: 100%; height: 2px; background: rgba(255,255,255,0.2); z-index: 1002; transition: background 0.3s ease; }
|
||||
.progress-fill { width: 0%; height: 100%; background: #ff9800; transition: width 0.3s, background 0.3s ease; }
|
||||
|
||||
/* ==================== 12款静态主题 ==================== */
|
||||
body.theme-deep-space { background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); }
|
||||
body.theme-deep-space .top-bar { background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-deep-space .top-bar, body.theme-deep-space .top-bar a, body.theme-deep-space .top-bar button { color: #fff; }
|
||||
body.theme-deep-space .ebook-chapter { background: rgba(30, 30, 50, 0.95); color: #e0e0e0; border: 1px solid rgba(255,255,255,0.1); }
|
||||
body.theme-deep-space .ebook-chapter .chapter-title { color: #9b59b6; border-bottom: 2px solid #9b59b6; }
|
||||
body.theme-deep-space .speed-panel, body.theme-deep-space .font-controls, body.theme-deep-space .theme-selector, body.theme-deep-space .bookmark-panel, body.theme-deep-space .stat-panel { background: rgba(15, 12, 41, 0.95); color: #e0e0e0; border-color: rgba(155, 89, 182, 0.3); }
|
||||
body.theme-deep-space .floating-btn { background: rgba(15, 12, 41, 0.9); color: #fff; border-color: rgba(155, 89, 182, 0.5); }
|
||||
body.theme-deep-space .floating-btn.bookmark-btn { background: linear-gradient(135deg, #9b59b6, #8e44ad); }
|
||||
body.theme-deep-space .global-progress-container { background: rgba(15, 12, 41, 0.92); border-top-color: rgba(155, 89, 182, 0.3); }
|
||||
body.theme-deep-space .page-turn-overlay { background: rgba(15, 12, 41, 0.92) !important; }
|
||||
body.theme-deep-space .page-turn-overlay .book-left, body.theme-deep-space .page-turn-overlay .book-right { background: rgba(48, 43, 99, 0.95) !important; border: 2px solid rgba(155, 89, 182, 0.6) !important; color: #bb86fc !important; }
|
||||
body.theme-deep-space .page-turn-overlay .message { background: rgba(48, 43, 99, 0.95) !important; color: #bb86fc !important; border: 1px solid rgba(155, 89, 182, 0.5) !important; }
|
||||
body.theme-deep-space .chapter-tooltip { background: rgba(15, 12, 41, 0.95) !important; border-color: #9b59b6 !important; color: #bb86fc !important; }
|
||||
body.theme-deep-space .shelf-item { background: rgba(15, 12, 41, 0.6) !important; border-color: rgba(155, 89, 182, 0.4) !important; }
|
||||
body.theme-deep-space .shelf-item:hover { background: rgba(48, 43, 99, 0.8) !important; border-color: #9b59b6 !important; }
|
||||
body.theme-deep-space .progress-slider-global::-webkit-slider-thumb { background: #9b59b6 !important; box-shadow: 0 0 8px rgba(155,89,182,0.8) !important; }
|
||||
body.theme-deep-space .progress-fill { background: #9b59b6 !important; }
|
||||
body.theme-deep-space .speed-preset.active { color: #9b59b6 !important; background: rgba(155,89,182,0.2) !important; }
|
||||
body.theme-deep-space .auto-chapter-line input { accent-color: #9b59b6 !important; }
|
||||
body.theme-deep-space .swipe-indicator, body.theme-deep-space .swipe-arrow-left, body.theme-deep-space .swipe-arrow-right { background: rgba(15, 12, 41, 0.8) !important; border-color: #9b59b6 !important; color: #bb86fc !important; }
|
||||
body.theme-deep-space .toast { background: rgba(15, 12, 41, 0.95) !important; color: #bb86fc !important; border: 1px solid #9b59b6 !important; }
|
||||
body.theme-deep-space .page-transition { background: rgba(15, 12, 41, 0.9) !important; }
|
||||
|
||||
body.theme-ocean { background: linear-gradient(135deg, #1a2980 0%, #26d0ce 100%); }
|
||||
body.theme-ocean .top-bar { background: rgba(0, 40, 60, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-ocean .top-bar, body.theme-ocean .top-bar a, body.theme-ocean .top-bar button { color: #e0f7fa; }
|
||||
body.theme-ocean .ebook-chapter { background: rgba(255, 255, 255, 0.95); color: #2c3e50; border: 1px solid rgba(0,0,0,0.1); }
|
||||
body.theme-ocean .ebook-chapter .chapter-title { color: #1a2980; border-bottom: 2px solid #1a2980; }
|
||||
body.theme-ocean .speed-panel, body.theme-ocean .font-controls, body.theme-ocean .theme-selector, body.theme-ocean .bookmark-panel, body.theme-ocean .stat-panel { background: rgba(26, 41, 128, 0.95); color: #e0f7fa; border-color: rgba(38, 208, 206, 0.3); }
|
||||
body.theme-ocean .floating-btn { background: rgba(38, 208, 206, 0.85); color: #e0f7fa; border-color: rgba(255,255,255,0.3); }
|
||||
body.theme-ocean .floating-btn.bookmark-btn { background: linear-gradient(135deg, #1a2980, #26d0ce); }
|
||||
body.theme-ocean .global-progress-container { background: rgba(26, 41, 128, 0.92); border-top-color: rgba(38, 208, 206, 0.3); }
|
||||
body.theme-ocean .page-turn-overlay { background: rgba(26, 41, 128, 0.92) !important; }
|
||||
body.theme-ocean .page-turn-overlay .book-left, body.theme-ocean .page-turn-overlay .book-right { background: rgba(38, 208, 206, 0.9) !important; border: 2px solid rgba(255,255,255,0.4) !important; color: #e0f7fa !important; }
|
||||
body.theme-ocean .page-turn-overlay .message { background: rgba(26, 41, 128, 0.95) !important; color: #e0f7fa !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-ocean .chapter-tooltip { background: rgba(26, 41, 128, 0.95) !important; border-color: #26d0ce !important; color: #e0f7fa !important; }
|
||||
body.theme-ocean .shelf-item { background: rgba(38, 208, 206, 0.15) !important; border-color: rgba(38, 208, 206, 0.3) !important; color: #e0f7fa !important; }
|
||||
body.theme-ocean .shelf-item:hover { background: rgba(38, 208, 206, 0.3) !important; }
|
||||
body.theme-ocean .progress-slider-global::-webkit-slider-thumb { background: #26d0ce !important; }
|
||||
body.theme-ocean .progress-fill { background: #26d0ce !important; }
|
||||
body.theme-ocean .speed-preset.active { color: #26d0ce !important; background: rgba(38,208,206,0.2) !important; }
|
||||
body.theme-ocean .swipe-indicator, body.theme-ocean .swipe-arrow-left, body.theme-ocean .swipe-arrow-right { background: rgba(26, 41, 128, 0.8) !important; border-color: #26d0ce !important; color: #e0f7fa !important; }
|
||||
|
||||
body.theme-cherry { background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%); }
|
||||
body.theme-cherry .top-bar { background: rgba(219, 112, 147, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-cherry .top-bar, body.theme-cherry .top-bar a, body.theme-cherry .top-bar button { color: #5a2e3e; }
|
||||
body.theme-cherry .ebook-chapter { background: rgba(255, 245, 245, 0.95); color: #5a3a3a; border: 1px solid rgba(0,0,0,0.05); }
|
||||
body.theme-cherry .ebook-chapter .chapter-title { color: #db7093; border-bottom: 2px solid #db7093; }
|
||||
body.theme-cherry .speed-panel, body.theme-cherry .font-controls, body.theme-cherry .theme-selector, body.theme-cherry .bookmark-panel, body.theme-cherry .stat-panel { background: rgba(255, 245, 245, 0.95); color: #5a2e3e; border-color: rgba(219,112,147,0.3); }
|
||||
body.theme-cherry .floating-btn { background: rgba(219, 112, 147, 0.85); color: #5a2e3e; }
|
||||
body.theme-cherry .floating-btn.bookmark-btn { background: linear-gradient(135deg, #db7093, #ff9a9e); }
|
||||
body.theme-cherry .global-progress-container { background: rgba(255, 245, 245, 0.92); border-top-color: rgba(219,112,147,0.2); }
|
||||
body.theme-cherry .page-turn-overlay { background: rgba(255, 154, 158, 0.92) !important; }
|
||||
body.theme-cherry .page-turn-overlay .book-left, body.theme-cherry .page-turn-overlay .book-right { background: rgba(254, 207, 239, 0.95) !important; border: 2px solid rgba(219, 112, 147, 0.6) !important; color: #5a2e3e !important; }
|
||||
body.theme-cherry .page-turn-overlay .message { background: rgba(219, 112, 147, 0.95) !important; color: #5a2e3e !important; border: 1px solid rgba(219,112,147,0.4) !important; }
|
||||
body.theme-cherry .chapter-tooltip { background: rgba(219, 112, 147, 0.95) !important; border-color: #ff9a9e !important; color: #5a2e3e !important; }
|
||||
body.theme-cherry .shelf-item { background: rgba(219,112,147,0.15) !important; color: #5a2e3e !important; }
|
||||
body.theme-cherry .shelf-item:hover { background: rgba(219,112,147,0.3) !important; }
|
||||
body.theme-cherry .progress-slider-global::-webkit-slider-thumb { background: #db7093 !important; }
|
||||
body.theme-cherry .progress-fill { background: #db7093 !important; }
|
||||
body.theme-cherry .speed-preset.active { color: #db7093 !important; background: rgba(219,112,147,0.2) !important; }
|
||||
body.theme-cherry .swipe-indicator, body.theme-cherry .swipe-arrow-left, body.theme-cherry .swipe-arrow-right { background: rgba(255, 245, 245, 0.9) !important; border-color: #db7093 !important; color: #5a2e3e !important; }
|
||||
|
||||
body.theme-night { background: #0a0a0a; }
|
||||
body.theme-night .top-bar { background: rgba(10, 10, 10, 0.95); backdrop-filter: blur(20px); border-bottom: 1px solid #333; }
|
||||
body.theme-night .top-bar, body.theme-night .top-bar a, body.theme-night .top-bar button { color: #aaa; }
|
||||
body.theme-night .ebook-chapter { background: #1a1a1a; color: #b0b0b0; border: 1px solid #333; }
|
||||
body.theme-night .ebook-chapter .chapter-title { color: #888; border-bottom: 2px solid #555; }
|
||||
body.theme-night .speed-panel, body.theme-night .font-controls, body.theme-night .theme-selector, body.theme-night .bookmark-panel, body.theme-night .stat-panel { background: rgba(10, 10, 10, 0.95); color: #aaa; border-color: #333; }
|
||||
body.theme-night .floating-btn { background: rgba(30, 30, 30, 0.95); color: #aaa; border-color: #444; }
|
||||
body.theme-night .floating-btn.bookmark-btn { background: linear-gradient(135deg, #555, #333); }
|
||||
body.theme-night .global-progress-container { background: rgba(10, 10, 10, 0.92); border-top-color: #333; }
|
||||
body.theme-night .page-turn-overlay { background: rgba(10, 10, 10, 0.95) !important; }
|
||||
body.theme-night .page-turn-overlay .book-left, body.theme-night .page-turn-overlay .book-right { background: rgba(30, 30, 30, 0.98) !important; border: 2px solid #555 !important; color: #aaa !important; }
|
||||
body.theme-night .page-turn-overlay .message { background: rgba(30, 30, 30, 0.98) !important; color: #aaa !important; border: 1px solid #555 !important; }
|
||||
body.theme-night .chapter-tooltip { background: rgba(30, 30, 30, 0.98) !important; border-color: #666 !important; color: #ccc !important; }
|
||||
body.theme-night .shelf-item { background: rgba(255,255,255,0.05) !important; border-color: #333 !important; color: #aaa !important; }
|
||||
body.theme-night .shelf-item:hover { background: rgba(255,255,255,0.1) !important; border-color: #666 !important; }
|
||||
body.theme-night .progress-slider-global::-webkit-slider-thumb { background: #888 !important; }
|
||||
body.theme-night .progress-fill { background: #888 !important; }
|
||||
body.theme-night .swipe-indicator, body.theme-night .swipe-arrow-left, body.theme-night .swipe-arrow-right { background: rgba(10, 10, 10, 0.9) !important; border-color: #555 !important; color: #aaa !important; }
|
||||
|
||||
body.theme-forest { background: linear-gradient(135deg, #134e5e 0%, #71b280 100%); }
|
||||
body.theme-forest .top-bar { background: rgba(20, 60, 40, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-forest .top-bar, body.theme-forest .top-bar a, body.theme-forest .top-bar button { color: #e8f5e9; }
|
||||
body.theme-forest .ebook-chapter { background: rgba(255, 255, 245, 0.95); color: #2d5a3b; border: 1px solid rgba(0,0,0,0.05); }
|
||||
body.theme-forest .ebook-chapter .chapter-title { color: #2e7d32; border-bottom: 2px solid #2e7d32; }
|
||||
body.theme-forest .speed-panel, body.theme-forest .font-controls, body.theme-forest .theme-selector, body.theme-forest .bookmark-panel, body.theme-forest .stat-panel { background: rgba(19, 78, 94, 0.95); color: #e8f5e9; border-color: rgba(113,178,128,0.3); }
|
||||
body.theme-forest .floating-btn { background: rgba(113, 178, 128, 0.85); color: #e8f5e9; }
|
||||
body.theme-forest .floating-btn.bookmark-btn { background: linear-gradient(135deg, #2e7d32, #71b280); }
|
||||
body.theme-forest .global-progress-container { background: rgba(19, 78, 94, 0.92); border-top-color: rgba(113,178,128,0.2); }
|
||||
body.theme-forest .page-turn-overlay { background: rgba(19, 78, 94, 0.92) !important; }
|
||||
body.theme-forest .page-turn-overlay .book-left, body.theme-forest .page-turn-overlay .book-right { background: rgba(113, 178, 128, 0.9) !important; border: 2px solid rgba(255,255,255,0.4) !important; color: #e8f5e9 !important; }
|
||||
body.theme-forest .page-turn-overlay .message { background: rgba(19, 78, 94, 0.95) !important; color: #e8f5e9 !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-forest .chapter-tooltip { background: rgba(19, 78, 94, 0.95) !important; border-color: #71b280 !important; color: #e8f5e9 !important; }
|
||||
body.theme-forest .shelf-item { background: rgba(113,178,128,0.15) !important; color: #e8f5e9 !important; }
|
||||
body.theme-forest .shelf-item:hover { background: rgba(113,178,128,0.3) !important; }
|
||||
body.theme-forest .progress-slider-global::-webkit-slider-thumb { background: #71b280 !important; }
|
||||
body.theme-forest .progress-fill { background: #71b280 !important; }
|
||||
body.theme-forest .swipe-indicator, body.theme-forest .swipe-arrow-left, body.theme-forest .swipe-arrow-right { background: rgba(19, 78, 94, 0.9) !important; border-color: #71b280 !important; color: #e8f5e9 !important; }
|
||||
|
||||
body.theme-sunset { background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 100%); }
|
||||
body.theme-sunset .top-bar { background: rgba(180, 70, 40, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-sunset .top-bar, body.theme-sunset .top-bar a, body.theme-sunset .top-bar button { color: #fff3e0; }
|
||||
body.theme-sunset .ebook-chapter { background: rgba(255, 248, 240, 0.96); color: #6b3e1f; }
|
||||
body.theme-sunset .ebook-chapter .chapter-title { color: #d84315; border-bottom: 2px solid #d84315; }
|
||||
body.theme-sunset .speed-panel, body.theme-sunset .font-controls, body.theme-sunset .theme-selector, body.theme-sunset .bookmark-panel, body.theme-sunset .stat-panel { background: rgba(255, 126, 95, 0.95); color: #fff3e0; border-color: rgba(254,180,123,0.3); }
|
||||
body.theme-sunset .floating-btn { background: rgba(254, 180, 123, 0.85); color: #fff3e0; }
|
||||
body.theme-sunset .floating-btn.bookmark-btn { background: linear-gradient(135deg, #d84315, #ff7e5f); }
|
||||
body.theme-sunset .global-progress-container { background: rgba(255, 126, 95, 0.92); border-top-color: rgba(254,180,123,0.2); }
|
||||
body.theme-sunset .page-turn-overlay { background: rgba(255, 126, 95, 0.92) !important; }
|
||||
body.theme-sunset .page-turn-overlay .book-left, body.theme-sunset .page-turn-overlay .book-right { background: rgba(254, 180, 123, 0.95) !important; border: 2px solid rgba(255,255,255,0.4) !important; color: #fff3e0 !important; }
|
||||
body.theme-sunset .page-turn-overlay .message { background: rgba(255, 126, 95, 0.95) !important; color: #fff3e0 !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-sunset .chapter-tooltip { background: rgba(180, 70, 40, 0.95) !important; border-color: #feb47b !important; color: #fff3e0 !important; }
|
||||
body.theme-sunset .shelf-item { background: rgba(254,180,123,0.15) !important; color: #fff3e0 !important; }
|
||||
body.theme-sunset .shelf-item:hover { background: rgba(254,180,123,0.3) !important; }
|
||||
body.theme-sunset .progress-slider-global::-webkit-slider-thumb { background: #feb47b !important; }
|
||||
body.theme-sunset .progress-fill { background: #feb47b !important; }
|
||||
|
||||
body.theme-lavender { background: linear-gradient(135deg, #8e9ecc 0%, #e0bbff 100%); }
|
||||
body.theme-lavender .top-bar { background: rgba(100, 80, 140, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-lavender .top-bar, body.theme-lavender .top-bar a, body.theme-lavender .top-bar button { color: #f3e5f5; }
|
||||
body.theme-lavender .ebook-chapter { background: rgba(245, 235, 255, 0.96); color: #4a3a6e; }
|
||||
body.theme-lavender .ebook-chapter .chapter-title { color: #7b1fa2; border-bottom: 2px solid #7b1fa2; }
|
||||
body.theme-lavender .speed-panel, body.theme-lavender .font-controls, body.theme-lavender .theme-selector, body.theme-lavender .bookmark-panel, body.theme-lavender .stat-panel { background: rgba(142, 158, 204, 0.95); color: #4a3a6e; border-color: rgba(224,187,255,0.3); }
|
||||
body.theme-lavender .floating-btn { background: rgba(224, 187, 255, 0.85); color: #4a3a6e; }
|
||||
body.theme-lavender .floating-btn.bookmark-btn { background: linear-gradient(135deg, #7b1fa2, #8e9ecc); }
|
||||
body.theme-lavender .global-progress-container { background: rgba(142, 158, 204, 0.92); border-top-color: rgba(224,187,255,0.2); }
|
||||
body.theme-lavender .page-turn-overlay { background: rgba(142, 158, 204, 0.92) !important; }
|
||||
body.theme-lavender .page-turn-overlay .book-left, body.theme-lavender .page-turn-overlay .book-right { background: rgba(224, 187, 255, 0.95) !important; border: 2px solid rgba(100, 80, 140, 0.6) !important; color: #4a3a6e !important; }
|
||||
body.theme-lavender .page-turn-overlay .message { background: rgba(142, 158, 204, 0.95) !important; color: #f3e5f5 !important; border: 1px solid rgba(100,80,140,0.4) !important; }
|
||||
body.theme-lavender .chapter-tooltip { background: rgba(100, 80, 140, 0.95) !important; border-color: #e0bbff !important; color: #f3e5f5 !important; }
|
||||
body.theme-lavender .shelf-item { background: rgba(224,187,255,0.15) !important; color: #4a3a6e !important; }
|
||||
body.theme-lavender .shelf-item:hover { background: rgba(224,187,255,0.3) !important; }
|
||||
body.theme-lavender .progress-slider-global::-webkit-slider-thumb { background: #e0bbff !important; }
|
||||
body.theme-lavender .progress-fill { background: #e0bbff !important; }
|
||||
|
||||
body.theme-blueberry { background: linear-gradient(135deg, #2c3e66 0%, #4a69bd 100%); }
|
||||
body.theme-blueberry .top-bar { background: rgba(30, 50, 80, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-blueberry .top-bar, body.theme-blueberry .top-bar a, body.theme-blueberry .top-bar button { color: #dfe6e9; }
|
||||
body.theme-blueberry .ebook-chapter { background: rgba(240, 245, 255, 0.96); color: #2c3e66; }
|
||||
body.theme-blueberry .ebook-chapter .chapter-title { color: #3b82f6; border-bottom: 2px solid #3b82f6; }
|
||||
body.theme-blueberry .speed-panel, body.theme-blueberry .font-controls, body.theme-blueberry .theme-selector, body.theme-blueberry .bookmark-panel, body.theme-blueberry .stat-panel { background: rgba(44, 62, 102, 0.95); color: #dfe6e9; border-color: rgba(74,105,189,0.3); }
|
||||
body.theme-blueberry .floating-btn { background: rgba(74, 105, 189, 0.85); color: #dfe6e9; }
|
||||
body.theme-blueberry .floating-btn.bookmark-btn { background: linear-gradient(135deg, #3b82f6, #2c3e66); }
|
||||
body.theme-blueberry .global-progress-container { background: rgba(44, 62, 102, 0.92); border-top-color: rgba(74,105,189,0.2); }
|
||||
body.theme-blueberry .page-turn-overlay { background: rgba(44, 62, 102, 0.92) !important; }
|
||||
body.theme-blueberry .page-turn-overlay .book-left, body.theme-blueberry .page-turn-overlay .book-right { background: rgba(74, 105, 189, 0.9) !important; border: 2px solid rgba(255,255,255,0.3) !important; color: #dfe6e9 !important; }
|
||||
body.theme-blueberry .page-turn-overlay .message { background: rgba(44, 62, 102, 0.95) !important; color: #dfe6e9 !important; border: 1px solid rgba(255,255,255,0.3) !important; }
|
||||
body.theme-blueberry .chapter-tooltip { background: rgba(44, 62, 102, 0.95) !important; border-color: #4a69bd !important; color: #dfe6e9 !important; }
|
||||
body.theme-blueberry .shelf-item { background: rgba(74,105,189,0.15) !important; color: #dfe6e9 !important; }
|
||||
body.theme-blueberry .shelf-item:hover { background: rgba(74,105,189,0.3) !important; }
|
||||
body.theme-blueberry .progress-slider-global::-webkit-slider-thumb { background: #4a69bd !important; }
|
||||
body.theme-blueberry .progress-fill { background: #4a69bd !important; }
|
||||
|
||||
body.theme-amber { background: linear-gradient(135deg, #ffb347 0%, #ffcc33 100%); }
|
||||
body.theme-amber .top-bar { background: rgba(160, 90, 30, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-amber .top-bar, body.theme-amber .top-bar a, body.theme-amber .top-bar button { color: #3e2723; }
|
||||
body.theme-amber .ebook-chapter { background: rgba(255, 250, 230, 0.96); color: #5d4037; }
|
||||
body.theme-amber .ebook-chapter .chapter-title { color: #f57c00; border-bottom: 2px solid #f57c00; }
|
||||
body.theme-amber .speed-panel, body.theme-amber .font-controls, body.theme-amber .theme-selector, body.theme-amber .bookmark-panel, body.theme-amber .stat-panel { background: rgba(255, 179, 71, 0.95); color: #3e2723; border-color: rgba(255,204,51,0.3); }
|
||||
body.theme-amber .floating-btn { background: rgba(255, 204, 51, 0.85); color: #3e2723; }
|
||||
body.theme-amber .floating-btn.bookmark-btn { background: linear-gradient(135deg, #f57c00, #ffb347); }
|
||||
body.theme-amber .global-progress-container { background: rgba(255, 179, 71, 0.92); border-top-color: rgba(255,204,51,0.2); }
|
||||
body.theme-amber .page-turn-overlay { background: rgba(255, 179, 71, 0.92) !important; }
|
||||
body.theme-amber .page-turn-overlay .book-left, body.theme-amber .page-turn-overlay .book-right { background: rgba(255, 204, 51, 0.95) !important; border: 2px solid rgba(160, 90, 30, 0.6) !important; color: #3e2723 !important; }
|
||||
body.theme-amber .page-turn-overlay .message { background: rgba(255, 179, 71, 0.95) !important; color: #3e2723 !important; border: 1px solid rgba(160,90,30,0.4) !important; }
|
||||
body.theme-amber .chapter-tooltip { background: rgba(160, 90, 30, 0.95) !important; border-color: #ffcc33 !important; color: #fff8e1 !important; }
|
||||
body.theme-amber .shelf-item { background: rgba(255,204,51,0.15) !important; color: #3e2723 !important; }
|
||||
body.theme-amber .shelf-item:hover { background: rgba(255,204,51,0.3) !important; }
|
||||
body.theme-amber .progress-slider-global::-webkit-slider-thumb { background: #ffcc33 !important; }
|
||||
body.theme-amber .progress-fill { background: #ffcc33 !important; }
|
||||
|
||||
body.theme-coral { background: linear-gradient(135deg, #ff6b6b 0%, #ffb8b8 100%); }
|
||||
body.theme-coral .top-bar { background: rgba(200, 80, 80, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-coral .top-bar, body.theme-coral .top-bar a, body.theme-coral .top-bar button { color: #fff; }
|
||||
body.theme-coral .ebook-chapter { background: rgba(255, 240, 240, 0.95); color: #5a3a3a; }
|
||||
body.theme-coral .ebook-chapter .chapter-title { color: #ff6b6b; border-bottom: 2px solid #ff6b6b; }
|
||||
body.theme-coral .speed-panel, body.theme-coral .font-controls, body.theme-coral .theme-selector, body.theme-coral .bookmark-panel, body.theme-coral .stat-panel { background: rgba(200, 80, 80, 0.95); color: #fff; border-color: rgba(255,184,184,0.3); }
|
||||
body.theme-coral .floating-btn { background: rgba(200, 80, 80, 0.9); color: #fff; }
|
||||
body.theme-coral .floating-btn.bookmark-btn { background: linear-gradient(135deg, #ff6b6b, #ffb8b8); }
|
||||
body.theme-coral .global-progress-container { background: rgba(200, 80, 80, 0.92); border-top-color: rgba(255,184,184,0.2); }
|
||||
body.theme-coral .page-turn-overlay { background: rgba(200, 80, 80, 0.92) !important; }
|
||||
body.theme-coral .page-turn-overlay .book-left, body.theme-coral .page-turn-overlay .book-right { background: rgba(255, 184, 184, 0.95) !important; border: 2px solid rgba(200, 80, 80, 0.6) !important; color: #fff !important; }
|
||||
body.theme-coral .page-turn-overlay .message { background: rgba(200, 80, 80, 0.95) !important; color: #fff !important; border: 1px solid rgba(200,80,80,0.4) !important; }
|
||||
body.theme-coral .chapter-tooltip { background: rgba(200, 80, 80, 0.95) !important; border-color: #ffb8b8 !important; color: #fff !important; }
|
||||
body.theme-coral .shelf-item { background: rgba(255,184,184,0.15) !important; color: #fff !important; }
|
||||
body.theme-coral .shelf-item:hover { background: rgba(255,184,184,0.3) !important; }
|
||||
body.theme-coral .progress-slider-global::-webkit-slider-thumb { background: #ffb8b8 !important; }
|
||||
body.theme-coral .progress-fill { background: #ffb8b8 !important; }
|
||||
|
||||
body.theme-mint { background: linear-gradient(135deg, #a8e6cf 0%, #80deea 100%); }
|
||||
body.theme-mint .top-bar { background: rgba(60, 120, 100, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-mint .top-bar, body.theme-mint .top-bar a, body.theme-mint .top-bar button { color: #2d5a3b; }
|
||||
body.theme-mint .ebook-chapter { background: rgba(255, 255, 250, 0.95); color: #2d5a3b; }
|
||||
body.theme-mint .ebook-chapter .chapter-title { color: #2ecc71; border-bottom: 2px solid #2ecc71; }
|
||||
body.theme-mint .speed-panel, body.theme-mint .font-controls, body.theme-mint .theme-selector, body.theme-mint .bookmark-panel, body.theme-mint .stat-panel { background: rgba(60, 120, 100, 0.95); color: #fff; border-color: rgba(168,230,207,0.3); }
|
||||
body.theme-mint .floating-btn { background: rgba(60, 120, 100, 0.9); color: #fff; }
|
||||
body.theme-mint .floating-btn.bookmark-btn { background: linear-gradient(135deg, #2ecc71, #a8e6cf); }
|
||||
body.theme-mint .global-progress-container { background: rgba(60, 120, 100, 0.92); border-top-color: rgba(168,230,207,0.2); }
|
||||
body.theme-mint .page-turn-overlay { background: rgba(60, 120, 100, 0.92) !important; }
|
||||
body.theme-mint .page-turn-overlay .book-left, body.theme-mint .page-turn-overlay .book-right { background: rgba(168, 230, 207, 0.9) !important; border: 2px solid rgba(60, 120, 100, 0.6) !important; color: #2d5a3b !important; }
|
||||
body.theme-mint .page-turn-overlay .message { background: rgba(60, 120, 100, 0.95) !important; color: #fff !important; border: 1px solid rgba(60,120,100,0.4) !important; }
|
||||
body.theme-mint .chapter-tooltip { background: rgba(60, 120, 100, 0.95) !important; border-color: #80deea !important; color: #fff !important; }
|
||||
body.theme-mint .shelf-item { background: rgba(168,230,207,0.15) !important; color: #2d5a3b !important; }
|
||||
body.theme-mint .shelf-item:hover { background: rgba(168,230,207,0.3) !important; }
|
||||
body.theme-mint .progress-slider-global::-webkit-slider-thumb { background: #80deea !important; }
|
||||
body.theme-mint .progress-fill { background: #80deea !important; }
|
||||
|
||||
body.theme-rosegold { background: linear-gradient(135deg, #e8b4b8 0%, #ffd9e2 100%); }
|
||||
body.theme-rosegold .top-bar { background: rgba(160, 100, 110, 0.85); backdrop-filter: blur(20px); }
|
||||
body.theme-rosegold .top-bar, body.theme-rosegold .top-bar a, body.theme-rosegold .top-bar button { color: #5a3a3e; }
|
||||
body.theme-rosegold .ebook-chapter { background: rgba(255, 248, 250, 0.95); color: #5a3a3e; }
|
||||
body.theme-rosegold .ebook-chapter .chapter-title { color: #e8b4b8; border-bottom: 2px solid #e8b4b8; }
|
||||
body.theme-rosegold .speed-panel, body.theme-rosegold .font-controls, body.theme-rosegold .theme-selector, body.theme-rosegold .bookmark-panel, body.theme-rosegold .stat-panel { background: rgba(160, 100, 110, 0.95); color: #fff; border-color: rgba(255,217,226,0.3); }
|
||||
body.theme-rosegold .floating-btn { background: rgba(160, 100, 110, 0.9); color: #fff; }
|
||||
body.theme-rosegold .floating-btn.bookmark-btn { background: linear-gradient(135deg, #e8b4b8, #ffd9e2); }
|
||||
body.theme-rosegold .global-progress-container { background: rgba(160, 100, 110, 0.92); border-top-color: rgba(255,217,226,0.2); }
|
||||
body.theme-rosegold .page-turn-overlay { background: rgba(160, 100, 110, 0.92) !important; }
|
||||
body.theme-rosegold .page-turn-overlay .book-left, body.theme-rosegold .page-turn-overlay .book-right { background: rgba(255, 217, 226, 0.95) !important; border: 2px solid rgba(160, 100, 110, 0.6) !important; color: #5a3a3e !important; }
|
||||
body.theme-rosegold .page-turn-overlay .message { background: rgba(160, 100, 110, 0.95) !important; color: #fff !important; border: 1px solid rgba(160,100,110,0.4) !important; }
|
||||
body.theme-rosegold .chapter-tooltip { background: rgba(160, 100, 110, 0.95) !important; border-color: #ffd9e2 !important; color: #fff5f5 !important; }
|
||||
body.theme-rosegold .shelf-item { background: rgba(255,217,226,0.15) !important; color: #5a3a3e !important; }
|
||||
body.theme-rosegold .shelf-item:hover { background: rgba(255,217,226,0.3) !important; }
|
||||
body.theme-rosegold .progress-slider-global::-webkit-slider-thumb { background: #ffd9e2 !important; }
|
||||
body.theme-rosegold .progress-fill { background: #ffd9e2 !important; }
|
||||
|
||||
/* ==================== 护眼主题 ==================== */
|
||||
body.theme-eyecare { background: #c7edcc !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .top-bar { background: rgba(199, 237, 204, 0.92) !important; backdrop-filter: blur(20px) !important; border-bottom: 1px solid rgba(100, 100, 80, 0.2) !important; }
|
||||
body.theme-eyecare .top-bar, body.theme-eyecare .top-bar a, body.theme-eyecare .top-bar button { color: #2d2d2d !important; }
|
||||
body.theme-eyecare .ebook-chapter { background: rgba(215, 245, 210, 0.95) !important; color: #2d2d2d !important; box-shadow: 0 8px 32px rgba(0,0,0,0.08) !important; border: 1px solid rgba(139,154,110,0.3) !important; }
|
||||
body.theme-eyecare .ebook-chapter .chapter-title { color: #5a6b3a !important; border-bottom-color: #a0b880 !important; }
|
||||
body.theme-eyecare .speed-panel, body.theme-eyecare .font-controls, body.theme-eyecare .theme-selector, body.theme-eyecare .bookmark-panel, body.theme-eyecare .stat-panel { background: rgba(215, 245, 210, 0.95) !important; color: #2d2d2d !important; border: 1px solid rgba(100, 100, 80, 0.2) !important; }
|
||||
body.theme-eyecare .floating-btn { background: rgba(199, 237, 204, 0.9) !important; color: #2d2d2d !important; border: 1px solid rgba(100, 100, 80, 0.3) !important; }
|
||||
body.theme-eyecare .floating-btn.bookmark-btn { background: linear-gradient(135deg, #8b9a6e, #a0b880) !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .global-progress-container { background: rgba(199, 237, 204, 0.92) !important; border-top-color: rgba(139,154,110,0.3) !important; }
|
||||
body.theme-eyecare .shelf-item { background: rgba(215, 245, 210, 0.8) !important; color: #2d2d2d !important; border-color: rgba(139,154,110,0.3) !important; }
|
||||
body.theme-eyecare .shelf-item:hover { background: rgba(199, 237, 204, 0.9) !important; border-color: #8b9a6e !important; }
|
||||
body.theme-eyecare .progress-slider-global::-webkit-slider-thumb { background: #8b9a6e !important; }
|
||||
body.theme-eyecare .progress-fill { background: #8b9a6e !important; }
|
||||
body.theme-eyecare .page-turn-overlay { background: rgba(199, 237, 204, 0.92) !important; }
|
||||
body.theme-eyecare .page-turn-overlay .book-left, body.theme-eyecare .page-turn-overlay .book-right { background: rgba(215, 245, 210, 0.95) !important; border: 2px solid rgba(139, 154, 110, 0.5) !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .page-turn-overlay .message { background: rgba(215, 245, 210, 0.95) !important; color: #2d2d2d !important; border: 1px solid rgba(139, 154, 110, 0.4) !important; }
|
||||
body.theme-eyecare .chapter-tooltip { background: rgba(215, 245, 210, 0.98) !important; border-color: #8b9a6e !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .speed-preset.active { color: #8b9a6e !important; background: rgba(139,154,110,0.2) !important; }
|
||||
body.theme-eyecare .auto-chapter-line input { accent-color: #8b9a6e !important; }
|
||||
body.theme-eyecare .swipe-indicator, body.theme-eyecare .swipe-arrow-left, body.theme-eyecare .swipe-arrow-right { background: rgba(215, 245, 210, 0.95) !important; border-color: #8b9a6e !important; color: #2d2d2d !important; }
|
||||
body.theme-eyecare .toast { background: rgba(199, 237, 204, 0.95) !important; color: #2d2d2d !important; border: 1px solid #8b9a6e !important; }
|
||||
body.theme-eyecare .page-transition { background: rgba(199, 237, 204, 0.95) !important; }
|
||||
body.theme-eyecare .page-transition .book-page { background: linear-gradient(135deg, #8b9a6e, #a0b880) !important; }
|
||||
body.theme-eyecare .page-transition .loading-text { color: #2d2d2d !important; }
|
||||
|
||||
/* ==================== 12款动态主题 ==================== */
|
||||
body.theme-aurora-dynamic { background: linear-gradient(270deg, #1a0b2e, #2d1b69, #1a4d8c, #0f5c6b); background-size: 400% 400%; animation: auroraFlow 12s ease infinite; color: #f0f0f0 !important; }
|
||||
@keyframes auroraFlow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
||||
body.theme-aurora-dynamic .top-bar { background: rgba(0, 0, 0, 0.5) !important; backdrop-filter: blur(20px) !important; border-bottom: 1px solid rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .top-bar, body.theme-aurora-dynamic .top-bar a, body.theme-aurora-dynamic .top-bar button { color: #7cffd0 !important; text-shadow: 0 0 5px rgba(124,255,208,0.3); }
|
||||
body.theme-aurora-dynamic .back-btn { background: rgba(124, 255, 208, 0.15) !important; }
|
||||
body.theme-aurora-dynamic .page-title { color: #7cffd0 !important; text-shadow: 0 0 10px rgba(124,255,208,0.4); }
|
||||
body.theme-aurora-dynamic .ebook-chapter { background: rgba(0, 0, 0, 0.4) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .ebook-chapter .chapter-title { color: #7cffd0 !important; border-bottom-color: rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .floating-btn, body.theme-aurora-dynamic .speed-panel, body.theme-aurora-dynamic .font-controls, body.theme-aurora-dynamic .theme-selector, body.theme-aurora-dynamic .bookmark-panel, body.theme-aurora-dynamic .stat-panel { background: rgba(0, 0, 0, 0.5) !important; border: 1px solid rgba(124, 255, 208, 0.3) !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .floating-btn { background: rgba(0, 0, 0, 0.4) !important; color: #7cffd0 !important; border: 1px solid rgba(124, 255, 208, 0.4) !important; }
|
||||
body.theme-aurora-dynamic .floating-btn.bookmark-btn { background: rgba(124, 255, 208, 0.2) !important; border: 1px solid #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .floating-btn.bookmark-btn.active { background: #7cffd0 !important; color: #1a0b2e !important; }
|
||||
body.theme-aurora-dynamic .speed-slider::-webkit-slider-thumb { background: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .progress-slider-global::-webkit-slider-thumb { background: #7cffd0 !important; box-shadow: 0 0 8px rgba(124,255,208,0.8) !important; }
|
||||
body.theme-aurora-dynamic .progress-fill { background: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .speed-preset.active { color: #7cffd0 !important; background: rgba(124,255,208,0.2) !important; }
|
||||
body.theme-aurora-dynamic .auto-chapter-line { border-top-color: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .auto-chapter-line input { accent-color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .global-progress-container { background: rgba(0, 0, 0, 0.5) !important; border-top: 1px solid rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .progress-info { color: rgba(124, 255, 208, 0.8) !important; }
|
||||
body.theme-aurora-dynamic .shelf-item { background: rgba(124, 255, 208, 0.1) !important; border-color: rgba(124, 255, 208, 0.3) !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .shelf-item:hover { background: rgba(124, 255, 208, 0.2) !important; border-color: rgba(124, 255, 208, 0.6) !important; }
|
||||
body.theme-aurora-dynamic .book-chapter-item { background: rgba(124, 255, 208, 0.1) !important; border-color: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .book-chapter-item a { color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .book-chapter-item:hover { background: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .bookmark-item .title { color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .bookmark-header { border-bottom-color: rgba(124, 255, 208, 0.2) !important; }
|
||||
body.theme-aurora-dynamic .chapter-tooltip { background: rgba(0, 0, 0, 0.75) !important; border-color: #7cffd0 !important; color: #7cffd0 !important; box-shadow: 0 0 15px rgba(124,255,208,0.3) !important; }
|
||||
body.theme-aurora-dynamic .page-turn-overlay { background: rgba(0, 0, 0, 0.6) !important; }
|
||||
body.theme-aurora-dynamic .page-turn-overlay .book-left, body.theme-aurora-dynamic .page-turn-overlay .book-right { background: rgba(0, 0, 0, 0.5) !important; border: 2px solid rgba(124, 255, 208, 0.4) !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .page-turn-overlay .message { background: rgba(0, 0, 0, 0.7) !important; color: #7cffd0 !important; border: 1px solid rgba(124, 255, 208, 0.5) !important; }
|
||||
body.theme-aurora-dynamic .swipe-indicator, body.theme-aurora-dynamic .swipe-arrow-left, body.theme-aurora-dynamic .swipe-arrow-right { background: rgba(0, 0, 0, 0.6) !important; border-color: #7cffd0 !important; color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .toast { background: rgba(0, 0, 0, 0.8) !important; color: #7cffd0 !important; border: 1px solid rgba(124, 255, 208, 0.3) !important; }
|
||||
body.theme-aurora-dynamic .page-transition { background: rgba(0, 0, 0, 0.6) !important; }
|
||||
body.theme-aurora-dynamic .page-transition .book-page { background: linear-gradient(135deg, #7cffd0, #2d1b69) !important; }
|
||||
body.theme-aurora-dynamic .page-transition .loading-text { color: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .page-transition .loading-dots span { background: #7cffd0 !important; }
|
||||
body.theme-aurora-dynamic .top-bar button.bookmark { background: rgba(124, 255, 208, 0.2) !important; border: 1px solid #7cffd0 !important; color: #7cffd0 !important; }
|
||||
|
||||
body.theme-neon-dynamic { background: #0a0a0a !important; animation: neonBgPulse 2s ease-in-out infinite; color: #fff !important; }
|
||||
@keyframes neonBgPulse { 0% { background: #0a0a0a; } 30% { background: #0d1a1a; } 100% { background: #0a0a0a; } }
|
||||
body.theme-neon-dynamic .top-bar { background: rgba(0, 0, 0, 0.7) !important; border-bottom: 1px solid rgba(0, 255, 255, 0.4) !important; animation: neonBorderFlash 1.5s ease-in-out infinite !important; }
|
||||
@keyframes neonBorderFlash { 0% { border-bottom-color: rgba(0, 255, 255, 0.2); } 50% { border-bottom-color: rgba(0, 255, 255, 0.8); } 100% { border-bottom-color: rgba(0, 255, 255, 0.2); } }
|
||||
body.theme-neon-dynamic .top-bar, body.theme-neon-dynamic .top-bar a, body.theme-neon-dynamic .top-bar button { color: #0ff !important; text-shadow: 0 0 5px rgba(0,255,255,0.5); }
|
||||
body.theme-neon-dynamic .back-btn { background: rgba(0, 255, 255, 0.1) !important; }
|
||||
body.theme-neon-dynamic .page-title { color: #0ff !important; text-shadow: 0 0 10px rgba(0,255,255,0.5); animation: neonTitlePulse 1.5s ease-in-out infinite; }
|
||||
@keyframes neonTitlePulse { 0% { text-shadow: 0 0 5px rgba(0,255,255,0.3); } 50% { text-shadow: 0 0 20px rgba(0,255,255,0.8); } 100% { text-shadow: 0 0 5px rgba(0,255,255,0.3); } }
|
||||
body.theme-neon-dynamic .ebook-chapter { background: rgba(0, 0, 0, 0.7) !important; border: 1px solid rgba(0, 255, 255, 0.2) !important; animation: neonBoxGlow 2s ease-in-out infinite !important; }
|
||||
@keyframes neonBoxGlow { 0% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.1); } 50% { box-shadow: 0 0 25px rgba(0, 255, 255, 0.4); } 100% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.1); } }
|
||||
body.theme-neon-dynamic .ebook-chapter .chapter-title { color: #0ff !important; border-bottom-color: rgba(0, 255, 255, 0.3) !important; }
|
||||
body.theme-neon-dynamic .floating-btn, body.theme-neon-dynamic .speed-panel, body.theme-neon-dynamic .font-controls, body.theme-neon-dynamic .theme-selector, body.theme-neon-dynamic .bookmark-panel, body.theme-neon-dynamic .stat-panel { background: rgba(0, 0, 0, 0.7) !important; color: #0ff !important; border: 1px solid rgba(0, 255, 255, 0.3) !important; animation: neonPanelGlow 1.5s ease-in-out infinite !important; }
|
||||
@keyframes neonPanelGlow { 0% { border-color: rgba(0, 255, 255, 0.2); } 50% { border-color: rgba(0, 255, 255, 0.6); } 100% { border-color: rgba(0, 255, 255, 0.2); } }
|
||||
body.theme-neon-dynamic .floating-btn { background: rgba(0, 0, 0, 0.6) !important; color: #0ff !important; border: 1px solid #0ff !important; animation: neonBtnPulse 1.5s ease-in-out infinite !important; }
|
||||
@keyframes neonBtnPulse { 0% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.3); } 50% { box-shadow: 0 0 15px rgba(0, 255, 255, 0.8); } 100% { box-shadow: 0 0 5px rgba(0, 255, 255, 0.3); } }
|
||||
body.theme-neon-dynamic .floating-btn.bookmark-btn { background: rgba(0, 255, 255, 0.15) !important; }
|
||||
body.theme-neon-dynamic .floating-btn.bookmark-btn.active { background: #0ff !important; color: #0a0a0a !important; }
|
||||
body.theme-neon-dynamic .speed-slider::-webkit-slider-thumb { background: #0ff !important; }
|
||||
body.theme-neon-dynamic .progress-slider-global::-webkit-slider-thumb { background: #0ff !important; box-shadow: 0 0 8px rgba(0,255,255,0.8) !important; }
|
||||
body.theme-neon-dynamic .progress-fill { background: #0ff !important; animation: neonFillPulse 1.5s ease-in-out infinite; }
|
||||
@keyframes neonFillPulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
|
||||
body.theme-neon-dynamic .speed-preset.active { color: #0ff !important; background: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .auto-chapter-line { border-top-color: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .auto-chapter-line input { accent-color: #0ff !important; }
|
||||
body.theme-neon-dynamic .global-progress-container { background: rgba(0, 0, 0, 0.7) !important; border-top: 1px solid rgba(0, 255, 255, 0.3) !important; }
|
||||
body.theme-neon-dynamic .progress-info { color: rgba(0, 255, 255, 0.8) !important; }
|
||||
body.theme-neon-dynamic .shelf-item { background: rgba(0, 255, 255, 0.08) !important; border-color: rgba(0, 255, 255, 0.3) !important; color: #0ff !important; }
|
||||
body.theme-neon-dynamic .shelf-item:hover { background: rgba(0, 255, 255, 0.18) !important; border-color: #0ff !important; box-shadow: 0 0 20px rgba(0,255,255,0.3) !important; }
|
||||
body.theme-neon-dynamic .book-chapter-item { background: rgba(0, 255, 255, 0.08) !important; border-color: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .book-chapter-item a { color: #0ff !important; }
|
||||
body.theme-neon-dynamic .book-chapter-item:hover { background: rgba(0, 255, 255, 0.18) !important; box-shadow: 0 0 15px rgba(0,255,255,0.2) !important; }
|
||||
body.theme-neon-dynamic .bookmark-item .title { color: #0ff !important; }
|
||||
body.theme-neon-dynamic .bookmark-header { border-bottom-color: rgba(0, 255, 255, 0.2) !important; }
|
||||
body.theme-neon-dynamic .chapter-tooltip { background: rgba(0, 0, 0, 0.9) !important; border-color: #0ff !important; color: #0ff !important; box-shadow: 0 0 15px rgba(0,255,255,0.4) !important; text-shadow: 0 0 3px #0ff !important; }
|
||||
body.theme-neon-dynamic .page-turn-overlay { background: rgba(0, 0, 0, 0.8) !important; }
|
||||
body.theme-neon-dynamic .page-turn-overlay .book-left, body.theme-neon-dynamic .page-turn-overlay .book-right { background: rgba(0, 0, 0, 0.7) !important; border: 2px solid #0ff !important; color: #0ff !important; }
|
||||
body.theme-neon-dynamic .page-turn-overlay .message { background: rgba(0, 0, 0, 0.9) !important; color: #0ff !important; border: 1px solid #0ff !important; }
|
||||
body.theme-neon-dynamic .swipe-indicator, body.theme-neon-dynamic .swipe-arrow-left, body.theme-neon-dynamic .swipe-arrow-right { background: rgba(0, 0, 0, 0.7) !important; border-color: #0ff !important; color: #0ff !important; }
|
||||
body.theme-neon-dynamic .toast { background: rgba(0, 0, 0, 0.85) !important; color: #0ff !important; border: 1px solid #0ff !important; }
|
||||
body.theme-neon-dynamic .page-transition { background: rgba(0, 0, 0, 0.7) !important; }
|
||||
body.theme-neon-dynamic .page-transition .book-page { background: linear-gradient(135deg, #0ff, #0a0a0a) !important; }
|
||||
body.theme-neon-dynamic .page-transition .loading-text { color: #0ff !important; text-shadow: 0 0 10px rgba(0,255,255,0.5); }
|
||||
body.theme-neon-dynamic .page-transition .loading-dots span { background: #0ff !important; }
|
||||
body.theme-neon-dynamic .top-bar button.bookmark { background: rgba(0, 255, 255, 0.15) !important; border: 1px solid #0ff !important; color: #0ff !important; animation: neonBtnPulse 1.5s ease-in-out infinite !important; }
|
||||
|
||||
body.theme-sunset-dynamic { background: linear-gradient(270deg, #1a0a2e, #5c2a4a, #c45c3a, #e8a04a); background-size: 400% 400%; animation: sunsetFlow 15s ease infinite; color: #f5e6d3 !important; }
|
||||
@keyframes sunsetFlow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
||||
body.theme-sunset-dynamic .top-bar { background: rgba(0, 0, 0, 0.4) !important; border-bottom: 1px solid rgba(255, 184, 107, 0.3) !important; }
|
||||
body.theme-sunset-dynamic .top-bar, body.theme-sunset-dynamic .top-bar a, body.theme-sunset-dynamic .top-bar button { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .back-btn { background: rgba(255, 184, 107, 0.15) !important; }
|
||||
body.theme-sunset-dynamic .page-title { color: #ffb86b !important; text-shadow: 0 0 8px rgba(255,184,107,0.3); }
|
||||
body.theme-sunset-dynamic .ebook-chapter { background: rgba(0, 0, 0, 0.4) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .ebook-chapter .chapter-title { color: #ffb86b !important; border-bottom-color: rgba(255, 184, 107, 0.3) !important; }
|
||||
body.theme-sunset-dynamic .floating-btn, body.theme-sunset-dynamic .speed-panel, body.theme-sunset-dynamic .font-controls, body.theme-sunset-dynamic .theme-selector, body.theme-sunset-dynamic .bookmark-panel, body.theme-sunset-dynamic .stat-panel { background: rgba(0, 0, 0, 0.45) !important; border: 1px solid rgba(255, 184, 107, 0.3) !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .floating-btn { background: rgba(0, 0, 0, 0.35) !important; color: #ffb86b !important; border: 1px solid rgba(255, 184, 107, 0.4) !important; }
|
||||
body.theme-sunset-dynamic .floating-btn.bookmark-btn { background: rgba(255, 184, 107, 0.15) !important; border: 1px solid #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .floating-btn.bookmark-btn.active { background: #ffb86b !important; color: #1a0a2e !important; }
|
||||
body.theme-sunset-dynamic .speed-slider::-webkit-slider-thumb { background: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .progress-slider-global::-webkit-slider-thumb { background: #ffb86b !important; box-shadow: 0 0 8px rgba(255,184,107,0.8) !important; }
|
||||
body.theme-sunset-dynamic .progress-fill { background: linear-gradient(90deg, #ffb86b, #ff6b6b) !important; }
|
||||
body.theme-sunset-dynamic .speed-preset.active { color: #ffb86b !important; background: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .auto-chapter-line { border-top-color: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .auto-chapter-line input { accent-color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .global-progress-container { background: rgba(0, 0, 0, 0.45) !important; border-top: 1px solid rgba(255, 184, 107, 0.3) !important; }
|
||||
body.theme-sunset-dynamic .progress-info { color: rgba(255, 184, 107, 0.85) !important; }
|
||||
body.theme-sunset-dynamic .shelf-item { background: rgba(255, 184, 107, 0.1) !important; border-color: rgba(255, 184, 107, 0.3) !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .shelf-item:hover { background: rgba(255, 184, 107, 0.2) !important; border-color: rgba(255, 184, 107, 0.6) !important; }
|
||||
body.theme-sunset-dynamic .book-chapter-item { background: rgba(255, 184, 107, 0.1) !important; border-color: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .book-chapter-item a { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .book-chapter-item:hover { background: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .bookmark-item .title { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .bookmark-header { border-bottom-color: rgba(255, 184, 107, 0.2) !important; }
|
||||
body.theme-sunset-dynamic .chapter-tooltip { background: rgba(30, 20, 30, 0.85) !important; border-color: #ffb86b !important; color: #ffb86b !important; box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; }
|
||||
body.theme-sunset-dynamic .page-turn-overlay { background: rgba(0, 0, 0, 0.5) !important; }
|
||||
body.theme-sunset-dynamic .page-turn-overlay .book-left, body.theme-sunset-dynamic .page-turn-overlay .book-right { background: rgba(30, 20, 30, 0.6) !important; border: 2px solid rgba(255, 184, 107, 0.4) !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .page-turn-overlay .message { background: rgba(30, 20, 30, 0.8) !important; color: #ffb86b !important; border: 1px solid rgba(255, 184, 107, 0.5) !important; }
|
||||
body.theme-sunset-dynamic .swipe-indicator, body.theme-sunset-dynamic .swipe-arrow-left, body.theme-sunset-dynamic .swipe-arrow-right { background: rgba(30, 20, 30, 0.7) !important; border-color: #ffb86b !important; color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .toast { background: rgba(0, 0, 0, 0.7) !important; color: #ffb86b !important; border: 1px solid rgba(255, 184, 107, 0.4) !important; }
|
||||
body.theme-sunset-dynamic .page-transition { background: rgba(0, 0, 0, 0.5) !important; }
|
||||
body.theme-sunset-dynamic .page-transition .book-page { background: linear-gradient(135deg, #ffb86b, #c45c3a) !important; }
|
||||
body.theme-sunset-dynamic .page-transition .loading-text { color: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .page-transition .loading-dots span { background: #ffb86b !important; }
|
||||
body.theme-sunset-dynamic .top-bar button.bookmark { background: rgba(255, 184, 107, 0.2) !important; border: 1px solid #ffb86b !important; color: #ffb86b !important; }
|
||||
|
||||
body.theme-wave-dynamic { background: linear-gradient(135deg, #0b2b44, #0d3b5e, #0a2a40, #0d3b5e, #0b2b44); background-size: 300% 300%; animation: waveMoveEnhanced 6s ease infinite; color: #c8e7f5 !important; }
|
||||
@keyframes waveMoveEnhanced { 0% { background-position: 0% 0%; } 25% { background-position: 100% 50%; } 50% { background-position: 50% 100%; } 75% { background-position: 0% 50%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-wave-dynamic .top-bar { background: rgba(0, 20, 30, 0.6) !important; border-bottom: 1px solid rgba(91, 192, 255, 0.3) !important; }
|
||||
body.theme-wave-dynamic .top-bar, body.theme-wave-dynamic .top-bar a, body.theme-wave-dynamic .top-bar button { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .back-btn { background: rgba(91, 192, 255, 0.15) !important; }
|
||||
body.theme-wave-dynamic .page-title { color: #5bc0ff !important; text-shadow: 0 0 8px rgba(91,192,255,0.3); }
|
||||
body.theme-wave-dynamic .ebook-chapter { background: rgba(0, 20, 30, 0.5) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .ebook-chapter .chapter-title { color: #5bc0ff !important; border-bottom-color: rgba(91, 192, 255, 0.3) !important; }
|
||||
body.theme-wave-dynamic .floating-btn, body.theme-wave-dynamic .speed-panel, body.theme-wave-dynamic .font-controls, body.theme-wave-dynamic .theme-selector, body.theme-wave-dynamic .bookmark-panel, body.theme-wave-dynamic .stat-panel { background: rgba(0, 20, 30, 0.65) !important; border: 1px solid rgba(91, 192, 255, 0.3) !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .floating-btn { background: rgba(0, 20, 30, 0.5) !important; color: #5bc0ff !important; border: 1px solid rgba(91, 192, 255, 0.4) !important; animation: waveBtnFloat 3s ease-in-out infinite !important; }
|
||||
@keyframes waveBtnFloat { 0% { transform: translateY(0px); } 50% { transform: translateY(-3px); } 100% { transform: translateY(0px); } }
|
||||
body.theme-wave-dynamic .floating-btn.bookmark-btn { background: rgba(91, 192, 255, 0.15) !important; }
|
||||
body.theme-wave-dynamic .floating-btn.bookmark-btn.active { background: #5bc0ff !important; color: #0b2b44 !important; }
|
||||
body.theme-wave-dynamic .speed-slider::-webkit-slider-thumb { background: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .progress-slider-global::-webkit-slider-thumb { background: #5bc0ff !important; box-shadow: 0 0 8px rgba(91,192,255,0.8) !important; }
|
||||
body.theme-wave-dynamic .progress-fill { background: linear-gradient(90deg, #5bc0ff, #2d9cdb) !important; }
|
||||
body.theme-wave-dynamic .speed-preset.active { color: #5bc0ff !important; background: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .auto-chapter-line { border-top-color: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .auto-chapter-line input { accent-color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .global-progress-container { background: rgba(0, 20, 30, 0.65) !important; border-top: 1px solid rgba(91, 192, 255, 0.3) !important; }
|
||||
body.theme-wave-dynamic .progress-info { color: rgba(91, 192, 255, 0.85) !important; }
|
||||
body.theme-wave-dynamic .shelf-item { background: rgba(91, 192, 255, 0.1) !important; border-color: rgba(91, 192, 255, 0.3) !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .shelf-item:hover { background: rgba(91, 192, 255, 0.2) !important; border-color: rgba(91, 192, 255, 0.6) !important; }
|
||||
body.theme-wave-dynamic .book-chapter-item { background: rgba(91, 192, 255, 0.1) !important; border-color: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .book-chapter-item a { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .book-chapter-item:hover { background: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .bookmark-item .title { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .bookmark-header { border-bottom-color: rgba(91, 192, 255, 0.2) !important; }
|
||||
body.theme-wave-dynamic .chapter-tooltip { background: rgba(0, 20, 30, 0.9) !important; border-color: #5bc0ff !important; color: #5bc0ff !important; box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; }
|
||||
body.theme-wave-dynamic .page-turn-overlay { background: rgba(10, 40, 60, 0.7) !important; }
|
||||
body.theme-wave-dynamic .page-turn-overlay .book-left, body.theme-wave-dynamic .page-turn-overlay .book-right { background: rgba(10, 40, 60, 0.6) !important; border: 2px solid rgba(91, 192, 255, 0.4) !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .page-turn-overlay .message { background: rgba(10, 40, 60, 0.8) !important; color: #5bc0ff !important; border: 1px solid rgba(91, 192, 255, 0.5) !important; }
|
||||
body.theme-wave-dynamic .swipe-indicator, body.theme-wave-dynamic .swipe-arrow-left, body.theme-wave-dynamic .swipe-arrow-right { background: rgba(10, 40, 60, 0.7) !important; border-color: #5bc0ff !important; color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .toast { background: rgba(0, 20, 30, 0.85) !important; color: #5bc0ff !important; border: 1px solid rgba(91, 192, 255, 0.4) !important; }
|
||||
body.theme-wave-dynamic .page-transition { background: rgba(0, 20, 30, 0.7) !important; }
|
||||
body.theme-wave-dynamic .page-transition .book-page { background: linear-gradient(135deg, #5bc0ff, #0d3b5e) !important; }
|
||||
body.theme-wave-dynamic .page-transition .loading-text { color: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .page-transition .loading-dots span { background: #5bc0ff !important; }
|
||||
body.theme-wave-dynamic .top-bar button.bookmark { background: rgba(91, 192, 255, 0.2) !important; border: 1px solid #5bc0ff !important; color: #5bc0ff !important; }
|
||||
|
||||
body.theme-fire-dynamic { background: linear-gradient(180deg, #4a0a0a, #8b2a1a, #d45a2a); background-size: 100% 200%; animation: firePulse 2s ease infinite alternate; color: #ffe0c0 !important; }
|
||||
@keyframes firePulse { 0% { background-position: 0% 0%; } 100% { background-position: 0% 100%; } }
|
||||
body.theme-fire-dynamic .top-bar { background: rgba(60, 10, 10, 0.6) !important; border-bottom: 1px solid rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .top-bar, body.theme-fire-dynamic .top-bar a, body.theme-fire-dynamic .top-bar button { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .back-btn { background: rgba(255, 140, 66, 0.15) !important; }
|
||||
body.theme-fire-dynamic .page-title { color: #ff8c42 !important; text-shadow: 0 0 8px rgba(255,140,66,0.4); }
|
||||
body.theme-fire-dynamic .ebook-chapter { background: rgba(60, 10, 10, 0.5) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 140, 66, 0.3) !important; }
|
||||
body.theme-fire-dynamic .ebook-chapter .chapter-title { color: #ff8c42 !important; border-bottom-color: rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .floating-btn, body.theme-fire-dynamic .speed-panel, body.theme-fire-dynamic .font-controls, body.theme-fire-dynamic .theme-selector, body.theme-fire-dynamic .bookmark-panel, body.theme-fire-dynamic .stat-panel { background: rgba(60, 10, 10, 0.7) !important; border: 1px solid rgba(255, 140, 66, 0.4) !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .floating-btn { background: rgba(60, 10, 10, 0.6) !important; color: #ff8c42 !important; border: 1px solid rgba(255, 140, 66, 0.5) !important; }
|
||||
body.theme-fire-dynamic .floating-btn.bookmark-btn { background: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .floating-btn.bookmark-btn.active { background: #ff8c42 !important; color: #4a0a0a !important; }
|
||||
body.theme-fire-dynamic .speed-slider::-webkit-slider-thumb { background: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .progress-slider-global::-webkit-slider-thumb { background: #ff8c42 !important; box-shadow: 0 0 8px rgba(255,140,66,0.8) !important; }
|
||||
body.theme-fire-dynamic .progress-fill { background: linear-gradient(90deg, #ff8c42, #ff5722) !important; }
|
||||
body.theme-fire-dynamic .speed-preset.active { color: #ff8c42 !important; background: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .auto-chapter-line { border-top-color: rgba(255, 140, 66, 0.3) !important; }
|
||||
body.theme-fire-dynamic .auto-chapter-line input { accent-color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .global-progress-container { background: rgba(60, 10, 10, 0.7) !important; border-top: 1px solid rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .progress-info { color: rgba(255, 140, 66, 0.85) !important; }
|
||||
body.theme-fire-dynamic .shelf-item { background: rgba(255, 140, 66, 0.1) !important; border-color: rgba(255, 140, 66, 0.3) !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .shelf-item:hover { background: rgba(255, 140, 66, 0.2) !important; border-color: rgba(255, 140, 66, 0.6) !important; }
|
||||
body.theme-fire-dynamic .book-chapter-item { background: rgba(255, 140, 66, 0.1) !important; border-color: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .book-chapter-item a { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .book-chapter-item:hover { background: rgba(255, 140, 66, 0.2) !important; }
|
||||
body.theme-fire-dynamic .bookmark-item .title { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .bookmark-header { border-bottom-color: rgba(255, 140, 66, 0.3) !important; }
|
||||
body.theme-fire-dynamic .chapter-tooltip { background: rgba(60, 10, 10, 0.92) !important; border-color: #ff8c42 !important; color: #ff8c42 !important; box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; }
|
||||
body.theme-fire-dynamic .page-turn-overlay { background: rgba(60, 10, 10, 0.7) !important; }
|
||||
body.theme-fire-dynamic .page-turn-overlay .book-left, body.theme-fire-dynamic .page-turn-overlay .book-right { background: rgba(60, 10, 10, 0.6) !important; border: 2px solid rgba(255, 140, 66, 0.4) !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .page-turn-overlay .message { background: rgba(60, 10, 10, 0.8) !important; color: #ff8c42 !important; border: 1px solid rgba(255, 140, 66, 0.5) !important; }
|
||||
body.theme-fire-dynamic .swipe-indicator, body.theme-fire-dynamic .swipe-arrow-left, body.theme-fire-dynamic .swipe-arrow-right { background: rgba(60, 10, 10, 0.8) !important; border-color: #ff8c42 !important; color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .toast { background: rgba(60, 10, 10, 0.9) !important; color: #ff8c42 !important; border: 1px solid rgba(255, 140, 66, 0.4) !important; }
|
||||
body.theme-fire-dynamic .page-transition { background: rgba(60, 10, 10, 0.7) !important; }
|
||||
body.theme-fire-dynamic .page-transition .book-page { background: linear-gradient(135deg, #ff8c42, #d45a2a) !important; }
|
||||
body.theme-fire-dynamic .page-transition .loading-text { color: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .page-transition .loading-dots span { background: #ff8c42 !important; }
|
||||
body.theme-fire-dynamic .top-bar button.bookmark { background: rgba(255, 140, 66, 0.2) !important; border: 1px solid #ff8c42 !important; color: #ff8c42 !important; }
|
||||
|
||||
body.theme-sakura-dynamic { background: linear-gradient(135deg, #ffeef8 0%, #ffd9e8 50%, #ffb7c5 100%); background-size: 200% 200%; animation: sakuraFlow 8s ease infinite; color: #6b3e4a !important; }
|
||||
@keyframes sakuraFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-sakura-dynamic .top-bar { background: rgba(255, 240, 245, 0.85) !important; backdrop-filter: blur(20px) !important; border-bottom: 1px solid rgba(255, 160, 180, 0.4) !important; }
|
||||
body.theme-sakura-dynamic .top-bar, body.theme-sakura-dynamic .top-bar a, body.theme-sakura-dynamic .top-bar button { color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .back-btn { background: rgba(184, 59, 94, 0.12) !important; }
|
||||
body.theme-sakura-dynamic .page-title { color: #b83b5e !important; text-shadow: 0 0 8px rgba(184,59,94,0.2); }
|
||||
body.theme-sakura-dynamic .ebook-chapter { background: rgba(255, 255, 255, 0.7) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 160, 180, 0.3) !important; color: #6b3e4a !important; }
|
||||
body.theme-sakura-dynamic .ebook-chapter .chapter-title { color: #e86f8f !important; border-bottom-color: rgba(232, 111, 143, 0.3) !important; }
|
||||
body.theme-sakura-dynamic .floating-btn, body.theme-sakura-dynamic .speed-panel, body.theme-sakura-dynamic .font-controls, body.theme-sakura-dynamic .theme-selector, body.theme-sakura-dynamic .bookmark-panel, body.theme-sakura-dynamic .stat-panel { background: rgba(255, 240, 245, 0.9) !important; border: 1px solid rgba(232, 111, 143, 0.3) !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .floating-btn { background: rgba(255, 240, 245, 0.85) !important; color: #e86f8f !important; border: 1px solid rgba(232, 111, 143, 0.4) !important; }
|
||||
body.theme-sakura-dynamic .floating-btn.bookmark-btn { background: rgba(232, 111, 143, 0.2) !important; border: 1px solid #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .speed-slider::-webkit-slider-thumb { background: #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .progress-slider-global::-webkit-slider-thumb { background: #e86f8f !important; box-shadow: 0 0 8px rgba(232,111,143,0.6) !important; }
|
||||
body.theme-sakura-dynamic .progress-fill { background: linear-gradient(90deg, #e86f8f, #b83b5e) !important; }
|
||||
body.theme-sakura-dynamic .speed-preset.active { color: #e86f8f !important; background: rgba(232, 111, 143, 0.15) !important; }
|
||||
body.theme-sakura-dynamic .global-progress-container { background: rgba(255, 240, 245, 0.9) !important; border-top: 1px solid rgba(232, 111, 143, 0.3) !important; }
|
||||
body.theme-sakura-dynamic .shelf-item { background: rgba(232, 111, 143, 0.1) !important; border-color: rgba(232, 111, 143, 0.3) !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .shelf-item:hover { background: rgba(232, 111, 143, 0.2) !important; }
|
||||
body.theme-sakura-dynamic .book-chapter-item { background: rgba(232, 111, 143, 0.1) !important; }
|
||||
body.theme-sakura-dynamic .book-chapter-item a { color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .chapter-tooltip { background: rgba(255, 240, 245, 0.95) !important; border-color: #e86f8f !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .page-turn-overlay { background: rgba(255, 240, 245, 0.85) !important; }
|
||||
body.theme-sakura-dynamic .page-turn-overlay .book-left, body.theme-sakura-dynamic .page-turn-overlay .book-right { background: rgba(255, 245, 250, 0.9) !important; border: 2px solid rgba(232, 111, 143, 0.5) !important; color: #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .page-turn-overlay .message { background: rgba(255, 240, 245, 0.95) !important; color: #b83b5e !important; border: 1px solid #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .swipe-indicator, body.theme-sakura-dynamic .swipe-arrow-left, body.theme-sakura-dynamic .swipe-arrow-right { background: rgba(255, 240, 245, 0.9) !important; border-color: #e86f8f !important; color: #b83b5e !important; }
|
||||
body.theme-sakura-dynamic .toast { background: rgba(255, 240, 245, 0.95) !important; color: #b83b5e !important; border: 1px solid #e86f8f !important; }
|
||||
body.theme-sakura-dynamic .page-transition { background: rgba(255, 240, 245, 0.85) !important; }
|
||||
body.theme-sakura-dynamic .page-transition .book-page { background: linear-gradient(135deg, #e86f8f, #b83b5e) !important; }
|
||||
body.theme-sakura-dynamic .page-transition .loading-text { color: #e86f8f !important; }
|
||||
|
||||
body.theme-mintfrost-dynamic { background: linear-gradient(135deg, #c8e8e9 0%, #a8d8ea 50%, #88c8e8 100%); background-size: 200% 200%; animation: mintFlow 6s ease infinite; color: #2c5a5a !important; }
|
||||
@keyframes mintFlow { 0% { background-position: 0% 0%; } 100% { background-position: 100% 100%; } }
|
||||
body.theme-mintfrost-dynamic .top-bar { background: rgba(200, 232, 233, 0.85) !important; border-bottom: 1px solid rgba(100, 180, 200, 0.4) !important; }
|
||||
body.theme-mintfrost-dynamic .top-bar, body.theme-mintfrost-dynamic .top-bar a, body.theme-mintfrost-dynamic .top-bar button { color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .ebook-chapter { background: rgba(255, 255, 250, 0.75) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(100, 180, 200, 0.3) !important; color: #2c5a5a !important; }
|
||||
body.theme-mintfrost-dynamic .ebook-chapter .chapter-title { color: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .floating-btn, body.theme-mintfrost-dynamic .speed-panel, body.theme-mintfrost-dynamic .font-controls, body.theme-mintfrost-dynamic .theme-selector, body.theme-mintfrost-dynamic .bookmark-panel, body.theme-mintfrost-dynamic .stat-panel { background: rgba(200, 232, 233, 0.9) !important; border: 1px solid rgba(100, 180, 200, 0.3) !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .floating-btn { background: rgba(200, 232, 233, 0.85) !important; color: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .progress-slider-global::-webkit-slider-thumb { background: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .progress-fill { background: linear-gradient(90deg, #3a9a9a, #2a7a7a) !important; }
|
||||
body.theme-mintfrost-dynamic .chapter-tooltip { background: rgba(200, 232, 233, 0.95) !important; border-color: #3a9a9a !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay { background: rgba(200, 232, 233, 0.85) !important; }
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay .book-left, body.theme-mintfrost-dynamic .page-turn-overlay .book-right { background: rgba(220, 245, 245, 0.9) !important; border: 2px solid rgba(58, 154, 154, 0.5) !important; color: #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .page-turn-overlay .message { background: rgba(200, 232, 233, 0.95) !important; color: #2a7a7a !important; border: 1px solid #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .swipe-indicator, body.theme-mintfrost-dynamic .swipe-arrow-left, body.theme-mintfrost-dynamic .swipe-arrow-right { background: rgba(200, 232, 233, 0.9) !important; border-color: #3a9a9a !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .toast { background: rgba(200, 232, 233, 0.95) !important; color: #2a7a7a !important; border: 1px solid #3a9a9a !important; }
|
||||
body.theme-mintfrost-dynamic .shelf-item { background: rgba(100, 180, 200, 0.15) !important; color: #2a7a7a !important; }
|
||||
body.theme-mintfrost-dynamic .page-transition .book-page { background: linear-gradient(135deg, #3a9a9a, #2a7a7a) !important; }
|
||||
|
||||
body.theme-lavenderfield-dynamic { background: linear-gradient(145deg, #d8cce8 0%, #b9a8d4 50%, #9b88c2 100%); background-size: 200% 200%; animation: lavenderFlow 10s ease infinite; color: #3a2a5a !important; }
|
||||
@keyframes lavenderFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-lavenderfield-dynamic .top-bar { background: rgba(216, 204, 232, 0.85) !important; border-bottom: 1px solid rgba(155, 136, 194, 0.4) !important; }
|
||||
body.theme-lavenderfield-dynamic .top-bar, body.theme-lavenderfield-dynamic .top-bar a, body.theme-lavenderfield-dynamic .top-bar button { color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .ebook-chapter { background: rgba(255, 250, 255, 0.75) !important; backdrop-filter: blur(10px) !important; color: #3a2a5a !important; }
|
||||
body.theme-lavenderfield-dynamic .ebook-chapter .chapter-title { color: #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .floating-btn, body.theme-lavenderfield-dynamic .speed-panel, body.theme-lavenderfield-dynamic .font-controls, body.theme-lavenderfield-dynamic .theme-selector, body.theme-lavenderfield-dynamic .bookmark-panel, body.theme-lavenderfield-dynamic .stat-panel { background: rgba(216, 204, 232, 0.9) !important; border: 1px solid rgba(155, 136, 194, 0.3) !important; color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .progress-slider-global::-webkit-slider-thumb { background: #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .chapter-tooltip { background: rgba(216, 204, 232, 0.95) !important; border-color: #8b6bbf !important; color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay { background: rgba(216, 204, 232, 0.85) !important; }
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay .book-left, body.theme-lavenderfield-dynamic .page-turn-overlay .book-right { background: rgba(230, 220, 245, 0.9) !important; border: 2px solid rgba(139, 107, 191, 0.5) !important; color: #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .page-turn-overlay .message { background: rgba(216, 204, 232, 0.95) !important; color: #5a4a8a !important; border: 1px solid #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .swipe-indicator, body.theme-lavenderfield-dynamic .swipe-arrow-left, body.theme-lavenderfield-dynamic .swipe-arrow-right { background: rgba(216, 204, 232, 0.9) !important; border-color: #8b6bbf !important; color: #5a4a8a !important; }
|
||||
body.theme-lavenderfield-dynamic .toast { background: rgba(216, 204, 232, 0.95) !important; color: #5a4a8a !important; border: 1px solid #8b6bbf !important; }
|
||||
body.theme-lavenderfield-dynamic .page-transition .book-page { background: linear-gradient(135deg, #8b6bbf, #6b4a9f) !important; }
|
||||
body.theme-lavenderfield-dynamic .shelf-item { background: rgba(139, 107, 191, 0.15) !important; color: #5a4a8a !important; }
|
||||
|
||||
body.theme-golden-dynamic { background: linear-gradient(135deg, #f5e6b8 0%, #e8d498 50%, #d4b86a 100%); background-size: 200% 200%; animation: goldenFlow 8s ease infinite; color: #5a4a2a !important; }
|
||||
@keyframes goldenFlow { 0% { background-position: 0% 0%; } 100% { background-position: 100% 100%; } }
|
||||
body.theme-golden-dynamic .top-bar { background: rgba(245, 230, 184, 0.85) !important; border-bottom: 1px solid rgba(212, 184, 106, 0.4) !important; }
|
||||
body.theme-golden-dynamic .top-bar, body.theme-golden-dynamic .top-bar a, body.theme-golden-dynamic .top-bar button { color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .ebook-chapter { background: rgba(255, 255, 240, 0.8) !important; backdrop-filter: blur(10px) !important; color: #5a4a2a !important; }
|
||||
body.theme-golden-dynamic .ebook-chapter .chapter-title { color: #c4a030 !important; }
|
||||
body.theme-golden-dynamic .floating-btn, body.theme-golden-dynamic .speed-panel, body.theme-golden-dynamic .font-controls, body.theme-golden-dynamic .theme-selector, body.theme-golden-dynamic .bookmark-panel, body.theme-golden-dynamic .stat-panel { background: rgba(245, 230, 184, 0.9) !important; border: 1px solid rgba(212, 184, 106, 0.3) !important; color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .progress-slider-global::-webkit-slider-thumb { background: #d4a030 !important; }
|
||||
body.theme-golden-dynamic .chapter-tooltip { background: rgba(245, 230, 184, 0.95) !important; border-color: #d4a030 !important; color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .page-turn-overlay { background: rgba(245, 230, 184, 0.85) !important; }
|
||||
body.theme-golden-dynamic .page-turn-overlay .book-left, body.theme-golden-dynamic .page-turn-overlay .book-right { background: rgba(255, 250, 220, 0.9) !important; border: 2px solid rgba(212, 160, 48, 0.5) !important; color: #d4a030 !important; }
|
||||
body.theme-golden-dynamic .page-turn-overlay .message { background: rgba(245, 230, 184, 0.95) !important; color: #8a6a2a !important; border: 1px solid #d4a030 !important; }
|
||||
body.theme-golden-dynamic .swipe-indicator, body.theme-golden-dynamic .swipe-arrow-left, body.theme-golden-dynamic .swipe-arrow-right { background: rgba(245, 230, 184, 0.9) !important; border-color: #d4a030 !important; color: #8a6a2a !important; }
|
||||
body.theme-golden-dynamic .toast { background: rgba(245, 230, 184, 0.95) !important; color: #8a6a2a !important; border: 1px solid #d4a030 !important; }
|
||||
body.theme-golden-dynamic .page-transition .book-page { background: linear-gradient(135deg, #d4a030, #b08020) !important; }
|
||||
|
||||
body.theme-coralreef-dynamic { background: linear-gradient(125deg, #ffaa88 0%, #ff8866 50%, #ff6644 100%); background-size: 200% 200%; animation: coralFlow 7s ease infinite; color: #4a2a1a !important; }
|
||||
@keyframes coralFlow { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-coralreef-dynamic .top-bar { background: rgba(255, 170, 136, 0.85) !important; border-bottom: 1px solid rgba(255, 100, 70, 0.4) !important; }
|
||||
body.theme-coralreef-dynamic .top-bar, body.theme-coralreef-dynamic .top-bar a, body.theme-coralreef-dynamic .top-bar button { color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .ebook-chapter { background: rgba(255, 250, 245, 0.8) !important; color: #4a2a1a !important; }
|
||||
body.theme-coralreef-dynamic .ebook-chapter .chapter-title { color: #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .floating-btn, body.theme-coralreef-dynamic .speed-panel, body.theme-coralreef-dynamic .font-controls, body.theme-coralreef-dynamic .theme-selector, body.theme-coralreef-dynamic .bookmark-panel, body.theme-coralreef-dynamic .stat-panel { background: rgba(255, 170, 136, 0.9) !important; color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .progress-slider-global::-webkit-slider-thumb { background: #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .chapter-tooltip { background: rgba(255, 170, 136, 0.95) !important; border-color: #ff6644 !important; color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .page-turn-overlay { background: rgba(255, 170, 136, 0.85) !important; }
|
||||
body.theme-coralreef-dynamic .page-turn-overlay .book-left, body.theme-coralreef-dynamic .page-turn-overlay .book-right { background: rgba(255, 200, 180, 0.9) !important; border: 2px solid rgba(255, 102, 68, 0.5) !important; color: #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .page-turn-overlay .message { background: rgba(255, 170, 136, 0.95) !important; color: #8a3010 !important; border: 1px solid #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .swipe-indicator, body.theme-coralreef-dynamic .swipe-arrow-left, body.theme-coralreef-dynamic .swipe-arrow-right { background: rgba(255, 170, 136, 0.9) !important; border-color: #ff6644 !important; color: #8a3010 !important; }
|
||||
body.theme-coralreef-dynamic .toast { background: rgba(255, 170, 136, 0.95) !important; color: #8a3010 !important; border: 1px solid #ff6644 !important; }
|
||||
body.theme-coralreef-dynamic .page-transition .book-page { background: linear-gradient(135deg, #ff8866, #ff6644) !important; }
|
||||
|
||||
body.theme-galaxy-dynamic { background: radial-gradient(ellipse at center, #0a0a2a 0%, #1a1a4a 50%, #2a2a5a 100%); background-size: 200% 200%; animation: galaxyTwinkle 15s ease infinite; color: #c8d0ff !important; }
|
||||
@keyframes galaxyTwinkle { 50% { background-size: 100% 100%; opacity: 1; } 50% { background-size: 120% 120%; opacity: 0.95; } 100% { background-size: 100% 100%; opacity: 1; } }
|
||||
body.theme-galaxy-dynamic .top-bar { background: rgba(10, 10, 42, 0.85) !important; border-bottom: 1px solid rgba(200, 200, 255, 0.3) !important; }
|
||||
body.theme-galaxy-dynamic .top-bar, body.theme-galaxy-dynamic .top-bar a, body.theme-galaxy-dynamic .top-bar button { color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .ebook-chapter { background: rgba(30, 30, 70, 0.8) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(170, 172, 255, 0.2) !important; color: #c8d0ff !important; }
|
||||
body.theme-galaxy-dynamic .ebook-chapter .chapter-title { color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .floating-btn, body.theme-galaxy-dynamic .speed-panel, body.theme-galaxy-dynamic .font-controls, body.theme-galaxy-dynamic .theme-selector, body.theme-galaxy-dynamic .bookmark-panel, body.theme-galaxy-dynamic .stat-panel { background: rgba(10, 10, 42, 0.9) !important; border: 1px solid rgba(170, 172, 255, 0.3) !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .progress-slider-global::-webkit-slider-thumb { background: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .chapter-tooltip { background: rgba(10, 10, 42, 0.95) !important; border-color: #aaacff !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .page-turn-overlay { background: rgba(10, 10, 42, 0.85) !important; }
|
||||
body.theme-galaxy-dynamic .page-turn-overlay .book-left, body.theme-galaxy-dynamic .page-turn-overlay .book-right { background: rgba(30, 30, 70, 0.9) !important; border: 2px solid rgba(170, 172, 255, 0.5) !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .page-turn-overlay .message { background: rgba(10, 10, 42, 0.95) !important; color: #aaacff !important; border: 1px solid #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .swipe-indicator, body.theme-galaxy-dynamic .swipe-arrow-left, body.theme-galaxy-dynamic .swipe-arrow-right { background: rgba(10, 10, 42, 0.9) !important; border-color: #aaacff !important; color: #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .toast { background: rgba(10, 10, 42, 0.95) !important; color: #aaacff !important; border: 1px solid #aaacff !important; }
|
||||
body.theme-galaxy-dynamic .page-transition .book-page { background: linear-gradient(135deg, #aaacff, #6a6acf) !important; }
|
||||
body.theme-galaxy-dynamic .shelf-item { background: rgba(170, 172, 255, 0.1) !important; color: #aaacff !important; }
|
||||
|
||||
body.theme-rosegarden-dynamic { background: linear-gradient(145deg, #f5c8d8 0%, #e8a8c0 50%, #d888a8 100%); background-size: 200% 200%; animation: roseFlow 9s ease infinite; color: #5a2a3a !important; }
|
||||
@keyframes roseFlow { 50% { background-position: 50% 50%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }
|
||||
body.theme-rosegarden-dynamic .top-bar { background: rgba(245, 200, 216, 0.85) !important; border-bottom: 1px solid rgba(216, 136, 168, 0.4) !important; }
|
||||
body.theme-rosegarden-dynamic .top-bar, body.theme-rosegarden-dynamic .top-bar a, body.theme-rosegarden-dynamic .top-bar button { color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .ebook-chapter { background: rgba(255, 245, 250, 0.8) !important; color: #5a2a3a !important; }
|
||||
body.theme-rosegarden-dynamic .ebook-chapter .chapter-title { color: #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .floating-btn, body.theme-rosegarden-dynamic .speed-panel, body.theme-rosegarden-dynamic .font-controls, body.theme-rosegarden-dynamic .theme-selector, body.theme-rosegarden-dynamic .bookmark-panel, body.theme-rosegarden-dynamic .stat-panel { background: rgba(245, 200, 216, 0.9) !important; border: 1px solid rgba(216, 136, 168, 0.3) !important; color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .progress-slider-global::-webkit-slider-thumb { background: #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .chapter-tooltip { background: rgba(245, 200, 216, 0.95) !important; border-color: #d888a8 !important; color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay { background: rgba(245, 200, 216, 0.85) !important; }
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay .book-left, body.theme-rosegarden-dynamic .page-turn-overlay .book-right { background: rgba(255, 230, 240, 0.9) !important; border: 2px solid rgba(216, 136, 168, 0.5) !important; color: #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .page-turn-overlay .message { background: rgba(245, 200, 216, 0.95) !important; color: #a03050 !important; border: 1px solid #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .swipe-indicator, body.theme-rosegarden-dynamic .swipe-arrow-left, body.theme-rosegarden-dynamic .swipe-arrow-right { background: rgba(245, 200, 216, 0.9) !important; border-color: #d888a8 !important; color: #a03050 !important; }
|
||||
body.theme-rosegarden-dynamic .toast { background: rgba(245, 200, 216, 0.95) !important; color: #a03050 !important; border: 1px solid #d888a8 !important; }
|
||||
body.theme-rosegarden-dynamic .page-transition .book-page { background: linear-gradient(135deg, #d888a8, #c06888) !important; }
|
||||
|
||||
/* 主题色块样式 */
|
||||
.theme-dot[data-theme="deep-space"] { background: linear-gradient(135deg, #0f0c29, #302b63); }
|
||||
.theme-dot[data-theme="ocean"] { background: linear-gradient(135deg, #1a2980, #26d0ce); }
|
||||
.theme-dot[data-theme="cherry"] { background: linear-gradient(135deg, #ff9a9e, #fecfef); }
|
||||
.theme-dot[data-theme="night"] { background: #1a1a1a; }
|
||||
.theme-dot[data-theme="forest"] { background: linear-gradient(135deg, #134e5e, #71b280); }
|
||||
.theme-dot[data-theme="sunset"] { background: linear-gradient(135deg, #ff7e5f, #feb47b); }
|
||||
.theme-dot[data-theme="lavender"] { background: linear-gradient(135deg, #8e9ecc, #e0bbff); }
|
||||
.theme-dot[data-theme="blueberry"] { background: linear-gradient(135deg, #2c3e66, #4a69bd); }
|
||||
.theme-dot[data-theme="amber"] { background: linear-gradient(135deg, #ffb347, #ffcc33); }
|
||||
.theme-dot[data-theme="coral"] { background: linear-gradient(135deg, #ff6b6b, #ffb8b8); }
|
||||
.theme-dot[data-theme="mint"] { background: linear-gradient(135deg, #a8e6cf, #80deea); }
|
||||
.theme-dot[data-theme="rosegold"] { background: linear-gradient(135deg, #e8b4b8, #ffd9e2); }
|
||||
.theme-dot[data-theme="eyecare"] { background: #c7edcc; border: 2px solid #8b9a6e; }
|
||||
.theme-dot[data-theme="aurora-dynamic"] { background: linear-gradient(270deg, #1a0b2e, #2d1b69, #1a4d8c, #0f5c6b); }
|
||||
.theme-dot[data-theme="neon-dynamic"] { background: #0a0a0a; border: 2px solid #0ff; box-shadow: 0 0 5px #0ff; }
|
||||
.theme-dot[data-theme="sunset-dynamic"] { background: linear-gradient(135deg, #5c2a4a, #e8a04a); }
|
||||
.theme-dot[data-theme="wave-dynamic"] { background: linear-gradient(135deg, #0b2b44, #0d3b5e); }
|
||||
.theme-dot[data-theme="fire-dynamic"] { background: linear-gradient(180deg, #4a0a0a, #d45a2a); }
|
||||
.theme-dot[data-theme="sakura-dynamic"] { background: linear-gradient(135deg, #ffeef8, #ffb7c5); }
|
||||
.theme-dot[data-theme="mintfrost-dynamic"] { background: linear-gradient(135deg, #c8e8e9, #88c8e8); }
|
||||
.theme-dot[data-theme="lavenderfield-dynamic"] { background: linear-gradient(145deg, #d8cce8, #9b88c2); }
|
||||
.theme-dot[data-theme="golden-dynamic"] { background: linear-gradient(135deg, #f5e6b8, #d4b86a); }
|
||||
.theme-dot[data-theme="coralreef-dynamic"] { background: linear-gradient(125deg, #ffaa88, #ff6644); }
|
||||
.theme-dot[data-theme="galaxy-dynamic"] { background: radial-gradient(ellipse at center, #0a0a2a, #2a2a5a); }
|
||||
.theme-dot[data-theme="rosegarden-dynamic"] { background: linear-gradient(145deg, #f5c8d8, #d888a8); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="theme-deep-space">
|
||||
|
||||
<div class="floating-buttons" id="floatingButtons">
|
||||
<div class="floating-btn bookmark-btn" id="bookmarkFloatBtn">📋</div>
|
||||
<div class="floating-btn scroll-down" id="scrollToggleBtn">▼</div>
|
||||
<div class="floating-btn" id="themeFloatBtn">🎨</div>
|
||||
<div class="floating-btn" id="statFloatBtn">📊</div>
|
||||
</div>
|
||||
|
||||
<div class="speed-panel" id="speedPanel">
|
||||
<div class="speed-label"><span>⚡ 滚动速度</span><span class="speed-value" id="speedValue">6 px/帧</span></div>
|
||||
<input type="range" class="speed-slider" id="speedSlider" min="1" max="30" value="6" step="1">
|
||||
<div class="speed-presets">
|
||||
<div class="speed-preset" data-speed="3">🐢 慢</div>
|
||||
<div class="speed-preset" data-speed="6">⚡ 中</div>
|
||||
<div class="speed-preset" data-speed="10">🚀 快</div>
|
||||
<div class="speed-preset" data-speed="18">💨 极快</div>
|
||||
</div>
|
||||
<div class="auto-chapter-line"><span>📖 自动翻章</span><input type="checkbox" id="autoChapterCheckbox" checked></div>
|
||||
</div>
|
||||
|
||||
<div class="font-controls" id="fontControls">
|
||||
<button id="fontMinus">A-</button>
|
||||
<button id="fontPlus">A+</button>
|
||||
</div>
|
||||
|
||||
<div class="theme-selector" id="themeSelector">
|
||||
<div class="theme-dot" data-theme="deep-space" title="深邃星空"></div>
|
||||
<div class="theme-dot" data-theme="ocean" title="深海宁静"></div>
|
||||
<div class="theme-dot" data-theme="cherry" title="樱花"></div>
|
||||
<div class="theme-dot" data-theme="night" title="黑夜模式"></div>
|
||||
<div class="theme-dot" data-theme="forest" title="森林绿意"></div>
|
||||
<div class="theme-dot" data-theme="sunset" title="日落橙"></div>
|
||||
<div class="theme-dot" data-theme="lavender" title="薰衣草"></div>
|
||||
<div class="theme-dot" data-theme="blueberry" title="蓝莓"></div>
|
||||
<div class="theme-dot" data-theme="amber" title="琥珀"></div>
|
||||
<div class="theme-dot" data-theme="coral" title="珊瑚粉"></div>
|
||||
<div class="theme-dot" data-theme="mint" title="薄荷绿"></div>
|
||||
<div class="theme-dot" data-theme="rosegold" title="玫瑰金"></div>
|
||||
<div class="theme-dot" data-theme="eyecare" title="护眼模式"></div>
|
||||
<div style="width:100%; height:1px; background:rgba(255,255,255,0.2); margin:5px 0;"></div>
|
||||
<div class="theme-dot" data-theme="aurora-dynamic" title="极光幻彩(动态)"></div>
|
||||
<div class="theme-dot" data-theme="neon-dynamic" title="霓虹脉冲(动态)"></div>
|
||||
<div class="theme-dot" data-theme="sunset-dynamic" title="暮色晚霞(动态)"></div>
|
||||
<div class="theme-dot" data-theme="wave-dynamic" title="深海波动(动态)"></div>
|
||||
<div class="theme-dot" data-theme="fire-dynamic" title="火焰之心(动态)"></div>
|
||||
<div class="theme-dot" data-theme="sakura-dynamic" title="樱花飘舞(动态)"></div>
|
||||
<div class="theme-dot" data-theme="mintfrost-dynamic" title="薄荷冰霜(动态)"></div>
|
||||
<div class="theme-dot" data-theme="lavenderfield-dynamic" title="薰衣草庄园(动态)"></div>
|
||||
<div class="theme-dot" data-theme="golden-dynamic" title="金色麦田(动态)"></div>
|
||||
<div class="theme-dot" data-theme="coralreef-dynamic" title="珊瑚海洋(动态)"></div>
|
||||
<div class="theme-dot" data-theme="galaxy-dynamic" title="星空银河(动态)"></div>
|
||||
<div class="theme-dot" data-theme="rosegarden-dynamic" title="玫瑰花园(动态)"></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-panel" id="statPanel">
|
||||
<div><span class="stat-label">📖 今日阅读</span><span class="stat-value" id="todayStat">0 分钟</span></div>
|
||||
<div><span class="stat-label">📚 累计阅读</span><span class="stat-value" id="totalStat">0 分钟</span></div>
|
||||
<div><span class="stat-label">⭐ 书签数量</span><span class="stat-value" id="bookmarkStat">0</span></div>
|
||||
<div><span class="stat-label">🌙 夜间模式</span><span class="stat-value" id="nightModeStat">22:00-07:00</span></div>
|
||||
</div>
|
||||
|
||||
<div class="bookmark-panel" id="bookmarkPanel">
|
||||
<div class="bookmark-header"><span>📖 我的书签</span><span id="closePanelBtn">✕</span></div>
|
||||
<div class="bookmark-list" id="bookmarkList"><div class="empty-bookmark">📭 暂无书签<br>点击 ⭐ 添加</div></div>
|
||||
</div>
|
||||
|
||||
<div id="globalProgressPlaceholder"></div>
|
||||
|
||||
<div id="pageTransition" class="page-transition">
|
||||
<div class="book-loader"><div class="book-page"></div><div class="book-page"></div><div class="book-page"></div><div class="book-page"></div></div>
|
||||
<div class="loading-text">加载中</div>
|
||||
<div class="loading-dots"><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let readingStartTime = null;
|
||||
let totalReadingMinutes = parseInt(localStorage.getItem('totalReadingMinutes') || '0');
|
||||
let todayReadingMinutes = parseInt(localStorage.getItem('todayReadingMinutes') || '0');
|
||||
let lastReadingDate = localStorage.getItem('lastReadingDate') || '';
|
||||
|
||||
function resetDailyStatIfNeeded() {
|
||||
const today = new Date().toDateString();
|
||||
if (lastReadingDate !== today) {
|
||||
todayReadingMinutes = 0;
|
||||
localStorage.setItem('todayReadingMinutes', '0');
|
||||
localStorage.setItem('lastReadingDate', today);
|
||||
}
|
||||
}
|
||||
|
||||
function startReadingTimer() {
|
||||
if (readingStartTime) return;
|
||||
readingStartTime = Date.now();
|
||||
}
|
||||
|
||||
function stopReadingTimer() {
|
||||
if (!readingStartTime) return;
|
||||
const elapsed = Math.floor((Date.now() - readingStartTime) / 60000);
|
||||
if (elapsed > 0 && elapsed < 60) {
|
||||
totalReadingMinutes += elapsed;
|
||||
todayReadingMinutes += elapsed;
|
||||
localStorage.setItem('totalReadingMinutes', totalReadingMinutes);
|
||||
localStorage.setItem('todayReadingMinutes', todayReadingMinutes);
|
||||
updateStatDisplay();
|
||||
}
|
||||
readingStartTime = null;
|
||||
}
|
||||
|
||||
function updateStatDisplay() {
|
||||
document.getElementById('todayStat').innerText = todayReadingMinutes + ' 分钟';
|
||||
document.getElementById('totalStat').innerText = totalReadingMinutes + ' 分钟';
|
||||
const bookmarkCount = getBookmarks().length;
|
||||
document.getElementById('bookmarkStat').innerText = bookmarkCount;
|
||||
}
|
||||
|
||||
resetDailyStatIfNeeded();
|
||||
updateStatDisplay();
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
startReadingTimer();
|
||||
if (window.scrollTimerStat) clearTimeout(window.scrollTimerStat);
|
||||
window.scrollTimerStat = setTimeout(() => stopReadingTimer(), 3000);
|
||||
});
|
||||
window.addEventListener('click', () => startReadingTimer());
|
||||
window.addEventListener('beforeunload', () => stopReadingTimer());
|
||||
|
||||
// 夜间模式定时切换
|
||||
let nightModeInterval = null;
|
||||
function checkNightMode() {
|
||||
const hour = new Date().getHours();
|
||||
const isNightTime = hour >= 22 || hour < 7;
|
||||
const currentTheme = document.body.className.replace('theme-', '');
|
||||
const nightThemes = ['night', 'neon-dynamic', 'galaxy-dynamic', 'deep-space'];
|
||||
|
||||
if (isNightTime && !nightThemes.includes(currentTheme)) {
|
||||
const savedTheme = localStorage.getItem('reader_theme');
|
||||
if (savedTheme && !nightThemes.includes(savedTheme)) {
|
||||
localStorage.setItem('day_theme', savedTheme);
|
||||
}
|
||||
setTheme('night');
|
||||
showToast('🌙 已自动切换夜间模式');
|
||||
} else if (!isNightTime && localStorage.getItem('day_theme') && document.body.className.includes('night')) {
|
||||
setTheme(localStorage.getItem('day_theme'));
|
||||
showToast('☀️ 已切换日间模式');
|
||||
}
|
||||
}
|
||||
nightModeInterval = setInterval(checkNightMode, 60000);
|
||||
checkNightMode();
|
||||
|
||||
const statFloatBtn = document.getElementById('statFloatBtn');
|
||||
const statPanel = document.getElementById('statPanel');
|
||||
if (statFloatBtn) {
|
||||
statFloatBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
updateStatDisplay();
|
||||
statPanel.classList.toggle('visible');
|
||||
resetHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(msg) {
|
||||
let t = document.querySelector('.toast');
|
||||
if (!t) { t = document.createElement('div'); t.className = 'toast'; document.body.appendChild(t); }
|
||||
t.textContent = msg;
|
||||
t.style.display = 'block';
|
||||
const bodyClass = document.body.className;
|
||||
if (bodyClass.includes('aurora')) { t.style.background = 'rgba(0,0,0,0.8)'; t.style.color = '#7cffd0'; t.style.border = '1px solid rgba(124,255,208,0.3)'; }
|
||||
else if (bodyClass.includes('neon')) { t.style.background = 'rgba(0,0,0,0.85)'; t.style.color = '#0ff'; t.style.border = '1px solid #0ff'; }
|
||||
else if (bodyClass.includes('eyecare')) { t.style.background = 'rgba(199,237,204,0.95)'; t.style.color = '#2d2d2d'; t.style.border = '1px solid rgba(139,154,110,0.4)'; }
|
||||
else { t.style.background = 'rgba(0,0,0,0.85)'; t.style.color = '#fff'; t.style.border = 'none'; }
|
||||
setTimeout(() => t.style.display = 'none', 1500);
|
||||
}
|
||||
window.showToast = showToast;
|
||||
|
||||
const floatingBtns = document.getElementById('floatingButtons');
|
||||
const speedPanelEl = document.getElementById('speedPanel');
|
||||
const fontControlsEl = document.getElementById('fontControls');
|
||||
const themeSelectorEl = document.getElementById('themeSelector');
|
||||
const bookmarkPanelEl = document.getElementById('bookmarkPanel');
|
||||
|
||||
let hideTimer = null;
|
||||
let globalProgressBar = null;
|
||||
|
||||
function showControls() {
|
||||
floatingBtns.classList.add('visible');
|
||||
speedPanelEl.classList.add('visible');
|
||||
if (globalProgressBar) globalProgressBar.classList.remove('hide');
|
||||
resetHideTimer();
|
||||
}
|
||||
function hideControls() {
|
||||
floatingBtns.classList.remove('visible');
|
||||
speedPanelEl.classList.remove('visible');
|
||||
fontControlsEl.classList.remove('visible');
|
||||
themeSelectorEl.classList.remove('visible');
|
||||
statPanel.classList.remove('visible');
|
||||
if (globalProgressBar) globalProgressBar.classList.add('hide');
|
||||
}
|
||||
function resetHideTimer() {
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(() => {
|
||||
if (!bookmarkPanelEl.classList.contains('show') && !themeSelectorEl.classList.contains('visible') &&
|
||||
!fontControlsEl.classList.contains('visible') && !speedPanelEl.classList.contains('visible') &&
|
||||
!statPanel.classList.contains('visible')) {
|
||||
hideControls();
|
||||
} else { resetHideTimer(); }
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
let lastTap = 0;
|
||||
document.body.addEventListener('click', (e) => {
|
||||
const now = Date.now();
|
||||
const timeDiff = now - lastTap;
|
||||
const isControl = e.target.closest('.floating-btn') || e.target.closest('.bookmark-panel') ||
|
||||
e.target.closest('.theme-selector') || e.target.closest('.font-controls') ||
|
||||
e.target.closest('.speed-panel') || e.target.closest('.global-progress-container') ||
|
||||
e.target.closest('.stat-panel');
|
||||
if (!isControl && timeDiff < 300 && timeDiff > 0) {
|
||||
e.preventDefault();
|
||||
if (floatingBtns.classList.contains('visible')) { hideControls(); if (hideTimer) clearTimeout(hideTimer); }
|
||||
else { showControls(); }
|
||||
}
|
||||
lastTap = now;
|
||||
});
|
||||
|
||||
const bookmarkFloatBtn = document.getElementById('bookmarkFloatBtn');
|
||||
const closePanelBtn = document.getElementById('closePanelBtn');
|
||||
if (bookmarkFloatBtn) {
|
||||
bookmarkFloatBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
bookmarkPanelEl.classList.toggle('show');
|
||||
if (bookmarkPanelEl.classList.contains('show')) { showControls(); if (hideTimer) clearTimeout(hideTimer); }
|
||||
else { resetHideTimer(); }
|
||||
});
|
||||
}
|
||||
if (closePanelBtn) {
|
||||
closePanelBtn.addEventListener('click', () => { bookmarkPanelEl.classList.remove('show'); resetHideTimer(); });
|
||||
}
|
||||
|
||||
const themeFloatBtn = document.getElementById('themeFloatBtn');
|
||||
if (themeFloatBtn) {
|
||||
themeFloatBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
themeSelectorEl.classList.toggle('visible');
|
||||
resetHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllThemeStyles() {
|
||||
const bodyClass = document.body.className;
|
||||
const scrollBtn = document.getElementById('scrollToggleBtn');
|
||||
const bookmarkBtn = document.getElementById('bookmarkFloatBtn');
|
||||
if (bodyClass.includes('aurora')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#7cffd0';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(124,255,208,0.2)'; bookmarkBtn.style.border = '1px solid #7cffd0'; bookmarkBtn.style.color = '#7cffd0'; }
|
||||
} else if (bodyClass.includes('neon')) {
|
||||
if (scrollBtn) scrollBtn.style.color = '#0ff';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(0,255,255,0.15)'; bookmarkBtn.style.border = '1px solid #0ff'; bookmarkBtn.style.color = '#0ff'; }
|
||||
} else if (bodyClass.includes('eyecare')) {
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = 'rgba(139,154,110,0.2)'; bookmarkBtn.style.border = '1px solid #8b9a6e'; bookmarkBtn.style.color = '#2d2d2d'; }
|
||||
} else {
|
||||
if (scrollBtn) scrollBtn.style.color = '';
|
||||
if (bookmarkBtn) { bookmarkBtn.style.background = ''; bookmarkBtn.style.border = ''; bookmarkBtn.style.color = ''; }
|
||||
}
|
||||
}
|
||||
|
||||
const THEMES = [
|
||||
'deep-space', 'ocean', 'cherry', 'night', 'forest', 'sunset',
|
||||
'lavender', 'blueberry', 'amber', 'coral', 'mint', 'rosegold',
|
||||
'eyecare',
|
||||
'aurora-dynamic', 'neon-dynamic', 'sunset-dynamic', 'wave-dynamic', 'fire-dynamic',
|
||||
'sakura-dynamic', 'mintfrost-dynamic', 'lavenderfield-dynamic', 'golden-dynamic',
|
||||
'coralreef-dynamic', 'galaxy-dynamic', 'rosegarden-dynamic'
|
||||
];
|
||||
function setTheme(themeName) {
|
||||
document.body.className = 'theme-' + themeName;
|
||||
localStorage.setItem('reader_theme', themeName);
|
||||
document.querySelectorAll('.theme-dot').forEach(dot => {
|
||||
if (dot.dataset.theme === themeName) dot.classList.add('active');
|
||||
else dot.classList.remove('active');
|
||||
});
|
||||
updateAllThemeStyles();
|
||||
setTimeout(() => { if (typeof updateGlobalProgress === 'function') updateGlobalProgress(); }, 50);
|
||||
}
|
||||
const savedTheme = localStorage.getItem('reader_theme');
|
||||
if (savedTheme && THEMES.includes(savedTheme)) setTheme(savedTheme);
|
||||
else setTheme('deep-space');
|
||||
|
||||
document.querySelectorAll('.theme-dot').forEach(dot => {
|
||||
dot.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
setTheme(dot.dataset.theme);
|
||||
themeSelectorEl.classList.remove('visible');
|
||||
showToast('🎨 主题已切换');
|
||||
resetHideTimer();
|
||||
});
|
||||
});
|
||||
|
||||
const scrollBtn = document.getElementById('scrollToggleBtn');
|
||||
if (scrollBtn) {
|
||||
scrollBtn.addEventListener('contextmenu', (e) => { e.preventDefault(); fontControlsEl.classList.toggle('visible'); resetHideTimer(); });
|
||||
}
|
||||
const speedSlider = document.getElementById('speedSlider');
|
||||
const speedValue = document.getElementById('speedValue');
|
||||
const speedPresets = document.querySelectorAll('.speed-preset');
|
||||
let currentSpeed = 6;
|
||||
let autoScrollInterval = null;
|
||||
let isAutoScrolling = false;
|
||||
const savedSpeed = localStorage.getItem('scroll_speed');
|
||||
if (savedSpeed) {
|
||||
currentSpeed = parseInt(savedSpeed);
|
||||
if (speedSlider) speedSlider.value = currentSpeed;
|
||||
if (speedValue) speedValue.innerText = currentSpeed + ' px/帧';
|
||||
speedPresets.forEach(preset => { if (parseInt(preset.dataset.speed) === currentSpeed) preset.classList.add('active'); else preset.classList.remove('active'); });
|
||||
}
|
||||
function updateSpeed(newSpeed) {
|
||||
currentSpeed = Math.min(30, Math.max(1, newSpeed));
|
||||
if (speedSlider) speedSlider.value = currentSpeed;
|
||||
if (speedValue) speedValue.innerText = currentSpeed + ' px/帧';
|
||||
localStorage.setItem('scroll_speed', currentSpeed);
|
||||
speedPresets.forEach(preset => { if (parseInt(preset.dataset.speed) === currentSpeed) preset.classList.add('active'); else preset.classList.remove('active'); });
|
||||
if (isAutoScrolling) { stopAutoScroll(); startAutoScroll(); }
|
||||
}
|
||||
if (speedSlider) { speedSlider.oninput = (e) => { updateSpeed(parseInt(e.target.value)); showToast(`⚡ 速度 ${currentSpeed} px/帧`); resetHideTimer(); }; }
|
||||
speedPresets.forEach(preset => { preset.onclick = () => { updateSpeed(parseInt(preset.dataset.speed)); showToast(`⚡ ${preset.innerText} ${currentSpeed} px/帧`); resetHideTimer(); }; });
|
||||
function startAutoScroll() {
|
||||
if (autoScrollInterval) clearInterval(autoScrollInterval);
|
||||
autoScrollInterval = setInterval(() => window.scrollBy(0, currentSpeed), 25);
|
||||
isAutoScrolling = true;
|
||||
if (scrollBtn) { scrollBtn.classList.add('active'); scrollBtn.innerHTML = "⏸"; }
|
||||
showToast(`▶ 滚动中 (${currentSpeed}px/帧)`);
|
||||
}
|
||||
function stopAutoScroll() {
|
||||
if (autoScrollInterval) { clearInterval(autoScrollInterval); autoScrollInterval = null; }
|
||||
isAutoScrolling = false;
|
||||
if (scrollBtn) { scrollBtn.classList.remove('active'); scrollBtn.innerHTML = "▼"; }
|
||||
showToast('⏹ 已停止');
|
||||
}
|
||||
if (scrollBtn) { scrollBtn.onclick = (e) => { e.stopPropagation(); if (isAutoScrolling) stopAutoScroll(); else startAutoScroll(); resetHideTimer(); }; }
|
||||
|
||||
const STORAGE_KEY = "bookmarks_v6";
|
||||
function getBookmarks(){ try{ return JSON.parse(localStorage.getItem(STORAGE_KEY)||'[]'); }catch(e){ return []; } }
|
||||
function saveBookmarks(list){ localStorage.setItem(STORAGE_KEY, JSON.stringify(list)); refreshBookmarkList(); updateStatDisplay(); }
|
||||
function refreshBookmarkList(){
|
||||
let list = getBookmarks();
|
||||
let container = document.getElementById('bookmarkList');
|
||||
if(!container) return;
|
||||
if(list.length===0){ container.innerHTML='<div class="empty-bookmark">📭 暂无书签<br>点击 ⭐ 添加</div>'; updateStatDisplay(); return; }
|
||||
list.sort((a,b)=>b.time-a.time);
|
||||
let html='';
|
||||
for(let b of list){
|
||||
let pageLabel = b.type==='txt'?'第'+b.page+'章':(b.type==='ebook'?'第'+b.page+'章':'第'+b.page+'页');
|
||||
html+=`<div class="bookmark-item" data-book="${escapeHtml(b.book)}" data-chapter="${escapeHtml(b.chapter)}" data-page="${b.page}"><span class="delete" data-book="${escapeHtml(b.book)}" data-chapter="${escapeHtml(b.chapter)}">🗑</span><div class="title">📖 ${escapeHtml(b.book.length>18?b.book.substring(0,18)+'...':b.book)}</div><div class="info">📄 ${escapeHtml(b.chapterName||b.chapter.substring(0,25))} | 📍 ${pageLabel}</div></div>`;
|
||||
}
|
||||
container.innerHTML=html;
|
||||
document.querySelectorAll('#bookmarkList .bookmark-item').forEach(item=>{
|
||||
let book=item.getAttribute('data-book'), chapter=item.getAttribute('data-chapter'), page=parseInt(item.getAttribute('data-page'))||1;
|
||||
item.onclick=(e)=>{ if(e.target.classList.contains('delete')) return; jumpToBookmark(book,chapter,page); };
|
||||
let db=item.querySelector('.delete');
|
||||
if(db) db.onclick=(e)=>{ e.stopPropagation(); removeBookmark(db.getAttribute('data-book'), db.getAttribute('data-chapter')); };
|
||||
});
|
||||
updateStatDisplay();
|
||||
}
|
||||
function removeBookmark(book,chapter){ let list=getBookmarks(); list=list.filter(b=>!(b.book===book&&b.chapter===chapter)); saveBookmarks(list); showToast('🗑 删除书签'); }
|
||||
function escapeHtml(s){ return (s||'').replace(/[&<>]/g,m=>({'&':'&','<':'<','>':'>'}[m])); }
|
||||
let CURRENT_BOOK = '', CURRENT_CHAPTER = '';
|
||||
function jumpToBookmark(book,chapter,page){
|
||||
if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){
|
||||
if (typeof jumpToPage === 'function') jumpToPage(page);
|
||||
else if (typeof renderChapter === 'function') renderChapter(Math.min(Math.max(1,page), (typeof chapters !== 'undefined' ? chapters.length : 1))-1);
|
||||
}else{
|
||||
sessionStorage.setItem('jump_target', JSON.stringify({book,chapter,page}));
|
||||
location.href = '<?php echo $currentFile; ?>?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);
|
||||
}
|
||||
}
|
||||
|
||||
let autoChapterEnabled = true;
|
||||
let isTurningPage = false;
|
||||
let turnTimer = null;
|
||||
let lastScrollTop = 0;
|
||||
let scrollDirection = 'down';
|
||||
const autoChapterCheckbox = document.getElementById('autoChapterCheckbox');
|
||||
|
||||
function showBookTurnAnimation(callback) {
|
||||
if (isTurningPage) { if (callback) callback(); return; }
|
||||
isTurningPage = true;
|
||||
let overlay = document.createElement('div');
|
||||
overlay.className = 'page-turn-overlay';
|
||||
overlay.style.background = 'rgba(0,0,0,0.6)';
|
||||
overlay.innerHTML = `<div class="book-container"><div class="book-left">📖</div><div class="book-right">📖</div><div class="message">✨ 正在翻开新篇章 ✨</div></div>`;
|
||||
document.body.appendChild(overlay);
|
||||
setTimeout(() => { if (callback) callback(); setTimeout(() => { if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); isTurningPage = false; }, 150); }, 450);
|
||||
}
|
||||
|
||||
if (localStorage.getItem('autoChapterEnabled') !== null) {
|
||||
autoChapterEnabled = localStorage.getItem('autoChapterEnabled') === 'true';
|
||||
if (autoChapterCheckbox) autoChapterCheckbox.checked = autoChapterEnabled;
|
||||
} else { autoChapterEnabled = true; if (autoChapterCheckbox) autoChapterCheckbox.checked = true; }
|
||||
if (autoChapterCheckbox) {
|
||||
autoChapterCheckbox.onchange = function(e) {
|
||||
autoChapterEnabled = this.checked;
|
||||
localStorage.setItem('autoChapterEnabled', autoChapterEnabled);
|
||||
showToast(autoChapterEnabled ? '✅ 自动翻章已开启' : '⏹ 自动翻章已关闭');
|
||||
resetHideTimer();
|
||||
};
|
||||
}
|
||||
|
||||
let scrollTimerAuto = null;
|
||||
function checkScrollBottom() {
|
||||
if (!autoChapterEnabled || isTurningPage) return;
|
||||
let totalHeight = document.body.scrollHeight, windowHeight = window.innerHeight, scrollTop = window.scrollY;
|
||||
if (scrollTop > lastScrollTop) scrollDirection = 'down';
|
||||
else if (scrollTop < lastScrollTop) { scrollDirection = 'up'; if (turnTimer) { clearTimeout(turnTimer); turnTimer = null; } }
|
||||
lastScrollTop = scrollTop;
|
||||
let isAtBottom = (scrollTop + windowHeight + 15) >= totalHeight;
|
||||
if (isAtBottom && scrollDirection === 'down' && !turnTimer) {
|
||||
let nextBtn = document.getElementById('nextChapterBtn');
|
||||
if (nextBtn && !nextBtn.disabled) {
|
||||
showToast('📖 3秒后自动翻到下一章...');
|
||||
turnTimer = setTimeout(() => {
|
||||
if (!autoChapterEnabled || isTurningPage) { turnTimer = null; return; }
|
||||
if ((window.scrollY + windowHeight + 15) >= document.body.scrollHeight) {
|
||||
showBookTurnAnimation(() => { if (nextBtn) nextBtn.click(); });
|
||||
}
|
||||
turnTimer = null;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', function() { if (scrollTimerAuto) clearTimeout(scrollTimerAuto); scrollTimerAuto = setTimeout(checkScrollBottom, 100); });
|
||||
function onChapterChange() { if (turnTimer) { clearTimeout(turnTimer); turnTimer = null; } isTurningPage = false; lastScrollTop = 0; scrollDirection = 'down'; window.scrollTo(0, 0); if (typeof updateGlobalProgress === 'function') updateGlobalProgress(); }
|
||||
|
||||
const pageTransition = {
|
||||
element: document.getElementById('pageTransition'),
|
||||
show() { if (!this.element) return; this.element.classList.add('active'); if (this.timeout) clearTimeout(this.timeout); this.timeout = setTimeout(() => { if (this.element) this.element.classList.remove('active'); }, 3000); },
|
||||
hide() { if (!this.element) return; this.element.classList.remove('active'); if (this.timeout) clearTimeout(this.timeout); }
|
||||
};
|
||||
function rippleHandler(e) {
|
||||
const ripple = document.createElement('span'); ripple.classList.add('ripple');
|
||||
const rect = this.getBoundingClientRect(); const size = Math.max(rect.width, rect.height);
|
||||
const x = e.clientX - rect.left - size / 2, y = e.clientY - rect.top - size / 2;
|
||||
ripple.style.width = ripple.style.height = size + 'px'; ripple.style.left = x + 'px'; ripple.style.top = y + 'px';
|
||||
this.style.position = 'relative'; this.style.overflow = 'hidden'; this.appendChild(ripple);
|
||||
setTimeout(() => ripple.remove(), 500);
|
||||
if (this.tagName === 'A' && this.getAttribute('href') && !this.hasAttribute('data-no-transition')) {
|
||||
e.preventDefault(); const targetUrl = this.getAttribute('href');
|
||||
if (targetUrl && targetUrl !== '#') { pageTransition.show(); setTimeout(() => { window.location.href = targetUrl; }, 280); }
|
||||
}
|
||||
}
|
||||
function enhanceBookCards() { document.querySelectorAll('.shelf-item, .book-chapter-item a').forEach(card => { card.removeEventListener('click', rippleHandler); card.addEventListener('click', rippleHandler); }); }
|
||||
document.addEventListener('DOMContentLoaded', () => { pageTransition.hide(); enhanceBookCards(); updateAllThemeStyles(); });
|
||||
window.addEventListener('pageshow', () => { pageTransition.hide(); });
|
||||
window.addEventListener('beforeunload', () => { stopReadingTimer(); pageTransition.hide(); });
|
||||
|
||||
let globalChapters = [], globalTotalChapters = 0, globalCurrentIndex = 0, lastChapterIndex = -1, tooltipHideTimer = null;
|
||||
function getThemeTooltipColors() {
|
||||
const bodyClass = document.body.className;
|
||||
if (bodyClass.includes('aurora')) return { bgColor: 'rgba(0,0,0,0.75)', textColor: '#7cffd0', borderColor: '#7cffd0' };
|
||||
if (bodyClass.includes('neon')) return { bgColor: 'rgba(0,0,0,0.9)', textColor: '#0ff', borderColor: '#0ff' };
|
||||
if (bodyClass.includes('eyecare')) return { bgColor: 'rgba(215,245,210,0.98)', textColor: '#2d2d2d', borderColor: '#8b9a6e' };
|
||||
return { bgColor: 'rgba(0,0,0,0.95)', textColor: '#ff9800', borderColor: 'rgba(255,152,0,0.6)' };
|
||||
}
|
||||
function updateTooltipStyle(tooltip) { if (!tooltip) return; const c = getThemeTooltipColors(); tooltip.style.backgroundColor = c.bgColor; tooltip.style.color = c.textColor; tooltip.style.border = `1px solid ${c.borderColor}`; }
|
||||
window.updateAllTooltipColors = function() { const tooltip = document.getElementById('chapterTooltip'); if (tooltip) updateTooltipStyle(tooltip); updateAllThemeStyles(); };
|
||||
function showChapterTooltip(chapterIndex, chapterTitle) {
|
||||
let tooltip = document.getElementById('chapterTooltip');
|
||||
if (!tooltip) return;
|
||||
if (tooltipHideTimer) clearTimeout(tooltipHideTimer);
|
||||
let displayText = `📖 第 ${chapterIndex+1} 章 · ${chapterTitle.substring(0, 32)}${chapterTitle.length > 32 ? '...' : ''}`;
|
||||
tooltip.textContent = displayText;
|
||||
updateTooltipStyle(tooltip);
|
||||
tooltip.style.display = 'block';
|
||||
tooltipHideTimer = setTimeout(() => { if (tooltip) tooltip.style.display = 'none'; }, 2000);
|
||||
}
|
||||
function createGlobalProgressBar(total, currentIdx, chaptersList) {
|
||||
let container = document.getElementById('globalProgressPlaceholder');
|
||||
if (!container) return;
|
||||
container.innerHTML = `<div class="global-progress-container" id="globalProgressBar"><div class="progress-range-area"><input type="range" class="progress-slider-global" id="globalProgressSlider" min="0" max="${total-1}" value="${currentIdx}" step="1"><div class="chapter-tooltip" id="chapterTooltip" style="display: none;">📖 ${escapeHtml(chaptersList[currentIdx]?.title || '章节')}</div></div><div class="progress-info"><div class="progress-label"><span>📖 第 ${currentIdx+1} / ${total} 章</span></div><div>${escapeHtml(chaptersList[currentIdx]?.title || '')}</div></div></div>`;
|
||||
globalProgressBar = document.getElementById('globalProgressBar');
|
||||
let slider = document.getElementById('globalProgressSlider');
|
||||
if (slider) {
|
||||
slider.addEventListener('input', (e) => { let idx = parseInt(e.target.value); if (idx !== lastChapterIndex) { lastChapterIndex = idx; showChapterTooltip(idx, chaptersList[idx]?.title || '章节'); } });
|
||||
slider.addEventListener('change', (e) => { let idx = parseInt(e.target.value); if (typeof renderChapter === 'function') { renderChapter(idx); showToast(`📖 跳转到第 ${idx+1} 章`); } resetHideTimer(); });
|
||||
}
|
||||
if (globalProgressBar) globalProgressBar.classList.add('hide');
|
||||
}
|
||||
function updateGlobalProgress() {
|
||||
let container = document.getElementById('globalProgressBar');
|
||||
if (!container) return;
|
||||
let slider = document.getElementById('globalProgressSlider');
|
||||
let infoLabel = container.querySelector('.progress-label span');
|
||||
let infoTitle = container.querySelector('.progress-info > div:last-child');
|
||||
if (slider && globalTotalChapters > 0) {
|
||||
slider.value = globalCurrentIndex;
|
||||
if (infoLabel) infoLabel.innerText = `📖 第 ${globalCurrentIndex+1} / ${globalTotalChapters} 章`;
|
||||
if (infoTitle && globalChapters[globalCurrentIndex]) infoTitle.innerText = globalChapters[globalCurrentIndex].title || '';
|
||||
lastChapterIndex = globalCurrentIndex;
|
||||
}
|
||||
}
|
||||
window.updateGlobalProgress = updateGlobalProgress;
|
||||
window.updateAllTooltipColors = updateAllTooltipColors;
|
||||
window.updateAllThemeStyles = updateAllThemeStyles;
|
||||
|
||||
(function() {
|
||||
let touchStartX = 0, minSwipeDistance = 70;
|
||||
function detectTextMode() { return document.getElementById('prevChapterBtn') !== null && document.getElementById('nextChapterBtn') !== null && !document.getElementById('comicViewer'); }
|
||||
function createSwipeUI() {
|
||||
if (document.querySelector('.swipe-indicator')) return;
|
||||
let indicator = document.createElement('div'); indicator.className = 'swipe-indicator'; indicator.innerHTML = '← 滑动切换章节 →'; document.body.appendChild(indicator);
|
||||
let leftArrow = document.createElement('div'); leftArrow.className = 'swipe-arrow-left'; leftArrow.innerHTML = '←'; document.body.appendChild(leftArrow);
|
||||
let rightArrow = document.createElement('div'); rightArrow.className = 'swipe-arrow-right'; rightArrow.innerHTML = '→'; document.body.appendChild(rightArrow);
|
||||
if (!localStorage.getItem('swipe_hint_shown')) { indicator.classList.add('show'); setTimeout(() => indicator.classList.remove('show'), 3000); localStorage.setItem('swipe_hint_shown', 'true'); }
|
||||
}
|
||||
function handleTouchStart(e) { if (!detectTextMode()) return; touchStartX = e.changedTouches[0].screenX; }
|
||||
function handleTouchEnd(e) {
|
||||
if (!detectTextMode()) return;
|
||||
let deltaX = e.changedTouches[0].screenX - touchStartX;
|
||||
if (Math.abs(deltaX) < minSwipeDistance) return;
|
||||
if (deltaX < -minSwipeDistance) {
|
||||
let nextBtn = document.getElementById('nextChapterBtn');
|
||||
if (nextBtn && !nextBtn.disabled) { if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20); nextBtn.click(); showToast('📖 下一章'); }
|
||||
else showToast('📖 已经是最后一章了');
|
||||
} else if (deltaX > minSwipeDistance) {
|
||||
let prevBtn = document.getElementById('prevChapterBtn');
|
||||
if (prevBtn && !prevBtn.disabled) { if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20); prevBtn.click(); showToast('📖 上一章'); }
|
||||
else showToast('📖 已经是第一章了');
|
||||
}
|
||||
}
|
||||
document.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
document.addEventListener('touchend', handleTouchEnd);
|
||||
setTimeout(() => { if (detectTextMode()) createSwipeUI(); }, 500);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php if ($isChapterPage && $isPdf): ?>
|
||||
<div class="top-bar"><div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div></div><div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div></div>
|
||||
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
|
||||
<div class="content" id="reader"><div class="loading-msg" id="loadingMsg">⏳ 正在加载 PDF...<br><?php echo htmlspecialchars($chapter); ?></div></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>"; CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>"; const PDF_URL = "<?php echo $fileUrl; ?>"; const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
let pdfDoc=null,totalPages=0,renderedPages=new Set(),targetPage=null,scale=1.5;
|
||||
document.getElementById('backBtn').onclick=()=>{ if(document.referrer&&document.referrer.includes(window.location.host))history.back(); else location.href=BASE_FILE; };
|
||||
function getCurrentPage(){ let cs=document.querySelectorAll('.canvas-container'); for(let i=0;i<cs.length;i++){ let r=cs[i].getBoundingClientRect(); if(r.top<=150&&r.bottom>=100){ let p=parseInt(cs[i].getAttribute('data-page')); if(!isNaN(p)) return p; } } return 1; }
|
||||
function addBookmark(){ let p=getCurrentPage(), l=getBookmarks(), i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER), n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:p,time:Date.now(),type:'pdf'}; if(i>=0) l[i]=n; else l.push(n); saveBookmarks(l); showToast('✅ 第 '+p+' 页'); }
|
||||
function jumpToPage(p){ p=Math.min(Math.max(1,p),totalPages); let t=document.querySelector(`.canvas-container[data-page="${p}"]`); if(t){ t.scrollIntoView({behavior:'smooth',block:'start'}); showToast('✨ 第 '+p+' 页'); }else{ showToast('📖 加载中...'); (async()=>{ let s=Math.max(1,p-2),e=Math.min(totalPages,p+2); for(let i=s;i<=e;i++) if(!renderedPages.has(i)) await renderPage(i); setTimeout(()=>{ let c=document.querySelector(`.canvas-container[data-page="${p}"]`); if(c){ c.scrollIntoView({behavior:'smooth',block:'start'}); showToast('✨ 第 '+p+' 页'); } },300); })(); } }
|
||||
window.jumpToBookmark = function(book,chapter,p){ if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER) jumpToPage(p); else{ sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page:p})); location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter); } };
|
||||
async function renderPage(n){ if(!pdfDoc||renderedPages.has(n)) return; renderedPages.add(n); let div=document.createElement('div'); div.className='canvas-container'; div.setAttribute('data-page',n); let p=document.createElement('div'); p.className='loading-placeholder'; p.innerText=`⏳ 第 ${n} 页...`; div.appendChild(p); let ins=false,ex=document.querySelectorAll('.canvas-container'); for(let i=0;i<ex.length;i++){ let ep=parseInt(ex[i].getAttribute('data-page')); if(ep>n){ ex[i].before(div); ins=true; break; } } if(!ins) document.getElementById('reader').appendChild(div); try{ let page=await pdfDoc.getPage(n), vp=page.getViewport({scale:scale}), cv=document.createElement('canvas'); cv.width=vp.width; cv.height=vp.height; cv.style.width='100%'; cv.style.height='auto'; await page.render({canvasContext:cv.getContext('2d'),viewport:vp}).promise; div.innerHTML=''; div.appendChild(cv); let pf=document.getElementById('progressFill'); if(pf) pf.style.width=(renderedPages.size/totalPages)*100+'%'; }catch(e){ p.innerText=`❌ 第 ${n} 页失败`; } }
|
||||
let st; function onScrollLoad(){ if(st) clearTimeout(st); st=setTimeout(()=>{ if(!pdfDoc) return; let cs=document.querySelectorAll('.canvas-container'), need=new Set(); cs.forEach(c=>{ let r=c.getBoundingClientRect(); if(r.top-600<window.innerHeight&&r.bottom+600>0){ let p=parseInt(c.getAttribute('data-page')); if(!isNaN(p)) need.add(p); } }); let toRender=[]; need.forEach(p=>{ for(let i=-2;i<=2;i++){ let np=p+i; if(np>=1&&np<=totalPages&&!renderedPages.has(np)) toRender.push(np); } }); toRender.sort((a,b)=>a-b).forEach(p=>renderPage(p)); },200); }
|
||||
async function loadPDF(){ try{ let lm=document.getElementById('loadingMsg'); lm.style.display='block'; pdfDoc=await pdfjsLib.getDocument(PDF_URL).promise; totalPages=pdfDoc.numPages; lm.innerText=`📄 共 ${totalPages} 页,加载中...`; let jump=sessionStorage.getItem('jump_target'); if(jump){ sessionStorage.removeItem('jump_target'); try{ let t=JSON.parse(jump); if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page) targetPage=t.page; }catch(e){} }else{ let bks=getBookmarks(), ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER); if(ex&&ex.page) targetPage=ex.page; } for(let i=1;i<=Math.min(3,totalPages);i++) await renderPage(i); lm.style.display='none'; if(targetPage){ let s=Math.max(1,targetPage-1),e=Math.min(totalPages,targetPage+1); for(let i=s;i<=e;i++) if(!renderedPages.has(i)) await renderPage(i); setTimeout(()=>{ let c=document.querySelector(`.canvas-container[data-page="${targetPage}"]`); if(c){ c.scrollIntoView({behavior:'smooth',block:'start'}); showToast('📖 第 '+targetPage+' 页'); } targetPage=null; },500); } window.addEventListener('scroll',onScrollLoad); }catch(e){ document.getElementById('loadingMsg').innerHTML=`❌ 加载失败<br>${e.message}`; } }
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark; loadPDF(); refreshBookmarkList(); updateAllThemeStyles();
|
||||
</script>
|
||||
|
||||
<?php elseif ($isChapterPage && $isTxt && $txtData): ?>
|
||||
<div class="top-bar"><div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div></div><div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div></div>
|
||||
<div class="content" id="reader"><div id="txtContent"></div><div class="ebook-nav"><button id="prevChapterBtn" disabled>◀ 上一章</button><button id="nextChapterBtn" disabled>下一章 ▶</button></div><div class="chapter-indicator" id="chapterIndicator"></div></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>"; CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>"; const BASE_FILE = "<?php echo $currentFile; ?>"; const TXT_DATA = <?php echo json_encode($txtData); ?>;
|
||||
let curIdx=0, chapters=TXT_DATA.chapters||[], fontSize=18;
|
||||
globalChapters = chapters; globalTotalChapters = chapters.length; globalCurrentIndex = 0;
|
||||
function applyStyles(){ let s=document.getElementById('txt-style'); if(!s){ s=document.createElement('style'); s.id='txt-style'; document.head.appendChild(s); } s.textContent=`.ebook-chapter{font-size:${fontSize}px}.ebook-chapter p{margin-bottom:1em;text-indent:2em}`; }
|
||||
function renderChapter(i){ if(!chapters||i<0||i>=chapters.length) return; onChapterChange(); curIdx=i; globalCurrentIndex=i; document.getElementById('txtContent').innerHTML=`<div class="ebook-chapter"><div class="chapter-title">${escapeHtml(chapters[i].title)}</div>${chapters[i].content}</div>`; document.getElementById('prevChapterBtn').disabled=(i<=0); document.getElementById('nextChapterBtn').disabled=(i>=chapters.length-1); document.getElementById('chapterIndicator').innerText=`第 ${i+1}/${chapters.length} 章 · ${chapters[i].title}`; saveProgress(i); if(typeof updateGlobalProgress==='function') updateGlobalProgress(); }
|
||||
function saveProgress(i){ let l=getBookmarks(), idx=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER), n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:i+1,time:Date.now(),type:'txt'}; if(idx>=0) l[idx]=n; else l.push(n); saveBookmarks(l); }
|
||||
window.jumpToBookmark = function(book,chapter,page){ if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){ renderChapter(Math.min(Math.max(1,page),chapters.length)-1); showToast('✨ 第 '+page+' 章'); }else{ sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'txt'})); location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter); } };
|
||||
function addBookmark(){ let n=curIdx+1, l=getBookmarks(), i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER), ni={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:n,time:Date.now(),type:'txt'}; if(i>=0) l[i]=ni; else l.push(ni); saveBookmarks(l); showToast('✅ 第 '+n+' 章'); }
|
||||
document.getElementById('fontPlus').onclick=()=>{ fontSize=Math.min(fontSize+2,32); applyStyles(); renderChapter(curIdx); showToast(`字体 ${fontSize}px`); resetHideTimer(); };
|
||||
document.getElementById('fontMinus').onclick=()=>{ fontSize=Math.max(fontSize-2,12); applyStyles(); renderChapter(curIdx); showToast(`字体 ${fontSize}px`); resetHideTimer(); };
|
||||
document.getElementById('backBtn').onclick=()=>{ if(document.referrer&&document.referrer.includes(window.location.host))history.back(); else location.href=BASE_FILE; };
|
||||
document.getElementById('prevChapterBtn').onclick=()=>{ if(curIdx>0) renderChapter(curIdx-1); resetHideTimer(); };
|
||||
document.getElementById('nextChapterBtn').onclick=()=>{ if(curIdx<chapters.length-1) renderChapter(curIdx+1); resetHideTimer(); };
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
applyStyles(); if(chapters.length>0){ let saved=0, jump=sessionStorage.getItem('jump_target'); if(jump){ sessionStorage.removeItem('jump_target'); try{ let t=JSON.parse(jump); if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page) saved=Math.min(Math.max(1,t.page),chapters.length)-1; }catch(e){} }else{ let bks=getBookmarks(), ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER); if(ex&&ex.page) saved=Math.min(Math.max(1,ex.page),chapters.length)-1; } renderChapter(saved); }
|
||||
refreshBookmarkList(); createGlobalProgressBar(globalTotalChapters, globalCurrentIndex, globalChapters); updateAllThemeStyles();
|
||||
</script>
|
||||
|
||||
<?php elseif ($isChapterPage && $isEpub && $epubData): ?>
|
||||
<div class="top-bar"><div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div></div><div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div></div>
|
||||
<div class="content" id="reader">
|
||||
<?php if ($epubData['type'] == 'comic' && !empty($epubData['images'])): ?>
|
||||
<?php $comicImages = $epubData['images']; ?>
|
||||
<div id="comicViewer">
|
||||
<?php foreach($comicImages as $idx => $imgPath): ?>
|
||||
<div class="canvas-container" data-page="<?php echo $idx+1; ?>" style="margin-bottom:20px;">
|
||||
<img src="<?php echo $imgPath; ?>" loading="lazy" style="max-width:100%; height:auto; border-radius:8px; display:block; margin:0 auto; box-shadow:0 4px 12px rgba(0,0,0,0.2);"
|
||||
onerror="this.onerror=null; this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22300%22 height=%22400%22%3E%3Crect width=%22300%22 height=%22400%22 fill=%22%23333%22/%3E%3Ctext x=%22150%22 y=%22200%22 fill=%22%23fff%22 text-anchor=%22middle%22%3E图片加载失败%3C/text%3E%3C/svg%3E';">
|
||||
<div class="comic-page-info" style="text-align:center; padding:8px; font-size:12px; color:rgba(255,255,255,0.6);">第 <?php echo $idx+1; ?> / <?php echo count($comicImages); ?> 页</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>"; CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>"; const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>"; const BASE_FILE = "<?php echo $currentFile; ?>"; const TOTAL_PAGES = <?php echo count($comicImages); ?>;
|
||||
globalChapters = [{title: CHAPTER_NAME}]; globalTotalChapters = 1; globalCurrentIndex = 0;
|
||||
function getCurrentPage(){ let cs=document.querySelectorAll('.canvas-container'), bestPage=1, bestDistance=Infinity, viewportHeight=window.innerHeight; for(let i=0;i<cs.length;i++){ let rect=cs[i].getBoundingClientRect(), center=rect.top+rect.height/2, distance=Math.abs(center-viewportHeight/2); if(distance<bestDistance){ bestDistance=distance; bestPage=i+1; } } return bestPage; }
|
||||
function updateGlobalProgressForComic(){ let c=document.getElementById('globalProgressBar'); if(!c) return; let s=document.getElementById('globalProgressSlider'), l=c.querySelector('.progress-label span'), t=c.querySelector('.progress-info > div:last-child'); if(s&&TOTAL_PAGES>0){ let p=getCurrentPage(); s.value=p; if(l) l.innerText=`📖 第 ${p} / ${TOTAL_PAGES} 页`; if(t) t.innerText=`第 ${p} 页 / 共 ${TOTAL_PAGES} 页`; } }
|
||||
window.updateGlobalProgress = updateGlobalProgressForComic;
|
||||
function saveComicProgress(page){ let list=getBookmarks(), idx=list.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER), bookmark={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:page,time:Date.now(),type:'comic'}; if(idx>=0) list[idx]=bookmark; else list.push(bookmark); saveBookmarks(list); }
|
||||
function addBookmark(){ let p=getCurrentPage(); saveComicProgress(p); showToast('✅ 第 '+p+' 页'); }
|
||||
function jumpToPage(p){ p=Math.min(Math.max(1,p),TOTAL_PAGES); let t=document.querySelector(`.canvas-container[data-page="${p}"]`); if(t){ t.scrollIntoView({behavior:'smooth',block:'start'}); showToast('✨ 第 '+p+' 页'); saveComicProgress(p); setTimeout(()=>{ if(typeof updateGlobalProgress==='function') updateGlobalProgress(); },300); }else{ showToast('📖 页面加载中...'); } }
|
||||
window.jumpToBookmark = function(book,chapter,page){ if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER) jumpToPage(page); else{ sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'comic'})); location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter); } };
|
||||
document.getElementById('backBtn').onclick=()=>{ if(document.referrer&&document.referrer.includes(window.location.host))history.back(); else location.href=BASE_FILE; };
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark; document.getElementById('fontControls').style.display='none'; refreshBookmarkList();
|
||||
function createComicProgressBar(){ let c=document.getElementById('globalProgressPlaceholder'); if(!c) return; c.innerHTML=`<div class="global-progress-container" id="globalProgressBar"><div class="progress-range-area"><input type="range" class="progress-slider-global" id="globalProgressSlider" min="1" max="${TOTAL_PAGES}" value="1" step="1"><div class="chapter-tooltip" id="chapterTooltip" style="display: none;">📖 第 1 / ${TOTAL_PAGES} 页</div></div><div class="progress-info"><div class="progress-label"><span>📖 第 1 / ${TOTAL_PAGES} 页</span></div><div>第 1 页 / 共 ${TOTAL_PAGES} 页</div></div></div>`; globalProgressBar=document.getElementById('globalProgressBar'); let s=document.getElementById('globalProgressSlider'), tip=document.getElementById('chapterTooltip'), tipTimer=null; function st(page){ if(tipTimer) clearTimeout(tipTimer); tip.textContent=`📖 第 ${page} / ${TOTAL_PAGES} 页`; tip.style.display='block'; tipTimer=setTimeout(()=>{ tip.style.display='none'; },2000); } if(s){ s.addEventListener('input',(e)=>{ let p=parseInt(e.target.value), l=c.querySelector('.progress-label span'), t=c.querySelector('.progress-info > div:last-child'); if(l) l.innerText=`📖 第 ${p} / ${TOTAL_PAGES} 页`; if(t) t.innerText=`第 ${p} 页 / 共 ${TOTAL_PAGES} 页`; st(p); }); s.addEventListener('change',(e)=>{ jumpToPage(parseInt(e.target.value)); resetHideTimer(); }); } if(globalProgressBar) globalProgressBar.classList.add('hide'); let stt=null; window.addEventListener('scroll',function(){ if(stt) clearTimeout(stt); stt=setTimeout(()=>{ if(typeof updateGlobalProgress==='function') updateGlobalProgress(); let p=getCurrentPage(); saveComicProgress(p); },200); }); }
|
||||
createComicProgressBar(); let jumpTarget=sessionStorage.getItem('jump_target'); if(jumpTarget){ sessionStorage.removeItem('jump_target'); try{ let t=JSON.parse(jumpTarget); if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page) setTimeout(()=>jumpToPage(t.page),500); }catch(e){} } else { let bookmarks=getBookmarks(); let lastProgress=bookmarks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER); if(lastProgress&&lastProgress.page) setTimeout(()=>jumpToPage(lastProgress.page),500); }
|
||||
setTimeout(()=>{ if(typeof updateGlobalProgress==='function') updateGlobalProgress(); },500); updateAllThemeStyles();
|
||||
</script>
|
||||
<?php else: ?>
|
||||
<div id="epubContent"></div><div class="ebook-nav"><button id="prevChapterBtn" disabled>◀ 上一章</button><button id="nextChapterBtn" disabled>下一章 ▶</button></div><div class="chapter-indicator" id="chapterIndicator"></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>"; CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>"; const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>"; const BASE_FILE = "<?php echo $currentFile; ?>"; const EPUB_DATA = <?php echo json_encode($epubData); ?>;
|
||||
let curIdx=0, chapters=EPUB_DATA.htmlContents||[], css=EPUB_DATA.cssContent||'', fontSize=18;
|
||||
globalChapters = chapters; globalTotalChapters = chapters.length; globalCurrentIndex = 0;
|
||||
function applyStyles(){ let s=document.getElementById('epub-style'); if(!s){ s=document.createElement('style'); s.id='epub-style'; document.head.appendChild(s); } s.textContent=`.ebook-chapter{font-size:${fontSize}px}.ebook-chapter img{max-width:100%;height:auto;display:block;margin:1em auto;border-radius:12px}.ebook-chapter p{margin-bottom:1em}${css}`; }
|
||||
function fixImagesInChapter(){ document.querySelectorAll('.ebook-chapter img').forEach(img=>{ img.onerror=function(){ if(!this.dataset.retried){ this.dataset.retried='true'; setTimeout(()=>{ this.src=this.src.split('?')[0]+'?t='+Date.now(); },500); }else{ this.outerHTML='<div class="img-placeholder" style="text-align:center; padding:40px; background:rgba(0,0,0,0.3); border-radius:8px; margin:1em 0;">📷 图片加载失败</div>'; } }; if(img.complete && img.naturalWidth===0) img.onerror(); }); }
|
||||
function renderChapter(i){ if(!chapters||i<0||i>=chapters.length) return; onChapterChange(); curIdx=i; globalCurrentIndex=i; let c=chapters[i]; document.getElementById('epubContent').innerHTML=`<div class="ebook-chapter"><div class="chapter-title">${escapeHtml(c.title)}</div>${c.content}</div>`; document.getElementById('prevChapterBtn').disabled=(i<=0); document.getElementById('nextChapterBtn').disabled=(i>=chapters.length-1); document.getElementById('chapterIndicator').innerText=`第 ${i+1}/${chapters.length} 章 · ${c.title}`; saveProgress(i); if(typeof updateGlobalProgress==='function') updateGlobalProgress(); fixImagesInChapter(); }
|
||||
function saveProgress(i){ let l=getBookmarks(), idx=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER), n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:i+1,time:Date.now(),type:'ebook'}; if(idx>=0) l[idx]=n; else l.push(n); saveBookmarks(l); }
|
||||
window.jumpToBookmark = function(book,chapter,page){ if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){ renderChapter(Math.min(Math.max(1,page),chapters.length)-1); showToast('✨ 第 '+page+' 章'); }else{ sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'ebook'})); location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter); } };
|
||||
function addBookmark(){ let n=curIdx+1, l=getBookmarks(), i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER), ni={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:n,time:Date.now(),type:'ebook'}; if(i>=0) l[i]=ni; else l.push(ni); saveBookmarks(l); showToast('✅ 第 '+n+' 章'); }
|
||||
document.getElementById('fontPlus').onclick=()=>{ fontSize=Math.min(fontSize+2,32); applyStyles(); renderChapter(curIdx); showToast(`字体 ${fontSize}px`); resetHideTimer(); };
|
||||
document.getElementById('fontMinus').onclick=()=>{ fontSize=Math.max(fontSize-2,12); applyStyles(); renderChapter(curIdx); showToast(`字体 ${fontSize}px`); resetHideTimer(); };
|
||||
document.getElementById('backBtn').onclick=()=>{ if(document.referrer&&document.referrer.includes(window.location.host))history.back(); else location.href=BASE_FILE; };
|
||||
document.getElementById('prevChapterBtn').onclick=()=>{ if(curIdx>0) renderChapter(curIdx-1); resetHideTimer(); };
|
||||
document.getElementById('nextChapterBtn').onclick=()=>{ if(curIdx<chapters.length-1) renderChapter(curIdx+1); resetHideTimer(); };
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
applyStyles(); if(chapters.length>0){ let saved=0, jump=sessionStorage.getItem('jump_target'); if(jump){ sessionStorage.removeItem('jump_target'); try{ let t=JSON.parse(jump); if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page) saved=Math.min(Math.max(1,t.page),chapters.length)-1; }catch(e){} }else{ let bks=getBookmarks(), ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER); if(ex&&ex.page) saved=Math.min(Math.max(1,ex.page),chapters.length)-1; } renderChapter(saved); }
|
||||
refreshBookmarkList(); createGlobalProgressBar(globalTotalChapters, globalCurrentIndex, globalChapters); updateAllThemeStyles();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif ($book): ?>
|
||||
<div class="content">
|
||||
<div class="top-bar" style="position:relative; margin-top:-70px; margin-bottom:20px;">
|
||||
<div class="top-bar-left">
|
||||
<button class="back-btn" id="backBtn">←</button>
|
||||
<div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="page-title">📖 <?php echo htmlspecialchars($book); ?></h2>
|
||||
<div class="shelf-grid">
|
||||
<?php
|
||||
$files = scanDirectory($baseDir . '/' . $book);
|
||||
if ($files) {
|
||||
foreach ($files as $f) {
|
||||
$name = basename($f);
|
||||
$url = $currentFile . "?book=" . rawurlencode($book) . "&chapter=" . rawurlencode($name);
|
||||
if (stripos($name, '.txt') !== false) $icon = '📖';
|
||||
elseif (stripos($name, '.epub') !== false) $icon = '📘';
|
||||
else $icon = (stripos($name, '.pdf') !== false ? '📕' : '📁');
|
||||
echo '<a href="' . $url . '" class="shelf-item"><div class="emoji">' . $icon . '</div><div>' . htmlspecialchars($name) . '</div></a>';
|
||||
}
|
||||
} else {
|
||||
echo '<div style="grid-column:1/-1; text-align:center; padding:50px; color:rgba(255,255,255,0.6);">📭 没有章节</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
refreshBookmarkList();
|
||||
document.getElementById('backBtn').onclick=()=>{ if(document.referrer?.includes(window.location.host))history.back(); else location.href='<?php echo $currentFile; ?>'; };
|
||||
updateAllThemeStyles();
|
||||
</script>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="content"><h1 class="page-title">📚 我的书架</h1><div class="shelf-grid"><?php $books = scanDirectory($baseDir); if ($books) { foreach ($books as $b) { if (is_dir($b)) { $name = basename($b); echo '<a href="' . $currentFile . '?book=' . rawurlencode($name) . '" class="shelf-item"><div class="emoji">📖</div><div>' . htmlspecialchars($name) . '</div></a>'; } } } else { echo '<div style="grid-column:1/-1; text-align:center; padding:50px; color:rgba(255,255,255,0.6);">📭 请在 PDF 文件夹里放入书籍文件夹</div>'; } ?></div></div>
|
||||
<script>refreshBookmarkList(); updateAllThemeStyles();</script>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2038 @@
|
||||
<?php
|
||||
// PDF阅读器.php - 全屏沉浸版:双击唤出工具栏 + 18主题 + 3D翻书动画 + 底部全局进度条
|
||||
header("Content-Type: text/html; charset=utf-8");
|
||||
$baseDir = 'PDF';
|
||||
|
||||
if (!is_dir($baseDir)) {
|
||||
mkdir($baseDir);
|
||||
echo "已自动创建 PDF 目录,请放入PDF/EPUB/TXT";
|
||||
exit;
|
||||
}
|
||||
|
||||
function scanDirectory($path) {
|
||||
$result = [];
|
||||
if (!is_dir($path)) return $result;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
if ($entry == '.epub_cache' || $entry == '.txt_cache') {
|
||||
continue;
|
||||
}
|
||||
if (strpos($entry, '.') === 0) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $path . '/' . $entry;
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
natsort($result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
function scanImages($path) {
|
||||
$images = [];
|
||||
$extensions = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
|
||||
if (!is_dir($path)) return $images;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
$ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $extensions)) {
|
||||
$images[] = $path . '/' . $entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
natsort($images);
|
||||
return $images;
|
||||
}
|
||||
|
||||
function parseTxtFile($txtPath, $book, $chapter, $baseDir) {
|
||||
$cacheDir = $baseDir . '/.txt_cache/' . $book . '/' . md5($chapter);
|
||||
$cacheFile = $cacheDir . '/chapters.json';
|
||||
|
||||
if (file_exists($cacheFile)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
if ($data && isset($data['chapters'])) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
$content = file_get_contents($txtPath);
|
||||
$encoding = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'BIG5'], true);
|
||||
if (!$encoding) $encoding = 'UTF-8';
|
||||
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
|
||||
|
||||
$patterns = [
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+章[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+节[\s]*[^\n]*/u',
|
||||
'/第[零〇一二三四五六七八九十百千万\d]+卷[\s]*[^\n]*/u',
|
||||
'/(?:Chapter|CHAPTER|Ch\.?)\s*\d+[.:\s]*[^\n]*/i',
|
||||
'/\[\d+\][\s]*[^\n]*/',
|
||||
'/(?:一|二|三|四|五|六|七|八|九|十)、[\s]*[^\n]*/u',
|
||||
];
|
||||
|
||||
$lines = preg_split('/\r\n|\r|\n/', $content);
|
||||
$chapters = [];
|
||||
$currentChapter = ['title' => '序章', 'content' => ''];
|
||||
$foundFirstChapter = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = rtrim($line);
|
||||
$isChapter = false;
|
||||
$chapterTitle = '';
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $line, $matches)) {
|
||||
$chapterTitle = trim($matches[0]);
|
||||
$isChapter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isChapter && $chapterTitle) {
|
||||
if ($foundFirstChapter && $currentChapter['content'] !== '') {
|
||||
$chapters[] = $currentChapter;
|
||||
}
|
||||
$currentChapter = ['title' => $chapterTitle, 'content' => ''];
|
||||
$foundFirstChapter = true;
|
||||
} else {
|
||||
if ($line !== '' || $currentChapter['content'] !== '') {
|
||||
$currentChapter['content'] .= $line . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($currentChapter['content'] !== '') {
|
||||
$chapters[] = $currentChapter;
|
||||
}
|
||||
|
||||
if (empty($chapters)) {
|
||||
$chapters = [['title' => basename($chapter, '.txt'), 'content' => $content]];
|
||||
}
|
||||
|
||||
foreach ($chapters as &$chap) {
|
||||
$chap['content'] = preg_replace('/\n\s*\n/', "</p><p>", $chap['content']);
|
||||
$chap['content'] = "<p>" . str_replace("\n", "<br>", $chap['content']) . "</p>";
|
||||
$chap['content'] = preg_replace('/<p>\s*<\/p>/', '', $chap['content']);
|
||||
}
|
||||
|
||||
if (!is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0777, true);
|
||||
}
|
||||
|
||||
$result = ['type' => 'txt', 'chapters' => $chapters, 'totalChapters' => count($chapters)];
|
||||
file_put_contents($cacheFile, json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
return $result;
|
||||
}
|
||||
|
||||
function parseEpub($epubFilePath, $book, $chapter, $baseDir) {
|
||||
$cacheDir = $baseDir . '/.epub_cache/' . $book . '/' . md5($chapter);
|
||||
$cacheTypeFile = $cacheDir . '/type.json';
|
||||
|
||||
if (!class_exists('ZipArchive')) {
|
||||
return ['error' => '请启用ZipArchive扩展'];
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($epubFilePath) !== true) {
|
||||
return ['error' => '无法打开EPUB文件'];
|
||||
}
|
||||
|
||||
$container = $zip->getFromName('META-INF/container.xml');
|
||||
if (!$container) {
|
||||
$zip->close();
|
||||
return ['error' => '无效的EPUB文件'];
|
||||
}
|
||||
|
||||
$rootFile = '';
|
||||
if (preg_match('/full-path="([^"]+)"/', $container, $matches)) {
|
||||
$rootFile = $matches[1];
|
||||
}
|
||||
|
||||
if (!$rootFile) {
|
||||
$zip->close();
|
||||
return ['error' => '无法解析EPUB结构'];
|
||||
}
|
||||
|
||||
$opfContent = $zip->getFromName($rootFile);
|
||||
if (!$opfContent) {
|
||||
$zip->close();
|
||||
return ['error' => '无法解析OPF文件'];
|
||||
}
|
||||
|
||||
$opfDir = dirname($rootFile);
|
||||
if ($opfDir == '.') $opfDir = '';
|
||||
else $opfDir .= '/';
|
||||
|
||||
$imagePaths = [];
|
||||
preg_match_all('/<item[^>]*href="([^"]+)"[^>]*media-type="image\/[^"]+"/i', $opfContent, $matches1);
|
||||
if (!empty($matches1[1])) {
|
||||
foreach ($matches1[1] as $imgPath) {
|
||||
$imagePaths[] = $opfDir . $imgPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($imagePaths)) {
|
||||
preg_match_all('/<item[^>]*href="([^"]+\.(jpg|jpeg|png|webp|gif))"[^>]*>/i', $opfContent, $matches2);
|
||||
foreach ($matches2[1] as $imgPath) {
|
||||
$imagePaths[] = $opfDir . $imgPath;
|
||||
}
|
||||
}
|
||||
|
||||
$spineItems = [];
|
||||
preg_match_all('/<itemref[^>]*idref="([^"]+)"/i', $opfContent, $spineMatches);
|
||||
if (!empty($spineMatches[1])) {
|
||||
foreach ($spineMatches[1] as $idref) {
|
||||
preg_match('/<item[^>]*id="' . preg_quote($idref) . '"[^>]*href="([^"]+)"/i', $opfContent, $itemMatch);
|
||||
if (!empty($itemMatch[1])) {
|
||||
$spineItems[] = $opfDir . $itemMatch[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$htmlItems = [];
|
||||
preg_match_all('/<item[^>]*href="([^"]+)"[^>]*media-type="application\/xhtml\+xml"[^>]*>/i', $opfContent, $matchesHtml);
|
||||
foreach ($matchesHtml[1] as $htmlPath) {
|
||||
$htmlItems[] = $opfDir . $htmlPath;
|
||||
}
|
||||
|
||||
$totalImages = count($imagePaths);
|
||||
$totalHtml = count($htmlItems) ?: count($spineItems);
|
||||
$isComic = ($totalImages > 0 && $totalHtml == 0) || ($totalImages > 30 && $totalImages / max($totalHtml, 1) > 5);
|
||||
|
||||
if (!is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0777, true);
|
||||
}
|
||||
|
||||
$result = ['type' => $isComic ? 'comic' : 'ebook'];
|
||||
|
||||
if ($isComic) {
|
||||
$cachedImages = [];
|
||||
$orderedImages = $imagePaths;
|
||||
$orderedImages = array_values(array_unique($orderedImages));
|
||||
|
||||
foreach ($orderedImages as $idx => $relativePath) {
|
||||
$ext = strtolower(pathinfo($relativePath, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif'])) $ext = 'jpg';
|
||||
$cacheFile = $cacheDir . '/' . sprintf('%03d', $idx+1) . '.' . $ext;
|
||||
$imageData = $zip->getFromName($relativePath);
|
||||
if ($imageData !== false) {
|
||||
file_put_contents($cacheFile, $imageData);
|
||||
$cachedImages[] = $cacheFile;
|
||||
} else {
|
||||
$decodedPath = urldecode($relativePath);
|
||||
$imageData = $zip->getFromName($decodedPath);
|
||||
if ($imageData !== false) {
|
||||
file_put_contents($cacheFile, $imageData);
|
||||
$cachedImages[] = $cacheFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result['images'] = $cachedImages;
|
||||
$result['totalPages'] = count($cachedImages);
|
||||
} else {
|
||||
$imagesDir = $cacheDir . '/images/';
|
||||
if (!is_dir($imagesDir)) mkdir($imagesDir, 0777, true);
|
||||
|
||||
$imageUrlMap = [];
|
||||
foreach ($imagePaths as $relativePath) {
|
||||
$originalFilename = basename($relativePath);
|
||||
$ext = strtolower(pathinfo($originalFilename, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif'])) $ext = 'jpg';
|
||||
|
||||
$cacheFilename = md5($relativePath) . '_' . preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $originalFilename);
|
||||
$cacheFile = $imagesDir . $cacheFilename;
|
||||
|
||||
$imageData = $zip->getFromName($relativePath);
|
||||
if ($imageData !== false) {
|
||||
file_put_contents($cacheFile, $imageData);
|
||||
$imageUrlMap[$originalFilename] = $cacheFile;
|
||||
$nameNoExt = pathinfo($originalFilename, PATHINFO_FILENAME);
|
||||
$imageUrlMap[$nameNoExt] = $cacheFile;
|
||||
}
|
||||
}
|
||||
|
||||
$cssContent = '';
|
||||
preg_match_all('/<item[^>]*href="([^"]+\.css)"[^>]*media-type="text\/css"[^>]*>/i', $opfContent, $cssMatches);
|
||||
foreach ($cssMatches[1] as $cssPath) {
|
||||
$fullPath = $opfDir . $cssPath;
|
||||
$cssData = $zip->getFromName($fullPath);
|
||||
if ($cssData !== false) {
|
||||
$cssContent .= $cssData . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$htmlContents = [];
|
||||
$itemsToProcess = !empty($htmlItems) ? $htmlItems : $spineItems;
|
||||
|
||||
foreach ($itemsToProcess as $idx => $htmlPath) {
|
||||
$htmlData = $zip->getFromName($htmlPath);
|
||||
if ($htmlData !== false) {
|
||||
$htmlData = preg_replace_callback('/src=["\']([^"\']+)["\']/i', function($matches) use ($imageUrlMap) {
|
||||
$src = $matches[1];
|
||||
if (strpos($src, 'http://') === 0 || strpos($src, 'https://') === 0 || strpos($src, 'data:') === 0) {
|
||||
return $matches[0];
|
||||
}
|
||||
if (strpos($src, '.epub_cache/') !== false) {
|
||||
return $matches[0];
|
||||
}
|
||||
$filename = basename(urldecode($src));
|
||||
if (isset($imageUrlMap[$filename])) {
|
||||
return 'src="' . $imageUrlMap[$filename] . '"';
|
||||
}
|
||||
$name = pathinfo($filename, PATHINFO_FILENAME);
|
||||
if (isset($imageUrlMap[$name])) {
|
||||
return 'src="' . $imageUrlMap[$name] . '"';
|
||||
}
|
||||
return $matches[0];
|
||||
}, $htmlData);
|
||||
|
||||
$htmlData = preg_replace_callback('/url\([\'"]?([^\'"\)]+)[\'"]?\)/i', function($matches) use ($imageUrlMap) {
|
||||
$url = $matches[1];
|
||||
$filename = basename(urldecode($url));
|
||||
if (isset($imageUrlMap[$filename])) {
|
||||
return 'url("' . $imageUrlMap[$filename] . '")';
|
||||
}
|
||||
return $matches[0];
|
||||
}, $htmlData);
|
||||
|
||||
$title = '';
|
||||
if (preg_match('/<title[^>]*>([^<]+)<\/title>/i', $htmlData, $titleMatch)) {
|
||||
$title = trim($titleMatch[1]);
|
||||
}
|
||||
if (!$title) {
|
||||
if (preg_match('/<h1[^>]*>([^<]+)<\/h1>/i', $htmlData, $h1Match)) {
|
||||
$title = trim($h1Match[1]);
|
||||
} else {
|
||||
$title = '第 ' . ($idx + 1) . ' 章';
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/<body[^>]*>([\s\S]*?)<\/body>/i', $htmlData, $bodyMatch)) {
|
||||
$htmlData = $bodyMatch[1];
|
||||
}
|
||||
|
||||
$htmlContents[] = [
|
||||
'title' => $title,
|
||||
'content' => $htmlData,
|
||||
'index' => $idx
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$result['htmlContents'] = $htmlContents;
|
||||
$result['cssContent'] = $cssContent;
|
||||
$result['totalChapters'] = count($htmlContents);
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
file_put_contents($cacheTypeFile, json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
return $result;
|
||||
}
|
||||
|
||||
$book = isset($_GET['book']) ? $_GET['book'] : '';
|
||||
$chapter = isset($_GET['chapter']) ? $_GET['chapter'] : '';
|
||||
$isChapterPage = ($book && $chapter);
|
||||
$isPdf = $chapter && (stripos($chapter, '.pdf') !== false);
|
||||
$isEpub = $chapter && (stripos($chapter, '.epub') !== false);
|
||||
$isTxt = $chapter && (stripos($chapter, '.txt') !== false);
|
||||
|
||||
if ($book && $chapter) {
|
||||
$encodedBook = rawurlencode($book);
|
||||
$encodedChapter = rawurlencode($chapter);
|
||||
$fileUrl = "$baseDir/$encodedBook/$encodedChapter";
|
||||
}
|
||||
|
||||
$images = [];
|
||||
$epubData = null;
|
||||
$txtData = null;
|
||||
$epubError = null;
|
||||
|
||||
if ($isTxt && $isChapterPage && $book && $chapter) {
|
||||
$txtPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
if (file_exists($txtPath)) {
|
||||
$txtData = parseTxtFile($txtPath, $book, $chapter, $baseDir);
|
||||
} else {
|
||||
$epubError = 'TXT文件不存在';
|
||||
}
|
||||
} elseif ($isEpub && $isChapterPage && $book && $chapter) {
|
||||
$epubPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
if (file_exists($epubPath)) {
|
||||
$result = parseEpub($epubPath, $book, $chapter, $baseDir);
|
||||
if (isset($result['error'])) {
|
||||
$epubError = $result['error'];
|
||||
} else {
|
||||
$epubData = $result;
|
||||
if ($epubData['type'] == 'comic') {
|
||||
$images = $epubData['images'];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$epubError = 'EPUB文件不存在';
|
||||
}
|
||||
} elseif (!$isPdf && $isChapterPage && $book && $chapter) {
|
||||
$localPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
$images = scanImages($localPath);
|
||||
}
|
||||
|
||||
$currentFile = 'PDF阅读器.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
|
||||
<script>pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';</script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; min-height: 100vh; transition: all 0.3s ease; }
|
||||
|
||||
.floating-buttons, .speed-panel, .theme-selector, .font-controls {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.floating-buttons.visible, .speed-panel.visible, .theme-selector.visible, .font-controls.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 底部全局进度条样式 */
|
||||
.global-progress-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1003;
|
||||
padding: 8px 16px 16px 16px;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
transition: transform 0.3s ease, background 0.3s ease;
|
||||
transform: translateY(0);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
.global-progress-container.hide {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
.progress-range-area {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.progress-slider-global {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.progress-slider-global::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #ff9800;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 8px rgba(255,152,0,0.8);
|
||||
border: 2px solid #fff;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
.progress-slider-global::-webkit-slider-thumb:hover { transform: scale(1.2); }
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
padding: 4px 0 2px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
}
|
||||
|
||||
/* 章节提示框 - 固定在右侧,bottom:280px,right:30px */
|
||||
.chapter-tooltip {
|
||||
position: fixed;
|
||||
background: rgba(0,0,0,0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
color: #ff9800;
|
||||
padding: 10px 20px;
|
||||
border-radius: 40px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
|
||||
z-index: 10007;
|
||||
border: 1px solid rgba(255,152,0,0.6);
|
||||
transition: all 0.1s ease;
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.5px;
|
||||
bottom: 280px;
|
||||
right: 12px;
|
||||
transform: translateX(0);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* 全屏3D翻页动画样式 */
|
||||
.page-turn-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeInOutFull 0.5s ease-out forwards;
|
||||
perspective: 2000px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.page-turn-overlay .book-container {
|
||||
position: relative;
|
||||
width: 70%;
|
||||
max-width: 500px;
|
||||
height: 70%;
|
||||
max-height: 500px;
|
||||
transform-style: preserve-3d;
|
||||
animation: bookFlipFull 0.5s ease-in-out forwards;
|
||||
}
|
||||
.page-turn-overlay .book-left, .page-turn-overlay .book-right {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 80px;
|
||||
box-shadow: 0 0 40px rgba(0,0,0,0.4);
|
||||
}
|
||||
.page-turn-overlay .book-left {
|
||||
left: 0;
|
||||
transform-origin: right center;
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
.page-turn-overlay .book-right {
|
||||
right: 0;
|
||||
transform-origin: left center;
|
||||
border-radius: 0 16px 16px 0;
|
||||
}
|
||||
.page-turn-overlay .message {
|
||||
position: absolute;
|
||||
bottom: 20%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 28px;
|
||||
border-radius: 50px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
backdrop-filter: blur(8px);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
@keyframes fadeInOutFull {
|
||||
0% { opacity: 0; backdrop-filter: blur(0px); }
|
||||
15% { opacity: 1; backdrop-filter: blur(4px); }
|
||||
85% { opacity: 1; backdrop-filter: blur(4px); }
|
||||
100% { opacity: 0; backdrop-filter: blur(0px); visibility: hidden; }
|
||||
}
|
||||
@keyframes bookFlipFull {
|
||||
0% { transform: scale(0.9) rotateY(0deg); opacity: 0.5; }
|
||||
30% { transform: scale(1.05) rotateY(-15deg); opacity: 1; }
|
||||
70% { transform: scale(1.05) rotateY(-5deg); opacity: 1; }
|
||||
100% { transform: scale(1) rotateY(0deg); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 滚动速度调节面板 - 右边缘60px */
|
||||
.speed-panel {
|
||||
position: fixed;
|
||||
right: 80px;
|
||||
bottom: 105px;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 12px 16px;
|
||||
border-radius: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
z-index: 10001;
|
||||
min-width: 170px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.speed-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
gap: 12px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.speed-value {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.speed-slider {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
.speed-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #ff9800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.speed-presets {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.speed-preset {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
.speed-preset.active { color: #ff9800; background: rgba(255,152,0,0.2); }
|
||||
.auto-chapter-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
.auto-chapter-line input { width: 36px; height: 20px; cursor: pointer; accent-color: #ff9800; }
|
||||
|
||||
/* 字体控制 - 右侧12px */
|
||||
.font-controls {
|
||||
position: fixed; right: 12px; bottom: 230px; backdrop-filter: blur(10px); padding: 8px 12px; border-radius: 30px; display: flex; gap: 12px; z-index: 10001; transition: background 0.3s ease;
|
||||
}
|
||||
.font-controls button { background: none; border: none; font-size: 18px; padding: 4px 8px; cursor: pointer; transition: color 0.3s ease; }
|
||||
|
||||
/* 主题选择器 - 右侧12px */
|
||||
.theme-selector {
|
||||
position: fixed; right: 12px; bottom: 250px; backdrop-filter: blur(12px); padding: 12px; border-radius: 20px; display: flex; flex-wrap: wrap; gap: 8px; z-index: 10001; max-width: 280px; width: max-content; transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
/* 浮动按钮 - 右侧12px */
|
||||
.floating-buttons { position: fixed; right: 12px; bottom: 100px; display: flex; flex-direction: column; gap: 12px; z-index: 10000; }
|
||||
.floating-btn { width: 52px; height: 52px; backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; transition: all 0.2s; transition: background 0.3s ease; }
|
||||
.floating-btn.bookmark-btn { background: linear-gradient(135deg, #ff9800, #ff5722); }
|
||||
.floating-btn.active { background: #ff9800; }
|
||||
|
||||
/* 书签面板 - 右侧12px */
|
||||
.bookmark-panel {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 220px;
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
width: 320px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
z-index: 10002;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.bookmark-panel.show { display: block; }
|
||||
.bookmark-header { padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,0.15); font-weight: 600; display: flex; justify-content: space-between; }
|
||||
.bookmark-header span:last-child { cursor: pointer; font-size: 22px; }
|
||||
.bookmark-list { padding: 10px; }
|
||||
.bookmark-item { background: rgba(255,255,255,0.1); margin: 8px 0; padding: 12px; border-radius: 14px; cursor: pointer; }
|
||||
.bookmark-item:hover { background: rgba(255,255,255,0.2); }
|
||||
.bookmark-item .title { font-weight: 600; color: #ffb347; font-size: 14px; }
|
||||
.bookmark-item .info { font-size: 11px; color: rgba(255,255,255,0.6); margin-top: 5px; }
|
||||
.bookmark-item .delete { float: right; color: #ff6b6b; font-size: 16px; cursor: pointer; }
|
||||
.empty-bookmark { color: rgba(255,255,255,0.5); text-align: center; padding: 30px; font-size: 13px; }
|
||||
|
||||
.theme-dot { width: 36px; height: 36px; border-radius: 12px; cursor: pointer; border: 2px solid rgba(255,255,255,0.5); transition: all 0.1s; box-sizing: border-box; }
|
||||
.theme-dot.active { border-color: #ff9800; transform: scale(1.05); box-shadow: 0 0 8px rgba(255,152,0,0.5); }
|
||||
|
||||
.top-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.top-bar-left { display: flex; align-items: center; gap: 12px; flex: 1; overflow: hidden; }
|
||||
.back-btn { width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.15); border: none; }
|
||||
.top-bar .nav-links { font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.top-bar a { text-decoration: none; }
|
||||
.top-bar button { padding: 8px 18px; border-radius: 30px; cursor: pointer; font-size: 14px; margin-left: 8px; background: rgba(255,255,255,0.15); border: none; }
|
||||
.top-bar button.bookmark { background: rgba(255, 152, 0, 0.8); color: white; }
|
||||
.content { margin-top: 70px; padding: 16px; position: relative; z-index: 1; margin-bottom: 70px; }
|
||||
.ebook-chapter { border-radius: 24px; padding: 30px 24px; margin: 20px auto; max-width: 800px; transition: all 0.2s ease; }
|
||||
.ebook-chapter p { margin-bottom: 1em; line-height: 1.8; }
|
||||
.ebook-chapter .chapter-title { font-size: 1.8em; text-align: center; margin-bottom: 1em; padding-bottom: 0.3em; }
|
||||
.ebook-nav { display: flex; justify-content: space-between; gap: 12px; margin: 20px auto; max-width: 800px; }
|
||||
.ebook-nav button { border: none; padding: 12px 24px; border-radius: 40px; cursor: pointer; font-size: 16px; flex: 1; background: linear-gradient(135deg, #667eea, #764ba2); color: white; }
|
||||
.ebook-nav button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.chapter-indicator { text-align: center; margin: 10px auto; font-size: 14px; }
|
||||
.toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); backdrop-filter: blur(20px); color: white; padding: 10px 20px; border-radius: 50px; font-size: 14px; z-index: 2000; pointer-events: none; white-space: nowrap; }
|
||||
.shelf-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 16px; padding: 16px; }
|
||||
.shelf-item { background: rgba(255,255,255,0.12); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2); padding: 24px 12px; text-align: center; text-decoration: none; color: white; border-radius: 24px; transition: all 0.2s; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
||||
.shelf-item:hover { transform: translateY(-3px); background: rgba(255,255,255,0.2); }
|
||||
.shelf-item .emoji { font-size: 48px; display: block; margin-bottom: 10px; }
|
||||
.page-title { font-size: 26px; font-weight: 600; color: white; padding: 16px; margin: 0; text-shadow: 1px 1px 2px rgba(0,0,0,0.3); }
|
||||
.progress-bar { position: fixed; top: 60px; left: 0; width: 100%; height: 2px; background: rgba(255,255,255,0.2); z-index: 1002; }
|
||||
.progress-fill { width: 0%; height: 100%; background: #ff9800; transition: width 0.3s; }
|
||||
|
||||
/* ==================== 18个主题样式 ==================== */
|
||||
|
||||
/* 1. 深邃星空 */
|
||||
body.theme-deep-space { background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); }
|
||||
body.theme-deep-space .top-bar { background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(20px); border-bottom-color: rgba(255,255,255,0.1); }
|
||||
body.theme-deep-space .top-bar, body.theme-deep-space .top-bar a, body.theme-deep-space .top-bar button { color: #fff; }
|
||||
body.theme-deep-space .ebook-chapter { background: rgba(30, 30, 50, 0.95); color: #e0e0e0; box-shadow: 0 8px 32px rgba(0,0,0,0.3); }
|
||||
body.theme-deep-space .ebook-chapter .chapter-title { color: #9b59b6; border-bottom-color: #9b59b6; }
|
||||
body.theme-deep-space .chapter-indicator { color: rgba(255,255,255,0.7); }
|
||||
body.theme-deep-space .speed-panel, body.theme-deep-space .font-controls, body.theme-deep-space .theme-selector, body.theme-deep-space .bookmark-panel { background: rgba(15, 12, 41, 0.95); color: #e0e0e0; }
|
||||
body.theme-deep-space .floating-btn { background: rgba(15, 12, 41, 0.9); color: #fff; }
|
||||
body.theme-deep-space .global-progress-container { background: rgba(15, 12, 41, 0.92); border-top-color: rgba(155, 89, 182, 0.4); }
|
||||
|
||||
/* 2. 羊皮纸 */
|
||||
body.theme-parchment { background: linear-gradient(135deg, #e6d5b8 0%, #c9a87b 100%); }
|
||||
body.theme-parchment .top-bar { background: rgba(101, 67, 33, 0.9); backdrop-filter: blur(20px); }
|
||||
body.theme-parchment .top-bar, body.theme-parchment .top-bar a, body.theme-parchment .top-bar button { color: #3e2723; }
|
||||
body.theme-parchment .ebook-chapter { background: rgba(253, 245, 220, 0.98); color: #4a3728; }
|
||||
body.theme-parchment .ebook-chapter .chapter-title { color: #8b4513; border-bottom-color: #8b4513; }
|
||||
body.theme-parchment .chapter-indicator { color: #3e2723; }
|
||||
body.theme-parchment .speed-panel, body.theme-parchment .font-controls, body.theme-parchment .theme-selector, body.theme-parchment .bookmark-panel { background: rgba(230, 213, 184, 0.95); color: #3e2723; }
|
||||
body.theme-parchment .floating-btn { background: rgba(201, 168, 123, 0.9); color: #3e2723; }
|
||||
body.theme-parchment .global-progress-container { background: rgba(230, 213, 184, 0.92); border-top-color: rgba(139, 69, 19, 0.3); }
|
||||
body.theme-parchment .progress-info { color: #3e2723; }
|
||||
body.theme-parchment .progress-slider-global { background: rgba(139, 69, 19, 0.3); }
|
||||
|
||||
/* 3. 深海宁静 */
|
||||
body.theme-ocean { background: linear-gradient(135deg, #1a2980 0%, #26d0ce 100%); }
|
||||
body.theme-ocean .top-bar { background: rgba(0, 40, 60, 0.85); }
|
||||
body.theme-ocean .top-bar, body.theme-ocean .top-bar a, body.theme-ocean .top-bar button { color: #e0f7fa; }
|
||||
body.theme-ocean .ebook-chapter { background: rgba(255, 255, 255, 0.95); color: #2c3e50; }
|
||||
body.theme-ocean .ebook-chapter .chapter-title { color: #1a2980; border-bottom-color: #1a2980; }
|
||||
body.theme-ocean .speed-panel, body.theme-ocean .font-controls, body.theme-ocean .theme-selector, body.theme-ocean .bookmark-panel { background: rgba(26, 41, 128, 0.95); color: #e0f7fa; }
|
||||
body.theme-ocean .floating-btn { background: rgba(38, 208, 206, 0.85); color: #e0f7fa; }
|
||||
body.theme-ocean .global-progress-container { background: rgba(26, 41, 128, 0.92); border-top-color: rgba(38, 208, 206, 0.4); }
|
||||
|
||||
/* 4. 樱花 */
|
||||
body.theme-cherry { background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%); }
|
||||
body.theme-cherry .top-bar { background: rgba(219, 112, 147, 0.85); }
|
||||
body.theme-cherry .top-bar, body.theme-cherry .top-bar a, body.theme-cherry .top-bar button { color: #5a2e3e; }
|
||||
body.theme-cherry .ebook-chapter { background: rgba(255, 245, 245, 0.95); color: #5a3a3a; }
|
||||
body.theme-cherry .ebook-chapter .chapter-title { color: #db7093; border-bottom-color: #db7093; }
|
||||
body.theme-cherry .speed-panel, body.theme-cherry .font-controls, body.theme-cherry .theme-selector, body.theme-cherry .bookmark-panel { background: rgba(255, 245, 245, 0.95); color: #5a2e3e; }
|
||||
body.theme-cherry .floating-btn { background: rgba(219, 112, 147, 0.85); color: #5a2e3e; }
|
||||
body.theme-cherry .global-progress-container { background: rgba(255, 245, 245, 0.92); border-top-color: rgba(219, 112, 147, 0.4); }
|
||||
body.theme-cherry .progress-info { color: #5a2e3e; }
|
||||
|
||||
/* 5. 黑夜模式 */
|
||||
body.theme-night { background: #0a0a0a; }
|
||||
body.theme-night .top-bar { background: rgba(10, 10, 10, 0.95); }
|
||||
body.theme-night .top-bar, body.theme-night .top-bar a, body.theme-night .top-bar button { color: #aaa; }
|
||||
body.theme-night .ebook-chapter { background: #1a1a1a; color: #b0b0b0; border: 1px solid #333; }
|
||||
body.theme-night .ebook-chapter .chapter-title { color: #888; border-bottom-color: #444; }
|
||||
body.theme-night .speed-panel, body.theme-night .font-controls, body.theme-night .theme-selector, body.theme-night .bookmark-panel { background: rgba(10, 10, 10, 0.95); color: #aaa; }
|
||||
body.theme-night .floating-btn { background: rgba(30, 30, 30, 0.95); color: #aaa; }
|
||||
body.theme-night .global-progress-container { background: rgba(10, 10, 10, 0.92); border-top-color: #333; }
|
||||
|
||||
/* 6. 森林绿意 */
|
||||
body.theme-forest { background: linear-gradient(135deg, #134e5e 0%, #71b280 100%); }
|
||||
body.theme-forest .top-bar { background: rgba(20, 60, 40, 0.85); }
|
||||
body.theme-forest .top-bar, body.theme-forest .top-bar a, body.theme-forest .top-bar button { color: #e8f5e9; }
|
||||
body.theme-forest .ebook-chapter { background: rgba(255, 255, 245, 0.95); color: #2d5a3b; }
|
||||
body.theme-forest .ebook-chapter .chapter-title { color: #2e7d32; border-bottom-color: #2e7d32; }
|
||||
body.theme-forest .speed-panel, body.theme-forest .font-controls, body.theme-forest .theme-selector, body.theme-forest .bookmark-panel { background: rgba(19, 78, 94, 0.95); color: #e8f5e9; }
|
||||
body.theme-forest .floating-btn { background: rgba(113, 178, 128, 0.85); color: #e8f5e9; }
|
||||
body.theme-forest .global-progress-container { background: rgba(19, 78, 94, 0.92); border-top-color: rgba(113, 178, 128, 0.4); }
|
||||
|
||||
/* 7. 日落橙 */
|
||||
body.theme-sunset { background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 100%); }
|
||||
body.theme-sunset .top-bar { background: rgba(180, 70, 40, 0.85); }
|
||||
body.theme-sunset .top-bar, body.theme-sunset .top-bar a, body.theme-sunset .top-bar button { color: #fff3e0; }
|
||||
body.theme-sunset .ebook-chapter { background: rgba(255, 248, 240, 0.96); color: #6b3e1f; }
|
||||
body.theme-sunset .ebook-chapter .chapter-title { color: #d84315; border-bottom-color: #d84315; }
|
||||
body.theme-sunset .speed-panel, body.theme-sunset .font-controls, body.theme-sunset .theme-selector, body.theme-sunset .bookmark-panel { background: rgba(255, 126, 95, 0.95); color: #fff3e0; }
|
||||
body.theme-sunset .floating-btn { background: rgba(254, 180, 123, 0.85); color: #fff3e0; }
|
||||
body.theme-sunset .global-progress-container { background: rgba(255, 126, 95, 0.92); }
|
||||
|
||||
/* 8. 薰衣草 */
|
||||
body.theme-lavender { background: linear-gradient(135deg, #8e9ecc 0%, #e0bbff 100%); }
|
||||
body.theme-lavender .top-bar { background: rgba(100, 80, 140, 0.85); }
|
||||
body.theme-lavender .top-bar, body.theme-lavender .top-bar a, body.theme-lavender .top-bar button { color: #f3e5f5; }
|
||||
body.theme-lavender .ebook-chapter { background: rgba(245, 235, 255, 0.96); color: #4a3a6e; }
|
||||
body.theme-lavender .ebook-chapter .chapter-title { color: #7b1fa2; border-bottom-color: #7b1fa2; }
|
||||
body.theme-lavender .speed-panel, body.theme-lavender .font-controls, body.theme-lavender .theme-selector, body.theme-lavender .bookmark-panel { background: rgba(142, 158, 204, 0.95); color: #4a3a6e; }
|
||||
body.theme-lavender .floating-btn { background: rgba(224, 187, 255, 0.85); color: #4a3a6e; }
|
||||
body.theme-lavender .global-progress-container { background: rgba(142, 158, 204, 0.92); }
|
||||
body.theme-lavender .progress-info { color: #4a3a6e; }
|
||||
|
||||
/* 9. 抹茶 */
|
||||
body.theme-matcha { background: linear-gradient(135deg, #a8c0aa 0%, #6b8c5c 100%); }
|
||||
body.theme-matcha .top-bar { background: rgba(70, 100, 60, 0.85); }
|
||||
body.theme-matcha .top-bar, body.theme-matcha .top-bar a, body.theme-matcha .top-bar button { color: #f1f8e9; }
|
||||
body.theme-matcha .ebook-chapter { background: rgba(248, 255, 240, 0.96); color: #3e5a2e; }
|
||||
body.theme-matcha .ebook-chapter .chapter-title { color: #558b2f; border-bottom-color: #558b2f; }
|
||||
body.theme-matcha .speed-panel, body.theme-matcha .font-controls, body.theme-matcha .theme-selector, body.theme-matcha .bookmark-panel { background: rgba(168, 192, 170, 0.95); color: #3e5a2e; }
|
||||
body.theme-matcha .floating-btn { background: rgba(107, 140, 92, 0.85); color: #3e5a2e; }
|
||||
body.theme-matcha .global-progress-container { background: rgba(168, 192, 170, 0.92); }
|
||||
body.theme-matcha .progress-info { color: #3e5a2e; }
|
||||
|
||||
/* 10. 蓝莓 */
|
||||
body.theme-blueberry { background: linear-gradient(135deg, #2c3e66 0%, #4a69bd 100%); }
|
||||
body.theme-blueberry .top-bar { background: rgba(30, 50, 80, 0.85); }
|
||||
body.theme-blueberry .top-bar, body.theme-blueberry .top-bar a, body.theme-blueberry .top-bar button { color: #dfe6e9; }
|
||||
body.theme-blueberry .ebook-chapter { background: rgba(240, 245, 255, 0.96); color: #2c3e66; }
|
||||
body.theme-blueberry .ebook-chapter .chapter-title { color: #3b82f6; border-bottom-color: #3b82f6; }
|
||||
body.theme-blueberry .speed-panel, body.theme-blueberry .font-controls, body.theme-blueberry .theme-selector, body.theme-blueberry .bookmark-panel { background: rgba(44, 62, 102, 0.95); color: #dfe6e9; }
|
||||
body.theme-blueberry .floating-btn { background: rgba(74, 105, 189, 0.85); color: #dfe6e9; }
|
||||
body.theme-blueberry .global-progress-container { background: rgba(44, 62, 102, 0.92); }
|
||||
|
||||
/* 11. 琥珀 */
|
||||
body.theme-amber { background: linear-gradient(135deg, #ffb347 0%, #ffcc33 100%); }
|
||||
body.theme-amber .top-bar { background: rgba(160, 90, 30, 0.85); }
|
||||
body.theme-amber .top-bar, body.theme-amber .top-bar a, body.theme-amber .top-bar button { color: #3e2723; }
|
||||
body.theme-amber .ebook-chapter { background: rgba(255, 250, 230, 0.96); color: #5d4037; }
|
||||
body.theme-amber .ebook-chapter .chapter-title { color: #f57c00; border-bottom-color: #f57c00; }
|
||||
body.theme-amber .speed-panel, body.theme-amber .font-controls, body.theme-amber .theme-selector, body.theme-amber .bookmark-panel { background: rgba(255, 179, 71, 0.95); color: #3e2723; }
|
||||
body.theme-amber .floating-btn { background: rgba(255, 204, 51, 0.85); color: #3e2723; }
|
||||
body.theme-amber .global-progress-container { background: rgba(255, 179, 71, 0.92); }
|
||||
body.theme-amber .progress-info { color: #3e2723; }
|
||||
|
||||
/* 12. 石墨 */
|
||||
body.theme-graphite { background: linear-gradient(135deg, #4a4a4a 0%, #2c2c2c 100%); }
|
||||
body.theme-graphite .top-bar { background: rgba(30, 30, 30, 0.9); }
|
||||
body.theme-graphite .top-bar, body.theme-graphite .top-bar a, body.theme-graphite .top-bar button { color: #ccc; }
|
||||
body.theme-graphite .ebook-chapter { background: rgba(50, 50, 55, 0.96); color: #c0c0c0; border: 1px solid #555; }
|
||||
body.theme-graphite .ebook-chapter .chapter-title { color: #aaa; border-bottom-color: #666; }
|
||||
body.theme-graphite .speed-panel, body.theme-graphite .font-controls, body.theme-graphite .theme-selector, body.theme-graphite .bookmark-panel { background: rgba(74, 74, 74, 0.95); color: #ccc; }
|
||||
body.theme-graphite .floating-btn { background: rgba(44, 44, 44, 0.95); color: #ccc; }
|
||||
body.theme-graphite .global-progress-container { background: rgba(74, 74, 74, 0.92); }
|
||||
|
||||
/* 13. 珊瑚粉 */
|
||||
body.theme-coral { background: linear-gradient(135deg, #ff6b6b 0%, #ffb8b8 100%); }
|
||||
body.theme-coral .top-bar { background: rgba(200, 80, 80, 0.85); }
|
||||
body.theme-coral .top-bar, body.theme-coral .top-bar a, body.theme-coral .top-bar button { color: #fff; }
|
||||
body.theme-coral .ebook-chapter { background: rgba(255, 240, 240, 0.95); color: #5a3a3a; }
|
||||
body.theme-coral .ebook-chapter .chapter-title { color: #ff6b6b; border-bottom-color: #ff6b6b; }
|
||||
body.theme-coral .speed-panel, body.theme-coral .font-controls, body.theme-coral .theme-selector, body.theme-coral .bookmark-panel { background: rgba(200, 80, 80, 0.95); color: #fff; }
|
||||
body.theme-coral .floating-btn { background: rgba(200, 80, 80, 0.9); color: #fff; }
|
||||
body.theme-coral .global-progress-container { background: rgba(200, 80, 80, 0.92); }
|
||||
|
||||
/* 14. 薄荷绿 */
|
||||
body.theme-mint { background: linear-gradient(135deg, #a8e6cf 0%, #80deea 100%); }
|
||||
body.theme-mint .top-bar { background: rgba(60, 120, 100, 0.85); }
|
||||
body.theme-mint .top-bar, body.theme-mint .top-bar a, body.theme-mint .top-bar button { color: #2d5a3b; }
|
||||
body.theme-mint .ebook-chapter { background: rgba(255, 255, 250, 0.95); color: #2d5a3b; }
|
||||
body.theme-mint .ebook-chapter .chapter-title { color: #2ecc71; border-bottom-color: #2ecc71; }
|
||||
body.theme-mint .speed-panel, body.theme-mint .font-controls, body.theme-mint .theme-selector, body.theme-mint .bookmark-panel { background: rgba(60, 120, 100, 0.95); color: #fff; }
|
||||
body.theme-mint .floating-btn { background: rgba(60, 120, 100, 0.9); color: #fff; }
|
||||
body.theme-mint .global-progress-container { background: rgba(60, 120, 100, 0.92); }
|
||||
|
||||
/* 15. 浆果紫 */
|
||||
body.theme-berry { background: linear-gradient(135deg, #6c3483 0%, #a569bd 100%); }
|
||||
body.theme-berry .top-bar { background: rgba(80, 40, 100, 0.85); }
|
||||
body.theme-berry .top-bar, body.theme-berry .top-bar a, body.theme-berry .top-bar button { color: #f3e5f5; }
|
||||
body.theme-berry .ebook-chapter { background: rgba(245, 235, 255, 0.95); color: #4a235a; }
|
||||
body.theme-berry .ebook-chapter .chapter-title { color: #9b59b6; border-bottom-color: #9b59b6; }
|
||||
body.theme-berry .speed-panel, body.theme-berry .font-controls, body.theme-berry .theme-selector, body.theme-berry .bookmark-panel { background: rgba(80, 40, 100, 0.95); color: #f3e5f5; }
|
||||
body.theme-berry .floating-btn { background: rgba(80, 40, 100, 0.9); color: #f3e5f5; }
|
||||
body.theme-berry .global-progress-container { background: rgba(80, 40, 100, 0.92); }
|
||||
|
||||
/* 16. 金盏花 */
|
||||
body.theme-marigold { background: linear-gradient(135deg, #ffb347 0%, #ffeaa7 100%); }
|
||||
body.theme-marigold .top-bar { background: rgba(180, 120, 50, 0.85); }
|
||||
body.theme-marigold .top-bar, body.theme-marigold .top-bar a, body.theme-marigold .top-bar button { color: #5d4037; }
|
||||
body.theme-marigold .ebook-chapter { background: rgba(255, 252, 240, 0.95); color: #5d4037; }
|
||||
body.theme-marigold .ebook-chapter .chapter-title { color: #f39c12; border-bottom-color: #f39c12; }
|
||||
body.theme-marigold .speed-panel, body.theme-marigold .font-controls, body.theme-marigold .theme-selector, body.theme-marigold .bookmark-panel { background: rgba(180, 120, 50, 0.95); color: #fff; }
|
||||
body.theme-marigold .floating-btn { background: rgba(180, 120, 50, 0.9); color: #fff; }
|
||||
body.theme-marigold .global-progress-container { background: rgba(180, 120, 50, 0.92); }
|
||||
body.theme-marigold .progress-info { color: #fff3e0; }
|
||||
|
||||
/* 17. 冰川蓝 */
|
||||
body.theme-glacier { background: linear-gradient(135deg, #4a90e2 0%, #dfe6e9 100%); }
|
||||
body.theme-glacier .top-bar { background: rgba(30, 70, 120, 0.85); }
|
||||
body.theme-glacier .top-bar, body.theme-glacier .top-bar a, body.theme-glacier .top-bar button { color: #ecf0f1; }
|
||||
body.theme-glacier .ebook-chapter { background: rgba(240, 248, 255, 0.95); color: #2c3e50; }
|
||||
body.theme-glacier .ebook-chapter .chapter-title { color: #4a90e2; border-bottom-color: #4a90e2; }
|
||||
body.theme-glacier .speed-panel, body.theme-glacier .font-controls, body.theme-glacier .theme-selector, body.theme-glacier .bookmark-panel { background: rgba(30, 70, 120, 0.95); color: #ecf0f1; }
|
||||
body.theme-glacier .floating-btn { background: rgba(30, 70, 120, 0.9); color: #ecf0f1; }
|
||||
body.theme-glacier .global-progress-container { background: rgba(30, 70, 120, 0.92); }
|
||||
|
||||
/* 18. 玫瑰金 */
|
||||
body.theme-rosegold { background: linear-gradient(135deg, #e8b4b8 0%, #ffd9e2 100%); }
|
||||
body.theme-rosegold .top-bar { background: rgba(160, 100, 110, 0.85); }
|
||||
body.theme-rosegold .top-bar, body.theme-rosegold .top-bar a, body.theme-rosegold .top-bar button { color: #5a3a3e; }
|
||||
body.theme-rosegold .ebook-chapter { background: rgba(255, 248, 250, 0.95); color: #5a3a3e; }
|
||||
body.theme-rosegold .ebook-chapter .chapter-title { color: #e8b4b8; border-bottom-color: #e8b4b8; }
|
||||
body.theme-rosegold .speed-panel, body.theme-rosegold .font-controls, body.theme-rosegold .theme-selector, body.theme-rosegold .bookmark-panel { background: rgba(160, 100, 110, 0.95); color: #fff; }
|
||||
body.theme-rosegold .floating-btn { background: rgba(160, 100, 110, 0.9); color: #fff; }
|
||||
body.theme-rosegold .global-progress-container { background: rgba(160, 100, 110, 0.92); }
|
||||
body.theme-rosegold .progress-info { color: #fff0f0; }
|
||||
|
||||
/* 主题色块样式 */
|
||||
.theme-dot[data-theme="deep-space"] { background: linear-gradient(135deg, #0f0c29, #302b63); }
|
||||
.theme-dot[data-theme="parchment"] { background: linear-gradient(135deg, #e6d5b8, #c9a87b); }
|
||||
.theme-dot[data-theme="ocean"] { background: linear-gradient(135deg, #1a2980, #26d0ce); }
|
||||
.theme-dot[data-theme="cherry"] { background: linear-gradient(135deg, #ff9a9e, #fecfef); }
|
||||
.theme-dot[data-theme="night"] { background: #1a1a1a; }
|
||||
.theme-dot[data-theme="forest"] { background: linear-gradient(135deg, #134e5e, #71b280); }
|
||||
.theme-dot[data-theme="sunset"] { background: linear-gradient(135deg, #ff7e5f, #feb47b); }
|
||||
.theme-dot[data-theme="lavender"] { background: linear-gradient(135deg, #8e9ecc, #e0bbff); }
|
||||
.theme-dot[data-theme="matcha"] { background: linear-gradient(135deg, #a8c0aa, #6b8c5c); }
|
||||
.theme-dot[data-theme="blueberry"] { background: linear-gradient(135deg, #2c3e66, #4a69bd); }
|
||||
.theme-dot[data-theme="amber"] { background: linear-gradient(135deg, #ffb347, #ffcc33); }
|
||||
.theme-dot[data-theme="graphite"] { background: linear-gradient(135deg, #4a4a4a, #2c2c2c); }
|
||||
.theme-dot[data-theme="coral"] { background: linear-gradient(135deg, #ff6b6b, #ffb8b8); }
|
||||
.theme-dot[data-theme="mint"] { background: linear-gradient(135deg, #a8e6cf, #80deea); }
|
||||
.theme-dot[data-theme="berry"] { background: linear-gradient(135deg, #6c3483, #a569bd); }
|
||||
.theme-dot[data-theme="marigold"] { background: linear-gradient(135deg, #ffb347, #ffeaa7); }
|
||||
.theme-dot[data-theme="glacier"] { background: linear-gradient(135deg, #4a90e2, #dfe6e9); }
|
||||
.theme-dot[data-theme="rosegold"] { background: linear-gradient(135deg, #e8b4b8, #ffd9e2); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="theme-deep-space">
|
||||
|
||||
<div class="floating-buttons" id="floatingButtons">
|
||||
<div class="floating-btn bookmark-btn" id="bookmarkFloatBtn">📋</div>
|
||||
<div class="floating-btn scroll-down" id="scrollToggleBtn">▼</div>
|
||||
<div class="floating-btn" id="themeFloatBtn">🎨</div>
|
||||
</div>
|
||||
|
||||
<div class="speed-panel" id="speedPanel">
|
||||
<div class="speed-label">
|
||||
<span>⚡ 滚动速度</span>
|
||||
<span class="speed-value" id="speedValue">6 px/帧</span>
|
||||
</div>
|
||||
<input type="range" class="speed-slider" id="speedSlider" min="1" max="30" value="6" step="1">
|
||||
<div class="speed-presets">
|
||||
<div class="speed-preset" data-speed="3">🐢 慢</div>
|
||||
<div class="speed-preset" data-speed="6">⚡ 中</div>
|
||||
<div class="speed-preset" data-speed="10">🚀 快</div>
|
||||
<div class="speed-preset" data-speed="18">💨 极快</div>
|
||||
</div>
|
||||
<div class="auto-chapter-line">
|
||||
<span>📖 自动翻章</span>
|
||||
<input type="checkbox" id="autoChapterCheckbox" checked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="font-controls" id="fontControls">
|
||||
<button id="fontMinus">A-</button>
|
||||
<button id="fontPlus">A+</button>
|
||||
</div>
|
||||
<div class="theme-selector" id="themeSelector">
|
||||
<div class="theme-dot" data-theme="deep-space" title="深邃星空"></div>
|
||||
<div class="theme-dot" data-theme="parchment" title="羊皮纸"></div>
|
||||
<div class="theme-dot" data-theme="ocean" title="深海宁静"></div>
|
||||
<div class="theme-dot" data-theme="cherry" title="樱花"></div>
|
||||
<div class="theme-dot" data-theme="night" title="黑夜模式"></div>
|
||||
<div class="theme-dot" data-theme="forest" title="森林绿意"></div>
|
||||
<div class="theme-dot" data-theme="sunset" title="日落橙"></div>
|
||||
<div class="theme-dot" data-theme="lavender" title="薰衣草"></div>
|
||||
<div class="theme-dot" data-theme="matcha" title="抹茶"></div>
|
||||
<div class="theme-dot" data-theme="blueberry" title="蓝莓"></div>
|
||||
<div class="theme-dot" data-theme="amber" title="琥珀"></div>
|
||||
<div class="theme-dot" data-theme="graphite" title="石墨"></div>
|
||||
<div class="theme-dot" data-theme="coral" title="珊瑚粉"></div>
|
||||
<div class="theme-dot" data-theme="mint" title="薄荷绿"></div>
|
||||
<div class="theme-dot" data-theme="berry" title="浆果紫"></div>
|
||||
<div class="theme-dot" data-theme="marigold" title="金盏花"></div>
|
||||
<div class="theme-dot" data-theme="glacier" title="冰川蓝"></div>
|
||||
<div class="theme-dot" data-theme="rosegold" title="玫瑰金"></div>
|
||||
</div>
|
||||
<div class="bookmark-panel" id="bookmarkPanel">
|
||||
<div class="bookmark-header">
|
||||
<span>📖 我的书签</span>
|
||||
<span id="closePanelBtn">✕</span>
|
||||
</div>
|
||||
<div class="bookmark-list" id="bookmarkList">
|
||||
<div class="empty-bookmark">📭 暂无书签<br>点击 ⭐ 添加</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部全局进度条占位 -->
|
||||
<div id="globalProgressPlaceholder"></div>
|
||||
|
||||
<script>
|
||||
const floatingBtns = document.getElementById('floatingButtons');
|
||||
const speedPanel = document.getElementById('speedPanel');
|
||||
const fontControls = document.getElementById('fontControls');
|
||||
const themeSelector = document.getElementById('themeSelector');
|
||||
const bookmarkPanel = document.getElementById('bookmarkPanel');
|
||||
|
||||
let hideTimer = null;
|
||||
let globalProgressBar = null;
|
||||
|
||||
function showControls() {
|
||||
floatingBtns.classList.add('visible');
|
||||
speedPanel.classList.add('visible');
|
||||
if (globalProgressBar) globalProgressBar.classList.remove('hide');
|
||||
resetHideTimer();
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
floatingBtns.classList.remove('visible');
|
||||
speedPanel.classList.remove('visible');
|
||||
fontControls.classList.remove('visible');
|
||||
themeSelector.classList.remove('visible');
|
||||
if (globalProgressBar) globalProgressBar.classList.add('hide');
|
||||
}
|
||||
|
||||
function resetHideTimer() {
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(() => {
|
||||
if (!bookmarkPanel.classList.contains('show') && !themeSelector.classList.contains('visible') && !fontControls.classList.contains('visible') && !speedPanel.classList.contains('visible')) {
|
||||
hideControls();
|
||||
} else {
|
||||
resetHideTimer();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
let lastTap = 0;
|
||||
document.body.addEventListener('click', (e) => {
|
||||
const now = Date.now();
|
||||
const timeDiff = now - lastTap;
|
||||
const isControlElement = e.target.closest('.floating-btn') || e.target.closest('.bookmark-panel') ||
|
||||
e.target.closest('.theme-selector') || e.target.closest('.font-controls') ||
|
||||
e.target.closest('.speed-panel') || e.target.closest('.global-progress-container');
|
||||
if (!isControlElement && timeDiff < 300 && timeDiff > 0) {
|
||||
e.preventDefault();
|
||||
if (floatingBtns.classList.contains('visible')) {
|
||||
hideControls();
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
} else {
|
||||
showControls();
|
||||
}
|
||||
}
|
||||
lastTap = now;
|
||||
});
|
||||
|
||||
const interactiveElements = [floatingBtns, speedPanel, fontControls, themeSelector];
|
||||
interactiveElements.forEach(el => {
|
||||
if (!el) return;
|
||||
el.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (floatingBtns.classList.contains('visible')) resetHideTimer();
|
||||
});
|
||||
const slider = el.querySelector('.speed-slider');
|
||||
if (slider) {
|
||||
slider.addEventListener('input', () => {
|
||||
if (floatingBtns.classList.contains('visible')) resetHideTimer();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const bookmarkFloatBtn = document.getElementById('bookmarkFloatBtn');
|
||||
const closePanelBtn = document.getElementById('closePanelBtn');
|
||||
if (bookmarkFloatBtn) {
|
||||
bookmarkFloatBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
bookmarkPanel.classList.toggle('show');
|
||||
if (bookmarkPanel.classList.contains('show')) {
|
||||
showControls();
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
} else {
|
||||
resetHideTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (closePanelBtn) {
|
||||
closePanelBtn.addEventListener('click', () => {
|
||||
bookmarkPanel.classList.remove('show');
|
||||
resetHideTimer();
|
||||
});
|
||||
}
|
||||
|
||||
const themeFloatBtn = document.getElementById('themeFloatBtn');
|
||||
if (themeFloatBtn) {
|
||||
themeFloatBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (themeSelector.classList.contains('visible')) {
|
||||
themeSelector.classList.remove('visible');
|
||||
} else {
|
||||
themeSelector.classList.add('visible');
|
||||
resetHideTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const THEMES = [
|
||||
'deep-space', 'parchment', 'ocean', 'cherry', 'night',
|
||||
'forest', 'sunset', 'lavender', 'matcha', 'blueberry', 'amber', 'graphite',
|
||||
'coral', 'mint', 'berry', 'marigold', 'glacier', 'rosegold'
|
||||
];
|
||||
function setTheme(themeName) {
|
||||
document.body.className = 'theme-' + themeName;
|
||||
localStorage.setItem('reader_theme', themeName);
|
||||
document.querySelectorAll('.theme-dot').forEach(dot => {
|
||||
if (dot.dataset.theme === themeName) dot.classList.add('active');
|
||||
else dot.classList.remove('active');
|
||||
});
|
||||
if (window.updateAllTooltipColors) window.updateAllTooltipColors();
|
||||
}
|
||||
const savedTheme = localStorage.getItem('reader_theme');
|
||||
if (savedTheme && THEMES.includes(savedTheme)) setTheme(savedTheme);
|
||||
else setTheme('deep-space');
|
||||
|
||||
document.querySelectorAll('.theme-dot').forEach(dot => {
|
||||
dot.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
setTheme(dot.dataset.theme);
|
||||
themeSelector.classList.remove('visible');
|
||||
showToast('🎨 主题已切换');
|
||||
resetHideTimer();
|
||||
});
|
||||
});
|
||||
|
||||
const scrollBtn = document.getElementById('scrollToggleBtn');
|
||||
if (scrollBtn) {
|
||||
scrollBtn.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
if (fontControls.classList.contains('visible')) {
|
||||
fontControls.classList.remove('visible');
|
||||
} else {
|
||||
fontControls.classList.add('visible');
|
||||
resetHideTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const speedSlider = document.getElementById('speedSlider');
|
||||
const speedValue = document.getElementById('speedValue');
|
||||
const speedPresets = document.querySelectorAll('.speed-preset');
|
||||
let currentSpeed = 6;
|
||||
let autoScrollInterval = null;
|
||||
let isAutoScrolling = false;
|
||||
const savedSpeed = localStorage.getItem('scroll_speed');
|
||||
if (savedSpeed) {
|
||||
currentSpeed = parseInt(savedSpeed);
|
||||
if (speedSlider) speedSlider.value = currentSpeed;
|
||||
if (speedValue) speedValue.innerText = currentSpeed + ' px/帧';
|
||||
speedPresets.forEach(preset => {
|
||||
const presetSpeed = parseInt(preset.dataset.speed);
|
||||
if (presetSpeed === currentSpeed) preset.classList.add('active');
|
||||
else preset.classList.remove('active');
|
||||
});
|
||||
}
|
||||
function updateSpeed(newSpeed) {
|
||||
currentSpeed = Math.min(30, Math.max(1, newSpeed));
|
||||
if (speedSlider) speedSlider.value = currentSpeed;
|
||||
if (speedValue) speedValue.innerText = currentSpeed + ' px/帧';
|
||||
localStorage.setItem('scroll_speed', currentSpeed);
|
||||
speedPresets.forEach(preset => {
|
||||
const presetSpeed = parseInt(preset.dataset.speed);
|
||||
if (presetSpeed === currentSpeed) preset.classList.add('active');
|
||||
else preset.classList.remove('active');
|
||||
});
|
||||
if (isAutoScrolling) {
|
||||
stopAutoScroll();
|
||||
startAutoScroll();
|
||||
}
|
||||
}
|
||||
if (speedSlider) {
|
||||
speedSlider.oninput = (e) => {
|
||||
updateSpeed(parseInt(e.target.value));
|
||||
showToast(`⚡ 速度 ${currentSpeed} px/帧`);
|
||||
resetHideTimer();
|
||||
};
|
||||
}
|
||||
speedPresets.forEach(preset => {
|
||||
preset.onclick = () => {
|
||||
const newSpeed = parseInt(preset.dataset.speed);
|
||||
updateSpeed(newSpeed);
|
||||
showToast(`⚡ ${preset.innerText} ${newSpeed} px/帧`);
|
||||
resetHideTimer();
|
||||
};
|
||||
});
|
||||
function startAutoScroll() {
|
||||
if (autoScrollInterval) clearInterval(autoScrollInterval);
|
||||
autoScrollInterval = setInterval(() => window.scrollBy(0, currentSpeed), 25);
|
||||
isAutoScrolling = true;
|
||||
if (scrollBtn) {
|
||||
scrollBtn.classList.add('active');
|
||||
scrollBtn.innerHTML = "⏸";
|
||||
}
|
||||
showToast(`▶ 滚动中 (${currentSpeed}px/帧)`);
|
||||
}
|
||||
function stopAutoScroll() {
|
||||
if (autoScrollInterval) {
|
||||
clearInterval(autoScrollInterval);
|
||||
autoScrollInterval = null;
|
||||
}
|
||||
isAutoScrolling = false;
|
||||
if (scrollBtn) {
|
||||
scrollBtn.classList.remove('active');
|
||||
scrollBtn.innerHTML = "▼";
|
||||
}
|
||||
showToast('⏹ 已停止');
|
||||
}
|
||||
if (scrollBtn) {
|
||||
scrollBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (isAutoScrolling) stopAutoScroll();
|
||||
else startAutoScroll();
|
||||
resetHideTimer();
|
||||
};
|
||||
}
|
||||
function showToast(msg) {
|
||||
let t = document.querySelector('.toast');
|
||||
if (!t) { t = document.createElement('div'); t.className = 'toast'; document.body.appendChild(t); }
|
||||
t.textContent = msg;
|
||||
t.style.display = 'block';
|
||||
setTimeout(() => t.style.display = 'none', 1500);
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "bookmarks_v6";
|
||||
function getBookmarks(){
|
||||
try{ return JSON.parse(localStorage.getItem(STORAGE_KEY)||'[]'); }catch(e){ return []; }
|
||||
}
|
||||
function saveBookmarks(list){
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
||||
refreshBookmarkList();
|
||||
}
|
||||
function refreshBookmarkList(){
|
||||
let list = getBookmarks();
|
||||
let container = document.getElementById('bookmarkList');
|
||||
if(!container) return;
|
||||
if(list.length===0){
|
||||
container.innerHTML='<div class="empty-bookmark">📭 暂无书签<br>点击 ⭐ 添加</div>';
|
||||
return;
|
||||
}
|
||||
list.sort((a,b)=>b.time-a.time);
|
||||
let html='';
|
||||
for(let b of list){
|
||||
let pageLabel = b.type==='txt'?'第'+b.page+'章':(b.type==='ebook'?'第'+b.page+'章':'第'+b.page+'页');
|
||||
html+=`<div class="bookmark-item" data-book="${escapeHtml(b.book)}" data-chapter="${escapeHtml(b.chapter)}" data-page="${b.page}">
|
||||
<span class="delete" data-book="${escapeHtml(b.book)}" data-chapter="${escapeHtml(b.chapter)}">🗑</span>
|
||||
<div class="title">📖 ${escapeHtml(b.book.length>18?b.book.substring(0,18)+'...':b.book)}</div>
|
||||
<div class="info">📄 ${escapeHtml(b.chapterName||b.chapter.substring(0,25))} | 📍 ${pageLabel}</div>
|
||||
</div>`;
|
||||
}
|
||||
container.innerHTML=html;
|
||||
document.querySelectorAll('#bookmarkList .bookmark-item').forEach(item=>{
|
||||
let book=item.getAttribute('data-book'), chapter=item.getAttribute('data-chapter'), page=parseInt(item.getAttribute('data-page'))||1;
|
||||
item.onclick=(e)=>{
|
||||
if(e.target.classList.contains('delete')) return;
|
||||
jumpToBookmark(book,chapter,page);
|
||||
};
|
||||
let db=item.querySelector('.delete');
|
||||
if(db) db.onclick=(e)=>{
|
||||
e.stopPropagation();
|
||||
removeBookmark(db.getAttribute('data-book'), db.getAttribute('data-chapter'));
|
||||
};
|
||||
});
|
||||
}
|
||||
function removeBookmark(book,chapter){
|
||||
let list=getBookmarks();
|
||||
list=list.filter(b=>!(b.book===book&&b.chapter===chapter));
|
||||
saveBookmarks(list);
|
||||
showToast('🗑 删除书签');
|
||||
}
|
||||
function escapeHtml(s){
|
||||
return (s||'').replace(/[&<>]/g,m=>({'&':'&','<':'<','>':'>'}[m]));
|
||||
}
|
||||
let CURRENT_BOOK = '', CURRENT_CHAPTER = '';
|
||||
function jumpToBookmark(book,chapter,page){
|
||||
if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){
|
||||
if (typeof jumpToPage === 'function') jumpToPage(page);
|
||||
else if (typeof renderChapter === 'function') renderChapter(Math.min(Math.max(1,page), (typeof chapters !== 'undefined' ? chapters.length : 1))-1);
|
||||
}else{
|
||||
sessionStorage.setItem('jump_target', JSON.stringify({book,chapter,page}));
|
||||
location.href = '<?php echo $currentFile; ?>?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 全屏3D翻书动画自动翻章功能 ====================
|
||||
let autoChapterEnabled = true;
|
||||
let isTurningPage = false;
|
||||
let turnTimer = null;
|
||||
let lastScrollTop = 0;
|
||||
let scrollDirection = 'down';
|
||||
const autoChapterCheckbox = document.getElementById('autoChapterCheckbox');
|
||||
|
||||
function getThemeColorsForAnimation() {
|
||||
const bodyClass = document.body.className;
|
||||
let overlayBg = 'rgba(15, 12, 41, 0.92)';
|
||||
let leftBg = 'rgba(48, 43, 99, 0.95)';
|
||||
let rightBg = 'rgba(48, 43, 99, 0.95)';
|
||||
let leftBorder = '2px solid rgba(155, 89, 182, 0.6)';
|
||||
let rightBorder = '2px solid rgba(155, 89, 182, 0.6)';
|
||||
let msgBg = 'rgba(48, 43, 99, 0.95)';
|
||||
let msgColor = '#bb86fc';
|
||||
let msgBorder = '1px solid rgba(155, 89, 182, 0.5)';
|
||||
|
||||
if (bodyClass.includes('parchment')) {
|
||||
overlayBg = 'rgba(230, 213, 184, 0.92)';
|
||||
leftBg = 'rgba(201, 168, 123, 0.95)';
|
||||
rightBg = 'rgba(201, 168, 123, 0.95)';
|
||||
leftBorder = '2px solid rgba(139, 69, 19, 0.5)';
|
||||
rightBorder = '2px solid rgba(139, 69, 19, 0.5)';
|
||||
msgBg = 'rgba(201, 168, 123, 0.95)';
|
||||
msgColor = '#3e2723';
|
||||
msgBorder = '1px solid rgba(139, 69, 19, 0.4)';
|
||||
} else if (bodyClass.includes('ocean')) {
|
||||
overlayBg = 'rgba(26, 41, 128, 0.92)';
|
||||
leftBg = 'rgba(38, 208, 206, 0.9)';
|
||||
rightBg = 'rgba(38, 208, 206, 0.9)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(26, 41, 128, 0.95)';
|
||||
msgColor = '#e0f7fa';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('cherry')) {
|
||||
overlayBg = 'rgba(255, 154, 158, 0.92)';
|
||||
leftBg = 'rgba(254, 207, 239, 0.95)';
|
||||
rightBg = 'rgba(254, 207, 239, 0.95)';
|
||||
leftBorder = '2px solid rgba(219, 112, 147, 0.6)';
|
||||
rightBorder = '2px solid rgba(219, 112, 147, 0.6)';
|
||||
msgBg = 'rgba(219, 112, 147, 0.95)';
|
||||
msgColor = '#5a2e3e';
|
||||
msgBorder = '1px solid rgba(219, 112, 147, 0.4)';
|
||||
} else if (bodyClass.includes('night')) {
|
||||
overlayBg = 'rgba(10, 10, 10, 0.95)';
|
||||
leftBg = 'rgba(30, 30, 30, 0.98)';
|
||||
rightBg = 'rgba(30, 30, 30, 0.98)';
|
||||
leftBorder = '2px solid #555';
|
||||
rightBorder = '2px solid #555';
|
||||
msgBg = 'rgba(30, 30, 30, 0.98)';
|
||||
msgColor = '#aaa';
|
||||
msgBorder = '1px solid #555';
|
||||
} else if (bodyClass.includes('forest')) {
|
||||
overlayBg = 'rgba(19, 78, 94, 0.92)';
|
||||
leftBg = 'rgba(113, 178, 128, 0.9)';
|
||||
rightBg = 'rgba(113, 178, 128, 0.9)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(19, 78, 94, 0.95)';
|
||||
msgColor = '#e8f5e9';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('sunset')) {
|
||||
overlayBg = 'rgba(255, 126, 95, 0.92)';
|
||||
leftBg = 'rgba(254, 180, 123, 0.95)';
|
||||
rightBg = 'rgba(254, 180, 123, 0.95)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(255, 126, 95, 0.95)';
|
||||
msgColor = '#fff3e0';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('lavender')) {
|
||||
overlayBg = 'rgba(142, 158, 204, 0.92)';
|
||||
leftBg = 'rgba(224, 187, 255, 0.95)';
|
||||
rightBg = 'rgba(224, 187, 255, 0.95)';
|
||||
leftBorder = '2px solid rgba(123, 31, 162, 0.5)';
|
||||
rightBorder = '2px solid rgba(123, 31, 162, 0.5)';
|
||||
msgBg = 'rgba(142, 158, 204, 0.95)';
|
||||
msgColor = '#4a3a6e';
|
||||
msgBorder = '1px solid rgba(123, 31, 162, 0.4)';
|
||||
} else if (bodyClass.includes('matcha')) {
|
||||
overlayBg = 'rgba(168, 192, 170, 0.92)';
|
||||
leftBg = 'rgba(107, 140, 92, 0.95)';
|
||||
rightBg = 'rgba(107, 140, 92, 0.95)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(168, 192, 170, 0.95)';
|
||||
msgColor = '#3e5a2e';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('blueberry')) {
|
||||
overlayBg = 'rgba(44, 62, 102, 0.92)';
|
||||
leftBg = 'rgba(74, 105, 189, 0.9)';
|
||||
rightBg = 'rgba(74, 105, 189, 0.9)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(44, 62, 102, 0.95)';
|
||||
msgColor = '#dfe6e9';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('amber')) {
|
||||
overlayBg = 'rgba(255, 179, 71, 0.92)';
|
||||
leftBg = 'rgba(255, 204, 51, 0.95)';
|
||||
rightBg = 'rgba(255, 204, 51, 0.95)';
|
||||
leftBorder = '2px solid rgba(160, 90, 30, 0.5)';
|
||||
rightBorder = '2px solid rgba(160, 90, 30, 0.5)';
|
||||
msgBg = 'rgba(255, 179, 71, 0.95)';
|
||||
msgColor = '#3e2723';
|
||||
msgBorder = '1px solid rgba(160, 90, 30, 0.4)';
|
||||
} else if (bodyClass.includes('graphite')) {
|
||||
overlayBg = 'rgba(74, 74, 74, 0.95)';
|
||||
leftBg = 'rgba(44, 44, 44, 0.98)';
|
||||
rightBg = 'rgba(44, 44, 44, 0.98)';
|
||||
leftBorder = '2px solid #777';
|
||||
rightBorder = '2px solid #777';
|
||||
msgBg = 'rgba(74, 74, 74, 0.98)';
|
||||
msgColor = '#ddd';
|
||||
msgBorder = '1px solid #666';
|
||||
} else if (bodyClass.includes('coral')) {
|
||||
overlayBg = 'rgba(255, 107, 107, 0.92)';
|
||||
leftBg = 'rgba(255, 142, 142, 0.95)';
|
||||
rightBg = 'rgba(255, 142, 142, 0.95)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(255, 107, 107, 0.95)';
|
||||
msgColor = '#fff';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('mint')) {
|
||||
overlayBg = 'rgba(168, 230, 207, 0.92)';
|
||||
leftBg = 'rgba(128, 222, 234, 0.95)';
|
||||
rightBg = 'rgba(128, 222, 234, 0.95)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(168, 230, 207, 0.95)';
|
||||
msgColor = '#2d5a3b';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('berry')) {
|
||||
overlayBg = 'rgba(108, 52, 131, 0.92)';
|
||||
leftBg = 'rgba(142, 68, 173, 0.95)';
|
||||
rightBg = 'rgba(142, 68, 173, 0.95)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(108, 52, 131, 0.95)';
|
||||
msgColor = '#f3e5f5';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('marigold')) {
|
||||
overlayBg = 'rgba(255, 179, 71, 0.92)';
|
||||
leftBg = 'rgba(255, 204, 51, 0.95)';
|
||||
rightBg = 'rgba(255, 204, 51, 0.95)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(255, 179, 71, 0.95)';
|
||||
msgColor = '#5d4037';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('glacier')) {
|
||||
overlayBg = 'rgba(74, 144, 226, 0.92)';
|
||||
leftBg = 'rgba(116, 185, 255, 0.95)';
|
||||
rightBg = 'rgba(116, 185, 255, 0.95)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(74, 144, 226, 0.95)';
|
||||
msgColor = '#ecf0f1';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
} else if (bodyClass.includes('rosegold')) {
|
||||
overlayBg = 'rgba(232, 180, 184, 0.92)';
|
||||
leftBg = 'rgba(245, 198, 203, 0.95)';
|
||||
rightBg = 'rgba(245, 198, 203, 0.95)';
|
||||
leftBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
rightBorder = '2px solid rgba(255,255,255,0.4)';
|
||||
msgBg = 'rgba(232, 180, 184, 0.95)';
|
||||
msgColor = '#5a3a3e';
|
||||
msgBorder = '1px solid rgba(255,255,255,0.3)';
|
||||
}
|
||||
|
||||
return { overlayBg, leftBg, rightBg, leftBorder, rightBorder, msgBg, msgColor, msgBorder };
|
||||
}
|
||||
|
||||
function showBookTurnAnimation(callback) {
|
||||
if (isTurningPage) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
isTurningPage = true;
|
||||
|
||||
const colors = getThemeColorsForAnimation();
|
||||
|
||||
let overlay = document.createElement('div');
|
||||
overlay.className = 'page-turn-overlay';
|
||||
overlay.style.background = colors.overlayBg;
|
||||
overlay.innerHTML = `
|
||||
<div class="book-container">
|
||||
<div class="book-left">📖</div>
|
||||
<div class="book-right">📖</div>
|
||||
<div class="message">✨ 正在翻开新篇章 ✨</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const bookLeft = overlay.querySelector('.book-left');
|
||||
const bookRight = overlay.querySelector('.book-right');
|
||||
const message = overlay.querySelector('.message');
|
||||
|
||||
if (bookLeft) {
|
||||
bookLeft.style.background = colors.leftBg;
|
||||
bookLeft.style.border = colors.leftBorder;
|
||||
bookLeft.style.boxShadow = '0 0 30px rgba(0,0,0,0.4)';
|
||||
}
|
||||
if (bookRight) {
|
||||
bookRight.style.background = colors.rightBg;
|
||||
bookRight.style.border = colors.rightBorder;
|
||||
bookRight.style.boxShadow = '0 0 30px rgba(0,0,0,0.4)';
|
||||
}
|
||||
if (message) {
|
||||
message.style.background = colors.msgBg;
|
||||
message.style.color = colors.msgColor;
|
||||
message.style.border = colors.msgBorder;
|
||||
message.style.backdropFilter = 'blur(8px)';
|
||||
}
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
setTimeout(() => {
|
||||
if (callback) callback();
|
||||
setTimeout(() => {
|
||||
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||
isTurningPage = false;
|
||||
}, 150);
|
||||
}, 450);
|
||||
}
|
||||
|
||||
if (localStorage.getItem('autoChapterEnabled') !== null) {
|
||||
autoChapterEnabled = localStorage.getItem('autoChapterEnabled') === 'true';
|
||||
if (autoChapterCheckbox) autoChapterCheckbox.checked = autoChapterEnabled;
|
||||
} else {
|
||||
autoChapterEnabled = true;
|
||||
if (autoChapterCheckbox) autoChapterCheckbox.checked = true;
|
||||
}
|
||||
|
||||
if (autoChapterCheckbox) {
|
||||
autoChapterCheckbox.onchange = function(e) {
|
||||
e.stopPropagation();
|
||||
autoChapterEnabled = this.checked;
|
||||
localStorage.setItem('autoChapterEnabled', autoChapterEnabled);
|
||||
showToast(autoChapterEnabled ? '✅ 自动翻章已开启' : '⏹ 自动翻章已关闭');
|
||||
resetHideTimer();
|
||||
};
|
||||
}
|
||||
|
||||
let scrollTimer = null;
|
||||
|
||||
function checkScrollBottom() {
|
||||
if (!autoChapterEnabled) return;
|
||||
if (isTurningPage) return;
|
||||
|
||||
let totalHeight = document.body.scrollHeight;
|
||||
let windowHeight = window.innerHeight;
|
||||
let scrollTop = window.scrollY;
|
||||
|
||||
if (scrollTop > lastScrollTop) {
|
||||
scrollDirection = 'down';
|
||||
} else if (scrollTop < lastScrollTop) {
|
||||
scrollDirection = 'up';
|
||||
if (turnTimer) {
|
||||
clearTimeout(turnTimer);
|
||||
turnTimer = null;
|
||||
}
|
||||
}
|
||||
lastScrollTop = scrollTop;
|
||||
|
||||
let isAtBottom = (scrollTop + windowHeight + 15) >= totalHeight;
|
||||
|
||||
if (isAtBottom && scrollDirection === 'down' && !turnTimer) {
|
||||
let hasNext = false;
|
||||
let nextBtn = document.getElementById('nextChapterBtn');
|
||||
if (nextBtn && !nextBtn.disabled) {
|
||||
hasNext = true;
|
||||
}
|
||||
|
||||
if (hasNext) {
|
||||
showToast('📖 3秒后自动翻到下一章...');
|
||||
turnTimer = setTimeout(() => {
|
||||
if (!autoChapterEnabled || isTurningPage) {
|
||||
turnTimer = null;
|
||||
return;
|
||||
}
|
||||
let currentScrollTop = window.scrollY;
|
||||
let currentIsAtBottom = (currentScrollTop + windowHeight + 15) >= document.body.scrollHeight;
|
||||
if (!currentIsAtBottom) {
|
||||
turnTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
showBookTurnAnimation(function() {
|
||||
if (nextBtn) {
|
||||
nextBtn.click();
|
||||
}
|
||||
setTimeout(() => {
|
||||
turnTimer = null;
|
||||
}, 1000);
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
if (scrollTimer) clearTimeout(scrollTimer);
|
||||
scrollTimer = setTimeout(checkScrollBottom, 100);
|
||||
});
|
||||
|
||||
function onChapterChange() {
|
||||
if (turnTimer) {
|
||||
clearTimeout(turnTimer);
|
||||
turnTimer = null;
|
||||
}
|
||||
isTurningPage = false;
|
||||
lastScrollTop = 0;
|
||||
scrollDirection = 'down';
|
||||
window.scrollTo(0, 0);
|
||||
if (typeof updateGlobalProgress === 'function') updateGlobalProgress();
|
||||
}
|
||||
|
||||
// ==================== 全局进度条功能 ====================
|
||||
let globalChapters = [];
|
||||
let globalTotalChapters = 0;
|
||||
let globalCurrentIndex = 0;
|
||||
let lastChapterIndex = -1;
|
||||
let tooltipHideTimer = null;
|
||||
|
||||
function getThemeTooltipColors() {
|
||||
const bodyClass = document.body.className;
|
||||
let bgColor = 'rgba(0,0,0,0.95)';
|
||||
let textColor = '#ff9800';
|
||||
let borderColor = 'rgba(255,152,0,0.6)';
|
||||
|
||||
if (bodyClass.includes('parchment')) {
|
||||
bgColor = 'rgba(201, 168, 123, 0.98)';
|
||||
textColor = '#3e2723';
|
||||
borderColor = 'rgba(139, 69, 19, 0.6)';
|
||||
} else if (bodyClass.includes('ocean')) {
|
||||
bgColor = 'rgba(26, 41, 128, 0.98)';
|
||||
textColor = '#e0f7fa';
|
||||
borderColor = 'rgba(38, 208, 206, 0.6)';
|
||||
} else if (bodyClass.includes('cherry')) {
|
||||
bgColor = 'rgba(219, 112, 147, 0.98)';
|
||||
textColor = '#5a2e3e';
|
||||
borderColor = 'rgba(255,255,255,0.5)';
|
||||
} else if (bodyClass.includes('night')) {
|
||||
bgColor = 'rgba(30, 30, 30, 0.98)';
|
||||
textColor = '#aaa';
|
||||
borderColor = '#555';
|
||||
} else if (bodyClass.includes('forest')) {
|
||||
bgColor = 'rgba(19, 78, 94, 0.98)';
|
||||
textColor = '#e8f5e9';
|
||||
borderColor = 'rgba(113, 178, 128, 0.6)';
|
||||
} else if (bodyClass.includes('sunset')) {
|
||||
bgColor = 'rgba(255, 126, 95, 0.98)';
|
||||
textColor = '#fff3e0';
|
||||
borderColor = 'rgba(255,255,255,0.5)';
|
||||
} else if (bodyClass.includes('lavender')) {
|
||||
bgColor = 'rgba(142, 158, 204, 0.98)';
|
||||
textColor = '#4a3a6e';
|
||||
borderColor = 'rgba(123, 31, 162, 0.5)';
|
||||
} else if (bodyClass.includes('matcha')) {
|
||||
bgColor = 'rgba(107, 140, 92, 0.98)';
|
||||
textColor = '#3e5a2e';
|
||||
borderColor = 'rgba(255,255,255,0.5)';
|
||||
} else if (bodyClass.includes('blueberry')) {
|
||||
bgColor = 'rgba(44, 62, 102, 0.98)';
|
||||
textColor = '#dfe6e9';
|
||||
borderColor = 'rgba(74, 105, 189, 0.6)';
|
||||
} else if (bodyClass.includes('amber')) {
|
||||
bgColor = 'rgba(255, 179, 71, 0.98)';
|
||||
textColor = '#3e2723';
|
||||
borderColor = 'rgba(160, 90, 30, 0.5)';
|
||||
} else if (bodyClass.includes('graphite')) {
|
||||
bgColor = 'rgba(74, 74, 74, 0.98)';
|
||||
textColor = '#ddd';
|
||||
borderColor = '#777';
|
||||
} else if (bodyClass.includes('coral')) {
|
||||
bgColor = 'rgba(200, 80, 80, 0.98)';
|
||||
textColor = '#fff';
|
||||
borderColor = 'rgba(255,255,255,0.5)';
|
||||
} else if (bodyClass.includes('mint')) {
|
||||
bgColor = 'rgba(60, 120, 100, 0.98)';
|
||||
textColor = '#fff';
|
||||
borderColor = 'rgba(255,255,255,0.5)';
|
||||
} else if (bodyClass.includes('berry')) {
|
||||
bgColor = 'rgba(80, 40, 100, 0.98)';
|
||||
textColor = '#f3e5f5';
|
||||
borderColor = 'rgba(255,255,255,0.5)';
|
||||
} else if (bodyClass.includes('marigold')) {
|
||||
bgColor = 'rgba(180, 120, 50, 0.98)';
|
||||
textColor = '#fff3e0';
|
||||
borderColor = 'rgba(255,255,255,0.5)';
|
||||
} else if (bodyClass.includes('glacier')) {
|
||||
bgColor = 'rgba(30, 70, 120, 0.98)';
|
||||
textColor = '#ecf0f1';
|
||||
borderColor = 'rgba(74, 144, 226, 0.6)';
|
||||
} else if (bodyClass.includes('rosegold')) {
|
||||
bgColor = 'rgba(160, 100, 110, 0.98)';
|
||||
textColor = '#fff0f0';
|
||||
borderColor = 'rgba(255,255,255,0.5)';
|
||||
}
|
||||
|
||||
return { bgColor, textColor, borderColor };
|
||||
}
|
||||
|
||||
function updateTooltipStyle(tooltip) {
|
||||
if (!tooltip) return;
|
||||
const colors = getThemeTooltipColors();
|
||||
tooltip.style.backgroundColor = colors.bgColor;
|
||||
tooltip.style.color = colors.textColor;
|
||||
tooltip.style.border = `1px solid ${colors.borderColor}`;
|
||||
}
|
||||
|
||||
window.updateAllTooltipColors = function() {
|
||||
const tooltip = document.getElementById('chapterTooltip');
|
||||
if (tooltip) updateTooltipStyle(tooltip);
|
||||
};
|
||||
|
||||
function showChapterTooltip(chapterIndex, chapterTitle) {
|
||||
let tooltip = document.getElementById('chapterTooltip');
|
||||
if (!tooltip) return;
|
||||
|
||||
if (tooltipHideTimer) clearTimeout(tooltipHideTimer);
|
||||
|
||||
let displayText = `📖 第 ${chapterIndex+1} 章 · ${chapterTitle.substring(0, 32)}`;
|
||||
if (chapterTitle.length > 32) displayText += '...';
|
||||
tooltip.textContent = displayText;
|
||||
|
||||
updateTooltipStyle(tooltip);
|
||||
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.style.opacity = '1';
|
||||
|
||||
tooltipHideTimer = setTimeout(() => {
|
||||
if (tooltip) tooltip.style.display = 'none';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function createGlobalProgressBar(total, currentIdx, chaptersList) {
|
||||
let container = document.getElementById('globalProgressPlaceholder');
|
||||
if (!container) return;
|
||||
container.innerHTML = `
|
||||
<div class="global-progress-container" id="globalProgressBar">
|
||||
<div class="progress-range-area" id="progressRangeArea">
|
||||
<input type="range" class="progress-slider-global" id="globalProgressSlider" min="0" max="${total-1}" value="${currentIdx}" step="1">
|
||||
<div class="chapter-tooltip" id="chapterTooltip" style="display: none;">📖 ${escapeHtml(chaptersList[currentIdx]?.title || '章节')}</div>
|
||||
</div>
|
||||
<div class="progress-info">
|
||||
<div class="progress-label">
|
||||
<span>📖 第 ${currentIdx+1} / ${total} 章</span>
|
||||
</div>
|
||||
<div>${escapeHtml(chaptersList[currentIdx]?.title || '')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
globalProgressBar = document.getElementById('globalProgressBar');
|
||||
let slider = document.getElementById('globalProgressSlider');
|
||||
let rangeArea = document.getElementById('progressRangeArea');
|
||||
|
||||
if (slider) {
|
||||
slider.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
slider.addEventListener('input', (e) => {
|
||||
let idx = parseInt(e.target.value);
|
||||
let chapterTitle = chaptersList[idx]?.title || '章节';
|
||||
if (idx !== lastChapterIndex) {
|
||||
lastChapterIndex = idx;
|
||||
showChapterTooltip(idx, chapterTitle);
|
||||
}
|
||||
});
|
||||
|
||||
slider.addEventListener('change', (e) => {
|
||||
let idx = parseInt(e.target.value);
|
||||
if (typeof renderChapter === 'function') {
|
||||
renderChapter(idx);
|
||||
showToast(`📖 跳转到第 ${idx+1} 章: ${chaptersList[idx]?.title || ''}`);
|
||||
}
|
||||
resetHideTimer();
|
||||
});
|
||||
|
||||
slider.addEventListener('mousemove', (e) => {
|
||||
let rect = slider.getBoundingClientRect();
|
||||
let percent = (e.clientX - rect.left) / rect.width;
|
||||
let idx = Math.round(percent * (slider.max - slider.min)) + parseInt(slider.min);
|
||||
idx = Math.min(slider.max, Math.max(slider.min, idx));
|
||||
if (chaptersList[idx] && idx !== lastChapterIndex) {
|
||||
lastChapterIndex = idx;
|
||||
showChapterTooltip(idx, chaptersList[idx].title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const tooltip = document.getElementById('chapterTooltip');
|
||||
if (tooltip && tooltip.style.display === 'block') {
|
||||
updateTooltipStyle(tooltip);
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
if (globalProgressBar) globalProgressBar.classList.add('hide');
|
||||
}
|
||||
|
||||
function updateGlobalProgress() {
|
||||
let container = document.getElementById('globalProgressBar');
|
||||
if (!container) return;
|
||||
let slider = document.getElementById('globalProgressSlider');
|
||||
let infoLabel = container.querySelector('.progress-label span');
|
||||
let infoTitle = container.querySelector('.progress-info > div:last-child');
|
||||
if (slider && globalTotalChapters > 0) {
|
||||
slider.value = globalCurrentIndex;
|
||||
if (infoLabel) infoLabel.innerText = `📖 第 ${globalCurrentIndex+1} / ${globalTotalChapters} 章`;
|
||||
if (infoTitle && globalChapters[globalCurrentIndex]) infoTitle.innerText = globalChapters[globalCurrentIndex].title || '';
|
||||
lastChapterIndex = globalCurrentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
window.updateGlobalProgress = updateGlobalProgress;
|
||||
window.updateAllTooltipColors = updateAllTooltipColors;
|
||||
</script>
|
||||
|
||||
<?php if ($isChapterPage && $isPdf): ?>
|
||||
<!-- PDF阅读页 -->
|
||||
<div class="top-bar">
|
||||
<div class="top-bar-left">
|
||||
<button class="back-btn" id="backBtn">←</button>
|
||||
<div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div>
|
||||
</div>
|
||||
<div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div>
|
||||
</div>
|
||||
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
|
||||
<div class="content" id="reader"><div class="loading-msg" id="loadingMsg">⏳ 正在加载 PDF...<br><?php echo htmlspecialchars($chapter); ?></div></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const PDF_URL = "<?php echo $fileUrl; ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
|
||||
let pdfDoc=null,totalPages=0,renderedPages=new Set(),targetPage=null,scale=1.5;
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
function getCurrentPage(){let cs=document.querySelectorAll('.canvas-container');for(let i=0;i<cs.length;i++){let r=cs[i].getBoundingClientRect();if(r.top<=150&&r.bottom>=100){let p=parseInt(cs[i].getAttribute('data-page'));if(!isNaN(p))return p;}}return 1;}
|
||||
function addBookmark(){let p=getCurrentPage(),l=getBookmarks(),i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:p,time:Date.now()};if(i>=0)l[i]=n;else l.push(n);saveBookmarks(l);showToast('✅ 第 '+p+' 页');}
|
||||
function jumpToPage(p){p=Math.min(Math.max(1,p),totalPages);let t=document.querySelector(`.canvas-container[data-page="${p}"]`);if(t){t.scrollIntoView({behavior:'smooth',block:'start'});showToast('✨ 第 '+p+' 页');}else{showToast('📖 加载中...');(async()=>{let s=Math.max(1,p-3),e=Math.min(totalPages,p+3);for(let i=s;i<=e;i++)if(!renderedPages.has(i))await renderPage(i);setTimeout(()=>{let c=document.querySelector(`.canvas-container[data-page="${p}"]`);if(c){c.scrollIntoView({behavior:'smooth',block:'start'});showToast('✨ 第 '+p+' 页');}},300);})();}}
|
||||
window.jumpToBookmark = function(book,chapter,p){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER)jumpToPage(p);else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page:p}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
async function renderPage(n){if(!pdfDoc||renderedPages.has(n))return;renderedPages.add(n);let div=document.createElement('div');div.className='canvas-container';div.setAttribute('data-page',n);let p=document.createElement('div');p.className='loading-placeholder';p.innerText=`⏳ 第 ${n} 页...`;div.appendChild(p);let ins=false,ex=document.querySelectorAll('.canvas-container');for(let i=0;i<ex.length;i++){let ep=parseInt(ex[i].getAttribute('data-page'));if(ep>n){ex[i].before(div);ins=true;break;}}if(!ins)document.getElementById('reader').appendChild(div);try{let page=await pdfDoc.getPage(n),vp=page.getViewport({scale:scale}),cv=document.createElement('canvas');cv.width=vp.width;cv.height=vp.height;cv.style.width='100%';cv.style.height='auto';await page.render({canvasContext:cv.getContext('2d'),viewport:vp}).promise;div.innerHTML='';div.appendChild(cv);let pf=document.getElementById('progressFill');if(pf)pf.style.width=(renderedPages.size/totalPages)*100+'%';}catch(e){p.innerText=`❌ 第 ${n} 页失败`;}}
|
||||
let st;function onScrollLoad(){if(st)clearTimeout(st);st=setTimeout(()=>{if(!pdfDoc)return;let cs=document.querySelectorAll('.canvas-container'),need=new Set();cs.forEach(c=>{let r=c.getBoundingClientRect();if(r.top-600<window.innerHeight&&r.bottom+600>0){let p=parseInt(c.getAttribute('data-page'));if(!isNaN(p))need.add(p);}});let toRender=[];need.forEach(p=>{for(let i=-2;i<=2;i++){let np=p+i;if(np>=1&&np<=totalPages&&!renderedPages.has(np))toRender.push(np);}});toRender.sort((a,b)=>a-b).forEach(p=>renderPage(p));},200);}
|
||||
async function loadPDF(){try{let lm=document.getElementById('loadingMsg');lm.style.display='block';pdfDoc=await pdfjsLib.getDocument(PDF_URL).promise;totalPages=pdfDoc.numPages;lm.innerText=`📄 共 ${totalPages} 页,加载中...`;let jump=sessionStorage.getItem('jump_target');if(jump){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jump);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)targetPage=t.page;}catch(e){}}else{let bks=getBookmarks(),ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);if(ex&&ex.page)targetPage=ex.page;}for(let i=1;i<=Math.min(5,totalPages);i++)await renderPage(i);lm.style.display='none';if(targetPage){let s=Math.max(1,targetPage-2),e=Math.min(totalPages,targetPage+2);for(let i=s;i<=e;i++)if(!renderedPages.has(i))await renderPage(i);setTimeout(()=>{let c=document.querySelector(`.canvas-container[data-page="${targetPage}"]`);if(c){c.scrollIntoView({behavior:'smooth',block:'start'});showToast('📖 第 '+targetPage+' 页');}targetPage=null;},500);}window.addEventListener('scroll',onScrollLoad);}catch(e){document.getElementById('loadingMsg').innerHTML=`❌ 加载失败<br>${e.message}`;}}
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
loadPDF(); refreshBookmarkList();
|
||||
</script>
|
||||
|
||||
<?php elseif ($isChapterPage && $isTxt && $txtData): ?>
|
||||
<!-- TXT小说阅读页 -->
|
||||
<div class="top-bar"><div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div></div><div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div></div>
|
||||
<div class="content" id="reader"><div id="txtContent"></div><div class="ebook-nav"><button id="prevChapterBtn" disabled>◀ 上一章</button><button id="nextChapterBtn" disabled>下一章 ▶</button></div><div class="chapter-indicator" id="chapterIndicator"></div></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
const TXT_DATA = <?php echo json_encode($txtData); ?>;
|
||||
let curIdx=0,chapters=TXT_DATA.chapters||[],fontSize=18;
|
||||
globalChapters = chapters;
|
||||
globalTotalChapters = chapters.length;
|
||||
globalCurrentIndex = 0;
|
||||
function applyStyles(){let s=document.getElementById('txt-style');if(!s){s=document.createElement('style');s.id='txt-style';document.head.appendChild(s);}s.textContent=`.ebook-chapter{font-size:${fontSize}px}.ebook-chapter p{margin-bottom:1em;text-indent:2em}`;}
|
||||
function renderChapter(i){if(!chapters||i<0||i>=chapters.length)return;onChapterChange();curIdx=i;globalCurrentIndex = i;document.getElementById('txtContent').innerHTML=`<div class="ebook-chapter"><div class="chapter-title">${escapeHtml(chapters[i].title)}</div>${chapters[i].content}</div>`;document.getElementById('prevChapterBtn').disabled=(i<=0);document.getElementById('nextChapterBtn').disabled=(i>=chapters.length-1);document.getElementById('chapterIndicator').innerText=`第 ${i+1}/${chapters.length} 章 · ${chapters[i].title}`;saveProgress(i);if(typeof updateGlobalProgress === 'function') updateGlobalProgress();}
|
||||
function saveProgress(i){let l=getBookmarks(),idx=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:i+1,time:Date.now(),type:'txt'};if(idx>=0)l[idx]=n;else l.push(n);saveBookmarks(l);}
|
||||
window.jumpToBookmark = function(book,chapter,page){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){renderChapter(Math.min(Math.max(1,page),chapters.length)-1);showToast('✨ 第 '+page+' 章');}else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'txt'}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
function addBookmark(){let n=curIdx+1,l=getBookmarks(),i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),ni={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:n,time:Date.now(),type:'txt'};if(i>=0)l[i]=ni;else l.push(ni);saveBookmarks(l);showToast('✅ 第 '+n+' 章');}
|
||||
document.getElementById('fontPlus').onclick=()=>{fontSize=Math.min(fontSize+2,32);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('fontMinus').onclick=()=>{fontSize=Math.max(fontSize-2,12);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
document.getElementById('prevChapterBtn').onclick=()=>{if(curIdx>0)renderChapter(curIdx-1);resetHideTimer();};
|
||||
document.getElementById('nextChapterBtn').onclick=()=>{if(curIdx<chapters.length-1)renderChapter(curIdx+1);resetHideTimer();};
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
applyStyles();if(chapters.length>0){let saved=0,jump=sessionStorage.getItem('jump_target');if(jump){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jump);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)saved=Math.min(Math.max(1,t.page),chapters.length)-1;}catch(e){}}else{let bks=getBookmarks(),ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);if(ex&&ex.page)saved=Math.min(Math.max(1,ex.page),chapters.length)-1;}renderChapter(saved);}
|
||||
refreshBookmarkList();
|
||||
createGlobalProgressBar(globalTotalChapters, globalCurrentIndex, globalChapters);
|
||||
</script>
|
||||
|
||||
<?php elseif ($isChapterPage && $isEpub && $epubData): ?>
|
||||
<!-- EPUB阅读页 -->
|
||||
<div class="top-bar"><div class="top-bar-left"><button class="back-btn" id="backBtn">←</button><div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a> / <a href="<?php echo $currentFile; ?>?book=<?php echo rawurlencode($book); ?>"><?php echo htmlspecialchars(mb_substr($book, 0, 12)); ?></a></div></div><div><button id="addBookmarkBtn" class="bookmark">⭐ 加书签</button></div></div>
|
||||
<div class="content" id="reader">
|
||||
<?php if ($epubData['type'] == 'comic'): ?>
|
||||
<?php $comicImages = $epubData['images']; ?>
|
||||
<div id="comicViewer">
|
||||
<?php foreach($comicImages as $idx => $imgPath): ?>
|
||||
<div class="canvas-container" data-page="<?php echo $idx+1; ?>">
|
||||
<img src="<?php echo $imgPath; ?>" loading="lazy" style="max-width:100%; height:auto; border-radius:8px; display:block; margin:0 auto;"
|
||||
onerror="this.onerror=null; this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22300%22 height=%22400%22%3E%3Crect width=%22300%22 height=%22400%22 fill=%22%23333%22/%3E%3Ctext x=%22150%22 y=%22200%22 fill=%22%23fff%22 text-anchor=%22middle%22%3E图片加载失败%3C/text%3E%3C/svg%3E';">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
const TOTAL_PAGES = <?php echo count($comicImages); ?>;
|
||||
|
||||
function getCurrentPage(){
|
||||
let cs = document.querySelectorAll('.canvas-container');
|
||||
let viewportTop = window.scrollY;
|
||||
let viewportHeight = window.innerHeight;
|
||||
let bestPage = 1;
|
||||
let bestDistance = Infinity;
|
||||
for(let i=0; i<cs.length; i++){
|
||||
let rect = cs[i].getBoundingClientRect();
|
||||
let center = rect.top + rect.height/2;
|
||||
let distance = Math.abs(center - viewportHeight/2);
|
||||
if(distance < bestDistance){
|
||||
bestDistance = distance;
|
||||
bestPage = i+1;
|
||||
}
|
||||
}
|
||||
return bestPage;
|
||||
}
|
||||
|
||||
function addBookmark(){
|
||||
let p = getCurrentPage();
|
||||
let l = getBookmarks();
|
||||
let i = l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);
|
||||
let n = {book:CURRENT_BOOK, chapter:CURRENT_CHAPTER, chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME, page:p, time:Date.now()};
|
||||
if(i>=0) l[i]=n;
|
||||
else l.push(n);
|
||||
saveBookmarks(l);
|
||||
showToast('✅ 第 '+p+' 页');
|
||||
}
|
||||
|
||||
function jumpToPage(p){
|
||||
p = Math.min(Math.max(1, p), TOTAL_PAGES);
|
||||
let t = document.querySelector(`.canvas-container[data-page="${p}"]`);
|
||||
if(t){
|
||||
t.scrollIntoView({behavior:'smooth', block:'start'});
|
||||
showToast('✨ 第 '+p+' 页');
|
||||
}
|
||||
}
|
||||
|
||||
window.jumpToBookmark = function(book,chapter,page){
|
||||
if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){
|
||||
jumpToPage(page);
|
||||
}else{
|
||||
sessionStorage.setItem('jump_target', JSON.stringify({book,chapter,page}));
|
||||
location.href = BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('backBtn').onclick = ()=>{
|
||||
if(document.referrer && document.referrer.includes(window.location.host)) history.back();
|
||||
else location.href = BASE_FILE;
|
||||
};
|
||||
document.getElementById('addBookmarkBtn').onclick = addBookmark;
|
||||
refreshBookmarkList();
|
||||
</script>
|
||||
<?php else: ?>
|
||||
<div id="epubContent"></div>
|
||||
<div class="ebook-nav"><button id="prevChapterBtn" disabled>◀ 上一章</button><button id="nextChapterBtn" disabled>下一章 ▶</button></div>
|
||||
<div class="chapter-indicator" id="chapterIndicator"></div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
CURRENT_CHAPTER = "<?php echo addslashes($chapter); ?>";
|
||||
const CHAPTER_NAME = "<?php echo addslashes($chapter); ?>";
|
||||
const BASE_FILE = "<?php echo $currentFile; ?>";
|
||||
const EPUB_DATA = <?php echo json_encode($epubData); ?>;
|
||||
let curIdx=0,chapters=EPUB_DATA.htmlContents||[],css=EPUB_DATA.cssContent||'',fontSize=18;
|
||||
globalChapters = chapters;
|
||||
globalTotalChapters = chapters.length;
|
||||
globalCurrentIndex = 0;
|
||||
function applyStyles(){let s=document.getElementById('epub-style');if(!s){s=document.createElement('style');s.id='epub-style';document.head.appendChild(s);}s.textContent=`.ebook-chapter{font-size:${fontSize}px}.ebook-chapter img{max-width:100%;height:auto;display:block;margin:1em auto;border-radius:12px}.ebook-chapter p{margin-bottom:1em}${css}`;}
|
||||
function renderChapter(i){if(!chapters||i<0||i>=chapters.length)return;onChapterChange();curIdx=i;globalCurrentIndex = i;let c=chapters[i];document.getElementById('epubContent').innerHTML=`<div class="ebook-chapter"><div class="chapter-title">${escapeHtml(c.title)}</div>${c.content}</div>`;document.getElementById('prevChapterBtn').disabled=(i<=0);document.getElementById('nextChapterBtn').disabled=(i>=chapters.length-1);document.getElementById('chapterIndicator').innerText=`第 ${i+1}/${chapters.length} 章 · ${c.title}`;saveProgress(i);if(typeof updateGlobalProgress === 'function') updateGlobalProgress();}
|
||||
function saveProgress(i){let l=getBookmarks(),idx=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),n={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:i+1,time:Date.now(),type:'ebook'};if(idx>=0)l[idx]=n;else l.push(n);saveBookmarks(l);}
|
||||
window.jumpToBookmark = function(book,chapter,page){if(book===CURRENT_BOOK&&chapter===CURRENT_CHAPTER){renderChapter(Math.min(Math.max(1,page),chapters.length)-1);showToast('✨ 第 '+page+' 章');}else{sessionStorage.setItem('jump_target',JSON.stringify({book,chapter,page,type:'ebook'}));location.href=BASE_FILE+'?book='+encodeURIComponent(book)+'&chapter='+encodeURIComponent(chapter);}};
|
||||
function addBookmark(){let n=curIdx+1,l=getBookmarks(),i=l.findIndex(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER),ni={book:CURRENT_BOOK,chapter:CURRENT_CHAPTER,chapterName:CHAPTER_NAME.length>35?CHAPTER_NAME.substring(0,32)+'...':CHAPTER_NAME,page:n,time:Date.now(),type:'ebook'};if(i>=0)l[i]=ni;else l.push(ni);saveBookmarks(l);showToast('✅ 第 '+n+' 章');}
|
||||
document.getElementById('fontPlus').onclick=()=>{fontSize=Math.min(fontSize+2,32);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('fontMinus').onclick=()=>{fontSize=Math.max(fontSize-2,12);applyStyles();renderChapter(curIdx);showToast(`字体 ${fontSize}px`);resetHideTimer();};
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer&&document.referrer.includes(window.location.host))history.back();else location.href=BASE_FILE;};
|
||||
document.getElementById('prevChapterBtn').onclick=()=>{if(curIdx>0)renderChapter(curIdx-1);resetHideTimer();};
|
||||
document.getElementById('nextChapterBtn').onclick=()=>{if(curIdx<chapters.length-1)renderChapter(curIdx+1);resetHideTimer();};
|
||||
document.getElementById('addBookmarkBtn').onclick=addBookmark;
|
||||
applyStyles();if(chapters.length>0){let saved=0,jump=sessionStorage.getItem('jump_target');if(jump){sessionStorage.removeItem('jump_target');try{let t=JSON.parse(jump);if(t.book===CURRENT_BOOK&&t.chapter===CURRENT_CHAPTER&&t.page)saved=Math.min(Math.max(1,t.page),chapters.length)-1;}catch(e){}}else{let bks=getBookmarks(),ex=bks.find(b=>b.book===CURRENT_BOOK&&b.chapter===CURRENT_CHAPTER);if(ex&&ex.page)saved=Math.min(Math.max(1,ex.page),chapters.length)-1;}renderChapter(saved);}
|
||||
refreshBookmarkList();
|
||||
createGlobalProgressBar(globalTotalChapters, globalCurrentIndex, globalChapters);
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif ($book): ?>
|
||||
<!-- 书籍章节列表页 -->
|
||||
<div class="content">
|
||||
<div class="top-bar" style="position:relative; margin-top:-70px; margin-bottom:20px;">
|
||||
<div class="top-bar-left">
|
||||
<button class="back-btn" id="backBtn">←</button>
|
||||
<div class="nav-links"><a href="<?php echo $currentFile; ?>">🏠 书架</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="page-title">📖 <?php echo htmlspecialchars($book); ?></h2>
|
||||
<div class="shelf-grid">
|
||||
<?php
|
||||
$files = scanDirectory($baseDir . '/' . $book);
|
||||
if ($files) {
|
||||
foreach ($files as $f) {
|
||||
$name = basename($f);
|
||||
$url = $currentFile . "?book=" . rawurlencode($book) . "&chapter=" . rawurlencode($name);
|
||||
if (stripos($name, '.txt') !== false) $icon = '📖';
|
||||
elseif (stripos($name, '.epub') !== false) $icon = '📘';
|
||||
else $icon = (stripos($name, '.pdf') !== false ? '📕' : '📁');
|
||||
echo '<a href="' . $url . '" class="shelf-item">';
|
||||
echo '<div class="emoji">' . $icon . '</div>';
|
||||
echo '<div>' . htmlspecialchars($name) . '</div>';
|
||||
echo '</a>';
|
||||
}
|
||||
} else {
|
||||
echo '<div style="grid-column:1/-1; text-align:center; padding:50px; color:rgba(255,255,255,0.6);">📭 没有章节</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
CURRENT_BOOK = "<?php echo addslashes($book); ?>";
|
||||
refreshBookmarkList();
|
||||
document.getElementById('backBtn').onclick=()=>{if(document.referrer?.includes(window.location.host))history.back();else location.href='<?php echo $currentFile; ?>';};
|
||||
</script>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- 书架首页 -->
|
||||
<div class="content">
|
||||
<h1 class="page-title">📚 我的书架</h1>
|
||||
<div class="shelf-grid">
|
||||
<?php
|
||||
$books = scanDirectory($baseDir);
|
||||
if ($books) {
|
||||
foreach ($books as $b) {
|
||||
if (is_dir($b)) {
|
||||
$name = basename($b);
|
||||
echo '<a href="' . $currentFile . '?book=' . rawurlencode($name) . '" class="shelf-item">';
|
||||
echo '<div class="emoji">📖</div>';
|
||||
echo '<div>' . htmlspecialchars($name) . '</div>';
|
||||
echo '</a>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo '<div style="grid-column:1/-1; text-align:center; padding:50px; color:rgba(255,255,255,0.6);">📭 请在 PDF 文件夹里放入书籍文件夹</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
refreshBookmarkList();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
// _bridge.php
|
||||
// Bridge script to call PHP spider methods from Node.js
|
||||
// Usage: php _bridge.php <file_path> <method_name> <env_json> <arg1_json> <arg2_json> ...
|
||||
|
||||
// Disable error output to stdout to avoid breaking JSON
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
|
||||
define('DRPY_BRIDGE', true);
|
||||
|
||||
// Helper to send JSON response
|
||||
function sendResponse($data) {
|
||||
// Ensure data is UTF-8 encoded
|
||||
// echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Helper to send Error response
|
||||
function sendError($message, $trace = '') {
|
||||
echo json_encode([
|
||||
'error' => $message,
|
||||
'traceback' => $trace
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Set global error handler to catch warnings/notices and prevent them from corrupting stdout
|
||||
set_error_handler(function($errno, $errstr, $errfile, $errline) {
|
||||
// We can log errors to stderr so they don't mess up stdout JSON
|
||||
fwrite(STDERR, "PHP Error [$errno]: $errstr in $errfile on line $errline\n");
|
||||
return false; // Let normal error handler continue (but display_errors is 0 so no stdout)
|
||||
});
|
||||
|
||||
// Set exception handler
|
||||
set_exception_handler(function($e) {
|
||||
sendError($e->getMessage(), $e->getTraceAsString());
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. Parse Arguments
|
||||
if ($argc < 4) {
|
||||
throw new Exception("Invalid arguments. Usage: php _bridge.php <file> <method> <env> [args...]");
|
||||
}
|
||||
|
||||
$filePath = $argv[1];
|
||||
$methodName = $argv[2];
|
||||
$envJson = $argv[3];
|
||||
$env = json_decode($envJson, true) ?? [];
|
||||
|
||||
$args = [];
|
||||
for ($i = 4; $i < $argc; $i++) {
|
||||
// Args are passed as individual JSON strings
|
||||
$args[] = json_decode($argv[$i], true);
|
||||
}
|
||||
|
||||
// 2. Load File
|
||||
if (!file_exists($filePath)) {
|
||||
throw new Exception("File not found: $filePath");
|
||||
}
|
||||
|
||||
// Capture any output during require (e.g. trailing newlines or echoes in file)
|
||||
ob_start();
|
||||
require_once $filePath;
|
||||
$output = ob_get_clean();
|
||||
if (trim($output) !== '') {
|
||||
fwrite(STDERR, "Output during require: $output\n");
|
||||
}
|
||||
|
||||
if (!class_exists('Spider')) {
|
||||
throw new Exception("Class 'Spider' not found in $filePath");
|
||||
}
|
||||
|
||||
// 3. Instantiate Spider
|
||||
$spider = new Spider();
|
||||
|
||||
// AUTO-INIT: Call init() before any other method if it's not init itself
|
||||
if ($methodName !== 'init' && method_exists($spider, 'init')) {
|
||||
$extend = $env['ext'] ?? '';
|
||||
$spider->init($extend);
|
||||
}
|
||||
|
||||
// 4. Check Method
|
||||
if (!method_exists($spider, $methodName)) {
|
||||
// If the method doesn't exist, we might be calling a mapped method that isn't implemented.
|
||||
// Or maybe we should check for magic method __call?
|
||||
// For now, throw error.
|
||||
throw new Exception("Method '$methodName' not found in Spider class");
|
||||
}
|
||||
|
||||
// 5. Call Method
|
||||
$result = call_user_func_array([$spider, $methodName], $args);
|
||||
|
||||
// 6. Return Result
|
||||
sendResponse($result);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
sendError($e->getMessage(), $e->getTraceAsString());
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
// crawler_bridge.php
|
||||
// Bridge script for Python to call PHP Spider methods and get JSON output.
|
||||
// Usage: php crawler_bridge.php <spider_path> <method> [args...]
|
||||
|
||||
ini_set('display_errors', 0); // Disable error printing to stdout
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$output = ['status' => 'error', 'data' => null, 'message' => ''];
|
||||
|
||||
try {
|
||||
if ($argc < 3) {
|
||||
throw new Exception("Usage: php crawler_bridge.php <spider_path> <method> [args...]");
|
||||
}
|
||||
|
||||
$spiderPath = $argv[1];
|
||||
$method = $argv[2];
|
||||
$args = array_slice($argv, 3);
|
||||
|
||||
if (!file_exists($spiderPath)) {
|
||||
throw new Exception("Spider file not found: $spiderPath");
|
||||
}
|
||||
|
||||
// Capture any output during include
|
||||
ob_start();
|
||||
require_once $spiderPath;
|
||||
ob_end_clean();
|
||||
|
||||
if (!class_exists('Spider')) {
|
||||
throw new Exception("Class 'Spider' not found in $spiderPath");
|
||||
}
|
||||
|
||||
$spider = new Spider();
|
||||
if (method_exists($spider, 'init')) {
|
||||
$spider->init();
|
||||
}
|
||||
|
||||
if (!method_exists($spider, $method)) {
|
||||
throw new Exception("Method '$method' not found in Spider class");
|
||||
}
|
||||
|
||||
// Call method with args
|
||||
// Note: Args passed from CLI are strings. Some methods might expect specific types.
|
||||
// However, PHP is loosely typed, so it usually works.
|
||||
// Special handling for extend field or complex structures might be needed if passed via CLI,
|
||||
// but standard DrPy methods usually take simple scalars (tid, page, filter) or arrays.
|
||||
// For complex args (like filter array), we might need to decode JSON passed as string.
|
||||
|
||||
$methodArgs = [];
|
||||
foreach ($args as $arg) {
|
||||
// Try to decode JSON args if they look like JSON
|
||||
$decoded = json_decode($arg, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$methodArgs[] = $decoded;
|
||||
} else {
|
||||
$methodArgs[] = $arg;
|
||||
}
|
||||
}
|
||||
|
||||
$result = call_user_func_array([$spider, $method], $methodArgs);
|
||||
|
||||
$output['status'] = 'success';
|
||||
$output['data'] = $result;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$output['message'] = $e->getMessage();
|
||||
$output['trace'] = $e->getTraceAsString();
|
||||
}
|
||||
|
||||
echo json_encode($output, JSON_UNESCAPED_UNICODE);
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"parses": [
|
||||
{
|
||||
"name": "J1",
|
||||
"url": "https://kalbim.xatut.top/kalbim2025/781718/play/video_player.php?url=",
|
||||
"type": 1,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "J2",
|
||||
"url": "http://sspa8.top:8100/api/?key=1060089351&url=",
|
||||
"type": 1,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "J芒果4k",
|
||||
"url": "http://mg.itufm.top/mg.php?url=",
|
||||
"type": 1,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W花旗",
|
||||
"url": "https://www.huaqi.live/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W冰豆",
|
||||
"url": "https://bd.jx.cn/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W盘古",
|
||||
"url": "https://www.playm3u8.cn/jiexi.php?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W1",
|
||||
"url": "https://jx.xymp4.cc/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W3",
|
||||
"url": "https://yparse.ik9.cc/index.php?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W4",
|
||||
"url": "https://jiexi.site/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W5",
|
||||
"url": "https://jx.m3u8.tv/jiexi/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W7",
|
||||
"url": "https://www.pangujiexi.com/jiexi/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W8",
|
||||
"url": "https://www.pouyun.com/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W9",
|
||||
"url": "https://jx.xmflv.com/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Wa",
|
||||
"url": "https://jx.xmflv.cc/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Wb",
|
||||
"url": "https://jx.yparse.com/index.php?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Wc",
|
||||
"url": "https://www.8090g.cn/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Wz",
|
||||
"url": "https://www.ckplayer.vip/jiexi/?url=",
|
||||
"type": 0,
|
||||
"header": {
|
||||
"User-Agent": "Mozilla/5.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"lives": []
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright 道长所有
|
||||
* Date: 2026/01/23
|
||||
*/
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
// http://127.0.0.1:9980/config.php
|
||||
// ==================
|
||||
// 1. 生成 sites
|
||||
// ==================
|
||||
// 动态获取服务器地址
|
||||
$isHttps = (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1))
|
||||
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
|
||||
$scheme = $isHttps ? 'https://' : 'http://';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
// 处理路径(适配子目录部署)
|
||||
$path = dirname($_SERVER['SCRIPT_NAME']);
|
||||
if ($path === '/' || $path === '\\') {
|
||||
$path = '';
|
||||
}
|
||||
$path = str_replace('\\', '/', $path); // 统一转为 /
|
||||
|
||||
$baseUrl = $scheme . $host . $path;
|
||||
|
||||
$dir = __DIR__;
|
||||
$self = basename(__FILE__);
|
||||
$files = scandir($dir);
|
||||
|
||||
$sites = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (pathinfo($file, PATHINFO_EXTENSION) !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 排除特定文件:
|
||||
// 1. 系统/框架文件 (index.php, spider.php 等)
|
||||
// 2. 当前文件 ($self)
|
||||
// 3. 以 _ 开头的文件 (如 _backup.php)
|
||||
// 4. config 开头的文件 (如 config_old.php)
|
||||
if (in_array($file, ['index.php', 'spider.php', 'example_t4.php', 'test_runner.php']) ||
|
||||
$file === $self ||
|
||||
strpos($file, '_') === 0 ||
|
||||
fnmatch('config*.php', $file) ||
|
||||
stripos($file, 'test') !== false ||
|
||||
stripos($file, 'bridge') !== false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
|
||||
$site = [
|
||||
"key" => "php_" . $filename,
|
||||
"name" => $filename . "(PHP)",
|
||||
"type" => 4,
|
||||
"api" => $baseUrl . "/" . $filename . ".php",
|
||||
"searchable" => 1,
|
||||
"quickSearch" => 1,
|
||||
"changeable" => 0
|
||||
];
|
||||
|
||||
if (strpos($filename, '[书]') !== false) {
|
||||
$site['类型'] = '小说';
|
||||
} elseif (strpos($filename, '[画]') !== false) {
|
||||
$site['类型'] = '漫画';
|
||||
}
|
||||
|
||||
$sites[] = $site;
|
||||
}
|
||||
|
||||
// ==================
|
||||
// 2. 尝试加载 index.json (同级) 或 ../drpy-node/index.json 或 ../../drpy-node/index.json
|
||||
// ==================
|
||||
$possiblePaths = [
|
||||
$dir . '/config.json',
|
||||
$dir . '/index.json',
|
||||
$dir . '/../drpy-node/index.json',
|
||||
$dir . '/../../drpy-node/index.json'
|
||||
];
|
||||
|
||||
$indexJsonPath = false;
|
||||
foreach ($possiblePaths as $path) {
|
||||
$realPath = realpath($path);
|
||||
if ($realPath && is_file($realPath)) {
|
||||
$indexJsonPath = $realPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($indexJsonPath && is_file($indexJsonPath)) {
|
||||
$content = file_get_contents($indexJsonPath);
|
||||
$json = json_decode($content, true);
|
||||
|
||||
// JSON 合法并且是数组
|
||||
if (is_array($json)) {
|
||||
// 替换 sites
|
||||
$json['sites'] = $sites;
|
||||
|
||||
echo json_encode(
|
||||
$json,
|
||||
JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================
|
||||
// 3. 找不到或失败,回退只返回 sites
|
||||
// ==================
|
||||
echo json_encode(
|
||||
["sites" => $sites],
|
||||
JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
import subprocess
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import threading
|
||||
import queue
|
||||
from datetime import datetime
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
# --- 用户配置区域 (User Configuration) ---
|
||||
# 默认使用的PHP爬虫文件路径
|
||||
# 获取当前脚本所在目录
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# 获取项目根目录 (假设脚本在 scripts/python,根目录在 ../../)
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "../../"))
|
||||
DEFAULT_SPIDER = os.path.join(SCRIPT_DIR, "74P福利图 ᵈᶻ[画].php")
|
||||
|
||||
# 每个分类默认最大爬取页数 (设置为 0 或 None 表示不限制,直到爬完)
|
||||
DEFAULT_MAX_PAGES = 1
|
||||
# 默认并发线程数
|
||||
DEFAULT_THREADS = 8
|
||||
# 是否解析最终播放地址 (True: 解析并存入resolved_url, False: 只存入原始链接)
|
||||
RESOLVE_FINAL_URLS = True
|
||||
# PHP 命令路径
|
||||
PHP_CMD = "php"
|
||||
# 桥接脚本路径
|
||||
BRIDGE_SCRIPT = os.path.join(SCRIPT_DIR, "_crawler_bridge.php")
|
||||
|
||||
# --- 数据库管理 (Database Manager) ---
|
||||
class DBManager:
|
||||
def __init__(self, db_path):
|
||||
# check_same_thread=False 允许在多线程中使用同一个连接,但需要我们自己加锁
|
||||
self.conn = sqlite3.connect(db_path, check_same_thread=False)
|
||||
self.cursor = self.conn.cursor()
|
||||
self.lock = threading.Lock()
|
||||
self.init_tables()
|
||||
self._source_cache = {}
|
||||
|
||||
def init_tables(self):
|
||||
with self.lock:
|
||||
# 优化:移除 source_file 字段 (假设每个DB只对应一个源)
|
||||
# 优化:移除 type_name (通过关联查询获取)
|
||||
# 优化:crawled_at 使用 INTEGER 时间戳
|
||||
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
tid TEXT PRIMARY KEY,
|
||||
name TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS vods (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
vod_id TEXT UNIQUE,
|
||||
vod_name TEXT,
|
||||
type_id TEXT,
|
||||
vod_pic TEXT,
|
||||
vod_remarks TEXT,
|
||||
vod_content TEXT,
|
||||
crawled_at INTEGER,
|
||||
FOREIGN KEY(type_id) REFERENCES categories(tid)
|
||||
)
|
||||
''')
|
||||
|
||||
# 新增:播放源表 (归一化 play_from)
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS play_sources (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE
|
||||
)
|
||||
''')
|
||||
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS episodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
vod_pk INTEGER,
|
||||
sid INTEGER,
|
||||
name TEXT,
|
||||
raw_url TEXT,
|
||||
resolved_url TEXT,
|
||||
FOREIGN KEY(vod_pk) REFERENCES vods(id),
|
||||
FOREIGN KEY(sid) REFERENCES play_sources(id)
|
||||
)
|
||||
''')
|
||||
self.conn.commit()
|
||||
|
||||
def get_or_create_source(self, name):
|
||||
# 简单缓存
|
||||
if name in self._source_cache:
|
||||
return self._source_cache[name]
|
||||
|
||||
with self.lock:
|
||||
try:
|
||||
self.cursor.execute('INSERT OR IGNORE INTO play_sources (name) VALUES (?)', (name,))
|
||||
self.cursor.execute('SELECT id FROM play_sources WHERE name = ?', (name,))
|
||||
row = self.cursor.fetchone()
|
||||
if row:
|
||||
sid = row[0]
|
||||
self._source_cache[name] = sid
|
||||
return sid
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"[DB Error] get_or_create_source: {e}")
|
||||
return 0
|
||||
|
||||
def save_category(self, tid, name):
|
||||
with self.lock:
|
||||
try:
|
||||
self.cursor.execute('INSERT OR IGNORE INTO categories (tid, name) VALUES (?, ?)',
|
||||
(tid, name))
|
||||
# 如果名称更新了,也可以 update
|
||||
self.cursor.execute('UPDATE categories SET name = ? WHERE tid = ? AND name != ?', (name, tid, name))
|
||||
self.conn.commit()
|
||||
except Exception as e:
|
||||
print(f"[DB Error] save_category: {e}")
|
||||
|
||||
def item_exists(self, vod_id):
|
||||
with self.lock:
|
||||
try:
|
||||
self.cursor.execute('SELECT 1 FROM vods WHERE vod_id = ?', (vod_id,))
|
||||
return self.cursor.fetchone() is not None
|
||||
except Exception as e:
|
||||
print(f"[DB Error] item_exists: {e}")
|
||||
return False
|
||||
|
||||
def save_vod(self, data):
|
||||
with self.lock:
|
||||
try:
|
||||
self.cursor.execute('''
|
||||
INSERT OR REPLACE INTO vods (vod_id, vod_name, type_id, vod_pic, vod_remarks, vod_content, crawled_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
data.get('vod_id'),
|
||||
data.get('vod_name'),
|
||||
data.get('type_id'),
|
||||
data.get('vod_pic'),
|
||||
data.get('vod_remarks'),
|
||||
data.get('vod_content'),
|
||||
int(time.time())
|
||||
))
|
||||
vod_pk = self.cursor.lastrowid
|
||||
if vod_pk == 0:
|
||||
self.cursor.execute('SELECT id FROM vods WHERE vod_id = ?', (data.get('vod_id'),))
|
||||
res = self.cursor.fetchone()
|
||||
if res: vod_pk = res[0]
|
||||
|
||||
self.conn.commit()
|
||||
return vod_pk
|
||||
except Exception as e:
|
||||
print(f"[DB Error] save_vod: {e}")
|
||||
return None
|
||||
|
||||
def save_episodes(self, vod_pk, episodes):
|
||||
# 预处理 source_id 以减少锁内操作时间
|
||||
# 但 get_or_create_source 本身也加锁,所以这里可以先收集
|
||||
processed_eps = []
|
||||
for ep in episodes:
|
||||
sid = self.get_or_create_source(ep['play_from'])
|
||||
processed_eps.append((sid, ep['name'], ep['url'], ep.get('resolved_url', '')))
|
||||
|
||||
with self.lock:
|
||||
try:
|
||||
self.cursor.execute('DELETE FROM episodes WHERE vod_pk = ?', (vod_pk,))
|
||||
self.cursor.executemany('''
|
||||
INSERT INTO episodes (vod_pk, sid, name, raw_url, resolved_url)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', [(vod_pk, sid, name, raw_url, res_url) for sid, name, raw_url, res_url in processed_eps])
|
||||
self.conn.commit()
|
||||
except Exception as e:
|
||||
print(f"[DB Error] save_episodes: {e}")
|
||||
|
||||
def close(self):
|
||||
self.conn.close()
|
||||
|
||||
# --- PHP 桥接调用 (PHP Bridge) ---
|
||||
class PHPBridge:
|
||||
def __init__(self, spider_path):
|
||||
self.spider_path = spider_path
|
||||
|
||||
def call(self, method, *args):
|
||||
# 构建命令
|
||||
cmd = [PHP_CMD, BRIDGE_SCRIPT, self.spider_path, method]
|
||||
cmd_args = []
|
||||
for arg in args:
|
||||
if isinstance(arg, (dict, list)):
|
||||
cmd_args.append(json.dumps(arg))
|
||||
else:
|
||||
cmd_args.append(str(arg))
|
||||
cmd.extend(cmd_args)
|
||||
|
||||
try:
|
||||
# subprocess.run 是同步阻塞的,但在多线程中调用是安全的
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
|
||||
if result.returncode != 0:
|
||||
if "Warning" not in result.stderr and "Notice" not in result.stderr:
|
||||
pass
|
||||
return None
|
||||
|
||||
output = result.stdout.strip()
|
||||
try:
|
||||
json_res = json.loads(output)
|
||||
if json_res['status'] == 'success':
|
||||
return json_res['data']
|
||||
else:
|
||||
return None
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Bridge Error] {e}")
|
||||
return None
|
||||
|
||||
# --- 任务追踪器 (Task Tracker) ---
|
||||
class TaskTracker:
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
self.cond = threading.Condition(self.lock)
|
||||
self.pending = 0
|
||||
|
||||
def add(self, n=1):
|
||||
with self.lock:
|
||||
self.pending += n
|
||||
|
||||
def done(self):
|
||||
with self.lock:
|
||||
self.pending -= 1
|
||||
if self.pending == 0:
|
||||
self.cond.notify_all()
|
||||
|
||||
def wait_until_done(self):
|
||||
with self.lock:
|
||||
while self.pending > 0:
|
||||
self.cond.wait()
|
||||
|
||||
# --- 统计与监控 (Stats & Monitor) ---
|
||||
class Stats:
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
self.categories_found = 0
|
||||
self.pages_scanned = 0
|
||||
self.items_found = 0
|
||||
self.items_processed = 0
|
||||
self.items_skipped = 0
|
||||
self.episodes_resolved = 0
|
||||
self.errors = 0
|
||||
self.start_time = time.time()
|
||||
|
||||
def inc(self, field, count=1):
|
||||
with self.lock:
|
||||
setattr(self, field, getattr(self, field) + count)
|
||||
|
||||
# --- 爬虫逻辑 (Crawler Logic) ---
|
||||
class Crawler:
|
||||
def __init__(self, spider_path, db_path, max_pages=DEFAULT_MAX_PAGES, max_workers=DEFAULT_THREADS):
|
||||
self.spider_path = spider_path
|
||||
self.bridge = PHPBridge(spider_path)
|
||||
self.db = DBManager(db_path)
|
||||
self.max_pages = max_pages
|
||||
self.max_workers = max_workers
|
||||
self.stats = Stats()
|
||||
self.executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||
self.tracker = TaskTracker()
|
||||
self.running = True
|
||||
|
||||
# 启动监控线程
|
||||
self.monitor_thread = threading.Thread(target=self.monitor_loop, daemon=True)
|
||||
self.monitor_thread.start()
|
||||
|
||||
def submit_task(self, func, *args):
|
||||
self.tracker.add()
|
||||
self.executor.submit(self._wrap_task, func, *args)
|
||||
|
||||
def _wrap_task(self, func, *args):
|
||||
try:
|
||||
func(*args)
|
||||
except Exception as e:
|
||||
print(f"[Task Error] {e}")
|
||||
self.stats.inc('errors')
|
||||
finally:
|
||||
self.tracker.done()
|
||||
|
||||
def run(self):
|
||||
print(f"🚀 开始并发爬取: {os.path.basename(self.spider_path)}")
|
||||
print(f"⚙️ 配置: 最大线程={self.max_workers}, 每个分类最大页数={self.max_pages}, 解 析地址={RESOLVE_FINAL_URLS}")
|
||||
|
||||
# 1. 获取首页分类
|
||||
home_data = self.bridge.call('homeContent', True)
|
||||
if not home_data or 'class' not in home_data:
|
||||
print("❌ 无法获取分类信息,退出。")
|
||||
return
|
||||
|
||||
categories = home_data['class']
|
||||
self.stats.categories_found = len(categories)
|
||||
print(f"📋 获取到 {len(categories)} 个分类,开始派发任务...")
|
||||
|
||||
# 2. 保存分类并派发分类任务
|
||||
for cat in categories:
|
||||
tid = str(cat['type_id'])
|
||||
name = cat['type_name']
|
||||
self.db.save_category(tid, name)
|
||||
self.submit_task(self.process_category, tid, name)
|
||||
|
||||
# 3. 等待所有任务完成
|
||||
self.tracker.wait_until_done()
|
||||
self.running = False
|
||||
|
||||
self.print_final_stats()
|
||||
|
||||
# 关闭 executor 和 db
|
||||
self.executor.shutdown(wait=True)
|
||||
self.db.close()
|
||||
|
||||
def monitor_loop(self):
|
||||
while self.running:
|
||||
self.print_progress()
|
||||
time.sleep(1)
|
||||
|
||||
def print_progress(self):
|
||||
elapsed = time.time() - self.stats.start_time
|
||||
speed = self.stats.items_processed / elapsed if elapsed > 0 else 0
|
||||
# \033[K 清除当前行剩余内容,确保更新时不会有残留字符
|
||||
sys.stdout.write(
|
||||
f"\r\033[K⏱️ {elapsed:.1f}s | "
|
||||
f"Pages: {self.stats.pages_scanned} | "
|
||||
f"Items: {self.stats.items_processed}/{self.stats.items_found} | "
|
||||
f"Skip: {self.stats.items_skipped} | "
|
||||
f"Eps: {self.stats.episodes_resolved} | "
|
||||
f"Speed: {speed:.2f} it/s | "
|
||||
f"Err: {self.stats.errors}"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
|
||||
def print_final_stats(self):
|
||||
elapsed = time.time() - self.stats.start_time
|
||||
print("\n" + "-" * 50)
|
||||
print(f"统计报告:")
|
||||
print(f" 总耗时: {elapsed:.2f} 秒")
|
||||
print(f" 扫描页数: {self.stats.pages_scanned}")
|
||||
print(f" 处理资源: {self.stats.items_processed}")
|
||||
print(f" 跳过资源: {self.stats.items_skipped}")
|
||||
print(f" 解析集数: {self.stats.episodes_resolved}")
|
||||
print(f" 错误数量: {self.stats.errors}")
|
||||
print("-" * 50)
|
||||
|
||||
def process_category(self, tid, tname):
|
||||
cat_data = self.bridge.call('categoryContent', tid, 1, False, {})
|
||||
|
||||
if not cat_data or 'list' not in cat_data:
|
||||
self.stats.inc('errors')
|
||||
return
|
||||
|
||||
items = cat_data.get('list', [])
|
||||
self.stats.inc('items_found', len(items))
|
||||
self.stats.inc('pages_scanned')
|
||||
|
||||
for item in items:
|
||||
item['type_id'] = tid
|
||||
item['type_name'] = tname
|
||||
self.submit_task(self.process_item, item)
|
||||
|
||||
page_count = 0
|
||||
if 'pagecount' in cat_data:
|
||||
try:
|
||||
page_count = int(cat_data['pagecount'])
|
||||
except:
|
||||
page_count = 9999
|
||||
|
||||
# 递归触发第2页(如果需要)
|
||||
# 如果明确返回只有1页,则停止;否则只要没达到max_pages就尝试下一页
|
||||
if page_count != 1:
|
||||
next_page = 2
|
||||
if not self.max_pages or next_page <= self.max_pages:
|
||||
self.submit_task(self.process_page, tid, tname, next_page)
|
||||
|
||||
def process_page(self, tid, tname, page):
|
||||
cat_data = self.bridge.call('categoryContent', tid, page, False, {})
|
||||
if not cat_data or 'list' not in cat_data:
|
||||
self.stats.inc('errors')
|
||||
return
|
||||
|
||||
items = cat_data.get('list', [])
|
||||
self.stats.inc('items_found', len(items))
|
||||
self.stats.inc('pages_scanned')
|
||||
|
||||
if len(items) == 0:
|
||||
return
|
||||
|
||||
for item in items:
|
||||
item['type_id'] = tid
|
||||
item['type_name'] = tname
|
||||
self.submit_task(self.process_item, item)
|
||||
|
||||
# 提交下一页任务(递归爬取)
|
||||
if len(items) > 0:
|
||||
next_page = page + 1
|
||||
if not self.max_pages or next_page <= self.max_pages:
|
||||
self.submit_task(self.process_page, tid, tname, next_page)
|
||||
|
||||
def process_item(self, item):
|
||||
vod_id = item['vod_id']
|
||||
vod_name = item['vod_name']
|
||||
|
||||
# 增量爬取检查:如果数据库中已存在该 vod_id,则跳过
|
||||
if self.db.item_exists(vod_id):
|
||||
# 即使跳过,也可以尝试更新 type_id (如果之前为空)
|
||||
# 但为了性能,这里暂时略过,除非强制更新
|
||||
self.stats.inc('items_skipped')
|
||||
return
|
||||
|
||||
# 详情页爬取
|
||||
detail_res = self.bridge.call('detailContent', [vod_id])
|
||||
if not detail_res or 'list' not in detail_res or not detail_res['list']:
|
||||
self.stats.inc('errors')
|
||||
return
|
||||
|
||||
vod_data = detail_res['list'][0]
|
||||
# 补全可能缺失的字段
|
||||
if 'vod_id' not in vod_data: vod_data['vod_id'] = vod_id
|
||||
if 'type_id' not in vod_data: vod_data['type_id'] = item.get('type_id')
|
||||
|
||||
# 存入 VOD 主表
|
||||
vod_pk = self.db.save_vod(vod_data)
|
||||
if not vod_pk:
|
||||
self.stats.inc('errors')
|
||||
return
|
||||
|
||||
self.stats.inc('items_processed')
|
||||
|
||||
# 处理播放列表
|
||||
play_from_str = vod_data.get('vod_play_from', '')
|
||||
play_url_str = vod_data.get('vod_play_url', '')
|
||||
|
||||
if not play_from_str or not play_url_str:
|
||||
return
|
||||
|
||||
play_from_list = play_from_str.split('$$$')
|
||||
play_url_list = play_url_str.split('$$$')
|
||||
|
||||
all_episodes = []
|
||||
|
||||
for i, source_name in enumerate(play_from_list):
|
||||
if i >= len(play_url_list): break
|
||||
url_text = play_url_list[i]
|
||||
|
||||
# 格式: 名字$地址#名字$地址
|
||||
episodes = url_text.split('#')
|
||||
for ep_str in episodes:
|
||||
if '$' in ep_str:
|
||||
ep_name, ep_url = ep_str.split('$', 1)
|
||||
else:
|
||||
ep_name, ep_url = '正片', ep_str
|
||||
|
||||
episode = {
|
||||
'play_from': source_name,
|
||||
'name': ep_name,
|
||||
'url': ep_url,
|
||||
'resolved_url': ''
|
||||
}
|
||||
|
||||
if RESOLVE_FINAL_URLS:
|
||||
play_res = self.bridge.call('playerContent', source_name, ep_url, [])
|
||||
if play_res and 'url' in play_res:
|
||||
episode['resolved_url'] = play_res['url']
|
||||
self.stats.inc('episodes_resolved')
|
||||
else:
|
||||
pass
|
||||
|
||||
all_episodes.append(episode)
|
||||
|
||||
if all_episodes:
|
||||
self.db.save_episodes(vod_pk, all_episodes)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="DrPy PHP Spider Concurrent Crawler")
|
||||
parser.add_argument("spider", nargs="?", default=DEFAULT_SPIDER, help="PHP spider file path")
|
||||
parser.add_argument("-p", "--max-pages", type=int, default=DEFAULT_MAX_PAGES, help="Max pages per category")
|
||||
parser.add_argument("-t", "--threads", type=int, default=DEFAULT_THREADS, help="Concurrency threshold (max workers)")
|
||||
parser.add_argument("-n", "--no-resolve", action="store_true", help="Skip resolving final playback URLs")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.no_resolve:
|
||||
RESOLVE_FINAL_URLS = False
|
||||
|
||||
spider_file = args.spider
|
||||
if not os.path.exists(spider_file):
|
||||
print(f"Error: File not found: {spider_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# 根据爬虫文件名生成数据库文件名 (例如: spider.php -> spider.db)
|
||||
# 确保数据库文件生成在爬虫文件同级目录
|
||||
spider_dir = os.path.dirname(os.path.abspath(spider_file))
|
||||
base_name = os.path.splitext(os.path.basename(spider_file))[0]
|
||||
db_path = os.path.join(spider_dir, f"{base_name}.db")
|
||||
|
||||
print(f"📁 数据库路径: {db_path}")
|
||||
|
||||
crawler = Crawler(spider_file, db_path, args.max_pages, args.threads)
|
||||
crawler.run()
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* PHP 服务状态检测 - Android 版本
|
||||
*/
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'ok',
|
||||
'message' => 'PHP 服务运行正常',
|
||||
'version' => PHP_VERSION,
|
||||
'platform' => 'Android',
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
'extensions' => get_loaded_extensions()
|
||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
$targetUrl = $_GET['url'] ?? '';
|
||||
|
||||
if (empty($targetUrl)) {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
echo '代理运行中,请使用: ?url=视频地址';
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: *");
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!filter_var($targetUrl, FILTER_VALIDATE_URL)) {
|
||||
http_response_code(400);
|
||||
echo "无效的 URL";
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $targetUrl,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
CURLOPT_HTTPHEADER => ['Accept: */*'],
|
||||
CURLOPT_ENCODING => '',
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new Exception(curl_error($ch));
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
curl_close($ch);
|
||||
|
||||
// 跨域头
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: *");
|
||||
|
||||
// 检测是否为 m3u8
|
||||
$isM3u8 = (
|
||||
strpos($contentType, 'mpegurl') !== false ||
|
||||
strpos($contentType, 'm3u8') !== false ||
|
||||
strpos($targetUrl, '.m3u8') !== false ||
|
||||
strpos($response, '#EXTM3U') !== false
|
||||
);
|
||||
|
||||
if ($isM3u8) {
|
||||
header("Content-Type: application/vnd.apple.mpegurl");
|
||||
|
||||
// 解析基础 URL
|
||||
$parsed = parse_url($targetUrl);
|
||||
$base = $parsed['scheme'] . '://' . $parsed['host'] .
|
||||
($parsed['port'] ? ':' . $parsed['port'] : '') .
|
||||
dirname($parsed['path'] ?? '/') . '/';
|
||||
|
||||
// 当前代理地址
|
||||
$proxyBase = 'http://' . $_SERVER['HTTP_HOST'] .
|
||||
dirname($_SERVER['SCRIPT_NAME']) . '/proxy.php?url=';
|
||||
|
||||
// 重写 m3u8 内容中的链接
|
||||
$lines = explode("\n", $response);
|
||||
$output = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// 保留注释和空行
|
||||
if (empty($line) || ($line[0] === '#' && strpos($line, '#EXTINF') === false)) {
|
||||
$output[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理媒体链接(非 # 开头)
|
||||
if ($line[0] !== '#') {
|
||||
// 补全相对路径
|
||||
if (strpos($line, 'http') !== 0) {
|
||||
$line = $base . ltrim($line, '/');
|
||||
}
|
||||
// 包装成代理链接
|
||||
$line = $proxyBase . urlencode($line);
|
||||
}
|
||||
|
||||
$output[] = $line;
|
||||
}
|
||||
|
||||
echo implode("\n", $output);
|
||||
|
||||
} else {
|
||||
// 非 m3u8 直接透传
|
||||
if ($contentType) header("Content-Type: $contentType");
|
||||
http_response_code($httpCode);
|
||||
echo $response;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo "代理失败: " . $e->getMessage();
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,451 @@
|
||||
# PHP Spider 开发与维护指南 (DZ 风格)
|
||||
|
||||
本文档总结了基于 `spider.php` 框架开发、调试、转换 PHP 爬虫源的核心经验与最佳实践。旨在帮助开发者快速上手,并作为后续开发的参考手册。
|
||||
|
||||
## 0. 环境搭建 (Windows)
|
||||
|
||||
为了运行和调试 PHP 爬虫,需要在本地配置 PHP 环境。推荐使用 PHP 8.3+ NTS (Non Thread Safe) 版本。
|
||||
|
||||
### 0.1 下载与安装
|
||||
1. **下载 PHP**:
|
||||
可以直接点击下载推荐版本:
|
||||
[php-8.3.29-nts-Win32-vs16-x64.zip](https://windows.php.net/downloads/releases/php-8.3.29-nts-Win32-vs16-x64.zip)
|
||||
2. **解压**:
|
||||
将下载的压缩包解压到固定目录,例如 `C:\php` (建议路径不包含空格和中文)。
|
||||
3. **配置环境变量**:
|
||||
- 右键 "此电脑" -> "属性" -> "高级系统设置" -> "环境变量"。
|
||||
- 在 "系统变量" 中找到 `Path`,点击 "编辑"。
|
||||
- 点击 "新建",输入你的 PHP 解压路径 (如 `C:\php`)。
|
||||
- 连续点击 "确定" 保存设置。
|
||||
|
||||
### 0.2 配置 php.ini
|
||||
1. 进入 PHP 解压目录,找到 `php.ini-development` 文件,复制一份并重命名为 `php.ini`。
|
||||
2. 使用文本编辑器打开 `php.ini`,查找并修改以下配置 (去掉行首的 `;` 分号以启用):
|
||||
```ini
|
||||
; 指定扩展目录
|
||||
extension_dir = "ext"
|
||||
|
||||
; 启用核心扩展 (爬虫必须)
|
||||
extension=curl
|
||||
extension=mbstring
|
||||
extension=openssl
|
||||
extension=sockets
|
||||
extension=sqlite3
|
||||
extension=pdo_mysql
|
||||
extension=mysqli
|
||||
```
|
||||
3. **验证安装**:
|
||||
打开新的 CMD 或 PowerShell 窗口,输入 `php -v`。
|
||||
如果看到类似 `PHP 8.3.29 (cli) ...` 的输出,即表示环境配置成功。
|
||||
|
||||
## 0.5 环境搭建 (Linux/Ubuntu - 升级至 PHP 8.3)
|
||||
|
||||
如果您在 Linux 环境(如 Ubuntu/Debian)下使用,建议通过 PPA 源安装或升级到 PHP 8.3。
|
||||
|
||||
### 0.5.1 卸载旧版 (可选)
|
||||
如果系统中已安装旧版(如 PHP 8.1),建议先卸载以避免冲突:
|
||||
```bash
|
||||
sudo apt purge php8.1* -y
|
||||
sudo apt autoremove -y
|
||||
```
|
||||
|
||||
### 0.5.2 添加 PPA 源
|
||||
使用 Ondřej Surý 的 PPA 源获取最新 PHP 版本:
|
||||
```bash
|
||||
sudo apt install software-properties-common -y
|
||||
sudo add-apt-repository ppa:ondrej/php -y
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
### 0.5.3 安装 PHP 8.3 及扩展
|
||||
安装 CLI 版本及 Drpy 爬虫所需的常用扩展 (curl, mbstring, xml, mysql 等):
|
||||
```bash
|
||||
# 注意:openssl 通常已包含在核心或 common 包中,无需单独指定 php8.3-openssl
|
||||
sudo apt install php8.3-cli php8.3-curl php8.3-mbstring php8.3-xml php8.3-mysql php8.3-sqlite3 -y
|
||||
```
|
||||
|
||||
### 0.5.4 验证安装
|
||||
```bash
|
||||
php -v
|
||||
# 输出应显示 PHP 8.3.x
|
||||
```
|
||||
|
||||
### 0.5.5 改init
|
||||
```bash
|
||||
php --ini
|
||||
cd /etc/php/8.3/cli
|
||||
vi php.ini
|
||||
# 找到 extension=sqlite3 并取消注释(用到下面安装命令安装完了会自动配置好,这里还是给注释掉)
|
||||
# 0.5.3已经包含了下面的命令,可以不管了
|
||||
apt-get install php8.3-sqlite3
|
||||
# php -S 127.0.0.1:8000 -t .
|
||||
```
|
||||
|
||||
## 1. 核心架构与工具
|
||||
|
||||
### 1.1 基础框架 (`lib/spider.php`)
|
||||
核心框架文件现已移动至 `lib` 目录。
|
||||
所有源必须包含 `lib/spider.php` 并继承 `BaseSpider` 类(通常在源文件中定义为 `class Spider extends BaseSpider`)。
|
||||
|
||||
**引用规范**:
|
||||
```php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
```
|
||||
|
||||
核心方法包括:
|
||||
- `init()`: 初始化(可选)。
|
||||
- `homeContent($filter)`: 获取首页分类与筛选配置。
|
||||
- `categoryContent($tid, $pg, $filter, $extend)`: 获取分类列表数据。
|
||||
- `detailContent($ids)`: 获取视频详情与播放列表。
|
||||
- `searchContent($key, $quick, $pg)`: 搜索视频。
|
||||
- `playerContent($flag, $id, $vipFlags)`: 解析真实播放链接。
|
||||
|
||||
### 1.2 文件命名与目录规范
|
||||
- **源文件命名**: 统一使用 ` ᵈᶻ.php` 后缀(注意包含空格),例如 `果果 ᵈᶻ.php`。对于特定类型,建议增加标识:小说使用 `[书]`,漫画使用 `[画]`,例如 `七猫小说 ᵈᶻ[书].php`。
|
||||
- **系统文件排除**: `config.php` 会自动忽略以下文件:
|
||||
- 系统文件 (`index.php`, `test_runner.php` 等)
|
||||
- 以 `_` 开头的文件 (如 `_backup.php`)
|
||||
- `config` 开头的文件
|
||||
- `lib` 目录下的文件
|
||||
|
||||
### 1.3 测试工具 (`test_runner.php`)
|
||||
用于本地验证源的接口功能。
|
||||
|
||||
**用法**:
|
||||
```bash
|
||||
php test_runner.php "e:\php_work\php\荐片影视 ᵈᶻ.php"
|
||||
```
|
||||
*(注意:由于文件名包含空格,命令行中路径建议加引号)*
|
||||
|
||||
**测试流程**:
|
||||
1. **首页测试**: 检查分类是否获取成功,筛选条件是否解析。
|
||||
2. **分类测试**: 选取第一个分类,获取第一页数据,检查 `vod_id` 和 `vod_name`。
|
||||
3. **详情测试**: 使用分类接口返回的 `vod_id`,检查详情信息及播放列表解析。
|
||||
4. **搜索测试**: 使用分类接口获取的名称进行搜索验证。
|
||||
5. **播放测试**: 选取第一个播放源,尝试解析播放链接。
|
||||
|
||||
---
|
||||
|
||||
## 2. 开发最佳实践
|
||||
|
||||
### 2.1 分页标准化 (`$this->pageResult`)
|
||||
不要手动拼接复杂的 JSON 返回结构。使用框架内置的辅助方法 `$this->pageResult`。
|
||||
|
||||
**推荐写法**:
|
||||
```php
|
||||
$videos = [];
|
||||
foreach ($items as $item) {
|
||||
$videos[] = [
|
||||
'vod_id' => $item['id'],
|
||||
'vod_name' => $item['name'],
|
||||
'vod_pic' => $item['pic'],
|
||||
'vod_remarks' => $item['remarks']
|
||||
];
|
||||
}
|
||||
return $this->pageResult($videos, $page, $total, $pageSize);
|
||||
```
|
||||
|
||||
### 2.2 数据传递技巧 (`vod_id` 组合)
|
||||
有时 `categoryContent` 到 `detailContent` 需要传递额外参数(如 `typeId`),但 `vod_id` 只能是字符串。
|
||||
**技巧**: 使用分隔符组合参数。
|
||||
```php
|
||||
// 在 categoryContent 中
|
||||
'vod_id' => $id . '*' . $typeId
|
||||
|
||||
// 在 detailContent 中
|
||||
$parts = explode('*', $ids[0]);
|
||||
$id = $parts[0];
|
||||
$typeId = $parts[1] ?? '';
|
||||
```
|
||||
|
||||
### 2.3 HTML 解析 (DOMDocument)
|
||||
处理 HTML 页面时,推荐使用 `DOMDocument` + `DOMXPath`,比正则更稳定。
|
||||
**IDE 爆红修复**:
|
||||
IDE 经常提示 `getAttribute` 方法不存在,因为 DOMNode 不一定是 Element。
|
||||
**正确写法**:
|
||||
```php
|
||||
$node = $xpath->query('//img')->item(0);
|
||||
if ($node instanceof DOMElement) { // 加上类型检查
|
||||
$pic = $node->getAttribute('src');
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 加密与解密 (JS -> PHP 转换)
|
||||
遇到 JS 源使用了加密(如 RSA, AES),需要用 PHP 的 `openssl` 扩展对应实现。
|
||||
|
||||
**案例:RSA 分块解密 (参考 `零度影视 ᵈᶻ.php`)**
|
||||
PHP 的 `openssl_private_decrypt` 有长度限制(通常 117 或 128 字节)。如果密文过长,必须**分块解密**。
|
||||
|
||||
```php
|
||||
private function rsaDecrypt($data) {
|
||||
$decoded = base64_decode($data);
|
||||
$keyRes = openssl_pkey_get_private($this->privateKey);
|
||||
$details = openssl_pkey_get_details($keyRes);
|
||||
$keySize = ceil($details['bits'] / 8); // e.g., 128 bytes
|
||||
|
||||
$result = '';
|
||||
$chunks = str_split($decoded, $keySize); // 按密钥长度分块
|
||||
|
||||
foreach ($chunks as $chunk) {
|
||||
if (openssl_private_decrypt($chunk, $decrypted, $this->privateKey, OPENSSL_PKCS1_PADDING)) {
|
||||
$result .= $decrypted;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 CURL Header 空值处理
|
||||
在 PHP CURL 中,如果需要发送一个值为空的 Header(如 `Authorization:`),**不能**使用 `"Header: "`(带空格)或 `"Header:"`(不带值),这可能导致 Header 被忽略或发送错误的格式。
|
||||
|
||||
**正确做法**: 使用分号结尾。
|
||||
```php
|
||||
$headers = [
|
||||
'Authorization;', // 发送 "Authorization:" 头,值为空
|
||||
'User-Agent: ...'
|
||||
];
|
||||
```
|
||||
此技巧在移植七猫小说时解决了个别接口(如章节内容)验签失败的问题。
|
||||
|
||||
### 2.6 HtmlParser 与 pd 函数的智能 UrlJoin
|
||||
在使用 `pd()` 函数提取链接(如图片 src、详情页 href)时,通常需要传入当前页面的 URL 作为 `baseUrl` 以便拼接相对路径。
|
||||
|
||||
**手动传入 (推荐用于详情页)**:
|
||||
```php
|
||||
$pic = $this->pd($html, 'img&&src', $currentUrl);
|
||||
```
|
||||
|
||||
**自动识别 (推荐用于列表页)**:
|
||||
如果你的 Spider 类定义了 `const HOST` 或 `$HOST` 属性,`pd()` 函数在未传入 `baseUrl` 时会自动使用它作为基准。
|
||||
```php
|
||||
class Spider extends BaseSpider {
|
||||
private const HOST = 'https://www.example.com';
|
||||
// ...
|
||||
// 这里不需要传 $url,会自动用 HOST 拼接
|
||||
$pic = $this->pd($itemHtml, 'img&&src');
|
||||
}
|
||||
```
|
||||
|
||||
### 2.7 IDE 兼容性与反射技巧
|
||||
在基类中访问子类的私有常量/属性(如 `$this->HOST`)时,直接访问会导致 IDE 报错(Undefined property)。
|
||||
**最佳实践**: 使用 `ReflectionClass` 动态获取。
|
||||
```php
|
||||
$ref = new ReflectionClass($this);
|
||||
if ($ref->hasConstant('HOST')) {
|
||||
return $ref->getConstant('HOST');
|
||||
}
|
||||
```
|
||||
这不仅消除了 IDE 警告,还支持了对 `private/protected` 属性的访问(需配合 `setAccessible(true)`,注意 PHP 8.1+ 已默认支持)。
|
||||
|
||||
---
|
||||
|
||||
## 3. HtmlParser 解析函数指南
|
||||
|
||||
为了与 JS 源(Hiker 规则)保持一致,我们在 `BaseSpider` 中内置了 `pdfa`, `pdfh`, `pd` 三个核心函数。它们支持 CSS 选择器风格的解析规则,并自动处理 DOM 操作。
|
||||
|
||||
### 3.1 规则语法 (Rule Syntax)
|
||||
- **层级**: 使用 `&&` 分隔层级(在 XPath 中对应 `//`)。例如 `div.list&&ul&&li`。
|
||||
- **属性/选项**: 规则的**最后一部分**指定要获取的内容。
|
||||
- `Text`: 获取纯文本(自动去除首尾空格和多余换行)。
|
||||
- `Html`: 获取元素的 OuterHTML。
|
||||
- `src`, `href`, `data-id`, ...: 获取指定属性值。
|
||||
- **选择器**:
|
||||
- `tag`: 标签名,如 `div`, `a`, `img`。
|
||||
- `.class`: 类名,如 `.title`。
|
||||
- `#id`: ID,如 `#content`。
|
||||
- `:eq(n)`: 索引选择(0 起始)。`:eq(0)` 是第一个,`:eq(-1)` 是最后一个。
|
||||
- 组合: `div.item:eq(0)`。
|
||||
|
||||
### 3.2 pdfa (Parse DOM For Array)
|
||||
**用途**: 解析列表,返回 HTML 字符串数组。通常用于 `categoryContent` 中解析视频列表。
|
||||
|
||||
**签名**:
|
||||
```php
|
||||
protected function pdfa(string $html, string $rule): array
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```php
|
||||
// 获取所有 ul 下的 li 元素的 HTML
|
||||
$items = $this->pdfa($html, 'ul.list&&li');
|
||||
foreach ($items as $itemHtml) {
|
||||
// 在循环中继续使用 pdfh/pd 解析具体字段
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 pdfh (Parse DOM For Html/Text)
|
||||
**用途**: 解析单个节点的内容(文本、HTML 或属性)。
|
||||
|
||||
**签名**:
|
||||
```php
|
||||
protected function pdfh(string $html, string $rule, string $baseUrl = ''): string
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```php
|
||||
// 获取标题文本
|
||||
$title = $this->pdfh($itemHtml, '.title&&Text');
|
||||
|
||||
// 获取描述(可能包含 HTML 标签)
|
||||
$desc = $this->pdfh($itemHtml, '.desc&&Html');
|
||||
|
||||
// 获取自定义属性
|
||||
$dataId = $this->pdfh($itemHtml, 'a&&data-id');
|
||||
```
|
||||
|
||||
### 3.4 pd (Parse DOM for Url)
|
||||
**用途**: 解析链接(图片、跳转链接),并**自动进行 URL 拼接**(UrlJoin)。
|
||||
|
||||
**签名**:
|
||||
```php
|
||||
protected function pd(string $html, string $rule, string $baseUrl = ''): string
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 等同于 `pdfh` + `urlJoin`。
|
||||
- 如果规则末尾是属性(如 `src`, `href`),会自动基于 `$baseUrl` 转换为绝对路径。
|
||||
- 如果未传入 `$baseUrl`,会自动尝试读取类常量 `HOST`。
|
||||
|
||||
**示例**:
|
||||
```php
|
||||
// 自动拼接 HOST (假设类中定义了 const HOST)
|
||||
$pic = $this->pd($itemHtml, 'img&&src');
|
||||
|
||||
// 手动指定 BaseUrl (如详情页解析推荐列表)
|
||||
$link = $this->pd($html, 'a.next&&href', 'https://m.example.com/list/');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 常见问题排查
|
||||
|
||||
- **Q: 为什么搜不到结果?**
|
||||
- A: 检查 `searchContent` 的 URL 参数是否正确编码。特别是中文关键词,部分站点需要 URL 编码,部分不需要。
|
||||
- **Q: 详情页没有章节?**
|
||||
- A: 很多小说/漫画源的详情页接口 (`/detail`) 返回的信息不全,通常需要额外调用章节列表接口 (`/chapter-list` 或类似)。务必抓包确认。
|
||||
- **Q: 图片加载失败?**
|
||||
- A: 检查图片链接是否为相对路径。如果是,请确保在 `pd()` 或手动处理时进行了完整的 URL 拼接。
|
||||
- **Q: 验签失败?**
|
||||
- A: 仔细比对 Python/JS 源的签名逻辑。注意参数排序(`ksort`)、空值处理、特殊字符编码差异。PHP 的 `md5` 输出默认是小写 hex。
|
||||
|
||||
---
|
||||
|
||||
## 5. Flutter 环境适配与 PHP 8.5+ 兼容性
|
||||
|
||||
在将 PHP 源部署到 Flutter 环境(如 TVBox 及其变种)并使用高版本 PHP (如 8.5.1) 时,可能会遇到类型严格性导致的兼容问题。
|
||||
|
||||
### 5.1 核心报错:`type 'String' is not a subtype of type 'int' of 'index'`
|
||||
**现象**:
|
||||
本地 `test_runner.php` 测试一切正常,但在 Flutter 端运行时报错,提示 String 类型无法作为 List 的索引。
|
||||
|
||||
**原因**:
|
||||
PHP 脚本执行结束后**没有输出任何内容**。
|
||||
- `test_runner.php` 是手动实例化类并调用方法,所以能拿到结果。
|
||||
- Flutter 端通过 CLI 调用 PHP 脚本,如果脚本末尾没有主动调用运行逻辑,输出为空字符串。
|
||||
- 适配层收到空字符串后,可能默认处理为 `[]` (空 List)。后续逻辑尝试以 Map 方式(如 `['class']`)访问这个 List 时,就会触发 Dart 的类型错误。
|
||||
|
||||
**解决方案**:
|
||||
确保每个源文件末尾都包含自动运行指令:
|
||||
```php
|
||||
// 必须在文件末尾加入此行
|
||||
(new Spider())->run();
|
||||
```
|
||||
|
||||
### 5.2 严格类型处理 (JSON 空对象)
|
||||
**现象**:
|
||||
PHP 的空数组 `[]` 在 `json_encode` 时默认为 `[]` (List)。如果在 PHP 8.5+ 环境下,客户端期望的是 Map `{}` (Object),可能会导致解析错误或类型不匹配。
|
||||
|
||||
**解决方案**:
|
||||
对于明确应该是对象的字段(如 `header`, `filters`, `ext`),如果为空,必须强制转换为 Object。
|
||||
```php
|
||||
// 错误 (输出 [])
|
||||
'header' => []
|
||||
|
||||
// 正确 (输出 {})
|
||||
'header' => (object)[]
|
||||
```
|
||||
|
||||
### 5.3 HTTPS 与 SSL 证书验证
|
||||
**现象**:
|
||||
在某些 Flutter 环境或 Android 设备上,cURL 请求 HTTPS 站点失败,无返回或报错。这是 because 系统证书库可能不完整或 curl 配置过严。
|
||||
|
||||
**解决方案**:
|
||||
显式关闭 SSL 证书校验。`BaseSpider` 的 `fetch` 方法已默认处理,但在重写 `fetch` 或使用原生 cURL 时需注意:
|
||||
```php
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
```
|
||||
|
||||
### 5.4 健壮性建议
|
||||
1. **JSON 解析容错**:`json_decode($str ?: '{}', true)`,避免对空字符串解析报错。
|
||||
2. **空 ID 容错**:在 `detailContent` 或 `playerContent` 中,检查 ID 是否为空,避免向 API 发送非法请求导致崩溃。
|
||||
|
||||
---
|
||||
|
||||
## 6. 最近实战经验汇总 (2026/01 更新)
|
||||
|
||||
### 6.1 漫画/图片源的标准协议 (`pics://`)
|
||||
在开发漫画或图片类源时,`playerContent` 返回的 `url` 字段应使用 `pics://` 协议。
|
||||
- **格式**: `pics://图片链接1&&图片链接2&&图片链接3...`
|
||||
- **注意**: 严禁使用非标准的 `mange://` 或其他自定义协议,除非客户端明确支持。使用 `pics://` 可确保通用播放器能正确识别为图片轮播模式。
|
||||
|
||||
### 6.2 静态资源智能过滤
|
||||
在解析漫画图片列表时,网页往往混杂大量的图标、LOGO、背景图或占位图(如 `grey.gif`)。必须建立过滤机制,否则会严重影响阅读体验。
|
||||
|
||||
**推荐过滤代码**:
|
||||
```php
|
||||
$uniqueImages = [];
|
||||
foreach ($imageList as $img) {
|
||||
// 1. 去重
|
||||
if (in_array($img, $uniqueImages)) continue;
|
||||
|
||||
// 2. 关键词过滤
|
||||
if (strpos($img, "grey.gif") !== false) continue; // 占位图
|
||||
if (strpos($img, "logo") !== false) continue; // 网站LOGO
|
||||
if (strpos($img, "icon") !== false) continue; // 图标
|
||||
if (strpos($img, "banner") !== false) continue; // 广告横幅
|
||||
|
||||
$uniqueImages[] = $img;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 中文参数的 URL 编码陷阱
|
||||
PHP 的 `curl` 不会自动对 URL 中的非 ASCII 字符进行编码。如果 URL 中包含中文(如搜索关键词、分类标签),**必须**手动调用 `urlencode`。
|
||||
- **错误**: `$url = "https://site.com/search?q=" . $key;`
|
||||
- **正确**: `$url = "https://site.com/search?q=" . urlencode($key);`
|
||||
未编码会导致服务端返回 400 Bad Request 或 404。
|
||||
|
||||
### 6.4 `config.php` 类型定义
|
||||
在 `config.php` 中注册源时,请注意字段命名。
|
||||
- **正确**: `"类型": "小说"` 或 `"类型": "漫画"`
|
||||
- **错误**: 不要使用 `"categories"` 或其他自定义字段名,否则前端可能无法正确分类显示。
|
||||
|
||||
### 6.5 PHP 8.5+ 与 Flutter JSON 深度兼容
|
||||
在 PHP 8.5.1 及 Flutter 混合环境下,JSON 格式的严谨性至关重要:
|
||||
1. **空 Map 强制转换**: 任何应当输出为 `{}` 的字段(如 `filters`, `ext`, `header`),若为空数组,**必须**使用 `(object)[]` 或 `(object)$arr` 转换。否则 `json_encode` 会输出 `[]`,导致 Flutter 客户端报 `type 'String' is not a subtype of type 'int' of 'index'` 错误。
|
||||
2. **Undefined Index 防御**: 数组索引访问必须使用 `?? ''` 或 `?? []` 提供默认值(如 `$item['key'] ?? ''`)。PHP 的 Warning 信息若混入 JSON 输出,会直接导致解析失败。
|
||||
|
||||
### 6.6 HTTPS 强制适配
|
||||
Android 9+ 及 Flutter 应用默认禁止明文 HTTP 请求(Cleartext traffic not permitted)。
|
||||
- **最佳实践**: 在提取图片链接 (`vod_pic`) 时,检测并自动替换协议。
|
||||
```php
|
||||
if (strpos($pic, 'http://') === 0) {
|
||||
$pic = str_replace('http://', 'https://', $pic);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.7 封面图片提取的高级策略
|
||||
针对结构复杂的详情页(如漫画站),单一规则往往不稳定:
|
||||
1. **属性顺序无关正则**: 避免假设 `src` 在 `class` 之前或之后。使用更灵活的正则:
|
||||
`/<img[^>]*class=["\'](?:classA|classB)["\'][^>]*src=.../`
|
||||
2. **多级回退机制**:
|
||||
- **L1**: 优先从元数据区域(Metadata)提取。
|
||||
- **L2**: 若失败,尝试从内容区域(Content Block)提取第一张图。
|
||||
- **L3**: 若仍失败,全局搜索非 Icon/Logo/Gif 的第一张大图。
|
||||
|
||||
### 6.8 测试驱动开发 (TDD) 增强
|
||||
不要仅依赖人工查看。建议在 `test_runner.php` 中增加关键字段断言:
|
||||
- **封面检查**: 在详情页测试中显式检查 `vod_pic` 是否为空,能提早发现 80% 的解析问题。
|
||||
|
||||
---
|
||||
*本文档更新于 2026/01/26,基于 Trae IDE 协作环境。*
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
// test_runner.php
|
||||
// 这是一个用于测试 Spider 插件接口的脚本
|
||||
// 用法: php test_runner.php [插件文件路径]
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// 设置默认时区,避免时间相关函数警告
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
|
||||
$file = $argv[1] ?? '';
|
||||
if (!$file || !file_exists($file)) {
|
||||
die("错误: 未找到文件 '$file'\n用法: php test_runner.php [插件文件路径]\n");
|
||||
}
|
||||
|
||||
echo "==================================================\n";
|
||||
echo "正在测试文件: $file\n";
|
||||
echo "==================================================\n";
|
||||
|
||||
try {
|
||||
// 使用输出缓冲捕获 require 过程中可能的输出(如 (new Spider())->run())
|
||||
// 防止污染后续的测试输出
|
||||
ob_start();
|
||||
require_once $file;
|
||||
ob_end_clean();
|
||||
|
||||
if (!class_exists('Spider')) {
|
||||
die("错误: 在文件 '$file' 中未找到 'Spider' 类\n");
|
||||
}
|
||||
|
||||
echo "[初始化] 实例化 Spider 类...\n";
|
||||
$spider = new Spider();
|
||||
$spider->init();
|
||||
echo "[初始化] 完成\n\n";
|
||||
|
||||
// --- 1. 测试首页接口 (Home Interface) ---
|
||||
echo ">>> [1/5] 测试首页接口 (homeContent)\n";
|
||||
$startTime = microtime(true);
|
||||
$home = $spider->homeContent(true);
|
||||
$cost = round((microtime(true) - $startTime) * 1000, 2);
|
||||
|
||||
$classes = $home['class'] ?? [];
|
||||
$filters = $home['filters'] ?? [];
|
||||
|
||||
if (!empty($classes)) {
|
||||
echo " ✅ 通过 (耗时: {$cost}ms)\n";
|
||||
echo " - 获取到 " . count($classes) . " 个分类\n";
|
||||
|
||||
// 打印前几个分类名称作为示例
|
||||
$classNames = array_column(array_slice($classes, 0, 5), 'type_name');
|
||||
echo " - 分类示例: " . implode(', ', $classNames) . (count($classes) > 5 ? ' ...' : '') . "\n";
|
||||
|
||||
if (!empty($filters)) {
|
||||
$filterCount = is_object($filters) ? count(get_object_vars($filters)) : count($filters);
|
||||
echo " - 包含筛选配置 (Filters): " . $filterCount . " 组\n";
|
||||
}
|
||||
} else {
|
||||
echo " ⚠️ 警告: 未获取到分类列表 (class 为空)\n";
|
||||
}
|
||||
|
||||
// 确定用于测试分类接口的 type_id
|
||||
$tid = $classes[0]['type_id'] ?? null;
|
||||
$tname = $classes[0]['type_name'] ?? '未知分类';
|
||||
|
||||
if (!$tid && !empty($filters)) {
|
||||
// 如果 class 为空但有 filters,尝试从 filters 获取 key
|
||||
foreach ($filters as $key => $val) {
|
||||
$tid = $key;
|
||||
$tname = "FilterKey:$key";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// --- 2. 测试分类接口 (Category Interface) ---
|
||||
$vodId = null;
|
||||
$vodName = null; // 用于搜索测试
|
||||
if ($tid) {
|
||||
echo ">>> [2/5] 测试分类接口 (categoryContent) - 测试分类: [$tname] (ID: $tid)\n";
|
||||
$startTime = microtime(true);
|
||||
// 模拟传入 filter 参数为空
|
||||
$cat = $spider->categoryContent($tid, 1, false, []);
|
||||
$cost = round((microtime(true) - $startTime) * 1000, 2);
|
||||
|
||||
$list = $cat['list'] ?? [];
|
||||
if (!empty($list)) {
|
||||
echo " ✅ 通过 (耗时: {$cost}ms)\n";
|
||||
echo " - 获取到 " . count($list) . " 个资源\n";
|
||||
|
||||
$firstItem = $list[0];
|
||||
$vodId = $firstItem['vod_id'] ?? null;
|
||||
$vodName = $firstItem['vod_name'] ?? '未知名称';
|
||||
echo " - 第一条数据: [$vodName] (ID: $vodId)\n";
|
||||
} else {
|
||||
echo " ❌ 失败: 未返回资源列表 (list 为空)\n";
|
||||
}
|
||||
} else {
|
||||
echo ">>> [2/5] 测试分类接口: ⏭️ 跳过 (未找到有效的分类ID)\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// --- 3. 测试详情接口 (Detail Interface) ---
|
||||
$playUrl = null;
|
||||
$playFrom = null;
|
||||
|
||||
if ($vodId) {
|
||||
echo ">>> [3/5] 测试详情接口 (detailContent) - 测试资源ID: $vodId\n";
|
||||
$startTime = microtime(true);
|
||||
$detail = $spider->detailContent([$vodId]);
|
||||
$cost = round((microtime(true) - $startTime) * 1000, 2);
|
||||
|
||||
$detailList = $detail['list'] ?? [];
|
||||
|
||||
if (!empty($detailList)) {
|
||||
$vod = $detailList[0];
|
||||
$name = $vod['vod_name'] ?? '未知';
|
||||
// 更新 vodName,详情页的名称通常更准确
|
||||
if ($name && $name !== '未知') {
|
||||
$vodName = $name;
|
||||
}
|
||||
$playUrl = $vod['vod_play_url'] ?? '';
|
||||
$playFrom = $vod['vod_play_from'] ?? '';
|
||||
$pic = $vod['vod_pic'] ?? '';
|
||||
$desc = $vod['vod_content'] ?? '';
|
||||
|
||||
echo " ✅ 通过 (耗时: {$cost}ms)\n";
|
||||
echo " - 资源名称: $name\n";
|
||||
echo " - 封面图片: " . ($pic ? $pic : "⚠️ 未获取到封面") . "\n";
|
||||
echo " - 播放源 (vod_play_from): $playFrom\n";
|
||||
|
||||
// 检查播放地址
|
||||
if (!empty($playUrl)) {
|
||||
$urlCount = substr_count($playUrl, '$');
|
||||
// 粗略估计集数,通常每集是 名称$url
|
||||
$episodeCount = $urlCount > 0 ? ($urlCount + 1) / 2 : 1;
|
||||
// 或者直接按 # 分割统计播放列表数
|
||||
$playlistCount = substr_count($playFrom, '$$$') + 1;
|
||||
|
||||
echo " - 播放列表数据长度: " . strlen($playUrl) . " 字符\n";
|
||||
// 简单展示部分播放链接
|
||||
$previewUrl = mb_substr($playUrl, 0, 50) . '...';
|
||||
echo " - 播放链接预览: $previewUrl\n";
|
||||
} else {
|
||||
echo " ⚠️ 警告: vod_play_url 为空!\n";
|
||||
}
|
||||
|
||||
if (!empty($desc)) {
|
||||
echo " - 简介长度: " . mb_strlen($desc) . " 字\n";
|
||||
}
|
||||
|
||||
} else {
|
||||
echo " ❌ 失败: 未返回详情数据\n";
|
||||
}
|
||||
} else {
|
||||
echo ">>> [3/5] 测试详情接口: ⏭️ 跳过 (未找到有效的资源ID)\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// --- 4. 测试搜索接口 (Search Interface) ---
|
||||
// 使用之前获取到的 vodName 进行搜索,如果没有则使用默认关键词 "爱"
|
||||
$searchKey = $vodName ?: "爱";
|
||||
echo ">>> [4/5] 测试搜索接口 (searchContent) - 关键词: [$searchKey]\n";
|
||||
|
||||
try {
|
||||
$startTime = microtime(true);
|
||||
$searchRes = $spider->searchContent($searchKey, false, 1);
|
||||
$cost = round((microtime(true) - $startTime) * 1000, 2);
|
||||
|
||||
$searchList = $searchRes['list'] ?? [];
|
||||
if (!empty($searchList)) {
|
||||
echo " ✅ 通过 (耗时: {$cost}ms)\n";
|
||||
echo " - 搜索到 " . count($searchList) . " 个结果\n";
|
||||
$firstSearch = $searchList[0];
|
||||
echo " - 第一条结果: " . ($firstSearch['vod_name'] ?? '未知') . "\n";
|
||||
} else {
|
||||
echo " ⚠️ 警告: 搜索未返回结果 (但这不代表接口错误)\n";
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
echo " ⚠️ 异常: 搜索接口调用失败 (允许失败)\n";
|
||||
echo " 错误信息: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// --- 5. 测试播放接口 (Player Interface) ---
|
||||
if ($playUrl && $playFrom) {
|
||||
// 解析播放链接,取第一组的第一个链接
|
||||
// 格式通常是: 播放源1$$$集数1$链接1#集数2$链接2...$$$播放源2...
|
||||
// 或者是: 集数1$链接1#集数2$链接2...
|
||||
|
||||
// 简单处理:先按 $$$ 分割取第一个播放源对应的链接串
|
||||
$playUrls = explode('$$$', $playUrl);
|
||||
$currentUrlBlock = $playUrls[0] ?? '';
|
||||
|
||||
// 再按 # 分割取第一集
|
||||
$episodes = explode('#', $currentUrlBlock);
|
||||
$firstEp = $episodes[0] ?? '';
|
||||
|
||||
// 再按 $ 分割取链接 (通常是 名称$链接)
|
||||
$parts = explode('$', $firstEp);
|
||||
$targetUrl = end($parts); // 取最后一部分作为链接
|
||||
|
||||
// 播放源flag
|
||||
$playFroms = explode('$$$', $playFrom);
|
||||
$flag = $playFroms[0] ?? 'default';
|
||||
|
||||
echo ">>> [5/5] 测试播放接口 (playerContent) - Flag: [$flag]\n";
|
||||
echo " - 目标链接: $targetUrl\n";
|
||||
|
||||
try {
|
||||
$startTime = microtime(true);
|
||||
// $flag, $id, $vipFlags
|
||||
$playerRes = $spider->playerContent($flag, $targetUrl, []);
|
||||
$cost = round((microtime(true) - $startTime) * 1000, 2);
|
||||
|
||||
if (!empty($playerRes)) {
|
||||
echo " ✅ 通过 (耗时: {$cost}ms)\n";
|
||||
// 打印返回的关键字段
|
||||
$parse = $playerRes['parse'] ?? 'N/A';
|
||||
$url = $playerRes['url'] ?? 'N/A';
|
||||
$header = $playerRes['header'] ?? 'N/A';
|
||||
|
||||
echo " - Parse: $parse\n";
|
||||
echo " - PlayUrl: $url\n";
|
||||
if (is_array($header)) {
|
||||
echo " - Header: " . json_encode($header, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
}
|
||||
} else {
|
||||
echo " ⚠️ 警告: 播放接口返回为空\n";
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
echo " ⚠️ 异常: 播放接口调用失败 (允许失败)\n";
|
||||
echo " 错误信息: " . $e->getMessage() . "\n";
|
||||
}
|
||||
} else {
|
||||
echo ">>> [5/5] 测试播放接口: ⏭️ 跳过 (未获取到有效的播放链接或播放源信息)\n";
|
||||
}
|
||||
|
||||
} catch (Throwable $e) {
|
||||
echo "\n⛔ 严重错误 (CRITICAL ERROR):\n";
|
||||
echo " 信息: " . $e->getMessage() . "\n";
|
||||
echo " 位置: " . $e->getFile() . " 第 " . $e->getLine() . " 行\n";
|
||||
echo " 堆栈:\n" . $e->getTraceAsString() . "\n";
|
||||
}
|
||||
|
||||
echo "==================================================\n";
|
||||
echo "测试结束\n";
|
||||
@@ -0,0 +1,1342 @@
|
||||
<?php
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
$id = isset($_GET['id']) ? $_GET['id'] : 'cctv1';
|
||||
$n = [
|
||||
'cctv1' => ['2024078201', '600001859', 'fhd'], //CCTV-1高清
|
||||
'cctv2' => ['2024075401', '600001800', 'fhd'], //CCTV-2高清
|
||||
'cctv3' => ['2024068501', '600001801', 'fhd'], //CCTV-3高清
|
||||
'cctv4' => ['2029797101', '600001814', 'fhd'], //CCTV-4高清
|
||||
'cctv5' => ['2024078401', '600001818', 'fhd'], //CCTV-5高清
|
||||
'cctv5p' => ['2024078001', '600001817', 'fhd'], //CCTV-5+高清
|
||||
'cctv6' => ['2013693901', '600108442', 'fhd'], //CCTV-6高清
|
||||
'cctv7' => ['2024072001', '600004092', 'fhd'], //CCTV-7高清
|
||||
'cctv8' => ['2029793001', '600001803', 'fhd'], //CCTV-8高清
|
||||
'cctv9' => ['2024078601', '600004078', 'fhd'], //CCTV-9高清
|
||||
'cctv10' => ['2024078701', '600001805', 'fhd'], //CCTV-10高清
|
||||
'cctv11' => ['2027248701', '600001806', 'fhd'], //CCTV-11高清
|
||||
'cctv12' => ['2027248801', '600001807', 'fhd'], //CCTV-12高清
|
||||
'cctv13' => ['2029797201', '600001811', 'fhd'], //CCTV-13高清
|
||||
'cctv14' => ['2027248901', '600001809', 'fhd'], //CCTV-14高清
|
||||
'cctv15' => ['2027249001', '600001815', 'fhd'], //CCTV-15高清
|
||||
'cctv16' => ['2027249101', '600098637', 'fhd'], //CCTV-16高清
|
||||
'cctv164k' => ['2027249301', '600099502', 'fhd'], //CCTV-16(4K)
|
||||
'cctv17' => ['2027249401', '600001810', 'fhd'], //CCTV-17高清
|
||||
'cctv4k' => ['2029810301', '600002264', 'fhd'], //CCTV-4K
|
||||
'cctv8k' => ['2026774101', '600156816', 'fhd'], //CCTV-8K
|
||||
'cgtn' => ['2024181701', '600014550', 'fhd'], //CGTN
|
||||
'cgtnfy' => ['2024181801', '600084704', 'fhd'], //CGTN法语频道
|
||||
'cgtney' => ['2024181901', '600084758', 'fhd'], //CGTN俄语频道
|
||||
'cgtnalby' => ['2024182001', '600084782', 'fhd'], //CGTN阿拉伯语频道
|
||||
'cgtnxby' => ['2024182101', '600084744', 'fhd'], //CGTN西班牙语频道
|
||||
'cgtnwyjl' => ['2024182301', '600084781', 'fhd'], //CGTN外语纪录频道
|
||||
'cctvfyjc' => ['2025637103', '600099658', 'shd'], //CCTV风云剧场频道
|
||||
'cctvdyjc' => ['2026874203', '600099655', 'shd'], //CCTV第一剧场频道
|
||||
'cctvhjjc' => ['2026874303', '600099620', 'shd'], //CCTV怀旧剧场频道
|
||||
'cctvsjdl' => ['2026874403', '600099637', 'shd'], //CCTV世界地理频道
|
||||
'cctvfyyy' => ['2026874503', '600099660', 'shd'], //CCTV风云音乐频道
|
||||
'cctvbqkj' => ['2026874603', '600099649', 'shd'], //CCTV兵器科技频道
|
||||
'cctvfyzq' => ['2026966203', '600099636', 'shd'], //CCTV风云足球频道
|
||||
'cctvgeqwq' => ['2026874703', '600099659', 'shd'], //CCTV高尔夫·网球频道
|
||||
'cctvnxss' => ['2026874803', '600099650', 'shd'], //CCTV女性时尚频道
|
||||
'cctvyswhjp' => ['2026874903', '600099653', 'shd'], //CCTV央视文化精品频道
|
||||
'cctvystq' => ['2026875003', '600099652', 'shd'], //CCTV央视台球频道
|
||||
'cctvdszn' => ['2026875103', '600099656', 'shd'], //CCTV电视指南频道
|
||||
'cctvwsjk' => ['2025637003', '600099651', 'shd'], //CCTV卫生健康频道
|
||||
'bjws' => ['2024052703', '600002309', 'fhd'], //北京卫视
|
||||
'jsws' => ['2024171103', '600002521', 'fhd'], //江苏卫视
|
||||
'dfws' => ['2024054503', '600002483', 'fhd'], //东方卫视
|
||||
'zjws' => ['2024054703', '600002520', 'fhd'], //浙江卫视
|
||||
'hnws' => ['2024054803', '600002475', 'fhd'], //湖南卫视
|
||||
'hbws' => ['2024171203', '600002508', 'fhd'], //湖北卫视
|
||||
'gdws' => ['2024060903', '600002485', 'fhd'], //广东卫视
|
||||
'gxws' => ['2024060703', '600002509', 'fhd'], //广西卫视
|
||||
'hljws' => ['2029797003', '600002498', 'fhd'], //黑龙江卫视
|
||||
'hnws2' => ['2024055603', '600002506', 'fhd'], //海南卫视
|
||||
'cqws' => ['2024061103', '600002531', 'fhd'], //重庆卫视
|
||||
'szws' => ['2024061303', '600002481', 'fhd'], //深圳卫视
|
||||
'scws' => ['2024061403', '600002516', 'fhd'], //四川卫视
|
||||
'henanws' => ['2029797303', '600002525', 'fhd'], //河南卫视
|
||||
'fjdnhz' => ['2024061503', '600002484', 'fhd'], //福建东南卫视
|
||||
'gzhws' => ['2024061603', '600002490', 'fhd'], //贵州卫视
|
||||
'jxws' => ['2024061703', '600002503', 'fhd'], //江西卫视
|
||||
'lnws' => ['2024171303', '600002505', 'fhd'], //辽宁卫视
|
||||
'ahws' => ['2024171403', '600002532', 'fhd'], //安徽卫视
|
||||
'hbws2' => ['2024171503', '600002493', 'fhd'], //河北卫视
|
||||
'sdws' => ['2029787903', '600002513', 'fhd'], //山东卫视
|
||||
'tjws' => ['2019927003', '600152137', 'fhd'], //天津卫视
|
||||
'jlws' => ['2025561503', '600190405', 'fhd'], //吉林卫视
|
||||
'shanxiws' => ['2029795103', '600190400', 'fhd'], //陕西卫视
|
||||
'nxws' => ['2025608503', '600190737', 'fhd'], //宁夏卫视
|
||||
'nmgws' => ['2025561203', '600190401', 'fhd'], //内蒙古卫视
|
||||
'ynws' => ['2025561303', '600190402', 'fhd'], //云南卫视
|
||||
'shanxiws2' => ['2025560803', '600190407', 'fhd'], //山西卫视
|
||||
'qhws' => ['2025559103', '600190406', 'fhd'], //青海卫视
|
||||
'xzws' => ['2025558003', '600190403', 'fhd'], //西藏卫视
|
||||
'cetv1' => ['2022823801', '600171827', 'fhd'], //中国教育电视台1频道
|
||||
'gxpd' => ['2029360403', '600213139', 'fhd'], //国学频道
|
||||
'xjws' => ['2019927403', '600152138', 'fhd'] //新疆卫视
|
||||
];
|
||||
|
||||
class CKeyManager
|
||||
{
|
||||
// 常量定义
|
||||
const DELTA = 0x9e3779b9;
|
||||
const ROUNDS = 16;
|
||||
const LOG_ROUNDS = 4;
|
||||
const SALT_LEN = 2;
|
||||
const ZERO_LEN = 7;
|
||||
const TEA_CKEY = '59b2f7cf725ef43c34fdd7c123411ed3';
|
||||
const GUARD_TEA_KEY = '110DBEC10C23E7D2E56A1CAD6914EF1B';
|
||||
|
||||
private $xorKey = [0x84, 0x2E, 0xED, 0x08, 0xF0, 0x66, 0xE6, 0xEA, 0x48, 0xB4, 0xCA, 0xA9, 0x91, 0xED, 0x6F, 0xF3];
|
||||
private $guardXorKey = [0xB3, 0xC9, 0x53, 0xA0, 0x69, 0x13, 0xAD, 0x4D];
|
||||
private $standardAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
private $customAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-=';
|
||||
|
||||
// 当前使用的GUID
|
||||
private $guid = '';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
// 初始化时生成一个随机GUID
|
||||
$this->generateGuid();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机GUID
|
||||
*/
|
||||
private function generateGuid()
|
||||
{
|
||||
$this->guid = sprintf('%08s%04s%04s%04s%12s',
|
||||
dechex(mt_rand(0, 0xffffffff)),
|
||||
dechex(mt_rand(0, 0xffff)),
|
||||
dechex(mt_rand(0, 0xffff)),
|
||||
dechex(mt_rand(0, 0xffff)),
|
||||
dechex(mt_rand(0, 0xffffffffffff))
|
||||
);
|
||||
|
||||
// 确保GUID是32位十六进制字符串(不带连字符)
|
||||
if (strlen($this->guid) !== 32) {
|
||||
$this->guid = str_pad($this->guid, 32, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return $this->guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前GUID
|
||||
*/
|
||||
public function getGuid()
|
||||
{
|
||||
return $this->guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义GUID
|
||||
*/
|
||||
public function setGuid($guid)
|
||||
{
|
||||
$this->guid = $guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置GUID(生成新的随机GUID)
|
||||
*/
|
||||
public function resetGuid()
|
||||
{
|
||||
return $this->generateGuid();
|
||||
}
|
||||
|
||||
// ================== 辅助函数 ===================
|
||||
|
||||
/**
|
||||
* 生成随机十六进制字符串
|
||||
*/
|
||||
private function randomHexStr($length)
|
||||
{
|
||||
$hex = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$hex .= dechex(mt_rand(0, 15));
|
||||
}
|
||||
return strtoupper($hex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成spvcode
|
||||
*/
|
||||
private function spvcode($defn) {
|
||||
$height = 1080;
|
||||
if (preg_match('/(4k|8k|hdr)/i', $defn)) {
|
||||
$height = 2160;
|
||||
}
|
||||
$frame_rates = [30, 60, 90, 120];
|
||||
$h264_parts = [];
|
||||
$h265_parts = [];
|
||||
foreach ($frame_rates as $fps) {
|
||||
$h264_parts[] = "{$fps}:{$height}";
|
||||
$h265_parts[] = "{$fps}:{$height}";
|
||||
}
|
||||
$h264_str = implode(',', $h264_parts);
|
||||
$h265_str = implode(',', $h265_parts);
|
||||
$spvcode_raw = "H({$h264_str}|{$h264_str});2({$h265_str}|{$h265_str})";
|
||||
return base64_encode($spvcode_raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算签名
|
||||
*/
|
||||
private function calcSignature($buffer)
|
||||
{
|
||||
$signature = 0;
|
||||
foreach ($buffer as $byte) {
|
||||
$signature = (0x83 * $signature + ($byte & 0xFF)) & 0x7FFFFFFF;
|
||||
}
|
||||
return $signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义Base64解码
|
||||
*/
|
||||
private function customDecode($text)
|
||||
{
|
||||
if (empty($text)) return '';
|
||||
$text = rtrim($text, '=');
|
||||
if (strlen($text) % 4 != 0) {
|
||||
$text .= str_repeat('=', 4 - (strlen($text) % 4));
|
||||
}
|
||||
|
||||
$translationTable = [];
|
||||
$len = min(strlen($this->customAlphabet), strlen($this->standardAlphabet));
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$translationTable[$this->customAlphabet[$i]] = $this->standardAlphabet[$i];
|
||||
}
|
||||
|
||||
$translatedStr = strtr($text, $translationTable);
|
||||
return base64_decode($translatedStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义Base64编码
|
||||
*/
|
||||
private function customEncode($text)
|
||||
{
|
||||
$encoded = base64_encode($text);
|
||||
|
||||
$translationTable = [];
|
||||
$len = min(strlen($this->standardAlphabet), strlen($this->customAlphabet));
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$translationTable[$this->standardAlphabet[$i]] = $this->customAlphabet[$i];
|
||||
}
|
||||
|
||||
return rtrim(strtr($encoded, $translationTable), '=');
|
||||
}
|
||||
|
||||
/**
|
||||
* XOR加密/解密
|
||||
*/
|
||||
private function xorArray($byteArray)
|
||||
{
|
||||
$retArray = [];
|
||||
$len = count($byteArray);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$retArray[] = $byteArray[$i] ^ $this->xorKey[$i & 0xF];
|
||||
}
|
||||
return $retArray;
|
||||
}
|
||||
|
||||
// ================== TEA加解密函数 ===================
|
||||
|
||||
/**
|
||||
* TEA ECB模式加密
|
||||
*/
|
||||
private function teaEncryptECB($pInBuf, $pKey)
|
||||
{
|
||||
if (strlen($pInBuf) < 8) {
|
||||
$pInBuf = str_pad($pInBuf, 8, "\0");
|
||||
}
|
||||
|
||||
$unpacked = unpack('N2', $pInBuf);
|
||||
$y = $unpacked[1];
|
||||
$z = $unpacked[2];
|
||||
|
||||
$k = [
|
||||
unpack('N', substr($pKey, 0, 4))[1],
|
||||
unpack('N', substr($pKey, 4, 4))[1],
|
||||
unpack('N', substr($pKey, 8, 4))[1],
|
||||
unpack('N', substr($pKey, 12, 4))[1]
|
||||
];
|
||||
|
||||
$sum = 0;
|
||||
for ($i = 0; $i < self::ROUNDS; $i++) {
|
||||
$sum = ($sum + self::DELTA) & 0xFFFFFFFF;
|
||||
$y = ($y + ((($z << 4) + $k[0]) ^ ($z + $sum) ^ (($z >> 5) + $k[1]))) & 0xFFFFFFFF;
|
||||
$z = ($z + ((($y << 4) + $k[2]) ^ ($y + $sum) ^ (($y >> 5) + $k[3]))) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
return pack('N2', $y, $z);
|
||||
}
|
||||
|
||||
/**
|
||||
* TEA ECB模式解密
|
||||
*/
|
||||
private function teaDecryptECB($pInBuf, $pKey)
|
||||
{
|
||||
$unpacked = unpack('N2', $pInBuf);
|
||||
$y = $unpacked[1];
|
||||
$z = $unpacked[2];
|
||||
|
||||
$k = [
|
||||
unpack('N', substr($pKey, 0, 4))[1],
|
||||
unpack('N', substr($pKey, 4, 4))[1],
|
||||
unpack('N', substr($pKey, 8, 4))[1],
|
||||
unpack('N', substr($pKey, 12, 4))[1]
|
||||
];
|
||||
|
||||
$sum = (self::DELTA << self::LOG_ROUNDS) & 0xFFFFFFFF;
|
||||
|
||||
for ($i = 0; $i < self::ROUNDS; $i++) {
|
||||
$z = ($z - ((($y << 4) + $k[2]) ^ ($y + $sum) ^ (($y >> 5) + $k[3]))) & 0xFFFFFFFF;
|
||||
$y = ($y - ((($z << 4) + $k[0]) ^ ($z + $sum) ^ (($z >> 5) + $k[1]))) & 0xFFFFFFFF;
|
||||
$sum = ($sum - self::DELTA) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
return pack('N2', $y, $z);
|
||||
}
|
||||
|
||||
// ================== CBC模式加解密 ===================
|
||||
|
||||
/**
|
||||
* CBC模式加密
|
||||
*/
|
||||
private function oiSymmetryEncrypt2($pInBuf, $nInBufLen, $pKey)
|
||||
{
|
||||
// 计算填充长度
|
||||
$nPadSaltBodyZeroLen = $nInBufLen + 1 + self::SALT_LEN + self::ZERO_LEN;
|
||||
$nPadlen = $nPadSaltBodyZeroLen % 8;
|
||||
if ($nPadlen) {
|
||||
$nPadlen = 8 - $nPadlen;
|
||||
}
|
||||
|
||||
$pOutBuf = '';
|
||||
|
||||
// 第一块数据
|
||||
$src_buf = array_fill(0, 8, 0);
|
||||
$src_buf[0] = (mt_rand(0, 255) & 0xF8) | $nPadlen;
|
||||
$src_i = 1;
|
||||
|
||||
// 填充
|
||||
while ($nPadlen) {
|
||||
$src_buf[$src_i] = mt_rand(0, 255);
|
||||
$src_i++;
|
||||
$nPadlen--;
|
||||
}
|
||||
|
||||
$iv_plain = array_fill(0, 8, 0);
|
||||
$iv_crypt = $iv_plain;
|
||||
|
||||
// 处理Salt
|
||||
$i = 0;
|
||||
while ($i < self::SALT_LEN) {
|
||||
if ($src_i < 8) {
|
||||
$src_buf[$src_i] = mt_rand(0, 255);
|
||||
$src_i++;
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($src_i == 8) {
|
||||
// 异或前一块密文
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$src_buf[$j] ^= $iv_crypt[$j];
|
||||
}
|
||||
|
||||
$temp_out = $this->teaEncryptECB(pack('C*', ...$src_buf), $pKey);
|
||||
$temp_bytes = array_values(unpack('C*', $temp_out));
|
||||
|
||||
// 异或前一块明文
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$temp_bytes[$j] ^= $iv_plain[$j];
|
||||
}
|
||||
|
||||
$iv_plain = $src_buf;
|
||||
$iv_crypt = $temp_bytes;
|
||||
$pOutBuf .= pack('C*', ...$temp_bytes);
|
||||
$src_i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理主体数据
|
||||
$pInBufIndex = 0;
|
||||
while ($nInBufLen) {
|
||||
if ($src_i < 8) {
|
||||
$src_buf[$src_i] = ord($pInBuf[$pInBufIndex]);
|
||||
$pInBufIndex++;
|
||||
$src_i++;
|
||||
$nInBufLen--;
|
||||
}
|
||||
|
||||
if ($src_i == 8) {
|
||||
// 异或前一块密文
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$src_buf[$j] ^= $iv_crypt[$j];
|
||||
}
|
||||
|
||||
$temp_out = $this->teaEncryptECB(pack('C*', ...$src_buf), $pKey);
|
||||
$temp_bytes = array_values(unpack('C*', $temp_out));
|
||||
|
||||
// 异或前一块明文
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$temp_bytes[$j] ^= $iv_plain[$j];
|
||||
}
|
||||
|
||||
$iv_plain = $src_buf;
|
||||
$iv_crypt = $temp_bytes;
|
||||
$pOutBuf .= pack('C*', ...$temp_bytes);
|
||||
$src_i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理Zero填充
|
||||
$i = 0;
|
||||
while ($i < self::ZERO_LEN) {
|
||||
if ($src_i < 8) {
|
||||
$src_buf[$src_i] = 0;
|
||||
$src_i++;
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($src_i == 8) {
|
||||
// 异或前一块密文
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$src_buf[$j] ^= $iv_crypt[$j];
|
||||
}
|
||||
|
||||
$temp_out = $this->teaEncryptECB(pack('C*', ...$src_buf), $pKey);
|
||||
$temp_bytes = array_values(unpack('C*', $temp_out));
|
||||
|
||||
// 异或前一块明文
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$temp_bytes[$j] ^= $iv_plain[$j];
|
||||
}
|
||||
|
||||
$iv_plain = $src_buf;
|
||||
$iv_crypt = $temp_bytes;
|
||||
$pOutBuf .= pack('C*', ...$temp_bytes);
|
||||
$src_i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后一组
|
||||
if ($src_i > 0) {
|
||||
// 填充剩余字节
|
||||
for ($j = $src_i; $j < 8; $j++) {
|
||||
$src_buf[$j] = 0;
|
||||
}
|
||||
|
||||
// 异或前一块密文
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$src_buf[$j] ^= $iv_crypt[$j];
|
||||
}
|
||||
|
||||
$temp_out = $this->teaEncryptECB(pack('C*', ...$src_buf), $pKey);
|
||||
$temp_bytes = array_values(unpack('C*', $temp_out));
|
||||
|
||||
// 异或前一块明文
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$temp_bytes[$j] ^= $iv_plain[$j];
|
||||
}
|
||||
|
||||
$pOutBuf .= pack('C*', ...$temp_bytes);
|
||||
}
|
||||
|
||||
return $pOutBuf;
|
||||
}
|
||||
|
||||
/**
|
||||
* CBC模式解密
|
||||
*/
|
||||
private function oiSymmetryDecrypt2($pInBuf, $nInBufLen, $pKey)
|
||||
{
|
||||
if (($nInBufLen % 8) != 0 || $nInBufLen < 16) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解密第一个块
|
||||
$dest_buf_str = $this->teaDecryptECB(substr($pInBuf, 0, 8), $pKey);
|
||||
$dest_buf = array_values(unpack('C*', $dest_buf_str));
|
||||
|
||||
$nPadLen = $dest_buf[0] & 0x07;
|
||||
|
||||
// 计算明文长度
|
||||
$i = $nInBufLen - 1;
|
||||
$i = $i - $nPadLen - self::SALT_LEN - self::ZERO_LEN;
|
||||
|
||||
if ($i < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pOutBufLen = $i;
|
||||
|
||||
// 初始化IV
|
||||
$iv_pre_crypt = array_fill(0, 8, 0);
|
||||
$iv_cur_crypt = array_values(unpack('C*', substr($pInBuf, 0, 8)));
|
||||
|
||||
$pInBufOffset = 8;
|
||||
$dest_i = 1;
|
||||
|
||||
// 跳过Padding
|
||||
$dest_i += $nPadLen;
|
||||
|
||||
// 跳过Salt
|
||||
$salt_count = 1;
|
||||
while ($salt_count <= self::SALT_LEN) {
|
||||
if ($dest_i < 8) {
|
||||
$dest_i++;
|
||||
$salt_count++;
|
||||
} elseif ($dest_i == 8) {
|
||||
$iv_pre_crypt = $iv_cur_crypt;
|
||||
$iv_cur_crypt = array_values(unpack('C*', substr($pInBuf, $pInBufOffset, 8)));
|
||||
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
if ($pInBufOffset + $j >= $nInBufLen) {
|
||||
return false;
|
||||
}
|
||||
$dest_buf[$j] ^= $iv_cur_crypt[$j];
|
||||
}
|
||||
|
||||
$temp_buf = $this->teaDecryptECB(pack('C*', ...$dest_buf), $pKey);
|
||||
$dest_buf = array_values(unpack('C*', $temp_buf));
|
||||
|
||||
$pInBufOffset += 8;
|
||||
$dest_i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 还原明文
|
||||
$nPlainLen = $pOutBufLen;
|
||||
$plain_bytes = [];
|
||||
|
||||
while ($nPlainLen > 0) {
|
||||
if ($dest_i < 8) {
|
||||
$plain_bytes[] = $dest_buf[$dest_i] ^ $iv_pre_crypt[$dest_i];
|
||||
$dest_i++;
|
||||
$nPlainLen--;
|
||||
} elseif ($dest_i == 8) {
|
||||
$iv_pre_crypt = $iv_cur_crypt;
|
||||
$iv_cur_crypt = array_values(unpack('C*', substr($pInBuf, $pInBufOffset, 8)));
|
||||
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
if ($pInBufOffset + $j >= $nInBufLen) {
|
||||
return false;
|
||||
}
|
||||
$dest_buf[$j] ^= $iv_cur_crypt[$j];
|
||||
}
|
||||
|
||||
$temp_buf = $this->teaDecryptECB(pack('C*', ...$dest_buf), $pKey);
|
||||
$dest_buf = array_values(unpack('C*', $temp_buf));
|
||||
|
||||
$pInBufOffset += 8;
|
||||
$dest_i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return pack('C*', ...$plain_bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 vsCKey::task_encGuard 生成 ck_guard_time。
|
||||
*/
|
||||
private function generateCkGuardTime($timestamp, $guid, $guardData = '-1', $packageName = 'null', $processName = 'null')
|
||||
{
|
||||
$body = pack('N', $timestamp);
|
||||
foreach ([
|
||||
$this->guardLastFive($guid),
|
||||
$this->guardLastFive($packageName),
|
||||
$this->guardLastFive($processName),
|
||||
$guardData
|
||||
] as $part) {
|
||||
$body .= pack('n', strlen($part)) . $part;
|
||||
}
|
||||
|
||||
$plain = pack('n', strlen($body)) . $body;
|
||||
$checksum = $this->calcSignature(array_values(unpack('C*', $plain)));
|
||||
|
||||
$encrypted = $this->oiSymmetryEncrypt2($plain, strlen($plain), hex2bin(self::GUARD_TEA_KEY));
|
||||
$encrypted .= pack('N', $checksum);
|
||||
|
||||
$bytes = array_values(unpack('C*', $encrypted));
|
||||
$len = count($bytes);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$bytes[$i] ^= $this->guardXorKey[$i & 7];
|
||||
}
|
||||
|
||||
return strtoupper(bin2hex(pack('C*', ...$bytes)));
|
||||
}
|
||||
|
||||
private function guardLastFive($value)
|
||||
{
|
||||
$value = (string)$value;
|
||||
return strlen($value) >= 5 ? substr($value, -5) : '';
|
||||
}
|
||||
|
||||
// ================== 公开的加解密方法 ===================
|
||||
|
||||
/**
|
||||
* 加密数据生成cKey
|
||||
* @param string $data 要加密的数据
|
||||
* @return string 生成的cKey
|
||||
*/
|
||||
public function encryptDataToCKey($data)
|
||||
{
|
||||
$teaCkey = hex2bin(self::TEA_CKEY);
|
||||
|
||||
// 计算数据长度
|
||||
$data_len = strlen($data);
|
||||
|
||||
// 计算校验和
|
||||
$data_array = array_values(unpack('C*', $data));
|
||||
$checksum = $this->calcSignature($data_array);
|
||||
|
||||
// TEA加密
|
||||
$encrypted = $this->oiSymmetryEncrypt2($data, $data_len, $teaCkey);
|
||||
|
||||
// 添加校验和
|
||||
$encrypted .= pack('N', $checksum);
|
||||
|
||||
// XOR加密
|
||||
$encrypted_array = array_values(unpack('C*', $encrypted));
|
||||
$xor_array = $this->xorArray($encrypted_array);
|
||||
$xor_encrypted = pack('C*', ...$xor_array);
|
||||
|
||||
// Base64编码
|
||||
$base64_encoded = $this->customEncode($xor_encrypted);
|
||||
|
||||
return "--01" . $base64_encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密cKey获取数据
|
||||
* @param string $ckey 要解密的cKey
|
||||
* @return array|false 解密后的数据和校验和,或false表示失败
|
||||
*/
|
||||
public function decryptCKeyToData($ckey)
|
||||
{
|
||||
$teaCkey = hex2bin(self::TEA_CKEY);
|
||||
|
||||
// 移除前缀
|
||||
$ckey_without_prefix = substr($ckey, 4);
|
||||
|
||||
// 自定义Base64解码
|
||||
$base64_decoded = $this->customDecode($ckey_without_prefix);
|
||||
if (!$base64_decoded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// XOR解密
|
||||
$xor_array = array_values(unpack('C*', $base64_decoded));
|
||||
$xor_decrypted_array = $this->xorArray($xor_array);
|
||||
$xor_decrypted = pack('C*', ...$xor_decrypted_array);
|
||||
|
||||
// 分离数据和校验和
|
||||
$data_len = strlen($xor_decrypted) - 4;
|
||||
$encrypted_data = substr($xor_decrypted, 0, $data_len);
|
||||
$checksum_bytes = substr($xor_decrypted, $data_len);
|
||||
$checksum = unpack('N', $checksum_bytes)[1];
|
||||
|
||||
// TEA解密
|
||||
$decrypted = $this->oiSymmetryDecrypt2($encrypted_data, $data_len, $teaCkey);
|
||||
|
||||
return [
|
||||
'data' => $decrypted,
|
||||
'checksum' => $checksum
|
||||
];
|
||||
}
|
||||
|
||||
// ================== 数据包构建方法 ===================
|
||||
|
||||
/**
|
||||
* 构建数据包
|
||||
* @param array $params 参数数组
|
||||
* @return string 构建的数据包
|
||||
*/
|
||||
public function buildPacket($params)
|
||||
{
|
||||
$data = '';
|
||||
|
||||
// 1. 头部 (12字节) - 固定值
|
||||
$data .= hex2bin('0000004200000004000004d2');
|
||||
|
||||
// 2. Platform (4字节)
|
||||
$data .= pack('N', $params['Platform']);
|
||||
|
||||
// 3. Signature (4字节) - 先置0,后面计算
|
||||
$data .= pack('N', 0);
|
||||
|
||||
// 4. Timestamp (4字节)
|
||||
$data .= pack('N', $params['Timestamp']);
|
||||
|
||||
// 5. Sdtfrom (长度+字符串)
|
||||
$sdtfrom = $params['Sdtfrom'];
|
||||
$data .= pack('n', strlen($sdtfrom)) . $sdtfrom;
|
||||
|
||||
// 6. randFlag (长度+字符串) - 使用传入的值
|
||||
$randFlag = $params['randFlag'];
|
||||
$data .= pack('n', strlen($randFlag)) . $randFlag;
|
||||
|
||||
// 7. appVer (长度+字符串)
|
||||
$appVer = $params['appVer'];
|
||||
$data .= pack('n', strlen($appVer)) . $appVer;
|
||||
|
||||
// 8. vid (长度+字符串)
|
||||
$vid = $params['vid'];
|
||||
$data .= pack('n', strlen($vid)) . $vid;
|
||||
|
||||
// 9. guid (长度+字符串)
|
||||
$guid = $params['guid'];
|
||||
$data .= pack('n', strlen($guid)) . $guid;
|
||||
|
||||
// 10. part1 (4字节)
|
||||
$data .= pack('N', 1);
|
||||
|
||||
// 11. isDlna (4字节) - 根据原始样本是0
|
||||
$data .= pack('N', 1);
|
||||
|
||||
// 12. uid (长度+字符串)
|
||||
$uid = "2622783A";
|
||||
$data .= pack('n', strlen($uid)) . $uid;
|
||||
|
||||
// 13. bundleID (长度+字符串)
|
||||
$bundleID = "nil";
|
||||
$data .= pack('n', strlen($bundleID)) . $bundleID;
|
||||
|
||||
// 14. uuid4 (长度+字符串)
|
||||
$uuid4 = $params['uuid4'];
|
||||
$data .= pack('n', strlen($uuid4)) . $uuid4;
|
||||
|
||||
// 15. bundleID1 (长度+字符串) - 重复bundleID
|
||||
$data .= pack('n', strlen($bundleID)) . $bundleID;
|
||||
|
||||
// 16. ckeyVersion (长度+字符串)
|
||||
$ckeyVersion = "v0.1.000";
|
||||
$data .= pack('n', strlen($ckeyVersion)) . $ckeyVersion;
|
||||
|
||||
// 17. packageName (长度+字符串)
|
||||
$packageName = "com.cctv.yangshipin.app.iphone";
|
||||
$data .= pack('n', strlen($packageName)) . $packageName;
|
||||
|
||||
// 18. platform_str (长度+字符串)
|
||||
$platform_str = "4330403";
|
||||
$data .= pack('n', strlen($platform_str)) . $platform_str;
|
||||
|
||||
// 19. ex_json_bus (长度+字符串)
|
||||
$ex_json_bus = "ex_json_bus";
|
||||
$data .= pack('n', strlen($ex_json_bus)) . $ex_json_bus;
|
||||
|
||||
// 20. ex_json_vs (长度+字符串)
|
||||
$ex_json_vs = "ex_json_vs";
|
||||
$data .= pack('n', strlen($ex_json_vs)) . $ex_json_vs;
|
||||
|
||||
// 21. ck_guard_time (长度+字符串) - 88个字符
|
||||
$ck_guard_time = $params['ck_guard_time'];
|
||||
$data .= pack('n', strlen($ck_guard_time)) . $ck_guard_time;
|
||||
|
||||
// 验证主体长度
|
||||
$body_length = strlen($data);
|
||||
|
||||
// 添加长度头
|
||||
$buffer = pack('n', $body_length) . $data;
|
||||
|
||||
// 计算签名
|
||||
$buffer_array = array_values(unpack('C*', $buffer));
|
||||
$signature = $this->calcSignature($buffer_array);
|
||||
|
||||
// 更新签名(位置:跳过长度头2字节+头部12字节+Platform4字节=18字节处)
|
||||
$buffer = substr($buffer, 0, 18) . pack('N', $signature) . substr($buffer, 22);
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成完整的cKey
|
||||
* @param string $cnlid 频道ID
|
||||
* @param int|null $timestamp 时间戳
|
||||
* @return array 包含cKey、参数和数据包的数组
|
||||
*/
|
||||
public function generateCKey($cnlid, $timestamp = null)
|
||||
{
|
||||
if ($timestamp === null) {
|
||||
$timestamp = time();
|
||||
}
|
||||
|
||||
// 生成所有随机值
|
||||
$randFlag = base64_encode(random_bytes(18));
|
||||
$uuid4 = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0x0fff) | 0x4000,
|
||||
mt_rand(0, 0x3fff) | 0x8000,
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||
);
|
||||
$ck_guard_time = $this->generateCkGuardTime($timestamp, $this->guid);
|
||||
$randFlag='_zj1A5Gh6QYcxWjIUGos2w==';
|
||||
$params = [
|
||||
'Platform' => 4330403,
|
||||
'Timestamp' => $timestamp,
|
||||
'Sdtfrom' => 'dcgh',
|
||||
'vid' => $cnlid,
|
||||
'guid' => $this->guid, // 使用当前GUID
|
||||
'appVer' => 'V8.22.1035.3031',
|
||||
'randFlag' => $randFlag,
|
||||
'uuid4' => '57eab0c4-2c58-44c6-8ae9-dd2757525dc5',
|
||||
'ck_guard_time' => $ck_guard_time
|
||||
];
|
||||
|
||||
$buffer = $this->buildPacket($params);
|
||||
$ckey = $this->encryptDataToCKey($buffer);
|
||||
|
||||
return [
|
||||
'ckey' => $ckey,
|
||||
'params' => $params,
|
||||
'buffer' => $buffer
|
||||
];
|
||||
}
|
||||
|
||||
// ================== 网络请求方法(优化版本) ===================
|
||||
|
||||
/**
|
||||
* 发起直播或回看请求(优化版)
|
||||
* @param string $cnlid 频道ID
|
||||
* @param string $livepid 直播ID
|
||||
* @param string $defn 清晰度
|
||||
* @param string|null $playseek 回看时间(格式:YYYYMMDDHHMMSS-YYYYMMDDHHMMSS)
|
||||
* @return array 请求结果
|
||||
*/
|
||||
public function makeLiveRequest($cnlid, $livepid = '600001859', $defn = 'fhd', $playseek = null)
|
||||
{
|
||||
// 每次请求生成新的随机GUID
|
||||
$this->generateGuid();
|
||||
|
||||
// 只生成一次cKey(直播和回看使用相同的cKey参数)
|
||||
$ckeyResult = $this->generateCKey($cnlid);
|
||||
$ckey = $ckeyResult['ckey'];
|
||||
$params = $ckeyResult['params'];
|
||||
|
||||
// 生成flowid
|
||||
$flowid = sprintf('%s_%d',
|
||||
sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X',
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0x0fff) | 0x4000,
|
||||
mt_rand(0, 0x3fff) | 0x8000,
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||
),
|
||||
4330403
|
||||
);
|
||||
|
||||
// 提前判断模式并设置参数
|
||||
$isPlayback = !empty($playseek);
|
||||
$playbackTimestamp = null;
|
||||
|
||||
// 处理回看时间
|
||||
if ($isPlayback) {
|
||||
try {
|
||||
$parts = explode('-', $playseek);
|
||||
$startTimeStr = $parts[0];
|
||||
$dateTime = DateTime::createFromFormat('YmdHis', $startTimeStr, new DateTimeZone('Asia/Shanghai'));
|
||||
if ($dateTime === false) {
|
||||
throw new Exception("回看时间格式错误: " . $startTimeStr);
|
||||
}
|
||||
$playbackTimestamp = $dateTime->getTimestamp();
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => '回看时间处理失败: ' . $e->getMessage(),
|
||||
'playseek' => $playseek
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 构建基础请求参数(根据模式设置不同参数)
|
||||
$spvcode = $this->spvcode($defn);
|
||||
$request_params = [
|
||||
"atime" => "120",
|
||||
"livepid" => $livepid,
|
||||
"cnlid" => $cnlid,
|
||||
"appVer" => "V8.22.1035.3031",
|
||||
"app_version" => "300090",
|
||||
"caplv" => "1",
|
||||
"cmd" => "2",
|
||||
"defn" => $defn,
|
||||
"device" => "iPhone",
|
||||
"encryptVer" => "4.2",
|
||||
"getpreviewinfo" => "0",
|
||||
"hevclv" => "33",
|
||||
"lang" => "zh-Hans_JP",
|
||||
"livequeue" => "0",
|
||||
"logintype" => "1",
|
||||
"nettype" => "1",
|
||||
"newnettype" => "1",
|
||||
"newplatform" => "4330403",
|
||||
"platform" => "4330403",
|
||||
"sdtfrom" => "v3021",
|
||||
"spacode" => "23",
|
||||
"spaudio" => "1",
|
||||
"spdemuxer" => "6",
|
||||
"spdrm" => "2",
|
||||
"spdynamicrange" => "7",
|
||||
"spflv" => "1",
|
||||
"spflvaudio" => "1",
|
||||
"sphdrfps" => "60",
|
||||
"sphttps" => "0",
|
||||
"spvcode" => "MSgzMDoyMTYwLDYwOjIxNjB8MzA6MjE2MCw2MDoyMTYwKTsyKDMwOjIxNjAsNjA6MjE2MHwzMDoyMTYwLDYwOjIxNjAp",
|
||||
"spvideo" => "4",
|
||||
"stream" => "1",
|
||||
"system" => "1",
|
||||
"sysver" => "ios18.2.1",
|
||||
"uhd_flag" => "4",
|
||||
"cKey" => $ckey,
|
||||
"guid" => $this->guid,
|
||||
"fntick" => $params['Timestamp'],
|
||||
"flowid" => $flowid,
|
||||
];
|
||||
// 根据模式设置不同的参数
|
||||
if ($isPlayback) {
|
||||
// 回看模式:第一次尝试 - 添加playbacktime参数
|
||||
$request_params['playbacktime'] = $playbackTimestamp;
|
||||
|
||||
// 发送第一次请求
|
||||
$response = $this->sendHttpRequest($request_params);
|
||||
|
||||
if ($response['success'] && isset($response['response']['playurl'])) {
|
||||
return $response;
|
||||
} else {
|
||||
// 第二次尝试 - 不添加playbacktime参数
|
||||
unset($request_params['playbacktime']);
|
||||
|
||||
// 发送第二次请求
|
||||
$response = $this->sendHttpRequest($request_params);
|
||||
|
||||
if ($response['success'] && isset($response['response']['playurl'])) {
|
||||
// 手动处理URL:修改域名并添加starttime参数
|
||||
$playurl = $response['response']['playurl'];
|
||||
$playurl = $this->processPlaybackUrl($playurl, $playbackTimestamp);
|
||||
$response['response']['playurl'] = $playurl;
|
||||
return $response;
|
||||
} else {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => '无法获取回看地址',
|
||||
'playseek' => $playseek,
|
||||
'response' => $response['response'] ?? null
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直播模式:设置playbacktime为0
|
||||
$request_params['playbacktime'] = "0";
|
||||
|
||||
// 发送请求
|
||||
return $this->sendHttpRequest($request_params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理回看URL
|
||||
* @param string $playurl 原始播放URL
|
||||
* @param int $playbackTimestamp 回看时间戳
|
||||
* @return string 处理后的URL
|
||||
*/
|
||||
private function processPlaybackUrl($playurl, $playbackTimestamp)
|
||||
{
|
||||
// 修改域名
|
||||
$urlParts = explode('/', $playurl);
|
||||
if (count($urlParts) >= 3) {
|
||||
$urlParts[2] = 'tlivecloud-playback-cdn.ysp.cctv.cn/tcloud.cctv.com';
|
||||
$playurl = implode('/', $urlParts);
|
||||
|
||||
// 添加starttime参数
|
||||
if (strpos($playurl, '?') !== false) {
|
||||
$playurl .= '&starttime=' . $playbackTimestamp;
|
||||
} else {
|
||||
$playurl .= '?starttime=' . $playbackTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
return $playurl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送HTTP请求
|
||||
* @param array $params 请求参数
|
||||
* @return array 请求结果
|
||||
*/
|
||||
private function sendHttpRequest($params)
|
||||
{
|
||||
$url = "https://bkliveinfo.ysp.cctv.cn";
|
||||
$query_string = http_build_query($params);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url . '?' . $query_string);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'User-Agent: qqlive',
|
||||
'Connection: Keep-Alive',
|
||||
'Accept: application/json'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$error = curl_error($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'cURL错误: ' . $error,
|
||||
'http_code' => $http_code
|
||||
];
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if ($data) {
|
||||
if (isset($data['iretcode'])) {
|
||||
$result = [
|
||||
'success' => $data['iretcode'] == 0,
|
||||
'iretcode' => $data['iretcode'],
|
||||
'http_code' => $http_code,
|
||||
'response' => $data
|
||||
];
|
||||
|
||||
if ($data['iretcode'] == 0) {
|
||||
$result['playurl'] = $data['playurl'] ?? null;
|
||||
} else {
|
||||
$result['error'] = $data['errinfo'] ?? '未知错误';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => '无效的JSON响应',
|
||||
'http_code' => $http_code,
|
||||
'raw_response' => substr($response, 0, 500)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化版获取播放地址(支持回看)
|
||||
* @param string $cnlid 频道ID
|
||||
* @param string $livepid 直播ID(可选,默认600001859)
|
||||
* @param string $defn 清晰度(可选,默认fhd)
|
||||
* @param string|null $playseek 回看时间(可选,null表示直播)
|
||||
* @return string|null 播放地址或null
|
||||
*/
|
||||
public function getPlayUrl($cnlid, $livepid = '600001859', $defn = 'fhd', $playseek = null)
|
||||
{
|
||||
$result = $this->makeLiveRequest($cnlid, $livepid, $defn, $playseek);
|
||||
if ($result['success'] && isset($result['playurl'])) {
|
||||
return $result['playurl'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ================== 数据包解析方法 ===================
|
||||
|
||||
/**
|
||||
* 解析数据包
|
||||
* @param string $data 数据包二进制数据
|
||||
* @return array 解析后的字段数组
|
||||
*/
|
||||
public function parsePacket($data)
|
||||
{
|
||||
if (strlen($data) < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pos = 0;
|
||||
$result = [];
|
||||
|
||||
// 1. 长度头 (2字节)
|
||||
$result['packet_len'] = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
|
||||
// 2. 固定头部 (12字节)
|
||||
$result['header'] = substr($data, $pos, 12);
|
||||
$pos += 12;
|
||||
|
||||
// 3. Platform (4字节)
|
||||
$result['platform'] = unpack('N', substr($data, $pos, 4))[1];
|
||||
$pos += 4;
|
||||
|
||||
// 4. Signature (4字节)
|
||||
$result['signature'] = unpack('N', substr($data, $pos, 4))[1];
|
||||
$pos += 4;
|
||||
|
||||
// 5. Timestamp (4字节)
|
||||
$result['timestamp'] = unpack('N', substr($data, $pos, 4))[1];
|
||||
$pos += 4;
|
||||
|
||||
// 6. Sdtfrom (长度+字符串)
|
||||
$sdtfrom_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['sdtfrom'] = substr($data, $pos, $sdtfrom_len);
|
||||
$pos += $sdtfrom_len;
|
||||
|
||||
// 7. randFlag (长度+字符串)
|
||||
$randFlag_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['randFlag'] = substr($data, $pos, $randFlag_len);
|
||||
$pos += $randFlag_len;
|
||||
|
||||
// 8. appVer (长度+字符串)
|
||||
$appVer_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['appVer'] = substr($data, $pos, $appVer_len);
|
||||
$pos += $appVer_len;
|
||||
|
||||
// 9. vid (长度+字符串)
|
||||
$vid_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['vid'] = substr($data, $pos, $vid_len);
|
||||
$pos += $vid_len;
|
||||
|
||||
// 10. guid (长度+字符串)
|
||||
$guid_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['guid'] = substr($data, $pos, $guid_len);
|
||||
$pos += $guid_len;
|
||||
|
||||
// 11. part1 (4字节)
|
||||
$result['part1'] = unpack('N', substr($data, $pos, 4))[1];
|
||||
$pos += 4;
|
||||
|
||||
// 12. isDlna (4字节)
|
||||
$result['isDlna'] = unpack('N', substr($data, $pos, 4))[1];
|
||||
$pos += 4;
|
||||
|
||||
// 13. uid (长度+字符串)
|
||||
$uid_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['uid'] = substr($data, $pos, $uid_len);
|
||||
$pos += $uid_len;
|
||||
|
||||
// 14. bundleID (长度+字符串)
|
||||
$bundleID_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['bundleID'] = substr($data, $pos, $bundleID_len);
|
||||
$pos += $bundleID_len;
|
||||
|
||||
// 15. uuid4 (长度+字符串)
|
||||
$uuid4_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['uuid4'] = substr($data, $pos, $uuid4_len);
|
||||
$pos += $uuid4_len;
|
||||
|
||||
// 16. bundleID1 (长度+字符串)
|
||||
$bundleID1_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['bundleID1'] = substr($data, $pos, $bundleID1_len);
|
||||
$pos += $bundleID1_len;
|
||||
|
||||
// 17. ckeyVersion (长度+字符串)
|
||||
$ckeyVersion_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['ckeyVersion'] = substr($data, $pos, $ckeyVersion_len);
|
||||
$pos += $ckeyVersion_len;
|
||||
|
||||
// 18. packageName (长度+字符串)
|
||||
$packageName_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['packageName'] = substr($data, $pos, $packageName_len);
|
||||
$pos += $packageName_len;
|
||||
|
||||
// 19. platform_str (长度+字符串)
|
||||
$platform_str_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['platform_str'] = substr($data, $pos, $platform_str_len);
|
||||
$pos += $platform_str_len;
|
||||
|
||||
// 20. ex_json_bus (长度+字符串)
|
||||
$ex_json_bus_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['ex_json_bus'] = substr($data, $pos, $ex_json_bus_len);
|
||||
$pos += $ex_json_bus_len;
|
||||
|
||||
// 21. ex_json_vs (长度+字符串)
|
||||
$ex_json_vs_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['ex_json_vs'] = substr($data, $pos, $ex_json_vs_len);
|
||||
$pos += $ex_json_vs_len;
|
||||
|
||||
// 22. ck_guard_time (长度+字符串)
|
||||
$ck_guard_time_len = unpack('n', substr($data, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$result['ck_guard_time'] = substr($data, $pos, $ck_guard_time_len);
|
||||
$pos += $ck_guard_time_len;
|
||||
|
||||
$result['total_size'] = strlen($data);
|
||||
$result['parsed_size'] = $pos;
|
||||
$result['remaining'] = substr($data, $pos);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证cKey
|
||||
* @param string $ckey cKey字符串
|
||||
* @return bool 是否验证通过
|
||||
*/
|
||||
public function verifyCKey($ckey)
|
||||
{
|
||||
$decrypt_result = $this->decryptCKeyToData($ckey);
|
||||
if (!$decrypt_result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$calculated_checksum = $this->calcSignature(array_values(unpack('C*', $decrypt_result['data'])));
|
||||
return $decrypt_result['checksum'] == $calculated_checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析回看时间字符串
|
||||
* @param string $playseek 回看时间字符串(格式:YYYYMMDDHHMMSS-YYYYMMDDHHMMSS)
|
||||
* @return array 包含开始时间和结束时间的数组
|
||||
*/
|
||||
public function parsePlayseek($playseek)
|
||||
{
|
||||
$parts = explode('-', $playseek);
|
||||
if (count($parts) !== 2) {
|
||||
throw new Exception("回看时间格式错误,应为: YYYYMMDDHHMMSS-YYYYMMDDHHMMSS");
|
||||
}
|
||||
|
||||
$startTimeStr = $parts[0];
|
||||
$endTimeStr = $parts[1];
|
||||
|
||||
$startTime = DateTime::createFromFormat('YmdHis', $startTimeStr, new DateTimeZone('Asia/Shanghai'));
|
||||
$endTime = DateTime::createFromFormat('YmdHis', $endTimeStr, new DateTimeZone('Asia/Shanghai'));
|
||||
|
||||
if ($startTime === false || $endTime === false) {
|
||||
throw new Exception("回看时间解析失败");
|
||||
}
|
||||
|
||||
return [
|
||||
'start_time' => $startTime,
|
||||
'end_time' => $endTime,
|
||||
'start_timestamp' => $startTime->getTimestamp(),
|
||||
'end_timestamp' => $endTime->getTimestamp(),
|
||||
'start_str' => $startTime->format('Y-m-d H:i:s'),
|
||||
'end_str' => $endTime->format('Y-m-d H:i:s'),
|
||||
'duration' => $endTime->getTimestamp() - $startTime->getTimestamp()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成回看时间字符串
|
||||
* @param string $startDateTime 开始时间(格式:Y-m-d H:i:s)
|
||||
* @param string $endDateTime 结束时间(格式:Y-m-d H:i:s)
|
||||
* @return string 回看时间字符串
|
||||
*/
|
||||
public function generatePlayseek($startDateTime, $endDateTime)
|
||||
{
|
||||
$startTime = DateTime::createFromFormat('Y-m-d H:i:s', $startDateTime, new DateTimeZone('Asia/Shanghai'));
|
||||
$endTime = DateTime::createFromFormat('Y-m-d H:i:s', $endDateTime, new DateTimeZone('Asia/Shanghai'));
|
||||
|
||||
if ($startTime === false || $endTime === false) {
|
||||
throw new Exception("时间格式错误,应为: Y-m-d H:i:s");
|
||||
}
|
||||
|
||||
return $startTime->format('YmdHis') . '-' . $endTime->format('YmdHis');
|
||||
}
|
||||
}
|
||||
|
||||
$ckeyManager = new CKeyManager();
|
||||
$playseek = $_GET['playseek'] ?? null;
|
||||
// 缓存配置(仅用于直播)
|
||||
$cookieKey = 'playurl_cache';
|
||||
$cacheTimeoutLive = 80; // 直播缓存超时 4分钟
|
||||
$cookieExpire = time() + 3600; // Cookie本身有效期1小时
|
||||
|
||||
// 读取缓存
|
||||
$cacheJson = $_COOKIE[$cookieKey] ?? '{}';
|
||||
$cache = json_decode($cacheJson, true) ?: [];
|
||||
|
||||
$now = time();
|
||||
$isLive = ($playseek === null || $playseek === ''); // 无playseek参数视为直播
|
||||
|
||||
$playUrl = null;
|
||||
$m3u8Content = false;
|
||||
$maxAttempts = 2;
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
||||
$needRefresh = true; // 默认需要刷新
|
||||
|
||||
// 仅在第一次尝试且为直播时检查缓存
|
||||
if ($attempt == 1 && $isLive) {
|
||||
if (isset($cache[$id]) && is_array($cache[$id])) {
|
||||
$entry = $cache[$id];
|
||||
if (($now - $entry['time']) <= $cacheTimeoutLive) {
|
||||
$needRefresh = false;
|
||||
$playUrl = $entry['url'];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 点播模式或直播缓存失效/不存在时,needRefresh保持true
|
||||
|
||||
if ($needRefresh) {
|
||||
$playUrl = $ckeyManager->getPlayUrl($n[$id][0], $n[$id][1], $n[$id][2], $playseek);
|
||||
if (!$playUrl) {
|
||||
die("获取播放地址失败\n");
|
||||
}
|
||||
|
||||
// 只有直播才更新缓存
|
||||
if ($isLive) {
|
||||
$cache[$id] = [
|
||||
'url' => $playUrl,
|
||||
'time' => $now
|
||||
];
|
||||
setcookie($cookieKey, json_encode($cache), $cookieExpire, '/');
|
||||
} else {
|
||||
// 点播模式直接跳转(不输出M3U8)
|
||||
header("Location: " . $playUrl);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 M3U8 内容(仅直播模式会走到这里,点播已在上面跳转)
|
||||
$m3u8Content = @file_get_contents($playUrl);
|
||||
if ($m3u8Content === false) {
|
||||
$m3u8Content = false; // 明确设为false,进入后续重试判断
|
||||
}
|
||||
|
||||
if ($m3u8Content !== false) {
|
||||
break; // 成功获取,跳出循环
|
||||
}
|
||||
|
||||
// 获取失败:如果是第一次尝试且使用了缓存地址,则清除缓存
|
||||
if ($attempt == 1 && $isLive && !$needRefresh) {
|
||||
unset($cache[$id]);
|
||||
setcookie($cookieKey, json_encode($cache), $cookieExpire, '/');
|
||||
}
|
||||
// 继续下一次尝试
|
||||
}
|
||||
|
||||
if ($m3u8Content === false) {
|
||||
die("无法获取 M3U8 内容,请稍后重试\n");
|
||||
}
|
||||
|
||||
// 补全 TS 路径(仅直播)
|
||||
$baseUrl = substr($playUrl, 0, strrpos($playUrl, '/') + 1);
|
||||
header('Content-Type: application/vnd.apple.mpegurl');
|
||||
print_r(preg_replace("/(.*?.ts)/i", $baseUrl."$1",$m3u8Content));
|
||||
exit();
|
||||
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
/**
|
||||
* 七猫小说[书]
|
||||
* 移植自 JS 源 (e:\php_work\php\js\七猫小说[书].js)
|
||||
*/
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private const HOST = 'https://www.qimao.com';
|
||||
private const LIST_URL_TEMPLATE = 'https://www.qimao.com/shuku/%s-%s-%s/';
|
||||
private const DETAIL_URL = 'https://api-ks.wtzw.com/api/v2/book/detail';
|
||||
private const SEARCH_URL = 'https://api-bc.wtzw.com/search/v1/words';
|
||||
private const CONTENT_URL = 'https://api-ks.wtzw.com/api/v1/chapter/content';
|
||||
|
||||
// JS 源中的 Sign Key
|
||||
private const SIGN_KEY = 'd3dGiJc651gSQ8w1';
|
||||
private const AES_KEY_HEX = '32343263636238323330643730396531';
|
||||
|
||||
private $startPage = 1;
|
||||
|
||||
public function init($extend = '') {
|
||||
$this->startPage = 1;
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$classes = [
|
||||
['type_id' => 'a', 'type_name' => '全部'],
|
||||
['type_id' => '1', 'type_name' => '女生原创'],
|
||||
['type_id' => '0', 'type_name' => '男生原创'],
|
||||
['type_id' => '2', 'type_name' => '出版图书']
|
||||
];
|
||||
|
||||
$filters = [];
|
||||
// Filter URL pattern: {{fl.作品分类 or 'a'}}-a-{{fl.作品字数 or 'a'}}-{{fl.更新时间 or 'a'}}-a-{{fl.是否完结 or 'a'}}-{{fl.排序 or 'click'}}
|
||||
// 注意 URL 结构: /shuku/{class}-{filter}-{page}/
|
||||
// class 是 type_id.
|
||||
// filter string: type-a-word-time-a-status-sort
|
||||
|
||||
$filterConfig = [
|
||||
'key' => 'filters',
|
||||
'name' => '筛选',
|
||||
'value' => [
|
||||
['n' => '作品分类', 'v' => 'type', 'init' => 'a', 'list' => [
|
||||
['n' => '全部', 'v' => 'a'],
|
||||
['n' => '言情', 'v' => '7'],
|
||||
['n' => '都市', 'v' => '1'],
|
||||
['n' => '玄幻', 'v' => '8'],
|
||||
['n' => '战神', 'v' => '295'],
|
||||
['n' => '赘婿', 'v' => '298'],
|
||||
['n' => '神医', 'v' => '297'],
|
||||
['n' => '脑洞', 'v' => '253'],
|
||||
['n' => '悬疑', 'v' => '10'],
|
||||
['n' => '历史', 'v' => '2'],
|
||||
['n' => '武侠', 'v' => '4'],
|
||||
['n' => '游戏', 'v' => '5'],
|
||||
['n' => '科幻', 'v' => '6'],
|
||||
['n' => '现言', 'v' => '17'],
|
||||
['n' => '古言', 'v' => '13'],
|
||||
['n' => '穿越', 'v' => '23'],
|
||||
['n' => '重生', 'v' => '24'],
|
||||
['n' => '豪门', 'v' => '32'],
|
||||
['n' => '其他', 'v' => '11'],
|
||||
]],
|
||||
['n' => '作品字数', 'v' => 'word', 'init' => 'a', 'list' => [
|
||||
['n' => '全部', 'v' => 'a'],
|
||||
['n' => '30万字以下', 'v' => '1'],
|
||||
['n' => '30-50万字', 'v' => '2'],
|
||||
['n' => '50-100万字', 'v' => '3'],
|
||||
['n' => '100-200万字', 'v' => '4'],
|
||||
['n' => '200万字以上', 'v' => '5'],
|
||||
]],
|
||||
['n' => '更新时间', 'v' => 'time', 'init' => 'a', 'list' => [
|
||||
['n' => '全部', 'v' => 'a'],
|
||||
['n' => '3日内', 'v' => '1'],
|
||||
['n' => '7日内', 'v' => '2'],
|
||||
['n' => '半月内', 'v' => '3'],
|
||||
['n' => '一月内', 'v' => '4'],
|
||||
]],
|
||||
['n' => '是否完结', 'v' => 'status', 'init' => 'a', 'list' => [
|
||||
['n' => '全部', 'v' => 'a'],
|
||||
['n' => '连载中', 'v' => '1'],
|
||||
['n' => '已完结', 'v' => '2'],
|
||||
]],
|
||||
['n' => '排序', 'v' => 'sort', 'init' => 'click', 'list' => [
|
||||
['n' => '人气', 'v' => 'click'],
|
||||
['n' => '更新', 'v' => 'date'],
|
||||
['n' => '评分', 'v' => 'score'],
|
||||
]]
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($classes as $class) {
|
||||
$filters[$class['type_id']] = [$filterConfig];
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => (object)$filters
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
// Filter logic:
|
||||
// {{fl.作品分类 or 'a'}}-a-{{fl.作品字数 or 'a'}}-{{fl.更新时间 or 'a'}}-a-{{fl.是否完结 or 'a'}}-{{fl.排序 or 'click'}}
|
||||
$f_type = $extend['type'] ?? 'a';
|
||||
$f_word = $extend['word'] ?? 'a';
|
||||
$f_time = $extend['time'] ?? 'a';
|
||||
$f_status = $extend['status'] ?? 'a';
|
||||
$f_sort = $extend['sort'] ?? 'click';
|
||||
|
||||
$filterStr = "{$f_type}-a-{$f_word}-{$f_time}-a-{$f_status}-{$f_sort}";
|
||||
|
||||
// URL: /shuku/{class}-{filter}-{page}/
|
||||
$url = sprintf(self::LIST_URL_TEMPLATE, $tid, $filterStr, $pg);
|
||||
|
||||
$html = $this->fetch($url);
|
||||
|
||||
$videos = [];
|
||||
if ($html) {
|
||||
$items = $this->pdfa($html, 'ul.qm-cover-text&&li');
|
||||
foreach ($items as $itemHtml) {
|
||||
$video = [
|
||||
'vod_id' => '',
|
||||
'vod_name' => $this->pdfh($itemHtml, '.s-tit&&Text'),
|
||||
'vod_pic' => $this->pd($itemHtml, 'img&&src', $url),
|
||||
'vod_remarks' => $this->pdfh($itemHtml, '.s-author&&Text'),
|
||||
'vod_content' => $this->pdfh($itemHtml, '.s-desc&&Text')
|
||||
];
|
||||
|
||||
$href = $this->pd($itemHtml, 'a&&href', $url);
|
||||
if (preg_match('/shuku\/(\d+)/', $href, $matches)) {
|
||||
$video['vod_id'] = $matches[1];
|
||||
}
|
||||
|
||||
if (!empty($video['vod_id'])) {
|
||||
$videos[] = $video;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($videos, $pg);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = $ids[0]; // This is book_id
|
||||
$url = self::HOST . "/shuku/$id/";
|
||||
|
||||
// 1. Fetch Detail Page for basic info
|
||||
$html = $this->fetch($url);
|
||||
$vod = [
|
||||
'vod_id' => $id,
|
||||
'vod_name' => '',
|
||||
'vod_pic' => '',
|
||||
'vod_content' => '',
|
||||
'vod_remarks' => '',
|
||||
'vod_director' => '',
|
||||
'vod_play_from' => '七猫小说',
|
||||
];
|
||||
|
||||
if ($html) {
|
||||
$vod['vod_name'] = $this->pdfh($html, 'span.txt&&Text');
|
||||
$vod['vod_pic'] = $this->pd($html, '.wrap-pic&&img&&src', $url);
|
||||
$vod['vod_content'] = $this->pdfh($html, '.book-introduction-item&&.qm-with-title-tb&&Text');
|
||||
$vod['vod_director'] = $this->pdfh($html, '.sub-title&&span&&a&&Text');
|
||||
$vod['vod_remarks'] = $this->pdfh($html, '.qm-tag&&Text');
|
||||
}
|
||||
|
||||
// 2. Fetch Chapter List via API
|
||||
// https://www.qimao.com/api/book/chapter-list?book_id=1699328
|
||||
$chapterUrl = self::HOST . "/api/book/chapter-list?book_id=$id";
|
||||
$json = $this->fetchJson($chapterUrl);
|
||||
|
||||
$playList = [];
|
||||
if (isset($json['data']['chapters'])) {
|
||||
foreach ($json['data']['chapters'] as $ch) {
|
||||
$title = $ch['title'] ?? '';
|
||||
$cid = $ch['id'] ?? '';
|
||||
// Format: title$book_id@@chapter_id@@title
|
||||
$playList[] = "$title$$id@@$cid@@$title";
|
||||
}
|
||||
}
|
||||
|
||||
$vod['vod_play_url'] = implode('#', $playList);
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$params = [
|
||||
'extend' => '',
|
||||
'tab' => '0', // Missing in previous version
|
||||
'gender' => '0',
|
||||
'refresh_state' => '8', // Missing in previous version
|
||||
'page' => $pg,
|
||||
'wd' => $key,
|
||||
'is_short_story_user' => '0'
|
||||
];
|
||||
|
||||
// Calculate Sign
|
||||
$signStr = "";
|
||||
ksort($params);
|
||||
foreach ($params as $k => $v) {
|
||||
$signStr .= $k . "=" . $v;
|
||||
}
|
||||
$signStr .= self::SIGN_KEY;
|
||||
$params['sign'] = md5($signStr);
|
||||
|
||||
$url = self::SEARCH_URL . '?' . http_build_query($params);
|
||||
// echo "DEBUG Search URL: $url\n";
|
||||
|
||||
$headers = $this->getSignHeaders();
|
||||
// Use fetch to see raw response
|
||||
$raw = $this->fetch($url, ['headers' => $headers]);
|
||||
// echo "DEBUG Search Response: " . substr($raw, 0, 200) . "\n";
|
||||
$json = json_decode($raw, true);
|
||||
|
||||
$videos = [];
|
||||
if (!empty($json['data']['books'])) {
|
||||
foreach ($json['data']['books'] as $item) {
|
||||
// Python filters by show_type == '0'
|
||||
if (isset($item['show_type']) && $item['show_type'] == '0') {
|
||||
$videos[] = [
|
||||
'vod_id' => $item['id'],
|
||||
'vod_name' => $item['original_title'],
|
||||
'vod_pic' => $item['image_link'] ?? '',
|
||||
'vod_remarks' => $item['author'] ?? '',
|
||||
'vod_content' => $item['intro'] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
'list' => $videos
|
||||
];
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
// id format: title$book_id@@chapter_id@@title
|
||||
$parts = explode('@@', $id);
|
||||
|
||||
// Use full ID part (Title$BookID) as in JS/Python source
|
||||
$bookId = $parts[0];
|
||||
$chapterId = $parts[1] ?? '';
|
||||
$title = $parts[2] ?? '';
|
||||
|
||||
$params = [
|
||||
'id' => $bookId,
|
||||
'chapterId' => $chapterId
|
||||
];
|
||||
|
||||
// Calculate Sign
|
||||
$signStr = "";
|
||||
ksort($params);
|
||||
foreach ($params as $k => $v) {
|
||||
$signStr .= $k . "=" . $v;
|
||||
}
|
||||
$signStr .= self::SIGN_KEY;
|
||||
$params['sign'] = md5($signStr);
|
||||
|
||||
// Debug info
|
||||
// echo "\nDEBUG Sign Str: $signStr\n";
|
||||
// echo "DEBUG Sign: " . $params['sign'] . "\n";
|
||||
|
||||
// Manual URL construction to match Python's order: id, chapterId, sign
|
||||
// Although ksort is used for sign calculation, the request URL might need specific order
|
||||
$query = 'id=' . $bookId . '&chapterId=' . $chapterId . '&sign=' . $params['sign'];
|
||||
$url = self::CONTENT_URL . '?' . $query;
|
||||
// echo "DEBUG URL: $url\n";
|
||||
|
||||
// Use BaseSpider fetch with specific options
|
||||
$options = [
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, // Force HTTP/1.1 to match Python requests behavior
|
||||
'headers' => $this->getSignHeaders()
|
||||
];
|
||||
|
||||
$raw = $this->fetch($url, $options);
|
||||
|
||||
// echo "DEBUG Response: " . substr($raw, 0, 100) . "\n";
|
||||
|
||||
$json = json_decode($raw, true) ?: [];
|
||||
|
||||
$content = '';
|
||||
if (isset($json['data']['content'])) {
|
||||
$content = $this->decodeContent($json['data']['content']);
|
||||
}
|
||||
|
||||
if (empty($content)) {
|
||||
$msg = $json['msg'] ?? 'unknown error';
|
||||
$code = $json['code'] ?? 'unknown';
|
||||
$preview = substr($raw, 0, 100);
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => 'novel://' . json_encode(['title' => "Error: $code - $msg ($preview)", 'content' => ''], JSON_UNESCAPED_UNICODE),
|
||||
'header' => (object)[]
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => 'novel://' . json_encode(['title' => $title, 'content' => $content], JSON_UNESCAPED_UNICODE),
|
||||
'header' => (object)[]
|
||||
];
|
||||
}
|
||||
|
||||
// ================== Helpers ==================
|
||||
|
||||
private function getSign($params) {
|
||||
ksort($params);
|
||||
$str = "";
|
||||
foreach ($params as $k => $v) {
|
||||
$str .= $k . "=" . $v;
|
||||
}
|
||||
$str .= self::SIGN_KEY;
|
||||
// Debug: return raw string for checking if needed, but for now just MD5
|
||||
// To debug: throw exception or log
|
||||
return md5($str);
|
||||
}
|
||||
|
||||
private function getSignHeaders() {
|
||||
return [
|
||||
"User-Agent" => "python-requests/2.31.0", // Mimic Python requests
|
||||
"Accept" => "*/*",
|
||||
"app-version" => "51110",
|
||||
"platform" => "android",
|
||||
"reg" => "0",
|
||||
"AUTHORIZATION" => "",
|
||||
"application-id" => "com.****.reader",
|
||||
"net-env" => "1",
|
||||
"channel" => "unknown",
|
||||
"qm-params" => "",
|
||||
"sign" => "fc697243ab534ebaf51d2fa80f251cb4"
|
||||
];
|
||||
}
|
||||
|
||||
private function decodeContent($base64Response) {
|
||||
// 1. Base64 Decode
|
||||
$bin = base64_decode($base64Response);
|
||||
if (!$bin) return '';
|
||||
|
||||
// 2. Extract IV (First 16 bytes)
|
||||
// JS logic: txt = Base64.parse(resp).toString() (Hex string)
|
||||
// iv = txt.slice(0, 32) (16 bytes hex)
|
||||
// content = txt.slice(32)
|
||||
// So raw binary: first 16 bytes are IV.
|
||||
|
||||
$iv = substr($bin, 0, 16);
|
||||
$data = substr($bin, 16);
|
||||
|
||||
$key = hex2bin(self::AES_KEY_HEX);
|
||||
|
||||
// 3. AES Decrypt
|
||||
$decrypted = openssl_decrypt($data, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||||
return trim($decrypted);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行爬虫
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $HOST = 'https://rrsp-api.kejiqianxian.com:60425';
|
||||
private $UA = 'rrsp.wang';
|
||||
|
||||
protected function getHeaders($isJson = true) {
|
||||
$headers = [
|
||||
'User-Agent: ' . $this->UA,
|
||||
'Origin: *',
|
||||
'Referer: https://docs.qq.com/',
|
||||
'Accept: application/json, text/plain, */*',
|
||||
'Accept-Language: zh-CN'
|
||||
];
|
||||
if ($isJson) {
|
||||
$headers[] = 'Content-Type: application/json';
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$classes = [
|
||||
['type_id' => '1', 'type_name' => '电影'],
|
||||
['type_id' => '2', 'type_name' => '电视剧'],
|
||||
['type_id' => '3', 'type_name' => '综艺'],
|
||||
['type_id' => '5', 'type_name' => '动漫'],
|
||||
['type_id' => '4', 'type_name' => '纪录片'],
|
||||
['type_id' => '6', 'type_name' => '短剧'],
|
||||
['type_id' => '7', 'type_name' => '特别节目'],
|
||||
['type_id' => '8', 'type_name' => '少儿内容']
|
||||
];
|
||||
|
||||
// 初始首页内容(空分类调用第一页数据)
|
||||
$data = $this->categoryContent('', 1);
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'list' => $data['list'] ?? [],
|
||||
'filters' => (object)[]
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$apiUrl = $this->HOST . '/api.php/main_program/moviesAll/';
|
||||
|
||||
$payload = [
|
||||
'type' => (string)$tid,
|
||||
'sort' => 'vod_time',
|
||||
'area' => '',
|
||||
'style' => '',
|
||||
'time' => '',
|
||||
'pay' => '',
|
||||
'page' => $pg,
|
||||
'limit' => '60'
|
||||
];
|
||||
|
||||
$jsonStr = $this->fetch($apiUrl, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => $this->getHeaders(),
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false // 补全SSL校验关闭,避免HTTPS请求失败
|
||||
]);
|
||||
|
||||
$jsonObj = json_decode($jsonStr ?: '{}', true);
|
||||
$list = [];
|
||||
|
||||
if (isset($jsonObj['data']['list']) && is_array($jsonObj['data']['list'])) {
|
||||
$list = $this->arr2vods($jsonObj['data']['list']);
|
||||
}
|
||||
// 补全total参数,适配分页逻辑
|
||||
$total = isset($jsonObj['data']['pagecount']) ? $jsonObj['data']['pagecount'] * 60 : 0;
|
||||
|
||||
return $this->pageResult($list, $pg, $total, 60);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? ($ids[0] ?? '') : $ids;
|
||||
if (empty($id)) return ['list' => []]; // 空ID容错
|
||||
|
||||
$apiUrl = $this->HOST . '/api.php/player/details/';
|
||||
|
||||
$payload = ['id' => (string)$id];
|
||||
|
||||
$jsonStr = $this->fetch($apiUrl, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => $this->getHeaders(),
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false
|
||||
]);
|
||||
|
||||
$jsonObj = json_decode($jsonStr ?: '{}', true);
|
||||
$vod = [];
|
||||
|
||||
if (isset($jsonObj['detailData']) && is_array($jsonObj['detailData'])) {
|
||||
$d = $jsonObj['detailData'];
|
||||
$vod = [
|
||||
'vod_id' => $d['vod_id'] ?? '',
|
||||
'vod_name' => $d['vod_name'] ?? '未知影片',
|
||||
'vod_pic' => $d['vod_pic'] ?? '',
|
||||
'vod_remarks' => $d['vod_remarks'] ?? '',
|
||||
'vod_year' => $d['vod_year'] ?? '',
|
||||
'vod_area' => $d['vod_area'] ?? '',
|
||||
'vod_actor' => $d['vod_actor'] ?? '',
|
||||
'vod_director' => $d['vod_director'] ?? '',
|
||||
'vod_content' => $d['vod_content'] ?? '暂无影片介绍',
|
||||
'vod_play_from' => $d['vod_play_from'] ?? '',
|
||||
'vod_play_url' => $d['vod_play_url'] ?? '',
|
||||
'type_name' => $d['vod_class'] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
if (empty($key) || $pg > 1) return $this->pageResult([], $pg, 0);
|
||||
|
||||
$apiUrl = $this->HOST . '/api.php/search/syntheticalSearch/';
|
||||
$payload = ['keyword' => $key];
|
||||
|
||||
$jsonStr = $this->fetch($apiUrl, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => $this->getHeaders(),
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false
|
||||
]);
|
||||
|
||||
$jsonObj = json_decode($jsonStr ?: '{}', true);
|
||||
$videos = [];
|
||||
|
||||
if (isset($jsonObj['data']) && is_array($jsonObj['data'])) {
|
||||
$data = $jsonObj['data'];
|
||||
if (!empty($data['chasingFanCorrelation']) && is_array($data['chasingFanCorrelation'])) {
|
||||
$videos = array_merge($videos, $this->arr2vods($data['chasingFanCorrelation']));
|
||||
}
|
||||
if (!empty($data['moviesCorrelation']) && is_array($data['moviesCorrelation'])) {
|
||||
$videos = array_merge($videos, $this->arr2vods($data['moviesCorrelation']));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($videos, $pg, count($videos));
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$apiUrl = $this->HOST . '/api.php/player/payVideoUrl/';
|
||||
$payload = ['url' => $id];
|
||||
|
||||
$jsonStr = $this->fetch($apiUrl, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => $this->getHeaders(),
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_SSL_VERIFYPEER => false
|
||||
]);
|
||||
|
||||
$jsonObj = json_decode($jsonStr, true);
|
||||
$url = $id;
|
||||
$jx = 0;
|
||||
|
||||
if (isset($jsonObj['data']['url']) && strpos($jsonObj['data']['url'], 'http') === 0) {
|
||||
$url = $jsonObj['data']['url'];
|
||||
}
|
||||
|
||||
// 匹配第三方大站开启解析
|
||||
if (preg_match('/(?:www\.iqiyi|v\.qq|v\.youku|www\.mgtv|www\.bilibili)\.com/', $url)) {
|
||||
$jx = 1;
|
||||
}
|
||||
|
||||
return [
|
||||
'jx' => $jx,
|
||||
'parse' => 0,
|
||||
'url' => $url,
|
||||
'header' => [
|
||||
'User-Agent' => $this->UA,
|
||||
'Referer' => 'https://docs.qq.com/'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function arr2vods($arr) {
|
||||
$videos = [];
|
||||
foreach ($arr as $i) {
|
||||
// 修复符号错误
|
||||
$remarks = ($i['vod_serial'] == '1')
|
||||
? $i['vod_serial'] . '集'
|
||||
: '评分' . ($i['vod_score'] ?? $i['vod_douban_score'] ?? '0');
|
||||
|
||||
$videos[] = [
|
||||
'vod_id' => $i['vod_id'] ?? '',
|
||||
'vod_name' => $i['vod_name'] ?? '',
|
||||
'vod_pic' => $i['vod_pic'] ?? '',
|
||||
'vod_remarks' => $remarks ?? ''
|
||||
];
|
||||
}
|
||||
return $videos;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行爬虫
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
|
||||
public function getName() {
|
||||
return "动漫啦";
|
||||
}
|
||||
|
||||
public function init($extend = "") {
|
||||
// pass
|
||||
}
|
||||
|
||||
public function isVideoFormat($url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function manualVideoCheck() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getHeader() {
|
||||
return [
|
||||
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Referer" => "https://www.dongman.la/",
|
||||
"Connection" => "keep-alive"
|
||||
];
|
||||
}
|
||||
|
||||
private function fetchHtml($url) {
|
||||
// 忽略 SSL 验证
|
||||
$options = [
|
||||
'headers' => $this->getHeader()
|
||||
];
|
||||
return $this->fetch($url, $options);
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$cats = [];
|
||||
try {
|
||||
$html = $this->fetchHtml("https://www.dongman.la/");
|
||||
if (preg_match('/<div class="cy_subnav">(.*?)<\/div>/s', $html, $matches)) {
|
||||
if (preg_match_all('/<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/s', $matches[1], $links, PREG_SET_ORDER)) {
|
||||
foreach ($links as $link) {
|
||||
$href = $link[1];
|
||||
$title = trim($link[2]);
|
||||
if (strpos($title, "首页") !== false) continue;
|
||||
|
||||
$typeId = trim(str_replace("https://www.dongman.la", "", $href), "/");
|
||||
$cats[] = ["type_name" => $title, "type_id" => $typeId];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// pass
|
||||
}
|
||||
|
||||
if (empty($cats)) {
|
||||
$cats = [
|
||||
["type_name" => "连载中", "type_id" => "manhua/list/lianzai"],
|
||||
["type_name" => "已完结", "type_id" => "manhua/list/wanjie"],
|
||||
["type_name" => "热血", "type_id" => "manhua/list/rexue"],
|
||||
["type_name" => "恋爱", "type_id" => "manhua/list/lianai"],
|
||||
["type_name" => "冒险", "type_id" => "manhua/list/maoxian"],
|
||||
["type_name" => "搞笑", "type_id" => "manhua/list/gaoxiao"]
|
||||
];
|
||||
}
|
||||
|
||||
return ["class" => $cats, "filters" => []];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
return $this->categoryContent("manhua/list/lianzai", 1, [], []);
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$tid = trim($tid, '/');
|
||||
$url = "https://www.dongman.la/{$tid}/{$pg}.html";
|
||||
return $this->getPostListByRegex($url, $pg);
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$url = "https://www.dongman.la/manhua/so/{$key}/{$pg}.html";
|
||||
return $this->getPostListByRegex($url, $pg);
|
||||
}
|
||||
|
||||
private function getPostListByRegex($url, $pg) {
|
||||
try {
|
||||
$html = $this->fetchHtml($url);
|
||||
if (!$html) return ["list" => []];
|
||||
|
||||
$vlist = [];
|
||||
$listHtml = "";
|
||||
|
||||
// 提取列表容器
|
||||
if (preg_match('/(?:class=["\']cy_list_mh["\']|id=["\']contaner["\'])[^>]*>(.*?)<div class="cy_page/s', $html, $match)) {
|
||||
$listHtml = $match[1];
|
||||
} else {
|
||||
$listHtml = $html;
|
||||
}
|
||||
|
||||
if (preg_match_all('/<li[^>]*>(.*?)<\/li>/s', $listHtml, $items)) {
|
||||
foreach ($items[1] as $item) {
|
||||
if (strpos($item, 'class="title"') === false && strpos($item, 'class="pic"') === false) continue;
|
||||
|
||||
if (!preg_match('/href=["\']([^"\']+)["\']/', $item, $hrefMatch)) continue;
|
||||
$href = $hrefMatch[1];
|
||||
|
||||
if (strpos($href, "javascript") !== false || in_array($href, ["/", "#"])) continue;
|
||||
|
||||
// 提取名称
|
||||
$name = "";
|
||||
if (preg_match('/<b>(.*?)<\/b>/s', $item, $bMatch)) {
|
||||
$name = trim($bMatch[1]);
|
||||
} elseif (preg_match('/class=["\']pic["\'][^>]*title=["\']([^"\']+)["\']/', $item, $tMatch)) {
|
||||
$name = $tMatch[1];
|
||||
} elseif (preg_match('/alt=["\']([^"\']+)["\']/', $item, $altMatch)) {
|
||||
$name = trim($altMatch[1]);
|
||||
}
|
||||
|
||||
$name = trim(strip_tags($name));
|
||||
$name = str_replace(["漫画", "在线观看"], "", $name);
|
||||
if (!$name) continue;
|
||||
|
||||
// 提取图片
|
||||
$pic = "";
|
||||
if (preg_match('/(?:data-src|src)=["\']([^"\']+)["\']/', $item, $imgMatch)) {
|
||||
$pic = $imgMatch[1];
|
||||
if (strpos($pic, "//") === 0) $pic = "https:" . $pic;
|
||||
}
|
||||
|
||||
// 提取备注
|
||||
$remark = "";
|
||||
if (preg_match('/<p[^>]*>(.*?)<\/p>/s', $item, $pMatch)) {
|
||||
// 确保不是 title 里的部分
|
||||
$tempItem = explode($pMatch[0], $item)[0];
|
||||
if (strpos($tempItem, 'title') === false) {
|
||||
$remark = trim(strip_tags($pMatch[1]));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$remark && preg_match('/class=["\']tt["\'][^>]*>(.*?)<\/span>/', $item, $ttMatch)) {
|
||||
$remark = trim($ttMatch[1]);
|
||||
}
|
||||
|
||||
$vlist[] = [
|
||||
'vod_id' => $href,
|
||||
'vod_name' => $name,
|
||||
'vod_pic' => $pic,
|
||||
'vod_remarks' => $remark
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ["list" => $vlist, "page" => $pg, "pagecount" => 9999, "limit" => 30, "total" => 999999];
|
||||
} catch (Exception $e) {
|
||||
return ["list" => []];
|
||||
}
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$vid = $ids[0];
|
||||
$url = (strpos($vid, 'http') === 0) ? $vid : "https://www.dongman.la{$vid}";
|
||||
|
||||
try {
|
||||
$html = $this->fetchHtml($url);
|
||||
|
||||
$name = "";
|
||||
if (preg_match('/<h1[^>]*>(.*?)<\/h1>/s', $html, $h1Match)) {
|
||||
$name = trim(strip_tags($h1Match[1]));
|
||||
}
|
||||
|
||||
if (!$name && preg_match('/<title>(.*?)<\/title>/s', $html, $titleMatch)) {
|
||||
$parts = explode('-', $titleMatch[1]);
|
||||
$parts = explode('_', $parts[0]);
|
||||
$name = trim($parts[0]);
|
||||
}
|
||||
|
||||
$name = trim(str_replace(["漫画", "在线观看", "免费阅读"], "", $name)) ?: "未知漫画";
|
||||
|
||||
$cover = "";
|
||||
if (preg_match('/<img[^>]*class=["\'](?:detail-info-cover|pic)["\'][^>]*src=["\']([^"\']+)["\']/', $html, $coverMatch) ||
|
||||
preg_match('/<img[^>]*src=["\']([^"\']+)["\'][^>]*class=["\'](?:detail-info-cover|pic)["\']/', $html, $coverMatch)) {
|
||||
$cover = $coverMatch[1];
|
||||
if (strpos($cover, "//") === 0) $cover = "https:" . $cover;
|
||||
}
|
||||
|
||||
$desc = "";
|
||||
if (preg_match('/id="comic-description"[^>]*>(.*?)<\/div>/s', $html, $descMatch)) {
|
||||
$desc = trim(strip_tags($descMatch[1]));
|
||||
$desc = str_replace([" ", "详细简介↓", "收起↑"], [" ", "", ""], $desc);
|
||||
$desc = preg_replace('/\s+/', ' ', $desc);
|
||||
}
|
||||
|
||||
// 提取章节
|
||||
$linksSource = $html;
|
||||
if (preg_match_all('/<(?:ul|ol)[^>]*class=["\'].*?list.*?["\'][^>]*>(.*?)<\/(?:ul|ol)>/s', $html, $listContainers)) {
|
||||
$linksSource = implode("", $listContainers[1]);
|
||||
}
|
||||
|
||||
$chapterList = [];
|
||||
$uniqueChapters = [];
|
||||
|
||||
if (preg_match_all('/<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/s', $linksSource, $rawLinks, PREG_SET_ORDER)) {
|
||||
foreach ($rawLinks as $link) {
|
||||
$href = $link[1];
|
||||
$text = $link[2];
|
||||
|
||||
if (strpos($href, "/chapter/") === false && !preg_match('/\d+\.html/', $href)) continue;
|
||||
if (strpos($href, "detail") !== false) continue;
|
||||
|
||||
$title = trim(strip_tags($text));
|
||||
if (!$title || strpos($title, "在线阅读") !== false || strpos($title, "开始阅读") !== false) continue;
|
||||
|
||||
if (!in_array($href, $uniqueChapters)) {
|
||||
$uniqueChapters[] = $href;
|
||||
$chapterList[] = "{$title}\${$href}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$chapterList = array_reverse($chapterList);
|
||||
$playUrl = implode("#", $chapterList);
|
||||
|
||||
return [
|
||||
"list" => [[
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => $name,
|
||||
"vod_pic" => $cover,
|
||||
"type_name" => "漫画",
|
||||
"vod_content" => $desc,
|
||||
"vod_play_from" => '动漫啦',
|
||||
"vod_play_url" => $playUrl
|
||||
]]
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return ["list" => []];
|
||||
}
|
||||
}
|
||||
|
||||
private function extractImgs($htmlText) {
|
||||
$found = [];
|
||||
// RE_PLAY_IMGS
|
||||
if (preg_match_all('/(?:data-original|data-src|src)=["\']([^"\']+\.(?:jpg|png|jpeg|webp))[^"\']*["\']/i', $htmlText, $matches)) {
|
||||
foreach ($matches[1] as $src) {
|
||||
if (preg_match('/(logo|icon|cover|banner|\.gif|loading)/', $src)) continue;
|
||||
|
||||
if (strpos($src, "//") === 0) {
|
||||
$src = "https:" . $src;
|
||||
} elseif (strpos($src, "/") === 0) {
|
||||
$src = "https://www.dongman.la" . $src;
|
||||
} elseif (strpos($src, "http") !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_array($src, $found)) {
|
||||
$found[] = $src;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$url = (strpos($id, 'http') === 0) ? $id : "https://www.dongman.la{$id}";
|
||||
|
||||
$cleanUrl = rtrim(str_replace('.html', '', $url), '/');
|
||||
$allUrl = "{$cleanUrl}/all.html";
|
||||
|
||||
$imgList = [];
|
||||
|
||||
// 1. 尝试 all.html
|
||||
try {
|
||||
$html = $this->fetchHtml($allUrl);
|
||||
if ($html) {
|
||||
$imgList = $this->extractImgs($html);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// pass
|
||||
}
|
||||
|
||||
// 2. 失败则循环抓取 (限制前40页)
|
||||
if (empty($imgList)) {
|
||||
$imageMap = [];
|
||||
// PHP 串行抓取
|
||||
for ($i = 1; $i < 40; $i++) {
|
||||
$targetUrl = ($i == 1) ? $url : "{$cleanUrl}/{$i}.html";
|
||||
try {
|
||||
$resHtml = $this->fetchHtml($targetUrl);
|
||||
if ($resHtml) {
|
||||
$imgs = $this->extractImgs($resHtml);
|
||||
if (!empty($imgs)) {
|
||||
$imageMap[$i] = $imgs[0];
|
||||
} else {
|
||||
// 如果某一页抓不到图片,可能就是结束了,或者反爬,这里可以考虑 break
|
||||
// 但是为了保险起见,Python 是并发抓取所有,这里我们也继续尝试
|
||||
}
|
||||
} else {
|
||||
// 404 or error likely means end of chapter
|
||||
break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 1; $i < 40; $i++) {
|
||||
if (isset($imageMap[$i])) {
|
||||
$imgList[] = $imageMap[$i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($imgList)) {
|
||||
// webview fallback
|
||||
return ['parse' => 1, 'url' => $url, 'header' => json_encode($this->getHeader())];
|
||||
}
|
||||
|
||||
$novelData = implode("&&", $imgList);
|
||||
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => "pics://{$novelData}",
|
||||
"header" => ""
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
|
||||
public function getName() {
|
||||
return "包子漫画";
|
||||
}
|
||||
|
||||
public function init($extend = "") {
|
||||
// pass
|
||||
}
|
||||
|
||||
public function isVideoFormat($url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function manualVideoCheck() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getHeader() {
|
||||
return [
|
||||
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Referer" => "https://cn.bzmanga.com/"
|
||||
];
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$classes = [
|
||||
["type_name" => "最新上架", "type_id" => "new"],
|
||||
["type_name" => "全部漫画", "type_id" => "all"],
|
||||
["type_name" => "地区", "type_id" => "region"],
|
||||
["type_name" => "进度", "type_id" => "status"],
|
||||
["type_name" => "题材", "type_id" => "type"]
|
||||
];
|
||||
|
||||
$filters = [];
|
||||
$filters['region'] = [["key" => "val", "name" => "地区", "value" => [["n" => "国漫", "v" => "cn"],["n" => "日本", "v" => "jp"],["n" => "欧美", "v" => "en"]]]];
|
||||
$filters['status'] = [["key" => "val", "name" => "进度", "value" => [["n" => "连载中", "v" => "serial"],["n" => "已完结", "v" => "pub"]]]];
|
||||
|
||||
$types = [
|
||||
["n" => "都市", "v" => "dushi"], ["n" => "冒险", "v" => "mouxian"],
|
||||
["n" => "热血", "v" => "rexie"], ["n" => "爱情", "v" => "aiqing"],
|
||||
["n" => "恋爱", "v" => "lianai"], ["n" => "耽美", "v" => "danmei"],
|
||||
["n" => "武侠", "v" => "wuxia"], ["n" => "格斗", "v" => "gedou"],
|
||||
["n" => "科幻", "v" => "kehuan"], ["n" => "魔幻", "v" => "mohuan"],
|
||||
["n" => "侦探", "v" => "zhentan"], ["n" => "推理", "v" => "tuili"],
|
||||
["n" => "玄幻", "v" => "xuanhuan"], ["n" => "日常", "v" => "richang"],
|
||||
["n" => "生活", "v" => "shenghuo"], ["n" => "搞笑", "v" => "gaoxiao"],
|
||||
["n" => "校园", "v" => "xiaoyuan"], ["n" => "奇幻", "v" => "qihuan"]
|
||||
];
|
||||
$filters['type'] = [["key" => "val", "name" => "类型", "value" => $types]];
|
||||
|
||||
return ["class" => $classes, "filters" => $filters];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
return $this->categoryContent("new", 1, [], []);
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
if ($tid == "new") {
|
||||
$url = ($pg == 1) ? "https://cn.bzmanga.com/list/new/" : "https://cn.bzmanga.com/list/new/?page={$pg}";
|
||||
} elseif ($tid == "all") {
|
||||
$url = "https://cn.bzmanga.com/classify?page={$pg}";
|
||||
} else {
|
||||
$val = $extend['val'] ?? '';
|
||||
if (!$val) {
|
||||
if ($tid == "region") $val = "cn";
|
||||
elseif ($tid == "status") $val = "serial";
|
||||
elseif ($tid == "type") $val = "dushi";
|
||||
}
|
||||
|
||||
$paramKey = $tid;
|
||||
if ($tid == "status") $paramKey = "state";
|
||||
|
||||
$url = "https://cn.bzmanga.com/classify?{$paramKey}={$val}&page={$pg}";
|
||||
}
|
||||
|
||||
try {
|
||||
$html = $this->fetch($url, ['headers' => $this->getHeader()]);
|
||||
$items = $this->pdfa($html, '.comics-card');
|
||||
|
||||
$videos = [];
|
||||
foreach ($items as $item) {
|
||||
$vid = $this->pd($item, 'a.comics-card__poster&&href');
|
||||
if (!$vid) continue;
|
||||
|
||||
$cover = $this->pd($item, 'amp-img&&src');
|
||||
if (strpos($cover, ".w=") !== false) {
|
||||
$cover = explode('.w=', $cover)[0];
|
||||
}
|
||||
|
||||
$name = $this->pd($item, '.comics-card__title&&Text');
|
||||
|
||||
$videos[] = [
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => $name,
|
||||
"vod_pic" => $cover,
|
||||
"vod_remarks" => ""
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
"list" => $videos,
|
||||
"page" => $pg,
|
||||
"pagecount" => 9999,
|
||||
"limit" => 36,
|
||||
"total" => 999999
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return ["list" => []];
|
||||
}
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$vid = $ids[0];
|
||||
$url = (strpos($vid, 'http') === 0) ? $vid : "https://cn.bzmanga.com{$vid}";
|
||||
|
||||
try {
|
||||
$html = $this->fetch($url, ['headers' => $this->getHeader()]);
|
||||
|
||||
$name = $this->pd($html, '.comics-detail__title&&Text') ?: "未知";
|
||||
$author = $this->pd($html, '.comics-detail__author&&Text');
|
||||
$desc = $this->pd($html, '.comics-detail__desc&&Text');
|
||||
|
||||
$cover = $this->pd($html, 'amp-img&&src');
|
||||
if (strpos($cover, ".w=") !== false) {
|
||||
$cover = explode('.w=', $cover)[0];
|
||||
}
|
||||
|
||||
$chapterItems = $this->pdfa($html, '.comics-chapters__item');
|
||||
|
||||
$rawUrlList = [];
|
||||
foreach ($chapterItems as $item) {
|
||||
$aTag = $this->pd($item, 'a', true); // get element
|
||||
if (!$aTag && strpos($item, '<a') === 0) { // simple check if item itself is a tag
|
||||
// HtmlParser doesn't fully support item itself as root sometimes, depend on implementation
|
||||
// Let's assume pdfa returns inner HTML or node.
|
||||
// BaseSpider pdfa returns array of strings (html fragments) usually.
|
||||
// So we can re-parse item
|
||||
}
|
||||
|
||||
$chapterName = $this->pd($item, 'a&&Text');
|
||||
if (!$chapterName) $chapterName = $this->pd($item, 'Text'); // fallback if item is 'a'
|
||||
|
||||
$rawHref = $this->pd($item, 'a&&href');
|
||||
if (!$rawHref) $rawHref = $this->pd($item, 'href');
|
||||
|
||||
if (!$chapterName || !$rawHref) continue;
|
||||
|
||||
$realChapterUrl = "";
|
||||
|
||||
if (preg_match('/comic_id=(\d+).*chapter_slot=(\d+)/', $rawHref, $matches)) {
|
||||
$cId = $matches[1];
|
||||
$cSlot = $matches[2];
|
||||
$realChapterUrl = "https://cn.dzmanga.com/comic/chapter/{$cId}/0_{$cSlot}.html";
|
||||
} else {
|
||||
if (strpos($rawHref, "/") === 0) {
|
||||
$realChapterUrl = "https://cn.dzmanga.com{$rawHref}";
|
||||
} elseif (strpos($rawHref, "http") !== false) {
|
||||
$realChapterUrl = str_replace("cn.bzmanga.com", "cn.dzmanga.com", $rawHref);
|
||||
} else {
|
||||
$realChapterUrl = "https://cn.dzmanga.com/{$rawHref}";
|
||||
}
|
||||
}
|
||||
|
||||
$rawUrlList[] = "{$chapterName}\${$realChapterUrl}";
|
||||
}
|
||||
|
||||
$descList = $rawUrlList;
|
||||
$ascList = array_reverse($rawUrlList);
|
||||
|
||||
$strDesc = implode("#", $descList);
|
||||
$strAsc = implode("#", $ascList);
|
||||
|
||||
return [
|
||||
"list" => [[
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => $name,
|
||||
"vod_pic" => $cover,
|
||||
"type_name" => "漫画",
|
||||
"vod_year" => "",
|
||||
"vod_area" => "",
|
||||
"vod_remarks" => $author,
|
||||
"vod_actor" => "",
|
||||
"vod_director" => "",
|
||||
"vod_content" => $desc,
|
||||
"vod_play_from" => '正序$$$倒序',
|
||||
"vod_play_url" => "{$strAsc}$$$" . $strDesc
|
||||
]]
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return ["list" => []];
|
||||
}
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$url = "https://cn.bzmanga.com/search?q={$key}";
|
||||
try {
|
||||
$html = $this->fetch($url, ['headers' => $this->getHeader()]);
|
||||
$items = $this->pdfa($html, '.comics-card');
|
||||
|
||||
$videos = [];
|
||||
foreach ($items as $item) {
|
||||
$vid = $this->pd($item, 'a.comics-card__poster&&href');
|
||||
if (!$vid) continue;
|
||||
|
||||
$cover = $this->pd($item, 'amp-img&&src');
|
||||
if (strpos($cover, ".w=") !== false) {
|
||||
$cover = explode('.w=', $cover)[0];
|
||||
}
|
||||
|
||||
$name = $this->pd($item, '.comics-card__title&&Text');
|
||||
|
||||
$videos[] = [
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => $name,
|
||||
"vod_pic" => $cover,
|
||||
"vod_remarks" => ""
|
||||
];
|
||||
}
|
||||
return ['list' => $videos];
|
||||
} catch (Exception $e) {
|
||||
return ['list' => []];
|
||||
}
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$url = $id;
|
||||
$headers = $this->getHeader();
|
||||
$headers['Referer'] = $url;
|
||||
|
||||
try {
|
||||
$html = $this->fetch($url, ['headers' => $headers]);
|
||||
|
||||
$imgList = [];
|
||||
|
||||
// 策略A:DOM解析
|
||||
$container = $this->pd($html, '.comic-contain', true);
|
||||
if (!$container) {
|
||||
// simple body check, or just parse whole html
|
||||
// HtmlParser usually handles whole html if no selector matched for subset
|
||||
}
|
||||
|
||||
$imgs = [];
|
||||
if ($container) {
|
||||
// If we had a specific object for container, we'd use it.
|
||||
// But base spider pd/pdfa usually works on string.
|
||||
// So let's just search in html
|
||||
}
|
||||
|
||||
// Use regex for specific container if possible, but here we can just try global selector on html
|
||||
// but restricted by container class if we could.
|
||||
// Simplified: Global search with selector
|
||||
$imgs = $this->pdfa($html, '.comic-contain amp-img');
|
||||
if (empty($imgs)) {
|
||||
$imgs = $this->pdfa($html, '.comic-contain img');
|
||||
}
|
||||
// If still empty, try body (global)
|
||||
if (empty($imgs)) {
|
||||
$imgs = $this->pdfa($html, 'amp-img');
|
||||
if (empty($imgs)) $imgs = $this->pdfa($html, 'img');
|
||||
}
|
||||
|
||||
foreach ($imgs as $img) {
|
||||
$src = $this->pd($img, 'src');
|
||||
if (!$src) $src = $this->pd($img, 'data-src');
|
||||
|
||||
if ($src) {
|
||||
if (strpos($src, "next_chapter") !== false || strpos($src, "prev_chapter") !== false || strpos($src, "icon") !== false || strpos($src, "logo") !== false) {
|
||||
continue;
|
||||
}
|
||||
if (strpos($src, "//") === 0) {
|
||||
$src = "https:" . $src;
|
||||
}
|
||||
$imgList[] = $src;
|
||||
}
|
||||
}
|
||||
|
||||
// 策略B:暴力正则
|
||||
if (count($imgList) < 2) {
|
||||
if (preg_match_all('/(https?:\/\/[^"\'\s]+static[^"\'\s]+\.(?:jpg|png|webp|jpeg)(?:\?[^"\'\s]*)?)/', $html, $matches)) {
|
||||
foreach ($matches[1] as $m) {
|
||||
if (!in_array($m, $imgList)) {
|
||||
if (strpos($m, "cover") !== false) continue;
|
||||
if (strpos($m, "icon") !== false) continue;
|
||||
if (strpos($m, "logo") !== false) continue;
|
||||
if (strpos($m, "bg") !== false) continue;
|
||||
$imgList[] = $m;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$uniqueImgs = array_unique($imgList);
|
||||
$novelData = implode("&&", $uniqueImgs);
|
||||
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => "pics://{$novelData}",
|
||||
"header" => ""
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return ["parse" => 0, "url" => "", "header" => ""];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private const AES_KEY = '242ccb8230d709e1';
|
||||
private const SIGN_KEY = 'd3dGiJc651gSQ8w1';
|
||||
private const APP_ID = 'com.kmxs.reader';
|
||||
|
||||
private $baseHeaders = [
|
||||
"app-version" => "51110",
|
||||
"platform" => "android",
|
||||
"reg" => "0",
|
||||
"AUTHORIZATION" => "",
|
||||
"application-id" => self::APP_ID,
|
||||
"net-env" => "1",
|
||||
"channel" => "unknown",
|
||||
"qm-params" => ""
|
||||
];
|
||||
|
||||
public function getName() {
|
||||
return "去读书";
|
||||
}
|
||||
|
||||
public function init($extend = "") {
|
||||
// pass
|
||||
}
|
||||
|
||||
public function isVideoFormat($url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function manualVideoCheck() {
|
||||
// pass
|
||||
}
|
||||
|
||||
private function getSign($params) {
|
||||
ksort($params);
|
||||
$signStr = "";
|
||||
foreach ($params as $k => $v) {
|
||||
$signStr .= "{$k}={$v}";
|
||||
}
|
||||
$signStr .= self::SIGN_KEY;
|
||||
return md5($signStr);
|
||||
}
|
||||
|
||||
private function getHeaders() {
|
||||
$headers = $this->baseHeaders;
|
||||
$headers['sign'] = $this->getSign($headers);
|
||||
$headers['User-Agent'] = 'okhttp/3.12.1';
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function decryptContent($base64Content) {
|
||||
try {
|
||||
$encryptedBytes = base64_decode($base64Content);
|
||||
if (strlen($encryptedBytes) < 16) {
|
||||
return "数据长度不足";
|
||||
}
|
||||
$iv = substr($encryptedBytes, 0, 16);
|
||||
$ciphertext = substr($encryptedBytes, 16);
|
||||
|
||||
$decrypted = openssl_decrypt(
|
||||
$ciphertext,
|
||||
'AES-128-CBC',
|
||||
self::AES_KEY,
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv
|
||||
);
|
||||
|
||||
if ($decrypted === false) {
|
||||
return "解密失败";
|
||||
}
|
||||
return trim($decrypted);
|
||||
} catch (Exception $e) {
|
||||
return "解密错误: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private function getApiUrl($path, $params, $domainType = "bc") {
|
||||
$params['sign'] = $this->getSign($params);
|
||||
$baseUrl = ($domainType == "bc") ? "https://api-bc.wtzw.com" : "https://api-ks.wtzw.com";
|
||||
if (strpos($path, "search") !== false) {
|
||||
$baseUrl = "https://api-bc.wtzw.com";
|
||||
}
|
||||
|
||||
$queryString = http_build_query($params);
|
||||
return ["{$baseUrl}{$path}?{$queryString}", $params];
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$cats = [
|
||||
["n" => "玄幻奇幻", "v" => "1|202"], ["n" => "都市人生", "v" => "1|203"], ["n" => "武侠仙侠", "v" => "1|205"],
|
||||
["n" => "历史军事", "v" => "1|56"], ["n" => "科幻末世", "v" => "1|64"], ["n" => "游戏竞技", "v" => "1|75"],
|
||||
["n" => "现代言情", "v" => "2|1"], ["n" => "古代言情", "v" => "2|2"], ["n" => "幻想言情", "v" => "2|4"],
|
||||
["n" => "婚恋情感", "v" => "2|6"], ["n" => "悬疑推理", "v" => "3|262"]
|
||||
];
|
||||
|
||||
$classes = [];
|
||||
foreach ($cats as $cat) {
|
||||
$classes[] = ["type_name" => $cat['n'], "type_id" => $cat['v']];
|
||||
}
|
||||
return ['class' => $classes, 'filters' => []];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
return ['list' => []];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$parts = explode("|", $tid);
|
||||
$gender = $parts[0] ?? "1";
|
||||
$catId = $parts[1] ?? "202";
|
||||
|
||||
$path = "/api/v4/category/get-list";
|
||||
$params = ['gender' => $gender, 'category_id' => $catId, 'need_filters' => '1', 'page' => $pg, 'need_category' => '1'];
|
||||
$headers = $this->getHeaders();
|
||||
list($url, $signedParams) = $this->getApiUrl($path, $params, "bc");
|
||||
|
||||
try {
|
||||
$j = $this->fetchJson($url, ['headers' => $headers]);
|
||||
$videos = [];
|
||||
$bookList = [];
|
||||
|
||||
if (isset($j['data']['books'])) {
|
||||
$bookList = $j['data']['books'];
|
||||
} elseif (isset($j['books'])) {
|
||||
$bookList = $j['books'];
|
||||
}
|
||||
|
||||
foreach ($bookList as $book) {
|
||||
$videos[] = [
|
||||
"vod_id" => (string)$book['id'],
|
||||
"vod_name" => $book['title'],
|
||||
"vod_pic" => $book['image_link'],
|
||||
"vod_remarks" => $book['author']
|
||||
];
|
||||
}
|
||||
return ['list' => $videos, 'page' => $pg, 'pagecount' => 999, 'limit' => 20, 'total' => 9999];
|
||||
} catch (Exception $e) {
|
||||
return ['list' => []];
|
||||
}
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$bid = $ids[0];
|
||||
$headers = $this->getHeaders();
|
||||
|
||||
$detailParams = ['id' => $bid, 'imei_ip' => '2937357107', 'teeny_mode' => '0'];
|
||||
list($detailUrl, $detailSignedParams) = $this->getApiUrl("/api/v4/book/detail", $detailParams, "bc");
|
||||
|
||||
$vod = ["vod_id" => $bid, "vod_name" => "获取中...", "vod_play_from" => "去读书"];
|
||||
|
||||
try {
|
||||
$j = $this->fetchJson($detailUrl, ['headers' => $headers]);
|
||||
if (isset($j['data']['book'])) {
|
||||
$bookInfo = $j['data']['book'];
|
||||
$vod["vod_name"] = $bookInfo['title'];
|
||||
$vod["vod_pic"] = $bookInfo['image_link'];
|
||||
$vod["type_name"] = $bookInfo['category_name'] ?? '';
|
||||
$vod["vod_remarks"] = ($bookInfo['words_num'] ?? '') . "字";
|
||||
$vod["vod_actor"] = $bookInfo['author'];
|
||||
$vod["vod_content"] = $bookInfo['intro'];
|
||||
}
|
||||
|
||||
// 获取目录
|
||||
$chapterParams = ['id' => $bid];
|
||||
list($chapterUrl, $chapterSignedParams) = $this->getApiUrl("/api/v1/chapter/chapter-list", $chapterParams, "ks");
|
||||
|
||||
$jc = $this->fetchJson($chapterUrl, ['headers' => $headers]);
|
||||
|
||||
$chapterList = [];
|
||||
$lists = [];
|
||||
if (isset($jc['data']['chapter_lists'])) {
|
||||
$lists = $jc['data']['chapter_lists'];
|
||||
}
|
||||
|
||||
foreach ($lists as $item) {
|
||||
$cid = (string)$item['id'];
|
||||
$cname = str_replace(["@@", "$"], ["-", ""], (string)$item['title']);
|
||||
$urlCode = "{$bid}@@{$cid}@@{$cname}";
|
||||
$chapterList[] = "{$cname}\${$urlCode}";
|
||||
}
|
||||
|
||||
$vod['vod_play_url'] = implode("#", $chapterList);
|
||||
return ["list" => [$vod]];
|
||||
} catch (Exception $e) {
|
||||
$vod["vod_content"] = "Error: " . $e->getMessage();
|
||||
return ["list" => [$vod]];
|
||||
}
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$path = "/api/v5/search/words";
|
||||
$params = ['gender' => '3', 'imei_ip' => '2937357107', 'page' => $pg, 'wd' => $key];
|
||||
$headers = $this->getHeaders();
|
||||
list($url, $signedParams) = $this->getApiUrl($path, $params, "bc");
|
||||
|
||||
try {
|
||||
$j = $this->fetchJson($url, ['headers' => $headers]);
|
||||
$videos = [];
|
||||
if (isset($j['data']['books'])) {
|
||||
foreach ($j['data']['books'] as $book) {
|
||||
$videos[] = [
|
||||
"vod_id" => (string)$book['id'],
|
||||
"vod_name" => $book['original_title'],
|
||||
"vod_pic" => $book['image_link'],
|
||||
"vod_remarks" => $book['original_author']
|
||||
];
|
||||
}
|
||||
}
|
||||
return ['list' => $videos, 'page' => $pg];
|
||||
} catch (Exception $e) {
|
||||
return ['list' => [], 'page' => $pg];
|
||||
}
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
try {
|
||||
$parts = explode("@@", $id);
|
||||
$bid = $parts[0];
|
||||
$cid = $parts[1];
|
||||
$title = isset($parts[2]) ? $parts[2] : "";
|
||||
|
||||
$params = ['id' => $bid, 'chapterId' => $cid];
|
||||
$headers = $this->getHeaders();
|
||||
list($url, $signedParams) = $this->getApiUrl("/api/v1/chapter/content", $params, "ks");
|
||||
|
||||
$j = $this->fetchJson($url, ['headers' => $headers]);
|
||||
|
||||
$content = "";
|
||||
if (isset($j['data']['content'])) {
|
||||
if (empty($title) && isset($j['data']['title'])) {
|
||||
$title = $j['data']['title'];
|
||||
}
|
||||
$content = $this->decryptContent($j['data']['content']);
|
||||
} else {
|
||||
$content = "加载失败: " . ($j['msg'] ?? '未知错误');
|
||||
}
|
||||
|
||||
if (empty($title)) {
|
||||
$title = "章节正文";
|
||||
}
|
||||
|
||||
$resultData = [
|
||||
'title' => $title,
|
||||
'content' => $content
|
||||
];
|
||||
|
||||
$ret = json_encode($resultData, JSON_UNESCAPED_UNICODE);
|
||||
$finalUrl = "novel://{$ret}";
|
||||
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => $finalUrl,
|
||||
"header" => ""
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$errData = [
|
||||
'title' => "错误",
|
||||
'content' => "发生异常: " . $e->getMessage()
|
||||
];
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => "novel://" . json_encode($errData, JSON_UNESCAPED_UNICODE),
|
||||
"header" => ""
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
// ================= 核心加解密类 =================
|
||||
class WawaCrypto {
|
||||
public static function decrypt($encrypted_data) {
|
||||
$key = base64_decode('Crm4FXWkk5JItpYirFDpqg=='); //
|
||||
$data = hex2bin(base64_decode($encrypted_data)); //
|
||||
return openssl_decrypt($data, 'AES-128-ECB', $key, OPENSSL_RAW_DATA);
|
||||
}
|
||||
|
||||
public static function sign($message, $privateKey) {
|
||||
$key = "-----BEGIN PRIVATE KEY-----\n" . wordwrap($privateKey, 64, "\n", true) . "\n-----END PRIVATE KEY-----";
|
||||
$res = openssl_get_privatekey($key);
|
||||
openssl_sign($message, $signature, $res, OPENSSL_ALGO_SHA256); // 使用 SHA256 签名
|
||||
return base64_encode($signature);
|
||||
}
|
||||
|
||||
public static function uuid() {
|
||||
return sprintf('%04x%04x%04x%04x%04x%04x%04x%04x',
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $HOST;
|
||||
private $APP_KEY;
|
||||
private $RSA_KEY;
|
||||
private $CONF;
|
||||
|
||||
public function init($extend = '') {
|
||||
$this->initConf();
|
||||
}
|
||||
|
||||
private function initConf() {
|
||||
$uid = WawaCrypto::uuid();
|
||||
$t = (string)(time() * 1000);
|
||||
$sign = md5("appKey=3bbf7348cf314874883a18d6b6fcf67a&uid=$uid&time=$t");
|
||||
|
||||
$url = 'https://gitee.com/api/v5/repos/aycapp/openapi/contents/wawaconf.txt?access_token=74d5879931b9774be10dee3d8c51008e';
|
||||
$res = json_decode($this->fetch($url, [], ["User-Agent: okhttp/4.9.3", "uid: $uid", "time: $t", "sign: $sign"]), true);
|
||||
|
||||
$this->CONF = json_decode(WawaCrypto::decrypt($res['content']), true);
|
||||
$this->HOST = $this->CONF['baseUrl'];
|
||||
$this->APP_KEY = $this->CONF['appKey'];
|
||||
$this->RSA_KEY = $this->CONF['appSecret'];
|
||||
}
|
||||
|
||||
private function getWawaHeaders() {
|
||||
$uid = WawaCrypto::uuid();
|
||||
$t = (string)(time() * 1000);
|
||||
$sign = WawaCrypto::sign("appKey={$this->APP_KEY}&time=$t&uid=$uid", $this->RSA_KEY);
|
||||
return [
|
||||
'User-Agent: okhttp/4.9.3',
|
||||
"uid: $uid",
|
||||
"time: $t",
|
||||
"appKey: {$this->APP_KEY}",
|
||||
"sign: $sign"
|
||||
];
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$typeData = json_decode($this->fetch("{$this->HOST}/api.php/zjv6.vod/types", [], $this->getWawaHeaders()), true);
|
||||
$classes = [];
|
||||
$filters = [];
|
||||
$dy = ["class" => "类型", "area" => "地区", "lang" => "语言", "year" => "年份", "letter" => "字母", "by" => "排序"];
|
||||
$sl = ['按更新' => 'time', '按播放' => 'hits', '按评分' => 'score', '按收藏' => 'store_num'];
|
||||
|
||||
if (isset($typeData['data']['list'])) {
|
||||
foreach ($typeData['data']['list'] as $item) {
|
||||
$classes[] = ['type_id' => $item['type_id'], 'type_name' => $item['type_name']];
|
||||
$tid = (string)$item['type_id'];
|
||||
$filters[$tid] = [];
|
||||
$item['type_extend']['by'] = '按更新,按播放,按评分,按收藏'; // 强制注入排序
|
||||
|
||||
foreach ($dy as $key => $name) {
|
||||
if (!empty($item['type_extend'][$key])) {
|
||||
$values = explode(',', $item['type_extend'][$key]);
|
||||
$value_array = [];
|
||||
foreach ($values as $v) {
|
||||
if (empty($v)) continue;
|
||||
$value_array[] = ["n" => $v, "v" => ($key == "by" ? ($sl[$v] ?? $v) : $v)];
|
||||
}
|
||||
$filters[$tid][] = ["key" => $key, "name" => $name, "value" => $value_array];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$homeList = json_decode($this->fetch("{$this->HOST}/api.php/zjv6.vod/vodPhbAll", [], $this->getWawaHeaders()), true);
|
||||
$list = $homeList['data']['list'][0]['vod_list'] ?: [];
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => $filters,
|
||||
'list' => $list
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$query = http_build_query([
|
||||
'type' => $tid, 'page' => $pg, 'limit' => '12',
|
||||
'class' => $extend['class'] ?? '', 'area' => $extend['area'] ?? '',
|
||||
'year' => $extend['year'] ?? '', 'by' => $extend['by'] ?? ''
|
||||
]);
|
||||
$res = json_decode($this->fetch("{$this->HOST}/api.php/zjv6.vod?$query", [], $this->getWawaHeaders()), true);
|
||||
|
||||
$list = $res['data']['list'] ?: [];
|
||||
// 哇哇影视未返回总数,估算分页
|
||||
return $this->pageResult($list, $pg, 0, 12);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
$res = json_decode($this->fetch("{$this->HOST}/api.php/zjv6.vod/detail?vod_id=$id&rel_limit=10", [], $this->getWawaHeaders()), true);
|
||||
$item = $res['data'];
|
||||
$playFrom = []; $playUrls = [];
|
||||
|
||||
if (isset($item['vod_play_list'])) {
|
||||
foreach ($item['vod_play_list'] as $list) {
|
||||
$playFrom[] = $list['player_info']['show'];
|
||||
$urls = [];
|
||||
foreach ($list['urls'] as $u) {
|
||||
$u['parse'] = $list['player_info']['parse2'];
|
||||
$urls[] = $u['name'] . '$' . base64_encode(json_encode($u));
|
||||
}
|
||||
$playUrls[] = implode('#', $urls);
|
||||
}
|
||||
}
|
||||
|
||||
return ['list' => [[
|
||||
'vod_id' => $item['vod_id'],
|
||||
'vod_name' => $item['vod_name'],
|
||||
'vod_pic' => $item['vod_pic'],
|
||||
'vod_remarks' => $item['vod_remarks'],
|
||||
'vod_content' => $item['vod_content'] ?? '',
|
||||
'vod_play_from' => implode('$$$', $playFrom),
|
||||
'vod_play_url' => implode('$$$', $playUrls)
|
||||
]]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$res = json_decode($this->fetch("{$this->HOST}/api.php/zjv6.vod?page=$pg&limit=20&wd=".urlencode($key), [], $this->getWawaHeaders()), true);
|
||||
$list = $res['data']['list'] ?: [];
|
||||
return $this->pageResult($list, $pg, 0, 20);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$playData = json_decode(base64_decode($id), true);
|
||||
return [
|
||||
'parse' => 1,
|
||||
'url' => $playData['url'],
|
||||
'header' => ['User-Agent' => 'dart:io']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $HOST = 'https://www.aowu.tv';
|
||||
// 使用手机 UA 防止拦截
|
||||
private $UA = 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36';
|
||||
|
||||
protected function getHeaders() {
|
||||
return [
|
||||
'User-Agent: ' . $this->UA,
|
||||
'Referer: ' . $this->HOST
|
||||
];
|
||||
}
|
||||
|
||||
private function fixUrl($url) {
|
||||
if (empty($url)) return '';
|
||||
if (strpos($url, '//') === 0) return 'https:' . $url;
|
||||
if (strpos($url, '/') === 0) return $this->HOST . $url;
|
||||
if (strpos($url, 'http') !== 0) return $this->HOST . '/' . $url;
|
||||
return $url;
|
||||
}
|
||||
|
||||
// 解析 HTML 列表 (首页/搜索用)
|
||||
private function parseHtmlList($html, $isSearch = false) {
|
||||
$videos = [];
|
||||
if (!$html) return $videos;
|
||||
|
||||
$pattern = $isSearch
|
||||
? '/<div class="search-list[^"]*">(.*?)<div class="right">/is'
|
||||
: '/<div class="public-list-box[^"]*">(.*?)<\/div>\s*<\/div>/is';
|
||||
|
||||
preg_match_all($pattern, $html, $matches);
|
||||
|
||||
if (!empty($matches[1])) {
|
||||
foreach ($matches[1] as $itemHtml) {
|
||||
if (!preg_match('/href="([^"]+)"/', $itemHtml, $m)) continue;
|
||||
$href = $m[1];
|
||||
|
||||
$title = '';
|
||||
if (preg_match('/alt="([^"]+)"/', $itemHtml, $m)) $title = $m[1];
|
||||
elseif (preg_match('/title="([^"]+)"/', $itemHtml, $m)) $title = $m[1];
|
||||
|
||||
$pic = '';
|
||||
if (preg_match('/data-src="([^"]+)"/', $itemHtml, $m)) $pic = $m[1];
|
||||
elseif (preg_match('/src="([^"]+)"/', $itemHtml, $m)) $pic = $m[1];
|
||||
|
||||
$remarks = '';
|
||||
if (preg_match('/<span class="public-list-prb[^"]*">([^<]+)<\/span>/', $itemHtml, $m)) {
|
||||
$remarks = strip_tags($m[1]);
|
||||
} elseif (preg_match('/<span class="public-prt"[^>]*>([^<]+)<\/span>/', $itemHtml, $m)) {
|
||||
$remarks = strip_tags($m[1]);
|
||||
}
|
||||
|
||||
if ($title) {
|
||||
$videos[] = [
|
||||
'vod_id' => $this->fixUrl($href),
|
||||
'vod_name' => trim($title),
|
||||
'vod_pic' => $this->fixUrl($pic),
|
||||
'vod_remarks' => trim($remarks)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $videos;
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
// 首页 (精选 + 筛选配置)
|
||||
$html = $this->fetch($this->HOST . '/', [], $this->getHeaders());
|
||||
$list = $this->parseHtmlList($html, false);
|
||||
$list = array_slice($list, 0, 20);
|
||||
|
||||
$classes = [
|
||||
['type_id' => '20', 'type_name' => '🔥 当季新番'],
|
||||
['type_id' => '21', 'type_name' => '🎬 番剧'],
|
||||
['type_id' => '22', 'type_name' => '🎥 剧场']
|
||||
];
|
||||
|
||||
// 筛选配置
|
||||
$filters = $this->getFilters();
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => $filters,
|
||||
'list' => $list
|
||||
];
|
||||
}
|
||||
|
||||
// 筛选配置 (参照 JS 源码配置)
|
||||
private function getFilters() {
|
||||
$classes = ['搞笑','恋爱','校园','后宫','治愈','日常','原创','战斗','百合','BL','卖肉','漫画改','游戏改','异世界','泡面番','轻小说改','OVA','OAD','京阿尼','芳文社','A-1Pictures','CloverWorks','J.C.STAFF','动画工房','SUNRISE','Production.I.G','MADHouse','BONES','P.A.WORKS','SHAFT','MAPPA','ufotable','TRIGGER','WITSTUDIO'];
|
||||
|
||||
$years = [];
|
||||
for ($i = 2026; $i >= 1990; $i--) $years[] = (string)$i;
|
||||
|
||||
// 构建筛选结构
|
||||
$classValues = [['n' => '全部', 'v' => '']];
|
||||
foreach ($classes as $c) $classValues[] = ['n' => $c, 'v' => $c];
|
||||
|
||||
$yearValues = [['n' => '全部', 'v' => '']];
|
||||
foreach ($years as $y) $yearValues[] = ['n' => $y, 'v' => $y];
|
||||
|
||||
$sortValues = [
|
||||
['n' => '按最新', 'v' => 'time'],
|
||||
['n' => '按最热', 'v' => 'hits'],
|
||||
['n' => '按评分', 'v' => 'score']
|
||||
];
|
||||
|
||||
$rules = [
|
||||
['key' => 'class', 'name' => '剧情', 'value' => $classValues],
|
||||
['key' => 'year', 'name' => '年份', 'value' => $yearValues],
|
||||
['key' => 'by', 'name' => '排序', 'value' => $sortValues]
|
||||
];
|
||||
|
||||
// 应用到所有分类
|
||||
return [
|
||||
'20' => $rules,
|
||||
'21' => $rules,
|
||||
'22' => $rules
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$apiUrl = $this->HOST . '/index.php/ds_api/vod';
|
||||
|
||||
// 构建 POST 数据
|
||||
$postParams = [
|
||||
'type' => $tid,
|
||||
'class' => $extend['class'] ?? '',
|
||||
'year' => $extend['year'] ?? '',
|
||||
'by' => $extend['by'] ?? 'time', // 默认按最新
|
||||
'page' => $pg
|
||||
];
|
||||
|
||||
// 发送 POST 请求 (必须带上 content-type)
|
||||
$headers = array_merge($this->getHeaders(), [
|
||||
'Content-Type: application/x-www-form-urlencoded; charset=utf-8'
|
||||
]);
|
||||
|
||||
$jsonStr = $this->fetch($apiUrl, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => http_build_query($postParams),
|
||||
CURLOPT_HTTPHEADER => $headers
|
||||
]);
|
||||
|
||||
$jsonObj = json_decode($jsonStr, true);
|
||||
$list = [];
|
||||
|
||||
if ($jsonObj && isset($jsonObj['list']) && is_array($jsonObj['list'])) {
|
||||
foreach ($jsonObj['list'] as $it) {
|
||||
$list[] = [
|
||||
'vod_id' => $this->fixUrl($it['url']),
|
||||
'vod_name' => $it['vod_name'],
|
||||
'vod_pic' => $this->fixUrl($it['vod_pic']),
|
||||
'vod_remarks' => $it['vod_remarks']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$total = $jsonObj['total'] ?? 0;
|
||||
$limit = $jsonObj['limit'] ?? 30;
|
||||
|
||||
return $this->pageResult($list, $pg, $total, $limit);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
$url = (strpos($id, 'http') === 0) ? $id : $this->fixUrl($id);
|
||||
$html = $this->fetch($url, [], $this->getHeaders());
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $id, 'vod_name' => '', 'vod_pic' => '',
|
||||
'vod_content' => '', 'vod_play_from' => '', 'vod_play_url' => ''
|
||||
];
|
||||
|
||||
if ($html) {
|
||||
if (preg_match('/<title>(.*?)<\/title>/', $html, $m))
|
||||
$vod['vod_name'] = trim(preg_replace('/\s*-\s*嗷呜动漫.*$/', '', $m[1]));
|
||||
|
||||
if (preg_match('/data-original="([^"]+)"/', $html, $m)) $vod['vod_pic'] = $this->fixUrl($m[1]);
|
||||
elseif (preg_match('/class="detail-pic"[^>]*src="([^"]+)"/', $html, $m)) $vod['vod_pic'] = $this->fixUrl($m[1]);
|
||||
|
||||
if (preg_match('/class="text cor3"[^>]*>(.*?)<\/div>/is', $html, $m))
|
||||
$vod['vod_content'] = trim(strip_tags($m[1]));
|
||||
|
||||
$playFrom = [];
|
||||
preg_match('/<div class="anthology-tab[^"]*">(.*?)<\/div>/is', $html, $tabHtml);
|
||||
if (!empty($tabHtml[1])) {
|
||||
preg_match_all('/<a[^>]*>([^<]+)<\/a>/', $tabHtml[1], $tabNames);
|
||||
if (!empty($tabNames[1])) {
|
||||
foreach($tabNames[1] as $idx => $name) {
|
||||
$name = trim(preg_replace('/ /', '', $name));
|
||||
$playFrom[] = $name ?: "线路".($idx+1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$playUrls = [];
|
||||
preg_match_all('/<div class="anthology-list-play[^"]*">(.*?)<\/div>\s*<\/div>/is', $html, $listBoxes);
|
||||
if (empty($listBoxes[1])) preg_match_all('/<ul class="anthology-list-play[^"]*">(.*?)<\/ul>/is', $html, $listBoxes);
|
||||
|
||||
if (!empty($listBoxes[1])) {
|
||||
foreach ($listBoxes[1] as $listHtml) {
|
||||
preg_match_all('/<a[^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/is', $listHtml, $links);
|
||||
$episodes = [];
|
||||
if (!empty($links[1])) {
|
||||
foreach ($links[1] as $k => $href) {
|
||||
$episodes[] = trim(strip_tags($links[2][$k])) . '$' . $this->fixUrl($href);
|
||||
}
|
||||
}
|
||||
$playUrls[] = implode('#', $episodes);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($playFrom) && !empty($playUrls)) {
|
||||
for($i=0; $i<count($playUrls); $i++) $playFrom[] = "线路".($i+1);
|
||||
}
|
||||
|
||||
if (count($playFrom) >= 3) {
|
||||
array_shift($playFrom);
|
||||
array_shift($playUrls);
|
||||
}
|
||||
|
||||
$vod['vod_play_from'] = implode('$$$', $playFrom);
|
||||
$vod['vod_play_url'] = implode('$$$', $playUrls);
|
||||
}
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$url = $this->HOST . '/search/' . urlencode($key) . '----------' . $pg . '---.html';
|
||||
$html = $this->fetch($url, [], $this->getHeaders());
|
||||
$list = $this->parseHtmlList($html, true);
|
||||
|
||||
return $this->pageResult($list, $pg, 0, 30);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$url = $id;
|
||||
if (strpos($url, 'http') === false) $url = $this->fixUrl($url);
|
||||
|
||||
return [
|
||||
'parse' => 1, // 开启嗅探
|
||||
'url' => $url,
|
||||
'header' => [
|
||||
'User-Agent' => $this->UA,
|
||||
'Referer' => $this->HOST . '/'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 运行爬虫
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $HOST = 'https://api.drama.9ddm.com';
|
||||
private $UA = 'okhttp/3.12.11';
|
||||
|
||||
protected function getHeaders() {
|
||||
return [
|
||||
'User-Agent: ' . $this->UA,
|
||||
'Content-Type: application/json;charset=utf-8'
|
||||
];
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
// 获取分类标签 (对应原 JS class_parse)
|
||||
$html = $this->fetch($this->HOST . '/drama/home/shortVideoTags', [], $this->getHeaders());
|
||||
$data = json_decode($html, true);
|
||||
|
||||
$classes = [];
|
||||
$filterObj = [];
|
||||
|
||||
if (isset($data['audiences'])) {
|
||||
foreach ($data['audiences'] as $audience) {
|
||||
$classes[] = ['type_id' => $audience, 'type_name' => $audience];
|
||||
|
||||
// 构建筛选 (标签)
|
||||
$tagValues = [['n' => '全部', 'v' => '']];
|
||||
if (isset($data['tags'])) {
|
||||
foreach ($data['tags'] as $tag) {
|
||||
$tagValues[] = ['n' => $tag, 'v' => $tag];
|
||||
}
|
||||
}
|
||||
|
||||
$filterObj[$audience] = [
|
||||
['key' => 'tag', 'name' => '标签', 'value' => $tagValues]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => $filterObj,
|
||||
'list' => [] // 首页展示可留空或调用 categoryContent
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$apiUrl = $this->HOST . '/drama/home/search';
|
||||
|
||||
$postData = [
|
||||
"audience" => $tid,
|
||||
"page" => (int)$pg,
|
||||
"pageSize" => 30,
|
||||
"searchWord" => "",
|
||||
"subject" => $extend['tag'] ?? ""
|
||||
];
|
||||
|
||||
$jsonStr = $this->fetch($apiUrl, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($postData),
|
||||
CURLOPT_HTTPHEADER => $this->getHeaders()
|
||||
]);
|
||||
|
||||
$response = json_decode($jsonStr, true);
|
||||
$list = [];
|
||||
|
||||
if (isset($response['data']) && is_array($response['data'])) {
|
||||
foreach ($response['data'] as $it) {
|
||||
$list[] = [
|
||||
'vod_id' => $it['oneId'],
|
||||
'vod_name' => $it['title'],
|
||||
'vod_pic' => $it['vertPoster'],
|
||||
'vod_remarks' => "集数:{$it['episodeCount']} 播放:{$it['viewCount']}",
|
||||
'vod_year' => (string)($it['publishDate'] ?? '')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($list, $pg, 999, 30);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
// 详情接口地址
|
||||
$url = $this->HOST . "/drama/home/shortVideoDetail?oneId={$id}&page=1&pageSize=1000";
|
||||
|
||||
$html = $this->fetch($url, [], $this->getHeaders());
|
||||
$response = json_decode($html, true);
|
||||
$data = $response['data'] ?? [];
|
||||
|
||||
if (empty($data)) return ['list' => []];
|
||||
|
||||
$first = $data[0];
|
||||
$vod = [
|
||||
'vod_id' => $id,
|
||||
'vod_name' => $first['title'],
|
||||
'vod_pic' => $first['vertPoster'],
|
||||
'vod_remarks' => "共" . count($data) . "集",
|
||||
'vod_content' => "播放量:{$first['collectionCount']} 评论:{$first['commentCount']} " . ($first['description'] ?? ""),
|
||||
'vod_play_from' => '围观短剧'
|
||||
];
|
||||
|
||||
$playUrls = [];
|
||||
foreach ($data as $episode) {
|
||||
// 原 JS 逻辑:将整个 playSetting JSON 存入 URL,在 lazy/playContent 中解析
|
||||
$playUrls[] = "第{$episode['playOrder']}集$" . $episode['playSetting'];
|
||||
}
|
||||
|
||||
$vod['vod_play_url'] = implode('#', $playUrls);
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$apiUrl = $this->HOST . '/drama/home/search';
|
||||
$postData = [
|
||||
"audience" => "",
|
||||
"page" => (int)$pg,
|
||||
"pageSize" => 30,
|
||||
"searchWord" => $key,
|
||||
"subject" => ""
|
||||
];
|
||||
|
||||
$jsonStr = $this->fetch($apiUrl, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($postData),
|
||||
CURLOPT_HTTPHEADER => $this->getHeaders()
|
||||
]);
|
||||
|
||||
$response = json_decode($jsonStr, true);
|
||||
$list = [];
|
||||
if (isset($response['data'])) {
|
||||
foreach ($response['data'] as $it) {
|
||||
$list[] = [
|
||||
'vod_id' => $it['oneId'],
|
||||
'vod_name' => $it['title'],
|
||||
'vod_pic' => $it['vertPoster'],
|
||||
'vod_remarks' => $it['description']
|
||||
];
|
||||
}
|
||||
}
|
||||
return $this->pageResult($list, $pg, 0, 30);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
// id 此时是 detailContent 传过来的 playSetting JSON 字符串
|
||||
$playSetting = json_decode($id, true);
|
||||
|
||||
// 优先级:高清 > 普通 > 流畅
|
||||
$videoUrl = $playSetting['high'] ?? $playSetting['normal'] ?? $playSetting['super'] ?? '';
|
||||
|
||||
return [
|
||||
'parse' => 0, // 短剧通常是直链,无需嗅探
|
||||
'url' => $videoUrl,
|
||||
'header' => [
|
||||
'User-Agent' => $this->UA
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 运行爬虫
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $HOST = "http://106.53.107.16"; // 默认起始地址
|
||||
private $UA = 'Dart/3.9 (dart:io)';
|
||||
|
||||
protected function getHeaders() {
|
||||
return [
|
||||
'User-Agent: ' . $this->UA,
|
||||
'Accept-Encoding: gzip',
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化:检测有效域名
|
||||
*/
|
||||
private function getValidHost() {
|
||||
// 如果 extend 传入了不同地址,可以在此处动态修改 $this->HOST
|
||||
// 此处还原 Python 中的检测逻辑
|
||||
$checkUrl = rtrim($this->HOST, '/') . '/success.txt';
|
||||
try {
|
||||
// 简单的存活性检测
|
||||
$res = $this->fetch($checkUrl, [CURLOPT_TIMEOUT => 5], $this->getHeaders());
|
||||
if ($res) {
|
||||
return rtrim($this->HOST, '/');
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
return rtrim($this->HOST, '/');
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$host = $this->getValidHost();
|
||||
$classes = [];
|
||||
|
||||
// 1. 获取常规分类
|
||||
$res1 = $this->fetch($host . '/api.php/type/get_list', [], $this->getHeaders());
|
||||
$data1 = json_decode($res1, true);
|
||||
if (isset($data1['info']['rows'])) {
|
||||
foreach ($data1['info']['rows'] as $row) {
|
||||
if ($row['type_status'] == 1 && !in_array($row['type_name'], ['漫画', '小说'])) {
|
||||
$classes[] = ['type_id' => $row['type_id'], 'type_name' => $row['type_name']];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取短视频分类
|
||||
try {
|
||||
$res2 = $this->fetch($host . '/addons/getstar/api.index/shortVideoCategory', [], $this->getHeaders());
|
||||
$data2 = json_decode($res2, true);
|
||||
if (isset($data2['data'])) {
|
||||
foreach ($data2['data'] as $row) {
|
||||
$classes[] = ['type_id' => $row['id'], 'type_name' => $row['name']];
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
|
||||
return ['class' => $classes];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
$host = $this->getValidHost();
|
||||
$url = $host . '/index.php/ajax/data?mid=1&limit=100&page=1&level=7';
|
||||
$res = $this->fetch($url, [], $this->getHeaders());
|
||||
return json_decode($res, true);
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$host = $this->getValidHost();
|
||||
$year = date("Y");
|
||||
$url = "{$host}/index.php/ajax/data?mid=1&limit=20&page={$pg}&tid={$tid}&year={$year}";
|
||||
$res = $this->fetch($url, [], $this->getHeaders());
|
||||
$json = json_decode($res, true);
|
||||
$list = $json['list'] ?? [];
|
||||
$total = $json['total'] ?? 0;
|
||||
return $this->pageResult($list, $pg, $total, 20);
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$host = $this->getValidHost();
|
||||
$url = "{$host}/index.php/ajax/data?mid=1&limit=20&page={$pg}&wd=" . urlencode($key);
|
||||
$res = $this->fetch($url, [], $this->getHeaders());
|
||||
$json = json_decode($res, true);
|
||||
$list = $json['list'] ?? [];
|
||||
$total = $json['total'] ?? 0;
|
||||
return $this->pageResult($list, $pg, $total, 20);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$host = $this->getValidHost();
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
|
||||
// 1. 获取解析配置 (PlayerParse)
|
||||
$playerConfigs = [];
|
||||
try {
|
||||
$pRes = $this->fetch($host . '/addons/getstar/api.index/getPlayerParse', [], $this->getHeaders());
|
||||
$pData = json_decode($pRes, true);
|
||||
if (isset($pData['data']) && is_array($pData['data'])) {
|
||||
$playerConfigs = $pData['data'];
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
|
||||
// 2. 获取视频详情
|
||||
$res = $this->fetch($host . "/api.php/vod/get_detail?vod_id={$id}", [], $this->getHeaders());
|
||||
$json = json_decode($res, true);
|
||||
$data = $json['info'][0];
|
||||
|
||||
if (!empty($data['vod_play_from']) && !empty($data['vod_play_url'])) {
|
||||
$froms = explode('$$$', $data['vod_play_from']);
|
||||
$urls = explode('$$$', $data['vod_play_url']);
|
||||
|
||||
$newFroms = [];
|
||||
$newUrls = [];
|
||||
|
||||
foreach ($froms as $key => $show) {
|
||||
$parseUrl = '';
|
||||
$isOpen = false;
|
||||
|
||||
// 匹配解析器
|
||||
foreach ($playerConfigs as $pConf) {
|
||||
if ($pConf['code'] == $show) {
|
||||
$isOpen = true;
|
||||
$name = $pConf['name'] ?? '';
|
||||
if ($name && $name != $show) {
|
||||
$show = "{$name} ({$show})";
|
||||
}
|
||||
$parseUrl = $pConf['url'] ?? '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isOpen) continue;
|
||||
|
||||
$episodeParts = explode('#', $urls[$key]);
|
||||
$formattedEpisodes = [];
|
||||
foreach ($episodeParts as $part) {
|
||||
if (empty($part)) continue;
|
||||
$temp = explode('$', $part, 2);
|
||||
$epName = $temp[0];
|
||||
$epUrl = $temp[1] ?? '';
|
||||
|
||||
// 将解析地址附加到 URL 后,供 playContent 使用
|
||||
$suffix = $parseUrl ? "@{$parseUrl}" : "";
|
||||
$formattedEpisodes[] = "{$epName}\${$epUrl}{$suffix}";
|
||||
}
|
||||
|
||||
$newFroms[] = $show;
|
||||
$newUrls[] = implode('#', $formattedEpisodes);
|
||||
}
|
||||
|
||||
$data['vod_play_from'] = implode('$$$', $newFroms);
|
||||
$data['vod_play_url'] = implode('$$$', $newUrls);
|
||||
}
|
||||
|
||||
return ['list' => [$data]];
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$rawUrl = $id;
|
||||
$url = "";
|
||||
$jx = 0;
|
||||
|
||||
// 处理带 @ 的自定义解析
|
||||
if (strpos($id, '@') !== false) {
|
||||
list($rawUrl, $parse) = explode('@', $id, 2);
|
||||
$headers = [
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
];
|
||||
try {
|
||||
$res = $this->fetch($parse . $rawUrl, [], $headers);
|
||||
$json = json_decode($res, true);
|
||||
if (!empty($json['url']) && $json['url'] != $rawUrl) {
|
||||
$url = $json['url'];
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
if (empty($url)) {
|
||||
$url = $rawUrl;
|
||||
// 匹配大站链接开启嗅探
|
||||
if (preg_match('/(?:www\.iqiyi|v\.qq|v\.youku|www\.mgtv|www\.bilibili)\.com/', $rawUrl)) {
|
||||
$jx = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'jx' => $jx,
|
||||
'parse' => 0,
|
||||
'url' => $url,
|
||||
'header' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Connection' => 'Keep-Alive'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 运行
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $host = 'https://www.iqiyi.com';
|
||||
|
||||
protected function getHeaders() {
|
||||
return [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer' => 'https://www.iqiyi.com',
|
||||
'Accept' => 'application/json, text/plain, */*',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Connection' => 'keep-alive'
|
||||
];
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$classes = [
|
||||
['type_id' => '1', 'type_name' => '电影'],
|
||||
['type_id' => '2', 'type_name' => '电视剧'],
|
||||
['type_id' => '6', 'type_name' => '综艺'],
|
||||
['type_id' => '4', 'type_name' => '动漫'],
|
||||
['type_id' => '3', 'type_name' => '纪录片'],
|
||||
['type_id' => '5', 'type_name' => '音乐'],
|
||||
['type_id' => '16', 'type_name' => '网络电影']
|
||||
];
|
||||
|
||||
$filters = [
|
||||
'1' => [[
|
||||
'key' => 'year',
|
||||
'name' => '年代',
|
||||
'value' => [['n' => '全部', 'v' => ''], ['n' => '2025', 'v' => '2025'], ['n' => '2024', 'v' => '2024'], ['n' => '2023', 'v' => '2023']]
|
||||
]],
|
||||
'2' => [[
|
||||
'key' => 'year',
|
||||
'name' => '年代',
|
||||
'value' => [['n' => '全部', 'v' => ''], ['n' => '2025', 'v' => '2025'], ['n' => '2024', 'v' => '2024'], ['n' => '2023', 'v' => '2023']]
|
||||
]]
|
||||
];
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => $filters
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$channelId = $tid;
|
||||
$dataType = 1;
|
||||
$extraParams = "";
|
||||
$page = max(1, intval($pg));
|
||||
|
||||
if ($tid === "16") {
|
||||
$channelId = "1";
|
||||
$extraParams = "&three_category_id=27401";
|
||||
} else if ($tid === "5") {
|
||||
$dataType = 2;
|
||||
}
|
||||
|
||||
// 处理筛选条件
|
||||
if (!empty($extend)) {
|
||||
if (isset($extend['year'])) {
|
||||
$extraParams .= "&market_release_date_level={$extend['year']}";
|
||||
}
|
||||
}
|
||||
|
||||
$url = "https://pcw-api.iqiyi.com/search/recommend/list?channel_id={$channelId}&data_type={$dataType}&page_id={$page}&ret_num=20{$extraParams}";
|
||||
|
||||
$jsonStr = $this->fetch($url, [], $this->getHeaders());
|
||||
$jsonData = json_decode($jsonStr, true);
|
||||
|
||||
$videos = [];
|
||||
if (isset($jsonData['data']['list'])) {
|
||||
foreach ($jsonData['data']['list'] as $item) {
|
||||
$vid = "{$item['channelId']}\${$item['albumId']}";
|
||||
$remarks = "";
|
||||
|
||||
if ($item['channelId'] == 1) {
|
||||
$remarks = isset($item['score']) ? "{$item['score']}分" : "";
|
||||
} else if ($item['channelId'] == 2 || $item['channelId'] == 4) {
|
||||
if (isset($item['latestOrder']) && isset($item['videoCount'])) {
|
||||
$remarks = ($item['latestOrder'] == $item['videoCount']) ?
|
||||
"{$item['latestOrder']}集全" :
|
||||
"更新至{$item['latestOrder']}集";
|
||||
} else {
|
||||
$remarks = $item['focus'] ?? "";
|
||||
}
|
||||
} else {
|
||||
$remarks = $item['period'] ?? ($item['focus'] ?? "");
|
||||
}
|
||||
|
||||
$pic = isset($item['imageUrl']) ? str_replace(".jpg", "_390_520.jpg", $item['imageUrl']) : "";
|
||||
|
||||
$videos[] = [
|
||||
'vod_id' => $vid,
|
||||
'vod_name' => $item['name'],
|
||||
'vod_pic' => $pic,
|
||||
'vod_remarks' => $remarks
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($videos, $page, 999999, 20);
|
||||
}
|
||||
|
||||
private function getPlaylists($channelId, $albumId, $data) {
|
||||
$playlists = [];
|
||||
$cid = intval($channelId ?: ($data['channelId'] ?? 0));
|
||||
|
||||
if ($cid === 1 || $cid === 5) {
|
||||
// 电影或音乐
|
||||
if (isset($data['playUrl'])) {
|
||||
$playlists[] = ['title' => $data['name'] ?? '正片', 'url' => $data['playUrl']];
|
||||
}
|
||||
} else if ($cid === 6 && isset($data['period'])) {
|
||||
// 综艺
|
||||
$qs = explode("-", (string)$data['period'])[0];
|
||||
$listUrl = "https://pcw-api.iqiyi.com/album/source/svlistinfo?cid=6&sourceid={$albumId}&timelist={$qs}";
|
||||
|
||||
$listResp = $this->fetch($listUrl, [], $this->getHeaders());
|
||||
$listJson = json_decode($listResp, true);
|
||||
|
||||
if (isset($listJson['data'][$qs])) {
|
||||
foreach ($listJson['data'][$qs] as $it) {
|
||||
$title = $it['shortTitle'] ?? ($it['period'] ?? ($it['focus'] ?? "期{$it['order']}"));
|
||||
$playlists[] = [
|
||||
'title' => $title,
|
||||
'url' => $it['playUrl']
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 电视剧、动漫等
|
||||
$listUrl = "https://pcw-api.iqiyi.com/albums/album/avlistinfo?aid={$albumId}&size=100&page=1";
|
||||
$listResp = $this->fetch($listUrl, [], $this->getHeaders());
|
||||
$listJson = json_decode($listResp, true);
|
||||
|
||||
if (isset($listJson['data']['epsodelist'])) {
|
||||
foreach ($listJson['data']['epsodelist'] as $item) {
|
||||
$title = $item['shortTitle'] ?? ($item['title'] ?? (isset($item['order']) ? "第{$item['order']}集" : "集{$item['timelist']}"));
|
||||
$playlists[] = [
|
||||
'title' => $title,
|
||||
'url' => $item['playUrl'] ?? ($item['url'] ?? '')
|
||||
];
|
||||
}
|
||||
|
||||
// 处理分页
|
||||
$total = $listJson['data']['total'] ?? 0;
|
||||
if ($total > 100) {
|
||||
$totalPages = ceil($total / 100);
|
||||
for ($i = 2; $i <= $totalPages; $i++) {
|
||||
$nextUrl = "https://pcw-api.iqiyi.com/albums/album/avlistinfo?aid={$albumId}&size=100&page={$i}";
|
||||
$nextResp = $this->fetch($nextUrl, [], $this->getHeaders());
|
||||
$nextJson = json_decode($nextResp, true);
|
||||
|
||||
if (isset($nextJson['data']['epsodelist'])) {
|
||||
foreach ($nextJson['data']['epsodelist'] as $item) {
|
||||
$title = $item['shortTitle'] ?? ($item['title'] ?? (isset($item['order']) ? "第{$item['order']}集" : "集{$item['timelist']}"));
|
||||
$playlists[] = [
|
||||
'title' => $title,
|
||||
'url' => $item['playUrl'] ?? ($item['url'] ?? '')
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $playlists;
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
$channelId = "";
|
||||
$albumId = $id;
|
||||
|
||||
if (strpos($id, '$') !== false) {
|
||||
$parts = explode('$', $id);
|
||||
$channelId = $parts[0];
|
||||
$albumId = $parts[1];
|
||||
}
|
||||
|
||||
// 获取视频基本信息
|
||||
$infoUrl = "https://pcw-api.iqiyi.com/video/video/videoinfowithuser/{$albumId}?agent_type=1&authcookie=&subkey={$albumId}&subscribe=1";
|
||||
$infoResp = $this->fetch($infoUrl, [], $this->getHeaders());
|
||||
$infoJson = json_decode($infoResp, true);
|
||||
$data = $infoJson['data'] ?? [];
|
||||
|
||||
// 获取播放列表
|
||||
$playlists = $this->getPlaylists($channelId, $albumId, $data);
|
||||
|
||||
// 构建播放地址
|
||||
$playUrls = [];
|
||||
foreach ($playlists as $item) {
|
||||
if (!empty($item['url'])) {
|
||||
$playUrls[] = "{$item['title']}\${$item['url']}";
|
||||
}
|
||||
}
|
||||
|
||||
$typeName = '';
|
||||
if (isset($data['categories'])) {
|
||||
$names = array_map(function($it) { return $it['name']; }, $data['categories']);
|
||||
$typeName = implode(',', $names);
|
||||
}
|
||||
|
||||
$area = '';
|
||||
if (isset($data['areas'])) {
|
||||
$names = array_map(function($it) { return $it['name']; }, $data['areas']);
|
||||
$area = implode(',', $names);
|
||||
}
|
||||
|
||||
$actors = '';
|
||||
if (isset($data['people']['main_charactor'])) {
|
||||
$names = array_map(function($it) { return $it['name']; }, $data['people']['main_charactor']);
|
||||
$actors = implode(',', $names);
|
||||
}
|
||||
|
||||
$director = '';
|
||||
if (isset($data['people']['director'])) {
|
||||
$names = array_map(function($it) { return $it['name']; }, $data['people']['director']);
|
||||
$director = implode(',', $names);
|
||||
}
|
||||
|
||||
$remarks = "";
|
||||
if (isset($data['latestOrder'])) {
|
||||
$remarks = "更新至{$data['latestOrder']}集";
|
||||
} else {
|
||||
$remarks = isset($data['period']) || count($playlists) > 0 ? count($playlists)."集" : "";
|
||||
}
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $id,
|
||||
'vod_name' => $data['name'] ?? '未知标题',
|
||||
'type_name' => $typeName,
|
||||
'vod_year' => $data['formatPeriod'] ?? '',
|
||||
'vod_area' => $area,
|
||||
'vod_remarks' => $remarks,
|
||||
'vod_actor' => $actors,
|
||||
'vod_director' => $director,
|
||||
'vod_content' => $data['description'] ?? '暂无简介',
|
||||
'vod_pic' => isset($data['imageUrl']) ? str_replace(".jpg", "_480_270.jpg", $data['imageUrl']) : '',
|
||||
'vod_play_from' => count($playUrls) > 0 ? '爱奇艺视频' : '',
|
||||
'vod_play_url' => implode('#', $playUrls)
|
||||
];
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$page = max(1, intval($pg));
|
||||
$url = "https://search.video.iqiyi.com/o?if=html5&key=" . urlencode($key) . "&pageNum={$page}&pos=1&pageSize=20&site=iqiyi";
|
||||
|
||||
$response = $this->fetch($url, [], $this->getHeaders());
|
||||
$jsonData = json_decode($response, true);
|
||||
|
||||
$videos = [];
|
||||
if (isset($jsonData['data']['docinfos'])) {
|
||||
foreach ($jsonData['data']['docinfos'] as $item) {
|
||||
if (isset($item['albumDocInfo'])) {
|
||||
$doc = $item['albumDocInfo'];
|
||||
$channelId = isset($doc['channel']) ? explode(',', $doc['channel'])[0] : '0';
|
||||
$videos[] = [
|
||||
'vod_id' => "{$channelId}\${$doc['albumId']}",
|
||||
'vod_name' => $doc['albumTitle'] ?? '',
|
||||
'vod_pic' => $doc['albumVImage'] ?? '',
|
||||
'vod_remarks' => $doc['tvFocus'] ?? ($doc['year'] ?? '')
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($videos, $page, count($videos) * 10, 20); // 搜索无法获取总数,简单估算
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$playUrl = $id;
|
||||
if (strpos($id, '$') !== false) {
|
||||
$playUrl = explode('$', $id)[1];
|
||||
}
|
||||
|
||||
// 壳子超级解析格式
|
||||
return [
|
||||
'parse' => 1,
|
||||
'jx' => 1,
|
||||
'play_parse' => true,
|
||||
'parse_type' => '壳子超级解析',
|
||||
'parse_source' => '爱奇艺视频',
|
||||
'url' => $playUrl,
|
||||
'header' => json_encode([
|
||||
'User-Agent' => $this->getHeaders()['User-Agent'],
|
||||
'Referer' => 'https://www.iqiyi.com',
|
||||
'Origin' => 'https://www.iqiyi.com'
|
||||
], JSON_UNESCAPED_UNICODE)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* 山有木兮 - PHP 适配版 (道长重构)
|
||||
* 按照 BaseSpider 结构重写
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
|
||||
private $HOST = 'https://film.symx.club';
|
||||
|
||||
public function init($extend = '') {
|
||||
$this->headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36';
|
||||
$this->headers['Sec-Ch-Ua'] = '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"';
|
||||
$this->headers['Sec-Ch-Ua-Mobile'] = '?0';
|
||||
$this->headers['Sec-Ch-Ua-Platform'] = '"Windows"';
|
||||
$this->headers['Sec-Fetch-Dest'] = 'empty';
|
||||
$this->headers['Sec-Fetch-Mode'] = 'cors';
|
||||
$this->headers['Sec-Fetch-Site'] = 'same-origin';
|
||||
$this->headers['X-Platform'] = 'web';
|
||||
$this->headers['Accept'] = 'application/json, text/plain, */*';
|
||||
|
||||
if (!empty($extend) && strpos($extend, 'http') === 0) {
|
||||
$this->HOST = rtrim($extend, '/');
|
||||
}
|
||||
}
|
||||
|
||||
private function getHeaders($referer = '/') {
|
||||
$headers = $this->headers;
|
||||
$headers['Referer'] = $this->HOST . $referer;
|
||||
return $headers;
|
||||
}
|
||||
|
||||
public function homeContent($filter = []) {
|
||||
$url = $this->HOST . "/api/category/top";
|
||||
$data = json_decode($this->fetch($url, [], $this->getHeaders()), true);
|
||||
|
||||
$classes = [];
|
||||
if (isset($data['data'])) {
|
||||
foreach ($data['data'] as $item) {
|
||||
$classes[] = [
|
||||
'type_id' => strval($item['id']),
|
||||
'type_name' => $item['name']
|
||||
];
|
||||
}
|
||||
}
|
||||
return ['class' => $classes];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
$url = $this->HOST . "/api/film/category";
|
||||
$data = json_decode($this->fetch($url, [], $this->getHeaders()), true);
|
||||
|
||||
$list = [];
|
||||
if (isset($data['data'])) {
|
||||
foreach ($data['data'] as $category) {
|
||||
$filmList = $category['filmList'] ?? [];
|
||||
foreach ($filmList as $film) {
|
||||
$list[] = [
|
||||
'vod_id' => strval($film['id']),
|
||||
'vod_name' => $film['name'],
|
||||
'vod_pic' => $film['cover'],
|
||||
'vod_remarks' => $film['doubanScore'] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['list' => array_slice($list, 0, 30)];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$pageNum = max(1, intval($pg));
|
||||
$url = $this->HOST . "/api/film/category/list?area=&categoryId={$tid}&language=&pageNum={$pageNum}&pageSize=15&sort=updateTime&year=";
|
||||
$data = json_decode($this->fetch($url, [], $this->getHeaders()), true);
|
||||
|
||||
$list = [];
|
||||
if (isset($data['data']['list'])) {
|
||||
foreach ($data['data']['list'] as $item) {
|
||||
$list[] = [
|
||||
'vod_id' => strval($item['id']),
|
||||
'vod_name' => $item['name'],
|
||||
'vod_pic' => $item['cover'],
|
||||
'vod_remarks' => $item['updateStatus']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$total = $data['data']['total'] ?? 0;
|
||||
return $this->pageResult($list, $pageNum, $total, 15);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
if (empty($ids)) return ['list' => []];
|
||||
$id = $ids[0]; // 只处理第一个ID
|
||||
|
||||
$url = $this->HOST . "/api/film/detail?id=" . urlencode($id);
|
||||
$data = json_decode($this->fetch($url, [], $this->getHeaders()), true);
|
||||
|
||||
if (!isset($data['data'])) {
|
||||
return ['list' => []];
|
||||
}
|
||||
|
||||
$info = $data['data'];
|
||||
$shows = [];
|
||||
$play_urls = [];
|
||||
|
||||
if (isset($info['playLineList'])) {
|
||||
foreach ($info['playLineList'] as $line) {
|
||||
$shows[] = $line['playerName'];
|
||||
$urls = [];
|
||||
if (isset($line['lines'])) {
|
||||
foreach ($line['lines'] as $episode) {
|
||||
$urls[] = $episode['name'] . '$' . $episode['id'];
|
||||
}
|
||||
}
|
||||
$play_urls[] = implode('#', $urls);
|
||||
}
|
||||
}
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $id,
|
||||
'vod_name' => $info['name'],
|
||||
'vod_pic' => $info['cover'],
|
||||
'vod_year' => $info['year'],
|
||||
'vod_area' => $info['other'],
|
||||
'vod_actor' => $info['actor'],
|
||||
'vod_director' => $info['director'],
|
||||
'vod_content' => $info['blurb'],
|
||||
'vod_score' => $info['doubanScore'],
|
||||
'vod_play_from' => implode('$$$', $shows),
|
||||
'vod_play_url' => implode('$$$', $play_urls),
|
||||
'type_name' => $info['vod_class'] ?? ''
|
||||
];
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$pageNum = max(1, intval($pg));
|
||||
$url = $this->HOST . "/api/film/search?keyword=" . urlencode($key) . "&pageNum={$pageNum}&pageSize=10";
|
||||
$data = json_decode($this->fetch($url, [], $this->getHeaders()), true);
|
||||
|
||||
$list = [];
|
||||
if (isset($data['data']['list'])) {
|
||||
foreach ($data['data']['list'] as $item) {
|
||||
$list[] = [
|
||||
'vod_id' => strval($item['id']),
|
||||
'vod_name' => $item['name'],
|
||||
'vod_pic' => $item['cover'],
|
||||
'vod_remarks' => $item['updateStatus'],
|
||||
'vod_year' => $item['year'],
|
||||
'vod_area' => $item['area'],
|
||||
'vod_director' => $item['director']
|
||||
];
|
||||
}
|
||||
}
|
||||
return $this->pageResult($list, $pageNum);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$url = $this->HOST . "/api/line/play/parse?lineId=" . urlencode($id);
|
||||
$data = json_decode($this->fetch($url, [], $this->getHeaders()), true);
|
||||
|
||||
$playUrl = $data['data'] ?? '';
|
||||
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => $playUrl,
|
||||
'header' => ['User-Agent' => $this->headers['User-Agent']]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 运行爬虫
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $HOST = 'http://read.api.duodutek.com';
|
||||
private $UA = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36';
|
||||
|
||||
// 固定的 API 参数
|
||||
private $COMMON_PARAMS = [
|
||||
"productId" => "2a8c14d1-72e7-498b-af23-381028eb47c0",
|
||||
"vestId" => "2be070e0-c824-4d0e-a67a-8f688890cadb",
|
||||
"channel" => "oppo19",
|
||||
"osType" => "android",
|
||||
"version" => "20",
|
||||
"token" => "202509271001001446030204698626"
|
||||
];
|
||||
|
||||
protected function getHeaders() {
|
||||
return [
|
||||
'User-Agent: ' . $this->UA
|
||||
];
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
// 定义分类
|
||||
$classes = [
|
||||
["type_id" => "1287", "type_name" => "甜宠"],
|
||||
["type_id" => "1288", "type_name" => "逆袭"],
|
||||
["type_id" => "1289", "type_name" => "热血"],
|
||||
["type_id" => "1290", "type_name" => "现代"],
|
||||
["type_id" => "1291", "type_name" => "古代"]
|
||||
];
|
||||
|
||||
// 首页推荐:取第一个分类的前几个视频
|
||||
$list = $this->categoryContent('1287', 1)['list'];
|
||||
$list = array_slice($list, 0, 12);
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'list' => $list,
|
||||
'filters' => (object)[]
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$apiUrl = $this->HOST . '/novel-api/app/pageModel/getResourceById';
|
||||
|
||||
$params = array_merge($this->COMMON_PARAMS, [
|
||||
"resourceId" => $tid,
|
||||
"pageNum" => (string)$pg,
|
||||
"pageSize" => "10"
|
||||
]);
|
||||
|
||||
$url = $apiUrl . '?' . http_build_query($params);
|
||||
$jsonStr = $this->fetch($url, [], $this->getHeaders());
|
||||
$jsonObj = json_decode($jsonStr, true);
|
||||
|
||||
$list = [];
|
||||
if ($jsonObj && isset($jsonObj['data']['datalist'])) {
|
||||
foreach ($jsonObj['data']['datalist'] as $vod) {
|
||||
$list[] = [
|
||||
// 仿照原 Python:id@@name@@introduction 存储
|
||||
'vod_id' => $vod['id'] . '@@' . $vod['name'] . '@@' . ($vod['introduction'] ?? ''),
|
||||
'vod_name' => $vod['name'],
|
||||
'vod_pic' => $vod['icon'],
|
||||
'vod_remarks' => $vod['heat'] . '万播放'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($list, $pg, 999, 10);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$did = is_array($ids) ? $ids[0] : $ids;
|
||||
$parts = explode('@@', $did);
|
||||
if (count($parts) >= 2) {
|
||||
$bookId = $parts[0];
|
||||
$bookName = $parts[1];
|
||||
$intro = $parts[2] ?? '';
|
||||
} else {
|
||||
// 兼容旧格式 id@intro
|
||||
$parts = explode('@', $did);
|
||||
$bookId = $parts[0];
|
||||
$bookName = '';
|
||||
$intro = $parts[1] ?? '';
|
||||
}
|
||||
|
||||
$apiUrl = $this->HOST . '/novel-api/basedata/book/getChapterList';
|
||||
$params = array_merge($this->COMMON_PARAMS, [
|
||||
"bookId" => $bookId
|
||||
]);
|
||||
|
||||
$url = $apiUrl . '?' . http_build_query($params);
|
||||
$jsonStr = $this->fetch($url, [], $this->getHeaders());
|
||||
$jsonObj = json_decode($jsonStr, true);
|
||||
|
||||
$playUrls = [];
|
||||
if ($jsonObj && isset($jsonObj['data'])) {
|
||||
$chapters = $jsonObj['data'];
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
// 提取短剧播放地址
|
||||
if (isset($chapter['shortPlayList'][0]['chapterShortPlayVoList'][0]['shortPlayUrl'])) {
|
||||
$vUrl = $chapter['shortPlayList'][0]['chapterShortPlayVoList'][0]['shortPlayUrl'];
|
||||
$epName = "第" . ($index + 1) . "集";
|
||||
$playUrls[] = $epName . '$' . $vUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $did,
|
||||
'vod_name' => $bookName, // 由列表页带入
|
||||
'vod_content' => $intro,
|
||||
'vod_play_from' => '短剧专线',
|
||||
'vod_play_url' => implode('#', $playUrls)
|
||||
];
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
// 原 Python 代码中 searchContentPage 为 pass,故此处留空返回
|
||||
return $this->pageResult([], $pg);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
return [
|
||||
'parse' => 0, // 直接播放
|
||||
'url' => $id,
|
||||
'header' => [
|
||||
'User-Agent' => $this->UA
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 运行爬虫
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $host = 'https://www.mgtv.com';
|
||||
|
||||
protected function getHeaders() {
|
||||
return [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
|
||||
'Referer' => 'https://www.mgtv.com/',
|
||||
'Accept' => 'application/json, text/plain, */*',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Connection' => 'keep-alive'
|
||||
];
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$classes = [
|
||||
['type_id' => '3', 'type_name' => '电影'],
|
||||
['type_id' => '2', 'type_name' => '电视剧'],
|
||||
['type_id' => '1', 'type_name' => '综艺'],
|
||||
['type_id' => '50', 'type_name' => '动漫'],
|
||||
['type_id' => '51', 'type_name' => '纪录片'],
|
||||
['type_id' => '115', 'type_name' => '教育'],
|
||||
['type_id' => '10', 'type_name' => '少儿']
|
||||
];
|
||||
|
||||
$filters = [
|
||||
'3' => [
|
||||
[
|
||||
'key' => 'year', 'name' => '年份', 'value' => [
|
||||
['n' => '全部', 'v' => 'all'], ['n' => '2025', 'v' => '2025'], ['n' => '2024', 'v' => '2024'],
|
||||
['n' => '2023', 'v' => '2023'], ['n' => '2022', 'v' => '2022'], ['n' => '2021', 'v' => '2021'],
|
||||
['n' => '2020', 'v' => '2020'], ['n' => '2019', 'v' => '2019'], ['n' => '2010-2019', 'v' => '2010-2019'],
|
||||
['n' => '2000-2009', 'v' => '2000-2009']
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'sort', 'name' => '排序', 'value' => [
|
||||
['n' => '综合', 'v' => 'c1'], ['n' => '最新', 'v' => 'c2'], ['n' => '最热', 'v' => 'c4']
|
||||
]
|
||||
]
|
||||
],
|
||||
'2' => [
|
||||
[
|
||||
'key' => 'year', 'name' => '年份', 'value' => [
|
||||
['n' => '全部', 'v' => 'all'], ['n' => '2025', 'v' => '2025'], ['n' => '2024', 'v' => '2024'],
|
||||
['n' => '2023', 'v' => '2023'], ['n' => '2022', 'v' => '2022'], ['n' => '2021', 'v' => '2021'],
|
||||
['n' => '2020', 'v' => '2020']
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'sort', 'name' => '排序', 'value' => [
|
||||
['n' => '综合', 'v' => 'c1'], ['n' => '最新', 'v' => 'c2'], ['n' => '最热', 'v' => 'c4']
|
||||
]
|
||||
]
|
||||
],
|
||||
'1' => [
|
||||
[
|
||||
'key' => 'sort', 'name' => '排序', 'value' => [
|
||||
['n' => '综合', 'v' => 'c1'], ['n' => '最新', 'v' => 'c2'], ['n' => '最热', 'v' => 'c4']
|
||||
]
|
||||
]
|
||||
],
|
||||
'50' => [
|
||||
[
|
||||
'key' => 'sort', 'name' => '排序', 'value' => [
|
||||
['n' => '综合', 'v' => 'c1'], ['n' => '最新', 'v' => 'c2'], ['n' => '最热', 'v' => 'c4']
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => $filters
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$page = max(1, intval($pg));
|
||||
$baseUrl = 'https://pianku.api.mgtv.com/rider/list/pcweb/v3';
|
||||
|
||||
$params = [
|
||||
'platform' => 'pcweb',
|
||||
'channelId' => $tid,
|
||||
'pn' => $page,
|
||||
'pc' => '20',
|
||||
'hudong' => '1',
|
||||
'_support' => '10000000',
|
||||
'kind' => 'a1',
|
||||
'area' => 'a1'
|
||||
];
|
||||
|
||||
if (!empty($extend)) {
|
||||
if (isset($extend['year']) && $extend['year'] !== 'all') {
|
||||
$params['year'] = $extend['year'];
|
||||
}
|
||||
if (isset($extend['sort'])) {
|
||||
$params['sort'] = $extend['sort'];
|
||||
}
|
||||
if (isset($extend['chargeInfo'])) {
|
||||
$params['chargeInfo'] = $extend['chargeInfo'];
|
||||
}
|
||||
}
|
||||
|
||||
$url = $baseUrl . '?' . http_build_query($params);
|
||||
$response = $this->fetch($url, [], $this->getHeaders());
|
||||
$json = json_decode($response, true);
|
||||
|
||||
$videos = [];
|
||||
if (isset($json['data']['hitDocs']) && is_array($json['data']['hitDocs'])) {
|
||||
foreach ($json['data']['hitDocs'] as $item) {
|
||||
$videos[] = [
|
||||
'vod_id' => $item['playPartId'] ?? '',
|
||||
'vod_name' => $item['title'] ?? '',
|
||||
'vod_pic' => $item['img'] ?? '',
|
||||
'vod_remarks' => $item['updateInfo'] ?? ($item['rightCorner']['text'] ?? '')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$totalHit = $json['data']['totalHit'] ?? 0;
|
||||
return $this->pageResult($videos, $page, $totalHit, 20);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$videoId = is_array($ids) ? $ids[0] : $ids;
|
||||
|
||||
// 获取视频基本信息
|
||||
$infoUrl = "https://pcweb.api.mgtv.com/video/info?video_id={$videoId}";
|
||||
$infoResponse = $this->fetch($infoUrl, [], $this->getHeaders());
|
||||
$infoJson = json_decode($infoResponse, true);
|
||||
$infoData = $infoJson['data']['info'] ?? [];
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $videoId,
|
||||
'vod_name' => $infoData['title'] ?? '',
|
||||
'type_name' => $infoData['root_kind'] ?? '',
|
||||
'vod_actor' => '',
|
||||
'vod_year' => $infoData['release_time'] ?? '',
|
||||
'vod_content' => $infoData['desc'] ?? '',
|
||||
'vod_remarks' => $infoData['time'] ?? '',
|
||||
'vod_pic' => $infoData['img'] ?? '',
|
||||
'vod_play_from' => '芒果TV',
|
||||
'vod_play_url' => ''
|
||||
];
|
||||
|
||||
// 分页获取所有剧集
|
||||
$pageSize = 50;
|
||||
$allEpisodes = [];
|
||||
|
||||
// 获取第一页
|
||||
$firstPageUrl = "https://pcweb.api.mgtv.com/episode/list?video_id={$videoId}&page=1&size={$pageSize}";
|
||||
$firstResponse = $this->fetch($firstPageUrl, [], $this->getHeaders());
|
||||
$firstJson = json_decode($firstResponse, true);
|
||||
$firstData = $firstJson['data'] ?? [];
|
||||
|
||||
if (isset($firstData['list']) && is_array($firstData['list'])) {
|
||||
$allEpisodes = array_merge($allEpisodes, $firstData['list']);
|
||||
$totalPages = $firstData['total_page'] ?? 1;
|
||||
|
||||
if ($totalPages > 1) {
|
||||
for ($i = 2; $i <= $totalPages; $i++) {
|
||||
$pageUrl = "https://pcweb.api.mgtv.com/episode/list?video_id={$videoId}&page={$i}&size={$pageSize}";
|
||||
// 简单串行获取,避免并发复杂性
|
||||
$resp = $this->fetch($pageUrl, [], $this->getHeaders());
|
||||
$data = json_decode($resp, true);
|
||||
if (isset($data['data']['list']) && is_array($data['data']['list'])) {
|
||||
$allEpisodes = array_merge($allEpisodes, $data['data']['list']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$playUrls = [];
|
||||
if (!empty($allEpisodes)) {
|
||||
// 过滤
|
||||
$validEpisodes = array_filter($allEpisodes, function($item) {
|
||||
return isset($item['isIntact']) && ($item['isIntact'] === "1" || $item['isIntact'] === 1);
|
||||
});
|
||||
|
||||
// 排序
|
||||
usort($validEpisodes, function($a, $b) {
|
||||
return intval($a['order'] ?? 0) - intval($b['order'] ?? 0);
|
||||
});
|
||||
|
||||
foreach ($validEpisodes as $item) {
|
||||
$name = $item['t4'] ?? ($item['t3'] ?? ($item['title'] ?? ("第" . ($item['order'] ?? '?') . "集")));
|
||||
$playLink = isset($item['url']) ? "https://www.mgtv.com{$item['url']}" : '';
|
||||
|
||||
if ($playLink) {
|
||||
$playUrls[] = "{$name}\${$playLink}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$vod['vod_play_url'] = implode('#', $playUrls);
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$page = max(1, intval($pg));
|
||||
$searchUrl = "https://mobileso.bz.mgtv.com/msite/search/v2?q=" . urlencode($key) . "&pn={$page}&pc=20";
|
||||
|
||||
$response = $this->fetch($searchUrl, [], $this->getHeaders());
|
||||
$json = json_decode($response, true);
|
||||
$data = $json['data'] ?? [];
|
||||
|
||||
$videos = [];
|
||||
if (isset($data['contents']) && is_array($data['contents'])) {
|
||||
foreach ($data['contents'] as $group) {
|
||||
if (($group['type'] ?? '') === 'media' && isset($group['data']) && is_array($group['data'])) {
|
||||
foreach ($group['data'] as $item) {
|
||||
if (($item['source'] ?? '') === 'imgo') {
|
||||
if (preg_match('/\/(\d+)\.html/', $item['url'], $match)) {
|
||||
$videos[] = [
|
||||
'vod_id' => $match[1],
|
||||
'vod_name' => isset($item['title']) ? str_replace(['<B>', '</B>'], '', $item['title']) : '',
|
||||
'vod_pic' => $item['img'] ?? '',
|
||||
'vod_remarks' => isset($item['desc']) ? implode(' ', $item['desc']) : ''
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($videos, $page, count($videos) * 10, 20);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
// 壳子超级解析格式
|
||||
return [
|
||||
'parse' => 1,
|
||||
'jx' => 1,
|
||||
'play_parse' => true,
|
||||
'parse_type' => '壳子超级解析',
|
||||
'parse_source' => '芒果TV2',
|
||||
'url' => $id,
|
||||
'header' => json_encode([
|
||||
'User-Agent' => $this->getHeaders()['User-Agent'],
|
||||
'Referer' => 'https://www.mgtv.com',
|
||||
'Origin' => 'https://www.mgtv.com'
|
||||
], JSON_UNESCAPED_UNICODE)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,2894 @@
|
||||
<?php
|
||||
// PDF漫画阅读器 - 最终精准修复版(下滑防抖+上滑灵敏+菜单跳转不回弹)
|
||||
header("Content-Type: text/html; charset=utf-8");
|
||||
$baseDir = '漫画';
|
||||
if (!is_dir($baseDir)) {
|
||||
mkdir($baseDir);
|
||||
echo "已自动创建 漫画 目录,请放入漫画";
|
||||
exit;
|
||||
}
|
||||
function scanDirectory($path, $pattern = '*') {
|
||||
$result = [];
|
||||
if (!is_dir($path)) return $result;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
$fullPath = $path . '/' . $entry;
|
||||
if ($pattern == '*' || fnmatch($pattern, $entry)) {
|
||||
$result[] = $fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
usort($result, function($a, $b) {
|
||||
return strnatcmp(basename($a), basename($b));
|
||||
});
|
||||
return $result;
|
||||
}
|
||||
function scanImages($path) {
|
||||
$images = [];
|
||||
$extensions = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
if (!is_dir($path)) return $images;
|
||||
$handle = opendir($path);
|
||||
if ($handle) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry != '.' && $entry != '..') {
|
||||
$ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $extensions)) {
|
||||
$images[] = $path . '/' . $entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
natsort($images);
|
||||
return $images;
|
||||
|
||||
// EPUB图片流输出
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'epub_img') {
|
||||
$epubFile = __DIR__ . '/漫画/' . ($_GET['book'] ?? '') . '/' . ($_GET['chapter'] ?? '');
|
||||
$imgPath = $_GET['path'] ?? '';
|
||||
if (file_exists($epubFile) && $imgPath && class_exists('ZipArchive')) {
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($epubFile) === true) {
|
||||
$data = $zip->getFromName($imgPath);
|
||||
$zip->close();
|
||||
if ($data) {
|
||||
$ext = strtolower(pathinfo($imgPath, PATHINFO_EXTENSION));
|
||||
$mime = $ext === 'png' ? 'image/png' : ($ext === 'webp' ? 'image/webp' : 'image/jpeg');
|
||||
header('Content-Type: ' . $mime);
|
||||
header('Cache-Control: max-age=86400');
|
||||
echo $data;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
header('HTTP/1.1 404 Not Found');
|
||||
exit;
|
||||
}
|
||||
|
||||
function parseEpub($epubPath) {
|
||||
if (!class_exists('ZipArchive')) return ['error' => 'ZipArchive不可用'];
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($epubPath) !== true) return ['error' => '无法打开EPUB'];
|
||||
|
||||
$container = $zip->getFromName('META-INF/container.xml');
|
||||
if (!$container) { $zip->close(); return ['error' => '无效EPUB']; }
|
||||
preg_match('/full-path="([^"]+)"/', $container, $m);
|
||||
$opfPath = $m[1] ?? '';
|
||||
if (!$opfPath) { $zip->close(); return ['error' => '找不到OPF']; }
|
||||
|
||||
$opfDir = dirname($opfPath);
|
||||
if ($opfDir == '.') $opfDir = ''; else $opfDir .= '/';
|
||||
$opf = $zip->getFromName($opfPath);
|
||||
if (!$opf) { $zip->close(); return ['error' => '无法读取OPF']; }
|
||||
|
||||
$manifest = [];
|
||||
preg_match_all('/<item[^>]*id="([^"]*)"[^>]*href="([^"]*)"[^>]*>/i', $opf, $items);
|
||||
foreach ($items[1] as $i => $id) {
|
||||
$manifest[$id] = $items[2][$i];
|
||||
}
|
||||
|
||||
$ncxPath = '';
|
||||
foreach ($manifest as $href) {
|
||||
if (stripos($href, '.ncx') !== false) {
|
||||
$ncxPath = $opfDir . $href;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$chapters = [];
|
||||
if ($ncxPath) {
|
||||
$ncx = $zip->getFromName($ncxPath);
|
||||
if ($ncx) {
|
||||
preg_match_all('/<navPoint[^>]*>.*?<text>(.*?)<\/text>.*?<content[^>]*src="([^"]*)".*?<\/navPoint>/si', $ncx, $navs);
|
||||
foreach ($navs[1] as $i => $title) {
|
||||
$htmlFile = $opfDir . $navs[2][$i];
|
||||
$html = $zip->getFromName($htmlFile);
|
||||
$images = [];
|
||||
if ($html) {
|
||||
preg_match_all('/<img[^>]*src=["\']([^"\']+)["\']/i', $html, $imgs);
|
||||
foreach ($imgs[1] as $src) {
|
||||
$imgPath = dirname($htmlFile) . '/' . $src;
|
||||
$imgPath = preg_replace('#/\./#', '/', $imgPath);
|
||||
$imgPath = preg_replace('#[^/]+/\.\./#', '', $imgPath);
|
||||
$images[] = $imgPath;
|
||||
}
|
||||
}
|
||||
$chapters[] = ['title' => trim($title), 'images' => $images];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
return ['chapters' => $chapters];
|
||||
}
|
||||
}
|
||||
|
||||
// EPUB图片流输出
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'epub_img') {
|
||||
$epubFile = $baseDir . '/' . ($_GET['book'] ?? '') . '/' . ($_GET['chapter'] ?? '');
|
||||
$imgPath = $_GET['path'] ?? '';
|
||||
if (file_exists($epubFile) && $imgPath && class_exists('ZipArchive')) {
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($epubFile) === true) {
|
||||
$data = $zip->getFromName($imgPath);
|
||||
$zip->close();
|
||||
if ($data) {
|
||||
$ext = strtolower(pathinfo($imgPath, PATHINFO_EXTENSION));
|
||||
$mime = $ext === 'png' ? 'image/png' : ($ext === 'webp' ? 'image/webp' : 'image/jpeg');
|
||||
header('Content-Type: ' . $mime);
|
||||
header('Cache-Control: max-age=86400');
|
||||
echo $data;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
header('HTTP/1.1 404 Not Found');
|
||||
exit;
|
||||
}
|
||||
|
||||
function parseEpub($epubPath) {
|
||||
if (!class_exists('ZipArchive')) return ['error' => 'ZipArchive不可用'];
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($epubPath) !== true) return ['error' => '无法打开EPUB'];
|
||||
|
||||
$container = $zip->getFromName('META-INF/container.xml');
|
||||
if (!$container) { $zip->close(); return ['error' => '无效EPUB']; }
|
||||
preg_match('/full-path="([^"]+)"/', $container, $m);
|
||||
$opfPath = $m[1] ?? '';
|
||||
if (!$opfPath) { $zip->close(); return ['error' => '找不到OPF']; }
|
||||
|
||||
$opfDir = dirname($opfPath);
|
||||
if ($opfDir == '.') $opfDir = ''; else $opfDir .= '/';
|
||||
$opf = $zip->getFromName($opfPath);
|
||||
if (!$opf) { $zip->close(); return ['error' => '无法读取OPF']; }
|
||||
|
||||
$manifest = [];
|
||||
preg_match_all('/<item[^>]*id="([^"]*)"[^>]*href="([^"]*)"[^>]*>/i', $opf, $items);
|
||||
foreach ($items[1] as $i => $id) {
|
||||
$manifest[$id] = $items[2][$i];
|
||||
}
|
||||
|
||||
$ncxPath = '';
|
||||
foreach ($manifest as $href) {
|
||||
if (stripos($href, '.ncx') !== false) {
|
||||
$ncxPath = $opfDir . $href;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$chapters = [];
|
||||
if ($ncxPath) {
|
||||
$ncx = $zip->getFromName($ncxPath);
|
||||
if ($ncx) {
|
||||
preg_match_all('/<navPoint[^>]*>.*?<text>(.*?)<\/text>.*?<content[^>]*src="([^"]*)".*?<\/navPoint>/si', $ncx, $navs);
|
||||
foreach ($navs[1] as $i => $title) {
|
||||
$htmlFile = $opfDir . $navs[2][$i];
|
||||
$html = $zip->getFromName($htmlFile);
|
||||
$images = [];
|
||||
if ($html) {
|
||||
preg_match_all('/<img[^>]*src=["\']([^"\']+)["\']/i', $html, $imgs);
|
||||
foreach ($imgs[1] as $src) {
|
||||
$imgPath = dirname($htmlFile) . '/' . $src;
|
||||
$imgPath = preg_replace('#/\./#', '/', $imgPath);
|
||||
$imgPath = preg_replace('#[^/]+/\.\./#', '', $imgPath);
|
||||
$images[] = $imgPath;
|
||||
}
|
||||
}
|
||||
$chapters[] = ['title' => trim($title), 'images' => $images];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
return ['chapters' => $chapters];
|
||||
}
|
||||
$chap = isset($_GET['chap']) ? intval($_GET['chap']) : 0;
|
||||
$book = isset($_GET['book']) ? $_GET['book'] : '';
|
||||
$chapter = isset($_GET['chapter']) ? $_GET['chapter'] : '';
|
||||
$chapterTitle = preg_replace('/\.(pdf|epub)/i', '', $chapter);
|
||||
$isChapterPage = ($book && $chapter);
|
||||
$isPdf = $chapter && stripos($chapter, '.pdf') !== false;
|
||||
$isEpub = $chapter && stripos($chapter, '.epub') !== false;
|
||||
$allChapters = [];
|
||||
$currentIdx = -1;
|
||||
if ($book) {
|
||||
$allChapters = scanDirectory($baseDir . '/' . $book);
|
||||
foreach ($allChapters as $i => $f) {
|
||||
if (basename($f) === $chapter) {
|
||||
$currentIdx = $i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($book && $chapter) {
|
||||
$encodedBook = rawurlencode($book);
|
||||
$encodedChapter = rawurlencode($chapter);
|
||||
$fileUrl = "$baseDir/$encodedBook/$encodedChapter";
|
||||
}
|
||||
$epubChapters = [];
|
||||
$images = [];
|
||||
if ($isEpub && $isChapterPage && $book && $chapter) {
|
||||
$epubPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
$epubData = parseEpub($epubPath);
|
||||
if (!isset($epubData['error'])) {
|
||||
$epubChapters = $epubData['chapters'];
|
||||
if (isset($epubChapters[$chap])) {
|
||||
$images = $epubChapters[$chap]['images'];
|
||||
}
|
||||
}
|
||||
} elseif (!$isPdf && $isChapterPage && $book && $chapter) {
|
||||
$localPath = $baseDir . '/' . $book . '/' . $chapter;
|
||||
$images = scanImages($localPath);
|
||||
}
|
||||
if ($isEpub && !empty($epubChapters) && isset($epubChapters[$chap])) {
|
||||
$chapterTitle = $epubChapters[$chap]['title'];
|
||||
}
|
||||
$currentFile = '漫画阅读器.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<?php if ($isChapterPage): ?>
|
||||
<script src="./pdf.min.js"></script>
|
||||
<script>pdfjsLib.GlobalWorkerOptions.workerSrc='./pdf.worker.min.js';</script>
|
||||
<?php endif; ?>
|
||||
<style>
|
||||
* {margin:0;padding:0;box-sizing:border-box}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: <?=$isChapterPage?'#e8ecf1':'#f0f2f5'?>;
|
||||
color: <?=$isChapterPage?'#1a1a2e':'#1a1a2e'?>;
|
||||
padding: <?=$isChapterPage?0:0?>px;
|
||||
}
|
||||
.top-nav {
|
||||
position: absolute; top:0; left:0; right:0; z-index:999;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border:none; margin:0;
|
||||
padding: 16px 12px;
|
||||
display: flex; align-items:center; gap:10px;
|
||||
font-size:18px; font-weight:800;
|
||||
}
|
||||
.top-nav a { text-decoration:none; font-weight:500; }
|
||||
.top-nav .split { }
|
||||
.top-nav .current { font-weight:700; }
|
||||
.mask {
|
||||
position:fixed; inset:0; background:rgba(0,0,0,0.6);
|
||||
z-index:9998; display:none;
|
||||
}
|
||||
.mask.show {display:block}
|
||||
.reader-menu {
|
||||
position:fixed; left:50%; top:50%; transform:translate(-50%,-50%);
|
||||
backdrop-filter: blur(20px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(200%);
|
||||
border:1px solid rgba(255,255,255,0.5);
|
||||
border-radius:24px;
|
||||
padding:24px;
|
||||
z-index:9999;
|
||||
display:none;
|
||||
min-width:280px; max-width:90vw;
|
||||
max-height:85vh; overflow-y:auto;
|
||||
}
|
||||
.reader-menu.show {display:block}
|
||||
.menu-title {
|
||||
text-align:center; margin-bottom:20px;
|
||||
font-size:18px; font-weight:800;
|
||||
letter-spacing:2px;
|
||||
text-shadow:0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
.section-box {
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
border-radius:18px;
|
||||
padding:16px;
|
||||
backdrop-filter:blur(8px);
|
||||
}
|
||||
.section-title {
|
||||
font-size:13px; font-weight:800;
|
||||
margin-bottom:14px; padding-left:4px;
|
||||
letter-spacing:1px; text-transform:uppercase;
|
||||
text-shadow:0 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
.auto-scroll-btn {
|
||||
width:100%; padding:16px;
|
||||
border:none; border-radius:18px;
|
||||
backdrop-filter:blur(12px);
|
||||
font-size:15px; font-weight:700;
|
||||
border:1px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
.speed-btn {
|
||||
padding:12px; border:none; border-radius:14px;
|
||||
backdrop-filter:blur(8px);
|
||||
font-size:13px; font-weight:600;
|
||||
border:1px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
.speed-btn.active {
|
||||
border-color:rgba(255,255,255,0.6);
|
||||
}
|
||||
.bookmark-btn {
|
||||
width:100%; padding:16px;
|
||||
border:none; border-radius:18px;
|
||||
backdrop-filter:blur(12px);
|
||||
font-size:15px; font-weight:700;
|
||||
margin-top:8px;
|
||||
display:flex; align-items:center; justify-content:center; gap:7px;
|
||||
border:1px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
.bookmark-btn.second {
|
||||
}
|
||||
.bookmark-panel {
|
||||
position:fixed; left:50%; top:50%; transform:translate(-50%,-50%);
|
||||
backdrop-filter:blur(20px) saturate(200%);
|
||||
-webkit-backdrop-filter:blur(20px) saturate(200%);
|
||||
border:1px solid rgba(255,255,255,0.5);
|
||||
border-radius:22px;
|
||||
padding:22px;
|
||||
z-index:10000;
|
||||
display:none;
|
||||
min-width:280px; max-width:90vw;
|
||||
max-height:70vh; overflow-y:auto;
|
||||
}
|
||||
.bookmark-panel.show {display:block}
|
||||
.bm-header {
|
||||
font-size:16px; font-weight:700;
|
||||
margin-bottom:12px;
|
||||
display:flex; justify-content:space-between;
|
||||
}
|
||||
.bm-close {
|
||||
color:#ff4d4d; cursor:pointer; font-size:18px;
|
||||
}
|
||||
.bm-item {
|
||||
border:1px solid rgba(255,255,255,0.6);
|
||||
border-radius:14px;
|
||||
padding:12px 14px;
|
||||
margin-bottom:8px;
|
||||
cursor:pointer;
|
||||
backdrop-filter:blur(8px);
|
||||
}
|
||||
.bm-item .book {
|
||||
font-size:12px;
|
||||
font-weight:600;
|
||||
}
|
||||
.bm-item .chap {
|
||||
font-size:13px;
|
||||
display:inline;
|
||||
}
|
||||
.bm-item .page {
|
||||
font-size:11px;
|
||||
display:inline;
|
||||
margin-left:8px;
|
||||
}
|
||||
.bm-del {
|
||||
color:#ff4d4d;
|
||||
font-size:15px;
|
||||
float:right;
|
||||
padding:4px;
|
||||
}
|
||||
.speed-row {
|
||||
display:grid; grid-template-columns:repeat(3,1fr); gap:8px; margin-top:10px;
|
||||
}
|
||||
.chapter-grid {
|
||||
display:grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap:8px;
|
||||
max-height:320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.chapter-item-btn {
|
||||
padding:4px 6px;
|
||||
border:none; border-radius:12px;
|
||||
backdrop-filter:blur(8px);
|
||||
text-align:center;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
cursor:pointer;
|
||||
height:44px;
|
||||
overflow:hidden;
|
||||
white-space:nowrap;
|
||||
font-size:12px; font-weight:600;
|
||||
border:1px solid rgba(255,255,255,0.4);
|
||||
}
|
||||
.chapter-item-btn.active {
|
||||
font-weight:700;
|
||||
border-color:rgba(255,255,255,0.6);
|
||||
}
|
||||
#reader {width:100%}
|
||||
#reader img {display:block;width:100%;height:auto}
|
||||
canvas {display:block;width:100%;height:auto;margin:0 auto}
|
||||
.book-chapter-grid {
|
||||
display:grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap:10px;
|
||||
padding:10px 12px;
|
||||
}
|
||||
.book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(168,140,255,0.35) 50%, rgba(140,180,255,0.2) 100%);
|
||||
backdrop-filter:blur(16px);
|
||||
-webkit-backdrop-filter:blur(16px);
|
||||
border-radius:18px;
|
||||
border:1px solid rgba(255,255,255,0.7);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(60,160,140,0.18),
|
||||
0 2px 6px rgba(60,160,140,0.1),
|
||||
inset 0 3px 6px rgba(255,255,255,0.55),
|
||||
inset 0 -3px 6px rgba(0,0,0,0.06);
|
||||
text-align:center;
|
||||
overflow:hidden;
|
||||
height:52px;
|
||||
margin-bottom:8px;
|
||||
position:relative;
|
||||
}
|
||||
.book-chapter-item::after {
|
||||
content:''; position:absolute;
|
||||
top:0; left:20%; right:20%;
|
||||
height:2px;
|
||||
border-radius:50%;
|
||||
background:rgba(255,255,255,0.7);
|
||||
box-shadow:0 0 10px rgba(255,255,255,0.5);
|
||||
}
|
||||
.book-chapter-item a {
|
||||
text-decoration:none;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
width:100%; height:100%;
|
||||
overflow:hidden; padding:4px 6px;
|
||||
white-space:nowrap; font-weight:600;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.book-chapter-item a.level2 {
|
||||
justify-content:flex-start;
|
||||
padding-left:14px;
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.toast {
|
||||
position: fixed;
|
||||
left: 50%; bottom: 80px;
|
||||
transform: translateX(-50%);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 12px 24px;
|
||||
border-radius: 30px;
|
||||
font-size:13px; font-weight:500;
|
||||
z-index:10001;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.toast.show {opacity:1}
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 10002;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid rgba(255,255,255,0.2);
|
||||
border-top-color: #a090ff;
|
||||
box-shadow: 0 0 20px rgba(160,144,255,0.3);
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.progress-bar-wrap {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
padding: 24px 18px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.progress-track {
|
||||
flex: 1;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.progress-text {
|
||||
white-space: nowrap;
|
||||
min-width: 52px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight:500;
|
||||
}
|
||||
h1,h3 {
|
||||
padding:16px 12px;
|
||||
text-align:center;
|
||||
font-weight:800;
|
||||
font-size:18px;
|
||||
letter-spacing:2px;
|
||||
backdrop-filter:blur(16px);
|
||||
-webkit-backdrop-filter:blur(16px);
|
||||
border-radius:0;
|
||||
margin:0 0 16px 0;
|
||||
border:none;
|
||||
box-shadow:
|
||||
inset 0 3px 6px rgba(255,255,255,0.5),
|
||||
inset 0 -3px 6px rgba(0,0,0,0.06);
|
||||
text-shadow:0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
h3 a { text-decoration:none; }
|
||||
@keyframes shimmer {
|
||||
0% { background-position:0% 50%; }
|
||||
50% { background-position:100% 50%; }
|
||||
100% { background-position:0% 50%; }
|
||||
}
|
||||
.grid {
|
||||
display:grid; grid-template-columns:1fr; gap:10px; padding:8px 12px;
|
||||
}
|
||||
.item {
|
||||
backdrop-filter:blur(16px);
|
||||
-webkit-backdrop-filter:blur(16px);
|
||||
border-radius:20px;
|
||||
border:1px solid rgba(255,255,255,0.7);
|
||||
box-shadow:
|
||||
inset 0 3px 6px rgba(255,255,255,0.6),
|
||||
inset 0 -3px 6px rgba(0,0,0,0.08);
|
||||
overflow:hidden;
|
||||
overflow:hidden;
|
||||
height:136px;
|
||||
margin-bottom:8px;
|
||||
position:relative;
|
||||
}
|
||||
.item::after {
|
||||
content:''; position:absolute;
|
||||
top:0; left:20%; right:20%;
|
||||
height:2px;
|
||||
border-radius:50%;
|
||||
background:rgba(255,255,255,0.8);
|
||||
box-shadow:0 0 12px rgba(255,255,255,0.5);
|
||||
}
|
||||
.item a {
|
||||
text-decoration:none; font-weight:600;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
width:100%; height:100%; padding:4px 6px;
|
||||
overflow:hidden; white-space:nowrap;
|
||||
}
|
||||
.debug {display:none}
|
||||
/* 底部导航栏 */
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
height: 64px;
|
||||
z-index: 998;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(168,140,255,0.45) 50%, rgba(140,180,255,0.25) 100%);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-top: 1px solid rgba(255,255,255,0.5);
|
||||
box-shadow: 0 -4px 20px rgba(100,80,200,0.15), inset 0 3px 6px rgba(255,255,255,0.5);
|
||||
}
|
||||
.bottom-nav a, .bottom-nav button {
|
||||
width: 48px; height: 48px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,0.4);
|
||||
background: rgba(255,255,255,0.3);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 22px;
|
||||
color: #2d1f5e;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 2px 3px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.bottom-nav a:active, .bottom-nav button:active {background: rgba(255,255,255,0.5);
|
||||
transform: scale(0.94);
|
||||
}
|
||||
/* ===== 主题切换 CSS ===== */
|
||||
/* 橘黄暖阳 */
|
||||
body.theme-orange { background:#f5f0e8 !important; color:#2d1f0e !important; }
|
||||
body.theme-orange .top-nav, body.theme-orange h1, body.theme-orange h3,
|
||||
body.theme-orange .item, body.theme-orange .bottom-nav, body.theme-orange .reader-menu,
|
||||
body.theme-orange .bookmark-panel, body.theme-orange .section-box,
|
||||
body.theme-orange .auto-scroll-btn, body.theme-orange .speed-btn, body.theme-orange .speed-btn.active,
|
||||
body.theme-orange .bookmark-btn, body.theme-orange .bookmark-btn.second,
|
||||
body.theme-orange .chapter-item-btn, body.theme-orange .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.35) 50%, rgba(255,140,30,0.2) 100%) !important;}
|
||||
body.theme-orange .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,180,100,0.3) 50%, rgba(255,160,60,0.15) 100%) !important;}
|
||||
body.theme-orange .progress-fill { background:linear-gradient(90deg, #ff8c42, #ffaa5a) !important; box-shadow:0 0 6px rgba(255,140,66,0.4) !important; }
|
||||
body.theme-orange .loading-spinner { border-color:rgba(255,140,66,0.2) !important; border-top-color:#ff8c42 !important; }
|
||||
body.theme-orange .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.35) 50%, rgba(255,140,30,0.2) 100%) !important; border-color:rgba(255,160,60,0.3) !important; color:#3d1f0e !important; }
|
||||
/* 橘黄暖阳 - 阅读页面UI */
|
||||
body.theme-orange .top-nav, body.theme-orange .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-orange .top-nav .current { color:#b85c00 !important; font-weight:700; }
|
||||
body.theme-orange .top-nav .split { color:rgba(60,30,10,0.4) !important; }
|
||||
body.theme-orange #reader, body.theme-orange #reader * { color:#3d1f0e !important; }
|
||||
body.theme-orange .reader-menu { background:rgba(255,240,220,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orange .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.35) 50%, rgba(255,140,30,0.2) 100%) !important; color:#3d1f0e !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orange .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,180,100,0.3) 100%) !important; color:#3d1f0e !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-orange .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.4) 50%, rgba(255,140,30,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(255,100,30,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orange .bookmark-btn, body.theme-orange .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.35) 50%, rgba(255,140,30,0.2) 100%) !important; color:#3d1f0e !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orange .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,180,100,0.3) 100%) !important; color:#3d1f0e !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-orange .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.4) 50%, rgba(255,140,30,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(255,100,30,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orange .bookmark-panel { background:rgba(255,240,220,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orange .bm-header { color:#3d1f0e !important; }
|
||||
body.theme-orange .bm-item { background:rgba(255,220,180,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#3d1f0e !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-orange .bm-item .book, body.theme-orange .bm-item .chap, body.theme-orange .bm-item .page { color:#5a3a1a !important; }
|
||||
body.theme-orange .canvas-container { background:rgba(255,180,100,0.12) !important; }
|
||||
body.theme-orange .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(245,230,210,0.95) 40%) !important; }
|
||||
body.theme-orange .progress-track { background:rgba(200,140,80,0.15) !important; }
|
||||
body.theme-orange .progress-text { color:rgba(60,30,10,0.7) !important; }
|
||||
body.theme-orange .chapter-indicator { color:#3d1f0e !important; }
|
||||
body.theme-orange .ebook-chapter { background:rgba(255,248,240,0.92) !important; color:#3d1f0e !important; }
|
||||
body.theme-orange .ebook-chapter .chapter-title { color:#b85c00 !important; }
|
||||
body.theme-orange .menu-title, body.theme-orange .section-title { color:#3d1f0e !important; }
|
||||
body.theme-orange .back-btn { color:#3d1f0e !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-orange .page-title { color:#3d1f0e !important; }
|
||||
body.theme-orange .shelf-item { color:#3d1f0e !important; }
|
||||
body.theme-orange h1, body.theme-orange h3, body.theme-orange h3 a { color:#3d1f0e !important; }
|
||||
body.theme-orange .item a { color:#3d1f0e !important; }
|
||||
body.theme-orange .book-chapter-item a { color:#3d1f0e !important; }
|
||||
|
||||
/* 深海蓝 */
|
||||
body.theme-blue { background:#e8f0f8 !important; color:#0d1f35 !important; }
|
||||
body.theme-blue .top-nav, body.theme-blue h1, body.theme-blue h3,
|
||||
body.theme-blue .item, body.theme-blue .bottom-nav, body.theme-blue .reader-menu,
|
||||
body.theme-blue .bookmark-panel, body.theme-blue .section-box,
|
||||
body.theme-blue .auto-scroll-btn, body.theme-blue .speed-btn, body.theme-blue .speed-btn.active,
|
||||
body.theme-blue .bookmark-btn, body.theme-blue .bookmark-btn.second,
|
||||
body.theme-blue .chapter-item-btn, body.theme-blue .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(74,144,217,0.35) 50%, rgba(100,180,255,0.2) 100%) !important;}
|
||||
body.theme-blue .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(100,180,220,0.3) 50%, rgba(74,144,217,0.15) 100%) !important;}
|
||||
body.theme-blue .progress-fill { background:linear-gradient(90deg, #4a90d9, #7ac0ff) !important; box-shadow:0 0 6px rgba(74,144,217,0.4) !important; }
|
||||
body.theme-blue .loading-spinner { border-color:rgba(74,144,217,0.2) !important; border-top-color:#4a90d9 !important; }
|
||||
body.theme-blue .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(74,144,217,0.35) 50%, rgba(100,180,255,0.2) 100%) !important; border-color:rgba(74,144,217,0.3) !important; color:#0d1f35 !important; }
|
||||
/* 深海蓝 - 阅读页面UI */
|
||||
body.theme-blue .top-nav, body.theme-blue .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-blue .top-nav .current { color:#1a56b0 !important; font-weight:700; }
|
||||
body.theme-blue .top-nav .split { color:rgba(15,30,50,0.4) !important; }
|
||||
body.theme-blue #reader, body.theme-blue #reader * { color:#0d1f35 !important; }
|
||||
body.theme-blue .reader-menu { background:rgba(235,245,255,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blue .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(74,144,217,0.35) 50%, rgba(100,180,255,0.2) 100%) !important; color:#0d1f35 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blue .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(100,160,217,0.3) 100%) !important; color:#0d1f35 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-blue .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(74,144,217,0.4) 50%, rgba(100,180,255,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blue .bookmark-btn, body.theme-blue .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(74,144,217,0.35) 50%, rgba(100,180,255,0.2) 100%) !important; color:#0d1f35 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blue .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(100,160,217,0.3) 100%) !important; color:#0d1f35 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-blue .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(74,144,217,0.4) 50%, rgba(100,180,255,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blue .bookmark-panel { background:rgba(235,245,255,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blue .bm-header { color:#0d1f35 !important; }
|
||||
body.theme-blue .bm-item { background:rgba(180,210,240,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#0d1f35 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-blue .canvas-container { background:rgba(74,144,217,0.08) !important; }
|
||||
body.theme-blue .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(230,240,250,0.95) 40%) !important; }
|
||||
body.theme-blue .progress-track { background:rgba(74,144,217,0.12) !important; }
|
||||
body.theme-blue .progress-text { color:rgba(15,30,50,0.7) !important; }
|
||||
body.theme-blue .chapter-indicator { color:#0d1f35 !important; }
|
||||
body.theme-blue .ebook-chapter { background:rgba(240,248,255,0.92) !important; color:#0d1f35 !important; }
|
||||
body.theme-blue .ebook-chapter .chapter-title { color:#1a56b0 !important; }
|
||||
body.theme-blue .menu-title, body.theme-blue .section-title { color:#0d1f35 !important; }
|
||||
body.theme-blue .back-btn { color:#0d1f35 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-blue .page-title { color:#0d1f35 !important; }
|
||||
body.theme-blue .shelf-item { color:#0d1f35 !important; }
|
||||
body.theme-blue h1, body.theme-blue h3, body.theme-blue h3 a { color:#0d1f35 !important; }
|
||||
body.theme-blue .item a { color:#0d1f35 !important; }
|
||||
body.theme-blue .book-chapter-item a { color:#0d1f35 !important; }
|
||||
|
||||
/* 樱花粉 */
|
||||
body.theme-pink { background:#fdf2f5 !important; color:#3d1520 !important; }
|
||||
body.theme-pink .top-nav, body.theme-pink h1, body.theme-pink h3,
|
||||
body.theme-pink .item, body.theme-pink .bottom-nav, body.theme-pink .reader-menu,
|
||||
body.theme-pink .bookmark-panel, body.theme-pink .section-box,
|
||||
body.theme-pink .auto-scroll-btn, body.theme-pink .speed-btn, body.theme-pink .speed-btn.active,
|
||||
body.theme-pink .bookmark-btn, body.theme-pink .bookmark-btn.second,
|
||||
body.theme-pink .chapter-item-btn, body.theme-pink .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,158,181,0.35) 50%, rgba(255,180,200,0.2) 100%) !important;}
|
||||
body.theme-pink .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,180,200,0.3) 50%, rgba(255,158,181,0.15) 100%) !important;}
|
||||
body.theme-pink .progress-fill { background:linear-gradient(90deg, #ff9eb5, #ffc8d6) !important; box-shadow:0 0 6px rgba(255,158,181,0.4) !important; }
|
||||
body.theme-pink .loading-spinner { border-color:rgba(255,158,181,0.2) !important; border-top-color:#ff9eb5 !important; }
|
||||
body.theme-pink .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,158,181,0.35) 50%, rgba(255,180,200,0.2) 100%) !important; border-color:rgba(255,158,181,0.3) !important; color:#3d1520 !important; }
|
||||
/* 樱花粉 - 阅读页面UI */
|
||||
body.theme-pink .top-nav, body.theme-pink .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-pink .top-nav .current { color:#c03060 !important; font-weight:700; }
|
||||
body.theme-pink .top-nav .split { color:rgba(60,20,30,0.4) !important; }
|
||||
body.theme-pink #reader, body.theme-pink #reader * { color:#3d1520 !important; }
|
||||
body.theme-pink .reader-menu { background:rgba(255,240,245,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-pink .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,158,181,0.35) 50%, rgba(255,180,200,0.2) 100%) !important; color:#3d1520 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-pink .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,180,200,0.3) 100%) !important; color:#3d1520 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-pink .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,158,181,0.4) 50%, rgba(255,180,200,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-pink .bookmark-btn, body.theme-pink .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,158,181,0.35) 50%, rgba(255,180,200,0.2) 100%) !important; color:#3d1520 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-pink .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,180,200,0.3) 100%) !important; color:#3d1520 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-pink .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,158,181,0.4) 50%, rgba(255,180,200,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-pink .bookmark-panel { background:rgba(255,240,245,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-pink .bm-header { color:#3d1520 !important; }
|
||||
body.theme-pink .bm-item { background:rgba(255,210,225,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#3d1520 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-pink .canvas-container { background:rgba(255,158,181,0.1) !important; }
|
||||
body.theme-pink .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(250,235,240,0.95) 40%) !important; }
|
||||
body.theme-pink .progress-track { background:rgba(255,158,181,0.15) !important; }
|
||||
body.theme-pink .progress-text { color:rgba(60,20,30,0.7) !important; }
|
||||
body.theme-pink .chapter-indicator { color:#3d1520 !important; }
|
||||
body.theme-pink .ebook-chapter { background:rgba(255,245,248,0.92) !important; color:#3d1520 !important; }
|
||||
body.theme-pink .ebook-chapter .chapter-title { color:#c03060 !important; }
|
||||
body.theme-pink .menu-title, body.theme-pink .section-title { color:#3d1520 !important; }
|
||||
body.theme-pink .back-btn { color:#3d1520 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-pink .page-title { color:#3d1520 !important; }
|
||||
body.theme-pink .shelf-item { color:#3d1520 !important; }
|
||||
body.theme-pink h1, body.theme-pink h3, body.theme-pink h3 a { color:#3d1520 !important; }
|
||||
body.theme-pink .item a { color:#3d1520 !important; }
|
||||
body.theme-pink .book-chapter-item a { color:#3d1520 !important; }
|
||||
|
||||
/* 森林绿 */
|
||||
body.theme-green { background:#eef5f0 !important; color:#0d2e1a !important; }
|
||||
body.theme-green .top-nav, body.theme-green h1, body.theme-green h3,
|
||||
body.theme-green .item, body.theme-green .bottom-nav, body.theme-green .reader-menu,
|
||||
body.theme-green .bookmark-panel, body.theme-green .section-box,
|
||||
body.theme-green .auto-scroll-btn, body.theme-green .speed-btn, body.theme-green .speed-btn.active,
|
||||
body.theme-green .bookmark-btn, body.theme-green .bookmark-btn.second,
|
||||
body.theme-green .chapter-item-btn, body.theme-green .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(90,171,138,0.35) 50%, rgba(120,200,160,0.2) 100%) !important;}
|
||||
body.theme-green .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(120,200,160,0.3) 50%, rgba(90,171,138,0.15) 100%) !important;}
|
||||
body.theme-green .progress-fill { background:linear-gradient(90deg, #5aab8a, #8cd4a8) !important; box-shadow:0 0 6px rgba(90,171,138,0.4) !important; }
|
||||
body.theme-green .loading-spinner { border-color:rgba(90,171,138,0.2) !important; border-top-color:#5aab8a !important; }
|
||||
body.theme-green .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(90,171,138,0.35) 50%, rgba(120,200,160,0.2) 100%) !important; border-color:rgba(90,171,138,0.3) !important; color:#0d2e1a !important; }
|
||||
/* 森林绿 - 阅读页面UI */
|
||||
body.theme-green .top-nav, body.theme-green .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-green .top-nav .current { color:#1a6b3c !important; font-weight:700; }
|
||||
body.theme-green .top-nav .split { color:rgba(15,45,25,0.4) !important; }
|
||||
body.theme-green #reader, body.theme-green #reader * { color:#0d2e1a !important; }
|
||||
body.theme-green .reader-menu { background:rgba(235,250,240,0.95) !important; }
|
||||
body.theme-green .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(90,171,138,0.35) 50%, rgba(120,200,160,0.2) 100%) !important; color:#0d2e1a !important; }
|
||||
body.theme-green .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(120,190,150,0.3) 100%) !important; color:#0d2e1a !important; }
|
||||
body.theme-green .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(90,171,138,0.4) 50%, rgba(120,200,160,0.25) 100%) !important; color:#fff !important; }
|
||||
body.theme-green .bookmark-btn, body.theme-green .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(90,171,138,0.35) 50%, rgba(120,200,160,0.2) 100%) !important; color:#0d2e1a !important; }
|
||||
body.theme-green .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(120,190,150,0.3) 100%) !important; color:#0d2e1a !important; }
|
||||
body.theme-green .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(90,171,138,0.4) 50%, rgba(120,200,160,0.25) 100%) !important; color:#fff !important; }
|
||||
body.theme-green .bookmark-panel { background:rgba(235,250,240,0.95) !important; }
|
||||
body.theme-green .bm-header { color:#0d2e1a !important; }
|
||||
body.theme-green .bm-item { background:rgba(180,220,200,0.6) !important; color:#0d2e1a !important; }
|
||||
body.theme-green .bm-item .book, body.theme-green .bm-item .chap, body.theme-green .bm-item .page { color:#1a4030 !important; }
|
||||
body.theme-green .canvas-container { background:rgba(90,171,138,0.08) !important; }
|
||||
body.theme-green .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(230,245,235,0.95) 40%) !important; }
|
||||
body.theme-green .progress-track { background:rgba(90,171,138,0.12) !important; }
|
||||
body.theme-green .progress-text { color:rgba(15,45,25,0.7) !important; }
|
||||
body.theme-green .chapter-indicator { color:#0d2e1a !important; }
|
||||
body.theme-green .ebook-chapter { background:rgba(240,255,245,0.92) !important; color:#0d2e1a !important; }
|
||||
body.theme-green .ebook-chapter .chapter-title { color:#1a6b3c !important; }
|
||||
body.theme-green .menu-title, body.theme-green .section-title { color:#0d2e1a !important; }
|
||||
body.theme-green .back-btn { color:#0d2e1a !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-green .page-title { color:#0d2e1a !important; }
|
||||
body.theme-green .shelf-item { color:#0d2e1a !important; }
|
||||
body.theme-green h1, body.theme-green h3, body.theme-green h3 a { color:#0d2e1a !important; }
|
||||
body.theme-green .item a { color:#0d2e1a !important; }
|
||||
body.theme-green .book-chapter-item a { color:#0d2e1a !important; }
|
||||
|
||||
/* 暗夜黑 */
|
||||
body.theme-dark { background:#1a1a2e !important; color:#ddd !important; }
|
||||
body.theme-dark .top-nav, body.theme-dark h1, body.theme-dark h3,
|
||||
body.theme-dark .item, body.theme-dark .bottom-nav, body.theme-dark .reader-menu,
|
||||
body.theme-dark .bookmark-panel, body.theme-dark .section-box,
|
||||
body.theme-dark .auto-scroll-btn, body.theme-dark .speed-btn, body.theme-dark .speed-btn.active,
|
||||
body.theme-dark .bookmark-btn, body.theme-dark .bookmark-btn.second,
|
||||
body.theme-dark .chapter-item-btn, body.theme-dark .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(200,200,210,0.35) 50%, rgba(180,180,190,0.2) 100%) !important;}
|
||||
body.theme-dark .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(200,200,210,0.3) 50%, rgba(180,180,190,0.15) 100%) !important;}
|
||||
body.theme-dark .progress-fill { background:linear-gradient(90deg, #888, #bbb) !important; box-shadow:0 0 6px rgba(180,180,190,0.4) !important; }
|
||||
body.theme-dark .loading-spinner { border-color:rgba(180,180,190,0.2) !important; border-top-color:#888 !important; }
|
||||
body.theme-dark .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(200,200,210,0.35) 50%, rgba(180,180,190,0.2) 100%) !important; border-color:rgba(180,180,190,0.3) !important; color:#ddd !important; }
|
||||
/* 暗夜黑 - 阅读页面UI */
|
||||
body.theme-dark .top-nav, body.theme-dark .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-dark .top-nav .current { color:#1a1a1a !important; font-weight:700; }
|
||||
body.theme-dark .top-nav .split { color:rgba(0,0,0,0.3) !important; }
|
||||
body.theme-dark #reader, body.theme-dark #reader * { color:#1a1a1a !important; }
|
||||
body.theme-dark .reader-menu { background:rgba(240,240,240,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-dark .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(80,80,80,0.35) 50%, rgba(60,60,60,0.2) 100%) !important; color:#1a1a1a !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-dark .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(80,80,80,0.3) 100%) !important; color:#1a1a1a !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-dark .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(80,80,80,0.4) 50%, rgba(60,60,60,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(80,80,80,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-dark .bookmark-btn, body.theme-dark .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(80,80,80,0.35) 50%, rgba(60,60,60,0.2) 100%) !important; color:#1a1a1a !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-dark .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(80,80,80,0.3) 100%) !important; color:#1a1a1a !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-dark .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(80,80,80,0.4) 50%, rgba(60,60,60,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(80,80,80,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-dark .bookmark-panel { background:rgba(240,240,240,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-dark .bm-header { color:#1a1a1a !important; }
|
||||
body.theme-dark .bm-item { background:rgba(200,200,200,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#1a1a1a !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-dark .bm-item .book, body.theme-dark .bm-item .chap, body.theme-dark .bm-item .page { color:#1a1a1a !important; }
|
||||
body.theme-dark .canvas-container { background:rgba(80,80,80,0.12) !important; }
|
||||
body.theme-dark .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(230,230,230,0.95) 40%) !important; }
|
||||
body.theme-dark .progress-track { background:rgba(0,0,0,0.06) !important; }
|
||||
body.theme-dark .progress-text { color:rgba(0,0,0,0.5) !important; }
|
||||
body.theme-dark .chapter-indicator { color:#1a1a1a !important; }
|
||||
body.theme-dark .ebook-chapter { background:rgba(240,240,240,0.9) !important; color:#1a1a1a !important; }
|
||||
body.theme-dark .ebook-chapter .chapter-title { color:#1a1a1a !important; }
|
||||
body.theme-dark .menu-title, body.theme-dark .section-title { color:#1a1a1a !important; }
|
||||
body.theme-dark .back-btn { color:#1a1a1a !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-dark .page-title { color:#1a1a1a !important; }
|
||||
body.theme-dark .shelf-item { color:#1a1a1a !important; }
|
||||
body.theme-dark h1, body.theme-dark h3, body.theme-dark h3 a { color:#1a1a1a !important; }
|
||||
body.theme-dark .item a { color:#1a1a1a !important; }
|
||||
body.theme-dark .book-chapter-item a { color:#1a1a1a !important; }
|
||||
|
||||
/* 经典紫 */
|
||||
body.theme-purple { background:#f5f0e6 !important; color:#1a1a1a !important; }
|
||||
body.theme-purple .top-nav, body.theme-purple h1, body.theme-purple h3,
|
||||
body.theme-purple .item, body.theme-purple .bottom-nav, body.theme-purple .reader-menu,
|
||||
body.theme-purple .bookmark-panel, body.theme-purple .section-box,
|
||||
body.theme-purple .auto-scroll-btn, body.theme-purple .speed-btn, body.theme-purple .speed-btn.active,
|
||||
body.theme-purple .bookmark-btn, body.theme-purple .bookmark-btn.second,
|
||||
body.theme-purple .chapter-item-btn, body.theme-purple .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(160,140,220,0.35) 50%, rgba(180,160,240,0.2) 100%) !important;}
|
||||
body.theme-purple .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(180,160,220,0.3) 50%, rgba(160,140,200,0.15) 100%) !important;}
|
||||
body.theme-purple .progress-fill { background:linear-gradient(90deg, #7a5aff, #c0a0ff) !important; box-shadow:0 0 6px rgba(160,140,220,0.4) !important; }
|
||||
body.theme-purple .loading-spinner { border-color:rgba(160,140,220,0.2) !important; border-top-color:#7a5aff !important; }
|
||||
body.theme-purple .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(160,140,220,0.35) 50%, rgba(180,160,240,0.2) 100%) !important; border-color:rgba(160,140,220,0.3) !important; color:#1a1a1a !important; }
|
||||
/* 经典紫 - 阅读页面UI */
|
||||
body.theme-purple .top-nav, body.theme-purple .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-purple .top-nav .current { color:#clalca !important; font-weight:700; }
|
||||
body.theme-purple .top-nav .split { color:rgba(0,0,0,0.3) !important; }
|
||||
body.theme-purple #reader, body.theme-purple #reader * { color:#1a1a1a !important; }
|
||||
body.theme-purple .reader-menu { background:rgba(255,255,255,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purple .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(160,140,220,0.35) 50%, rgba(180,160,240,0.2) 100%) !important; color:#1a1a1a !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purple .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(180,160,220,0.3) 100%) !important; color:#1a1a1a !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-purple .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(160,140,220,0.4) 50%, rgba(180,160,240,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(160,140,220,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purple .bookmark-btn, body.theme-purple .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(160,140,220,0.35) 50%, rgba(180,160,240,0.2) 100%) !important; color:#1a1a1a !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purple .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(180,160,220,0.3) 100%) !important; color:#1a1a1a !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-purple .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(160,140,220,0.4) 50%, rgba(180,160,240,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(160,140,220,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purple .bookmark-panel { background:rgba(255,255,255,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purple .bm-header { color:#1a1a1a !important; }
|
||||
body.theme-purple .bm-item { background:rgba(200,190,240,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#1a1a1a !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-purple .bm-item .book, body.theme-purple .bm-item .chap, body.theme-purple .bm-item .page { color:#1a1a1a !important; }
|
||||
body.theme-purple .canvas-container { background:rgba(180,160,240,0.12) !important; }
|
||||
body.theme-purple .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(245,240,235,0.95) 40%) !important; }
|
||||
body.theme-purple .progress-track { background:rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purple .progress-text { color:rgba(0,0,0,0.5) !important; }
|
||||
body.theme-purple .chapter-indicator { color:#1a1a1a !important; }
|
||||
body.theme-purple .ebook-chapter { background:rgba(255,255,255,0.9) !important; color:#1a1a1a !important; }
|
||||
body.theme-purple .ebook-chapter .chapter-title { color:#1a1a1a !important; }
|
||||
body.theme-purple .menu-title, body.theme-purple .section-title { color:#1a1a1a !important; }
|
||||
body.theme-purple .back-btn { color:#1a1a1a !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-purple .page-title { color:#1a1a1a !important; }
|
||||
body.theme-purple .shelf-item { color:#1a1a1a !important; }
|
||||
body.theme-purple h1, body.theme-purple h3, body.theme-purple h3 a { color:#1a1a1a !important; }
|
||||
body.theme-purple .item a { color:#1a1a1a !important; }
|
||||
body.theme-purple .book-chapter-item a { color:#1a1a1a !important; }
|
||||
|
||||
/* 猩红 */
|
||||
body.theme-crimson { background:#1a0005 !important; color:#ffc0c0 !important; }
|
||||
body.theme-crimson .top-nav, body.theme-crimson h1, body.theme-crimson h3,
|
||||
body.theme-crimson .item, body.theme-crimson .bottom-nav, body.theme-crimson .reader-menu,
|
||||
body.theme-crimson .bookmark-panel, body.theme-crimson .section-box,
|
||||
body.theme-crimson .auto-scroll-btn, body.theme-crimson .speed-btn, body.theme-crimson .speed-btn.active,
|
||||
body.theme-crimson .bookmark-btn, body.theme-crimson .bookmark-btn.second,
|
||||
body.theme-crimson .chapter-item-btn, body.theme-crimson .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,40,60,0.35) 50%, rgba(240,50,70,0.2) 100%) !important;}
|
||||
body.theme-crimson .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(240,60,80,0.3) 50%, rgba(220,40,60,0.15) 100%) !important;}
|
||||
body.theme-crimson .progress-fill { background:linear-gradient(90deg, #e01020, #ff4060) !important; box-shadow:0 0 6px rgba(220,40,60,0.4) !important; }
|
||||
body.theme-crimson .loading-spinner { border-color:rgba(220,40,60,0.2) !important; border-top-color:#e01020 !important; }
|
||||
body.theme-crimson .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,40,60,0.35) 50%, rgba(240,50,70,0.2) 100%) !important; border-color:rgba(220,40,60,0.3) !important; color:#ffc0c0 !important; }
|
||||
/* 猩红 - 阅读页面UI */
|
||||
body.theme-crimson .top-nav, body.theme-crimson .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-crimson .top-nav .current { color:#e01020 !important; font-weight:700; }
|
||||
body.theme-crimson .top-nav .split { color:rgba(200,150,155,0.4) !important; }
|
||||
body.theme-crimson #reader, body.theme-crimson #reader * { color:#ffc0c0 !important; }
|
||||
body.theme-crimson .reader-menu { background:rgba(30,5,10,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimson .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,40,60,0.35) 50%, rgba(240,50,70,0.2) 100%) !important; color:#ffc0c0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimson .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(240,80,90,0.3) 100%) !important; color:#ffc0c0 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-crimson .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,40,60,0.4) 50%, rgba(240,50,70,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(220,40,60,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimson .bookmark-btn, body.theme-crimson .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,40,60,0.35) 50%, rgba(240,50,70,0.2) 100%) !important; color:#ffc0c0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimson .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(240,80,90,0.3) 100%) !important; color:#ffc0c0 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-crimson .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,40,60,0.4) 50%, rgba(240,50,70,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(220,40,60,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimson .bookmark-panel { background:rgba(30,5,10,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimson .bm-header { color:#ffc0c0 !important; }
|
||||
body.theme-crimson .bm-item { background:rgba(240,100,120,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#ffc0c0 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-crimson .bm-item .book, body.theme-crimson .bm-item .chap, body.theme-crimson .bm-item .page { color:#ff8080 !important; }
|
||||
body.theme-crimson .canvas-container { background:rgba(240,60,80,0.12) !important; }
|
||||
body.theme-crimson .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(30,5,10,0.95) 40%) !important; }
|
||||
body.theme-crimson .progress-track { background:rgba(220,80,100,0.15) !important; }
|
||||
body.theme-crimson .progress-text { color:rgba(200,150,155,0.7) !important; }
|
||||
body.theme-crimson .chapter-indicator { color:#ffc0c0 !important; }
|
||||
body.theme-crimson .ebook-chapter { background:rgba(30,5,10,0.92) !important; color:#ffc0c0 !important; }
|
||||
body.theme-crimson .ebook-chapter .chapter-title { color:#e01020 !important; }
|
||||
body.theme-crimson .menu-title, body.theme-crimson .section-title { color:#ffc0c0 !important; }
|
||||
body.theme-crimson .back-btn { color:#ffc0c0 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-crimson .page-title { color:#ffc0c0 !important; }
|
||||
body.theme-crimson .shelf-item { color:#ffc0c0 !important; }
|
||||
body.theme-crimson h1, body.theme-crimson h3, body.theme-crimson h3 a { color:#ffc0c0 !important; }
|
||||
body.theme-crimson .item a { color:#ffc0c0 !important; }
|
||||
body.theme-crimson .book-chapter-item a { color:#ffc0c0 !important; }
|
||||
|
||||
/* 熔岩 */
|
||||
body.theme-lava { background:#1a0600 !important; color:#ffccaa !important; }
|
||||
body.theme-lava .top-nav, body.theme-lava h1, body.theme-lava h3,
|
||||
body.theme-lava .item, body.theme-lava .bottom-nav, body.theme-lava .reader-menu,
|
||||
body.theme-lava .bookmark-panel, body.theme-lava .section-box,
|
||||
body.theme-lava .auto-scroll-btn, body.theme-lava .speed-btn, body.theme-lava .speed-btn.active,
|
||||
body.theme-lava .bookmark-btn, body.theme-lava .bookmark-btn.second,
|
||||
body.theme-lava .chapter-item-btn, body.theme-lava .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,90,20,0.35) 50%, rgba(255,110,40,0.2) 100%) !important;}
|
||||
body.theme-lava .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,120,50,0.3) 50%, rgba(255,100,30,0.15) 100%) !important;}
|
||||
body.theme-lava .progress-fill { background:linear-gradient(90deg, #ff5a00, #ff8840) !important; box-shadow:0 0 6px rgba(255,90,20,0.4) !important; }
|
||||
body.theme-lava .loading-spinner { border-color:rgba(255,90,20,0.2) !important; border-top-color:#ff5a00 !important; }
|
||||
body.theme-lava .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,90,20,0.35) 50%, rgba(255,110,40,0.2) 100%) !important; border-color:rgba(255,90,20,0.3) !important; color:#ffccaa !important; }
|
||||
/* 熔岩 - 阅读页面UI */
|
||||
body.theme-lava .top-nav, body.theme-lava .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-lava .top-nav .current { color:#ff5a00 !important; font-weight:700; }
|
||||
body.theme-lava .top-nav .split { color:rgba(220,150,120,0.4) !important; }
|
||||
body.theme-lava #reader, body.theme-lava #reader * { color:#ffccaa !important; }
|
||||
body.theme-lava .reader-menu { background:rgba(30,8,0,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-lava .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,90,20,0.35) 50%, rgba(255,110,40,0.2) 100%) !important; color:#ffccaa !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-lava .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,140,80,0.3) 100%) !important; color:#ffccaa !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-lava .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,90,20,0.4) 50%, rgba(255,110,40,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(255,90,20,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-lava .bookmark-btn, body.theme-lava .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,90,20,0.35) 50%, rgba(255,110,40,0.2) 100%) !important; color:#ffccaa !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-lava .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,140,80,0.3) 100%) !important; color:#ffccaa !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-lava .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,90,20,0.4) 50%, rgba(255,110,40,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(255,90,20,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-lava .bookmark-panel { background:rgba(30,8,0,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-lava .bm-header { color:#ffccaa !important; }
|
||||
body.theme-lava .bm-item { background:rgba(255,120,60,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#ffccaa !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-lava .bm-item .book, body.theme-lava .bm-item .chap, body.theme-lava .bm-item .page { color:#ff9966 !important; }
|
||||
body.theme-lava .canvas-container { background:rgba(255,110,40,0.12) !important; }
|
||||
body.theme-lava .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(30,8,0,0.95) 40%) !important; }
|
||||
body.theme-lava .progress-track { background:rgba(220,120,80,0.15) !important; }
|
||||
body.theme-lava .progress-text { color:rgba(220,150,120,0.7) !important; }
|
||||
body.theme-lava .chapter-indicator { color:#ffccaa !important; }
|
||||
body.theme-lava .ebook-chapter { background:rgba(30,8,0,0.92) !important; color:#ffccaa !important; }
|
||||
body.theme-lava .ebook-chapter .chapter-title { color:#ff5a00 !important; }
|
||||
body.theme-lava .menu-title, body.theme-lava .section-title { color:#ffccaa !important; }
|
||||
body.theme-lava .back-btn { color:#ffccaa !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-lava .page-title { color:#ffccaa !important; }
|
||||
body.theme-lava .shelf-item { color:#ffccaa !important; }
|
||||
body.theme-lava h1, body.theme-lava h3, body.theme-lava h3 a { color:#ffccaa !important; }
|
||||
body.theme-lava .item a { color:#ffccaa !important; }
|
||||
body.theme-lava .book-chapter-item a { color:#ffccaa !important; }
|
||||
|
||||
/* bronze 古铜 */
|
||||
body.theme-bronze { background:#120a02 !important; color:#ffe8c0 !important; }
|
||||
body.theme-bronze .top-nav, body.theme-bronze h1, body.theme-bronze h3,
|
||||
body.theme-bronze .item, body.theme-bronze .bottom-nav, body.theme-bronze .reader-menu,
|
||||
body.theme-bronze .bookmark-panel, body.theme-bronze .section-box,
|
||||
body.theme-bronze .auto-scroll-btn, body.theme-bronze .speed-btn, body.theme-bronze .speed-btn.active,
|
||||
body.theme-bronze .bookmark-btn, body.theme-bronze .bookmark-btn.second,
|
||||
body.theme-bronze .chapter-item-btn, body.theme-bronze .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(200,150,40,0.35) 50%, rgba(220,170,60,0.2) 100%) !important;}
|
||||
body.theme-bronze .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,170,60,0.3) 50%, rgba(200,150,40,0.15) 100%) !important;}
|
||||
body.theme-bronze .progress-fill { background:linear-gradient(90deg, #c89830, #e0b850) !important; box-shadow:0 0 6px rgba(200,150,40,0.4) !important; }
|
||||
body.theme-bronze .loading-spinner { border-color:rgba(200,150,40,0.2) !important; border-top-color:#c89830 !important; }
|
||||
body.theme-bronze .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(200,150,40,0.35) 50%, rgba(220,170,60,0.2) 100%) !important; border-color:rgba(200,150,40,0.3) !important; color:#ffe8c0 !important; }
|
||||
/* bronze 阅读页面UI */
|
||||
body.theme-bronze .top-nav, body.theme-bronze .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-bronze .top-nav .current { color:#c89830 !important; font-weight:700; }
|
||||
body.theme-bronze .top-nav .split { color:rgba(200,160,100,0.4) !important; }
|
||||
body.theme-bronze #reader, body.theme-bronze #reader * { color:#ffe8c0 !important; }
|
||||
body.theme-bronze .reader-menu { background:rgba(20,12,3,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-bronze .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(200,150,40,0.35) 50%, rgba(220,170,60,0.2) 100%) !important; color:#ffe8c0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-bronze .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(220,170,80,0.3) 100%) !important; color:#ffe8c0 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-bronze .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(200,150,40,0.4) 50%, rgba(220,170,60,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(200,150,40,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-bronze .bookmark-btn, body.theme-bronze .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(200,150,40,0.35) 50%, rgba(220,170,60,0.2) 100%) !important; color:#ffe8c0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-bronze .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(220,170,80,0.3) 100%) !important; color:#ffe8c0 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-bronze .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(200,150,40,0.4) 50%, rgba(220,170,60,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(200,150,40,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-bronze .bookmark-panel { background:rgba(20,12,3,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-bronze .bm-header { color:#ffe8c0 !important; }
|
||||
body.theme-bronze .bm-item { background:rgba(200,150,60,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#ffe8c0 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-bronze .bm-item .book, body.theme-bronze .bm-item .chap, body.theme-bronze .bm-item .page { color:#e0c080 !important; }
|
||||
body.theme-bronze .canvas-container { background:rgba(220,170,60,0.12) !important; }
|
||||
body.theme-bronze .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(20,12,3,0.95) 40%) !important; }
|
||||
body.theme-bronze .progress-track { background:rgba(200,150,60,0.15) !important; }
|
||||
body.theme-bronze .progress-text { color:rgba(200,160,100,0.7) !important; }
|
||||
body.theme-bronze .chapter-indicator { color:#ffe8c0 !important; }
|
||||
body.theme-bronze .ebook-chapter { background:rgba(20,12,3,0.92) !important; color:#ffe8c0 !important; }
|
||||
body.theme-bronze .ebook-chapter .chapter-title { color:#c89830 !important; }
|
||||
body.theme-bronze .menu-title, body.theme-bronze .section-title { color:#ffe8c0 !important; }
|
||||
body.theme-bronze .back-btn { color:#ffe8c0 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-bronze .page-title { color:#ffe8c0 !important; }
|
||||
body.theme-bronze .shelf-item { color:#ffe8c0 !important; }
|
||||
body.theme-bronze h1, body.theme-bronze h3, body.theme-bronze h3 a { color:#ffe8c0 !important; }
|
||||
body.theme-bronze .item a { color:#ffe8c0 !important; }
|
||||
body.theme-bronze .book-chapter-item a { color:#ffe8c0 !important; }
|
||||
|
||||
/* emerald 翡翠 */
|
||||
body.theme-emerald { background:#000a05 !important; color:#b0ffd0 !important; }
|
||||
body.theme-emerald .top-nav, body.theme-emerald h1, body.theme-emerald h3,
|
||||
body.theme-emerald .item, body.theme-emerald .bottom-nav, body.theme-emerald .reader-menu,
|
||||
body.theme-emerald .bookmark-panel, body.theme-emerald .section-box,
|
||||
body.theme-emerald .auto-scroll-btn, body.theme-emerald .speed-btn, body.theme-emerald .speed-btn.active,
|
||||
body.theme-emerald .bookmark-btn, body.theme-emerald .bookmark-btn.second,
|
||||
body.theme-emerald .chapter-item-btn, body.theme-emerald .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,190,90,0.35) 50%, rgba(0,210,110,0.2) 100%) !important;}
|
||||
body.theme-emerald .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,210,110,0.3) 50%, rgba(0,190,90,0.15) 100%) !important;}
|
||||
body.theme-emerald .progress-fill { background:linear-gradient(90deg, #00c060, #20e080) !important; box-shadow:0 0 6px rgba(0,190,90,0.4) !important; }
|
||||
body.theme-emerald .loading-spinner { border-color:rgba(0,190,90,0.2) !important; border-top-color:#00c060 !important; }
|
||||
body.theme-emerald .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,190,90,0.35) 50%, rgba(0,210,110,0.2) 100%) !important; border-color:rgba(0,190,90,0.3) !important; color:#b0ffd0 !important; }
|
||||
/* emerald 阅读页面UI */
|
||||
body.theme-emerald .top-nav, body.theme-emerald .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-emerald .top-nav .current { color:#00c060 !important; font-weight:700; }
|
||||
body.theme-emerald .top-nav .split { color:rgba(100,200,150,0.4) !important; }
|
||||
body.theme-emerald #reader, body.theme-emerald #reader * { color:#b0ffd0 !important; }
|
||||
body.theme-emerald .reader-menu { background:rgba(0,15,8,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-emerald .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,190,90,0.35) 50%, rgba(0,210,110,0.2) 100%) !important; color:#b0ffd0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-emerald .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(0,210,130,0.3) 100%) !important; color:#b0ffd0 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-emerald .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,190,90,0.4) 50%, rgba(0,210,110,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(0,190,90,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-emerald .bookmark-btn, body.theme-emerald .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,190,90,0.35) 50%, rgba(0,210,110,0.2) 100%) !important; color:#b0ffd0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-emerald .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(0,210,130,0.3) 100%) !important; color:#b0ffd0 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-emerald .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,190,90,0.4) 50%, rgba(0,210,110,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(0,190,90,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-emerald .bookmark-panel { background:rgba(0,15,8,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-emerald .bm-header { color:#b0ffd0 !important; }
|
||||
body.theme-emerald .bm-item { background:rgba(0,190,110,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#b0ffd0 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-emerald .bm-item .book, body.theme-emerald .bm-item .chap, body.theme-emerald .bm-item .page { color:#60e0a0 !important; }
|
||||
body.theme-emerald .canvas-container { background:rgba(0,210,110,0.12) !important; }
|
||||
body.theme-emerald .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(0,15,8,0.95) 40%) !important; }
|
||||
body.theme-emerald .progress-track { background:rgba(0,190,110,0.15) !important; }
|
||||
body.theme-emerald .progress-text { color:rgba(100,200,150,0.7) !important; }
|
||||
body.theme-emerald .chapter-indicator { color:#b0ffd0 !important; }
|
||||
body.theme-emerald .ebook-chapter { background:rgba(0,15,8,0.92) !important; color:#b0ffd0 !important; }
|
||||
body.theme-emerald .ebook-chapter .chapter-title { color:#00c060 !important; }
|
||||
body.theme-emerald .menu-title, body.theme-emerald .section-title { color:#b0ffd0 !important; }
|
||||
body.theme-emerald .back-btn { color:#b0ffd0 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-emerald .page-title { color:#b0ffd0 !important; }
|
||||
body.theme-emerald .shelf-item { color:#b0ffd0 !important; }
|
||||
body.theme-emerald h1, body.theme-emerald h3, body.theme-emerald h3 a { color:#b0ffd0 !important; }
|
||||
body.theme-emerald .item a { color:#b0ffd0 !important; }
|
||||
body.theme-emerald .book-chapter-item a { color:#b0ffd0 !important; }
|
||||
|
||||
/* teal 青翠 */
|
||||
body.theme-teal { background:#000a08 !important; color:#b0ffe8 !important; }
|
||||
body.theme-teal .top-nav, body.theme-teal h1, body.theme-teal h3,
|
||||
body.theme-teal .item, body.theme-teal .bottom-nav, body.theme-teal .reader-menu,
|
||||
body.theme-teal .bookmark-panel, body.theme-teal .section-box,
|
||||
body.theme-teal .auto-scroll-btn, body.theme-teal .speed-btn, body.theme-teal .speed-btn.active,
|
||||
body.theme-teal .bookmark-btn, body.theme-teal .bookmark-btn.second,
|
||||
body.theme-teal .chapter-item-btn, body.theme-teal .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,170,140,0.35) 50%, rgba(0,200,160,0.2) 100%) !important;}
|
||||
body.theme-teal .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,200,160,0.3) 50%, rgba(0,180,150,0.15) 100%) !important;}
|
||||
body.theme-teal .progress-fill { background:linear-gradient(90deg, #00b090, #20e0c0) !important; box-shadow:0 0 6px rgba(0,170,140,0.4) !important; }
|
||||
body.theme-teal .loading-spinner { border-color:rgba(0,170,140,0.2) !important; border-top-color:#00b090 !important; }
|
||||
body.theme-teal .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,170,140,0.35) 50%, rgba(0,200,160,0.2) 100%) !important; border-color:rgba(0,170,140,0.3) !important; color:#b0ffe8 !important; }
|
||||
/* teal 阅读页面UI */
|
||||
body.theme-teal .top-nav, body.theme-teal .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-teal .top-nav .current { color:#00b090 !important; font-weight:700; }
|
||||
body.theme-teal .top-nav .split { color:rgba(100,200,180,0.4) !important; }
|
||||
body.theme-teal #reader, body.theme-teal #reader * { color:#b0ffe8 !important; }
|
||||
body.theme-teal .reader-menu { background:rgba(0,15,12,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-teal .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,170,140,0.35) 50%, rgba(0,200,160,0.2) 100%) !important; color:#b0ffe8 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-teal .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(0,200,170,0.3) 100%) !important; color:#b0ffe8 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-teal .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,170,140,0.4) 50%, rgba(0,200,160,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(0,170,140,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-teal .bookmark-btn, body.theme-teal .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,170,140,0.35) 50%, rgba(0,200,160,0.2) 100%) !important; color:#b0ffe8 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-teal .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(0,200,170,0.3) 100%) !important; color:#b0ffe8 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-teal .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(0,170,140,0.4) 50%, rgba(0,200,160,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(0,170,140,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-teal .bookmark-panel { background:rgba(0,15,12,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-teal .bm-header { color:#b0ffe8 !important; }
|
||||
body.theme-teal .bm-item { background:rgba(0,180,150,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#b0ffe8 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-teal .bm-item .book, body.theme-teal .bm-item .chap, body.theme-teal .bm-item .page { color:#60e0c0 !important; }
|
||||
body.theme-teal .canvas-container { background:rgba(0,200,160,0.12) !important; }
|
||||
body.theme-teal .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(0,15,12,0.95) 40%) !important; }
|
||||
body.theme-teal .progress-track { background:rgba(0,180,150,0.15) !important; }
|
||||
body.theme-teal .progress-text { color:rgba(100,200,180,0.7) !important; }
|
||||
body.theme-teal .chapter-indicator { color:#b0ffe8 !important; }
|
||||
body.theme-teal .ebook-chapter { background:rgba(0,15,12,0.92) !important; color:#b0ffe8 !important; }
|
||||
body.theme-teal .ebook-chapter .chapter-title { color:#00b090 !important; }
|
||||
body.theme-teal .menu-title, body.theme-teal .section-title { color:#b0ffe8 !important; }
|
||||
body.theme-teal .back-btn { color:#b0ffe8 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-teal .page-title { color:#b0ffe8 !important; }
|
||||
body.theme-teal .shelf-item { color:#b0ffe8 !important; }
|
||||
body.theme-teal h1, body.theme-teal h3, body.theme-teal h3 a { color:#b0ffe8 !important; }
|
||||
body.theme-teal .item a { color:#b0ffe8 !important; }
|
||||
body.theme-teal .book-chapter-item a { color:#b0ffe8 !important; }
|
||||
|
||||
/* cobalt 钴蓝 */
|
||||
body.theme-cobalt { background:#000518 !important; color:#c0d0ff !important; }
|
||||
body.theme-cobalt .top-nav, body.theme-cobalt h1, body.theme-cobalt h3,
|
||||
body.theme-cobalt .item, body.theme-cobalt .bottom-nav, body.theme-cobalt .reader-menu,
|
||||
body.theme-cobalt .bookmark-panel, body.theme-cobalt .section-box,
|
||||
body.theme-cobalt .auto-scroll-btn, body.theme-cobalt .speed-btn, body.theme-cobalt .speed-btn.active,
|
||||
body.theme-cobalt .bookmark-btn, body.theme-cobalt .bookmark-btn.second,
|
||||
body.theme-cobalt .chapter-item-btn, body.theme-cobalt .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,90,255,0.35) 50%, rgba(50,120,255,0.2) 100%) !important;}
|
||||
body.theme-cobalt .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(50,120,255,0.3) 50%, rgba(30,100,240,0.15) 100%) !important;}
|
||||
body.theme-cobalt .progress-fill { background:linear-gradient(90deg, #2060ff, #5090ff) !important; box-shadow:0 0 6px rgba(30,90,255,0.4) !important; }
|
||||
body.theme-cobalt .loading-spinner { border-color:rgba(30,90,255,0.2) !important; border-top-color:#2060ff !important; }
|
||||
body.theme-cobalt .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,90,255,0.35) 50%, rgba(50,120,255,0.2) 100%) !important; border-color:rgba(30,90,255,0.3) !important; color:#c0d0ff !important; }
|
||||
/* cobalt 阅读页面UI */
|
||||
body.theme-cobalt .top-nav, body.theme-cobalt .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-cobalt .top-nav .current { color:#2060ff !important; font-weight:700; }
|
||||
body.theme-cobalt .top-nav .split { color:rgba(150,170,220,0.4) !important; }
|
||||
body.theme-cobalt #reader, body.theme-cobalt #reader * { color:#c0d0ff !important; }
|
||||
body.theme-cobalt .reader-menu { background:rgba(3,8,30,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-cobalt .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,90,255,0.35) 50%, rgba(50,120,255,0.2) 100%) !important; color:#c0d0ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-cobalt .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(60,130,255,0.3) 100%) !important; color:#c0d0ff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-cobalt .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,90,255,0.4) 50%, rgba(50,120,255,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(30,90,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-cobalt .bookmark-btn, body.theme-cobalt .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,90,255,0.35) 50%, rgba(50,120,255,0.2) 100%) !important; color:#c0d0ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-cobalt .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(60,130,255,0.3) 100%) !important; color:#c0d0ff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-cobalt .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,90,255,0.4) 50%, rgba(50,120,255,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(30,90,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-cobalt .bookmark-panel { background:rgba(3,8,30,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-cobalt .bm-header { color:#c0d0ff !important; }
|
||||
body.theme-cobalt .bm-item { background:rgba(40,100,240,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#c0d0ff !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-cobalt .bm-item .book, body.theme-cobalt .bm-item .chap, body.theme-cobalt .bm-item .page { color:#80a0ff !important; }
|
||||
body.theme-cobalt .canvas-container { background:rgba(50,120,255,0.12) !important; }
|
||||
body.theme-cobalt .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(3,8,30,0.95) 40%) !important; }
|
||||
body.theme-cobalt .progress-track { background:rgba(40,100,240,0.15) !important; }
|
||||
body.theme-cobalt .progress-text { color:rgba(150,170,220,0.7) !important; }
|
||||
body.theme-cobalt .chapter-indicator { color:#c0d0ff !important; }
|
||||
body.theme-cobalt .ebook-chapter { background:rgba(3,8,30,0.92) !important; color:#c0d0ff !important; }
|
||||
body.theme-cobalt .ebook-chapter .chapter-title { color:#2060ff !important; }
|
||||
body.theme-cobalt .menu-title, body.theme-cobalt .section-title { color:#c0d0ff !important; }
|
||||
body.theme-cobalt .back-btn { color:#c0d0ff !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-cobalt .page-title { color:#c0d0ff !important; }
|
||||
body.theme-cobalt .shelf-item { color:#c0d0ff !important; }
|
||||
body.theme-cobalt h1, body.theme-cobalt h3, body.theme-cobalt h3 a { color:#c0d0ff !important; }
|
||||
body.theme-cobalt .item a { color:#c0d0ff !important; }
|
||||
body.theme-cobalt .book-chapter-item a { color:#c0d0ff !important; }
|
||||
|
||||
/* violet 霓虹紫 */
|
||||
body.theme-violet { background:#080010 !important; color:#d8c0ff !important; }
|
||||
body.theme-violet .top-nav, body.theme-violet h1, body.theme-violet h3,
|
||||
body.theme-violet .item, body.theme-violet .bottom-nav, body.theme-violet .reader-menu,
|
||||
body.theme-violet .bookmark-panel, body.theme-violet .section-box,
|
||||
body.theme-violet .auto-scroll-btn, body.theme-violet .speed-btn, body.theme-violet .speed-btn.active,
|
||||
body.theme-violet .bookmark-btn, body.theme-violet .bookmark-btn.second,
|
||||
body.theme-violet .chapter-item-btn, body.theme-violet .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(140,40,255,0.35) 50%, rgba(160,70,255,0.2) 100%) !important;}
|
||||
body.theme-violet .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(160,70,255,0.3) 50%, rgba(140,50,240,0.15) 100%) !important;}
|
||||
body.theme-violet .progress-fill { background:linear-gradient(90deg, #9030ff, #b060ff) !important; box-shadow:0 0 6px rgba(140,40,255,0.4) !important; }
|
||||
body.theme-violet .loading-spinner { border-color:rgba(140,40,255,0.2) !important; border-top-color:#9030ff !important; }
|
||||
body.theme-violet .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(140,40,255,0.35) 50%, rgba(160,70,255,0.2) 100%) !important; border-color:rgba(140,40,255,0.3) !important; color:#d8c0ff !important; }
|
||||
/* violet 阅读页面UI */
|
||||
body.theme-violet .top-nav, body.theme-violet .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-violet .top-nav .current { color:#9030ff !important; font-weight:700; }
|
||||
body.theme-violet .top-nav .split { color:rgba(180,150,220,0.4) !important; }
|
||||
body.theme-violet #reader, body.theme-violet #reader * { color:#d8c0ff !important; }
|
||||
body.theme-violet .reader-menu { background:rgba(10,3,25,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-violet .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(140,40,255,0.35) 50%, rgba(160,70,255,0.2) 100%) !important; color:#d8c0ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-violet .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(170,90,250,0.3) 100%) !important; color:#d8c0ff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-violet .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(140,40,255,0.4) 50%, rgba(160,70,255,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(140,40,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-violet .bookmark-btn, body.theme-violet .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(140,40,255,0.35) 50%, rgba(160,70,255,0.2) 100%) !important; color:#d8c0ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-violet .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(170,90,250,0.3) 100%) !important; color:#d8c0ff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-violet .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(140,40,255,0.4) 50%, rgba(160,70,255,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(140,40,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-violet .bookmark-panel { background:rgba(10,3,25,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-violet .bm-header { color:#d8c0ff !important; }
|
||||
body.theme-violet .bm-item { background:rgba(150,60,250,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#d8c0ff !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-violet .bm-item .book, body.theme-violet .bm-item .chap, body.theme-violet .bm-item .page { color:#b880ff !important; }
|
||||
body.theme-violet .canvas-container { background:rgba(160,70,255,0.12) !important; }
|
||||
body.theme-violet .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(10,3,25,0.95) 40%) !important; }
|
||||
body.theme-violet .progress-track { background:rgba(150,60,250,0.15) !important; }
|
||||
body.theme-violet .progress-text { color:rgba(180,150,220,0.7) !important; }
|
||||
body.theme-violet .chapter-indicator { color:#d8c0ff !important; }
|
||||
body.theme-violet .ebook-chapter { background:rgba(10,3,25,0.92) !important; color:#d8c0ff !important; }
|
||||
body.theme-violet .ebook-chapter .chapter-title { color:#9030ff !important; }
|
||||
body.theme-violet .menu-title, body.theme-violet .section-title { color:#d8c0ff !important; }
|
||||
body.theme-violet .back-btn { color:#d8c0ff !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-violet .page-title { color:#d8c0ff !important; }
|
||||
body.theme-violet .shelf-item { color:#d8c0ff !important; }
|
||||
body.theme-violet h1, body.theme-violet h3, body.theme-violet h3 a { color:#d8c0ff !important; }
|
||||
body.theme-violet .item a { color:#d8c0ff !important; }
|
||||
body.theme-violet .book-chapter-item a { color:#d8c0ff !important; }
|
||||
|
||||
/* amber 琥珀 */
|
||||
body.theme-amber { background:#100800 !important; color:#ffe8b0 !important; }
|
||||
body.theme-amber .top-nav, body.theme-amber h1, body.theme-amber h3,
|
||||
body.theme-amber .item, body.theme-amber .bottom-nav, body.theme-amber .reader-menu,
|
||||
body.theme-amber .bookmark-panel, body.theme-amber .section-box,
|
||||
body.theme-amber .auto-scroll-btn, body.theme-amber .speed-btn, body.theme-amber .speed-btn.active,
|
||||
body.theme-amber .bookmark-btn, body.theme-amber .bookmark-btn.second,
|
||||
body.theme-amber .chapter-item-btn, body.theme-amber .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,170,0,0.35) 50%, rgba(255,190,30,0.2) 100%) !important;}
|
||||
body.theme-amber .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,190,30,0.3) 50%, rgba(255,170,10,0.15) 100%) !important;}
|
||||
body.theme-amber .progress-fill { background:linear-gradient(90deg, #ffb000, #ffd040) !important; box-shadow:0 0 6px rgba(255,170,0,0.4) !important; }
|
||||
body.theme-amber .loading-spinner { border-color:rgba(255,170,0,0.2) !important; border-top-color:#ffb000 !important; }
|
||||
body.theme-amber .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,170,0,0.35) 50%, rgba(255,190,30,0.2) 100%) !important; border-color:rgba(255,170,0,0.3) !important; color:#ffe8b0 !important; }
|
||||
/* amber 阅读页面UI */
|
||||
body.theme-amber .top-nav, body.theme-amber .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-amber .top-nav .current { color:#ffb000 !important; font-weight:700; }
|
||||
body.theme-amber .top-nav .split { color:rgba(220,180,100,0.4) !important; }
|
||||
body.theme-amber #reader, body.theme-amber #reader * { color:#ffe8b0 !important; }
|
||||
body.theme-amber .reader-menu { background:rgba(20,12,0,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-amber .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,170,0,0.35) 50%, rgba(255,190,30,0.2) 100%) !important; color:#ffe8b0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-amber .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,190,50,0.3) 100%) !important; color:#ffe8b0 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-amber .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,170,0,0.4) 50%, rgba(255,190,30,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(255,170,0,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-amber .bookmark-btn, body.theme-amber .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,170,0,0.35) 50%, rgba(255,190,30,0.2) 100%) !important; color:#ffe8b0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-amber .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,190,50,0.3) 100%) !important; color:#ffe8b0 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-amber .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,170,0,0.4) 50%, rgba(255,190,30,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(255,170,0,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-amber .bookmark-panel { background:rgba(20,12,0,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-amber .bm-header { color:#ffe8b0 !important; }
|
||||
body.theme-amber .bm-item { background:rgba(255,180,30,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#ffe8b0 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-amber .bm-item .book, body.theme-amber .bm-item .chap, body.theme-amber .bm-item .page { color:#ffd060 !important; }
|
||||
body.theme-amber .canvas-container { background:rgba(255,190,30,0.12) !important; }
|
||||
body.theme-amber .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(20,12,0,0.95) 40%) !important; }
|
||||
body.theme-amber .progress-track { background:rgba(255,180,40,0.15) !important; }
|
||||
body.theme-amber .progress-text { color:rgba(220,180,100,0.7) !important; }
|
||||
body.theme-amber .chapter-indicator { color:#ffe8b0 !important; }
|
||||
body.theme-amber .ebook-chapter { background:rgba(20,12,0,0.92) !important; color:#ffe8b0 !important; }
|
||||
body.theme-amber .ebook-chapter .chapter-title { color:#ffb000 !important; }
|
||||
body.theme-amber .menu-title, body.theme-amber .section-title { color:#ffe8b0 !important; }
|
||||
body.theme-amber .back-btn { color:#ffe8b0 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-amber .page-title { color:#ffe8b0 !important; }
|
||||
body.theme-amber .shelf-item { color:#ffe8b0 !important; }
|
||||
body.theme-amber h1, body.theme-amber h3, body.theme-amber h3 a { color:#ffe8b0 !important; }
|
||||
body.theme-amber .item a { color:#ffe8b0 !important; }
|
||||
body.theme-amber .book-chapter-item a { color:#ffe8b0 !important; }
|
||||
|
||||
/* magenta 品红 */
|
||||
body.theme-magenta { background:#100008 !important; color:#ffc0e0 !important; }
|
||||
body.theme-magenta .top-nav, body.theme-magenta h1, body.theme-magenta h3,
|
||||
body.theme-magenta .item, body.theme-magenta .bottom-nav, body.theme-magenta .reader-menu,
|
||||
body.theme-magenta .bookmark-panel, body.theme-magenta .section-box,
|
||||
body.theme-magenta .auto-scroll-btn, body.theme-magenta .speed-btn, body.theme-magenta .speed-btn.active,
|
||||
body.theme-magenta .bookmark-btn, body.theme-magenta .bookmark-btn.second,
|
||||
body.theme-magenta .chapter-item-btn, body.theme-magenta .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,0,160,0.35) 50%, rgba(240,20,180,0.2) 100%) !important;}
|
||||
body.theme-magenta .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(240,20,180,0.3) 50%, rgba(220,10,160,0.15) 100%) !important;}
|
||||
body.theme-magenta .progress-fill { background:linear-gradient(90deg, #e000a0, #ff20c0) !important; box-shadow:0 0 6px rgba(220,0,160,0.4) !important; }
|
||||
body.theme-magenta .loading-spinner { border-color:rgba(220,0,160,0.2) !important; border-top-color:#e000a0 !important; }
|
||||
body.theme-magenta .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,0,160,0.35) 50%, rgba(240,20,180,0.2) 100%) !important; border-color:rgba(220,0,160,0.3) !important; color:#ffc0e0 !important; }
|
||||
/* magenta 阅读页面UI */
|
||||
body.theme-magenta .top-nav, body.theme-magenta .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-magenta .top-nav .current { color:#e000a0 !important; font-weight:700; }
|
||||
body.theme-magenta .top-nav .split { color:rgba(220,150,190,0.4) !important; }
|
||||
body.theme-magenta #reader, body.theme-magenta #reader * { color:#ffc0e0 !important; }
|
||||
body.theme-magenta .reader-menu { background:rgba(20,3,12,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-magenta .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,0,160,0.35) 50%, rgba(240,20,180,0.2) 100%) !important; color:#ffc0e0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-magenta .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(240,40,190,0.3) 100%) !important; color:#ffc0e0 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-magenta .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,0,160,0.4) 50%, rgba(240,20,180,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(220,0,160,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-magenta .bookmark-btn, body.theme-magenta .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,0,160,0.35) 50%, rgba(240,20,180,0.2) 100%) !important; color:#ffc0e0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-magenta .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(240,40,190,0.3) 100%) !important; color:#ffc0e0 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-magenta .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(220,0,160,0.4) 50%, rgba(240,20,180,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(220,0,160,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-magenta .bookmark-panel { background:rgba(20,3,12,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-magenta .bm-header { color:#ffc0e0 !important; }
|
||||
body.theme-magenta .bm-item { background:rgba(230,30,170,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#ffc0e0 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-magenta .bm-item .book, body.theme-magenta .bm-item .chap, body.theme-magenta .bm-item .page { color:#ff80d0 !important; }
|
||||
body.theme-magenta .canvas-container { background:rgba(240,20,180,0.12) !important; }
|
||||
body.theme-magenta .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(20,3,12,0.95) 40%) !important; }
|
||||
body.theme-magenta .progress-track { background:rgba(230,30,170,0.15) !important; }
|
||||
body.theme-magenta .progress-text { color:rgba(220,150,190,0.7) !important; }
|
||||
body.theme-magenta .chapter-indicator { color:#ffc0e0 !important; }
|
||||
body.theme-magenta .ebook-chapter { background:rgba(20,3,12,0.92) !important; color:#ffc0e0 !important; }
|
||||
body.theme-magenta .ebook-chapter .chapter-title { color:#e000a0 !important; }
|
||||
body.theme-magenta .menu-title, body.theme-magenta .section-title { color:#ffc0e0 !important; }
|
||||
body.theme-magenta .back-btn { color:#ffc0e0 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-magenta .page-title { color:#ffc0e0 !important; }
|
||||
body.theme-magenta .shelf-item { color:#ffc0e0 !important; }
|
||||
body.theme-magenta h1, body.theme-magenta h3, body.theme-magenta h3 a { color:#ffc0e0 !important; }
|
||||
body.theme-magenta .item a { color:#ffc0e0 !important; }
|
||||
body.theme-magenta .book-chapter-item a { color:#ffc0e0 !important; }
|
||||
|
||||
/* indigo 靛青 */
|
||||
body.theme-indigo { background:#050818 !important; color:#c8c0ff !important; }
|
||||
body.theme-indigo .top-nav, body.theme-indigo h1, body.theme-indigo h3,
|
||||
body.theme-indigo .item, body.theme-indigo .bottom-nav, body.theme-indigo .reader-menu,
|
||||
body.theme-indigo .bookmark-panel, body.theme-indigo .section-box,
|
||||
body.theme-indigo .auto-scroll-btn, body.theme-indigo .speed-btn, body.theme-indigo .speed-btn.active,
|
||||
body.theme-indigo .bookmark-btn, body.theme-indigo .bookmark-btn.second,
|
||||
body.theme-indigo .chapter-item-btn, body.theme-indigo .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(70,60,200,0.35) 50%, rgba(90,80,220,0.2) 100%) !important;}
|
||||
body.theme-indigo .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(90,80,220,0.3) 50%, rgba(70,60,200,0.15) 100%) !important;}
|
||||
body.theme-indigo .progress-fill { background:linear-gradient(90deg, #4a40d0, #7a70f0) !important; box-shadow:0 0 6px rgba(70,60,200,0.4) !important; }
|
||||
body.theme-indigo .loading-spinner { border-color:rgba(70,60,200,0.2) !important; border-top-color:#4a40d0 !important; }
|
||||
body.theme-indigo .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(70,60,200,0.35) 50%, rgba(90,80,220,0.2) 100%) !important; border-color:rgba(70,60,200,0.3) !important; color:#c8c0ff !important; }
|
||||
/* indigo 阅读页面UI */
|
||||
body.theme-indigo .top-nav, body.theme-indigo .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-indigo .top-nav .current { color:#4a40d0 !important; font-weight:700; }
|
||||
body.theme-indigo .top-nav .split { color:rgba(160,150,210,0.4) !important; }
|
||||
body.theme-indigo #reader, body.theme-indigo #reader * { color:#c8c0ff !important; }
|
||||
body.theme-indigo .reader-menu { background:rgba(8,10,25,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-indigo .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(70,60,200,0.35) 50%, rgba(90,80,220,0.2) 100%) !important; color:#c8c0ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-indigo .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(100,90,230,0.3) 100%) !important; color:#c8c0ff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-indigo .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(70,60,200,0.4) 50%, rgba(90,80,220,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(70,60,200,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-indigo .bookmark-btn, body.theme-indigo .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(70,60,200,0.35) 50%, rgba(90,80,220,0.2) 100%) !important; color:#c8c0ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-indigo .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(100,90,230,0.3) 100%) !important; color:#c8c0ff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-indigo .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(70,60,200,0.4) 50%, rgba(90,80,220,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(70,60,200,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-indigo .bookmark-panel { background:rgba(8,10,25,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-indigo .bm-header { color:#c8c0ff !important; }
|
||||
body.theme-indigo .bm-item { background:rgba(80,70,210,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#c8c0ff !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-indigo .bm-item .book, body.theme-indigo .bm-item .chap, body.theme-indigo .bm-item .page { color:#9080ff !important; }
|
||||
body.theme-indigo .canvas-container { background:rgba(90,80,220,0.12) !important; }
|
||||
body.theme-indigo .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(8,10,25,0.95) 40%) !important; }
|
||||
body.theme-indigo .progress-track { background:rgba(80,70,210,0.15) !important; }
|
||||
body.theme-indigo .progress-text { color:rgba(160,150,210,0.7) !important; }
|
||||
body.theme-indigo .chapter-indicator { color:#c8c0ff !important; }
|
||||
body.theme-indigo .ebook-chapter { background:rgba(8,10,25,0.92) !important; color:#c8c0ff !important; }
|
||||
body.theme-indigo .ebook-chapter .chapter-title { color:#4a40d0 !important; }
|
||||
body.theme-indigo .menu-title, body.theme-indigo .section-title { color:#c8c0ff !important; }
|
||||
body.theme-indigo .back-btn { color:#c8c0ff !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-indigo .page-title { color:#c8c0ff !important; }
|
||||
body.theme-indigo .shelf-item { color:#c8c0ff !important; }
|
||||
body.theme-indigo h1, body.theme-indigo h3, body.theme-indigo h3 a { color:#c8c0ff !important; }
|
||||
body.theme-indigo .item a { color:#c8c0ff !important; }
|
||||
body.theme-indigo .book-chapter-item a { color:#c8c0ff !important; }
|
||||
|
||||
/* coral 珊瑚 */
|
||||
body.theme-coral { background:#180505 !important; color:#ffc0c0 !important; }
|
||||
body.theme-coral .top-nav, body.theme-coral h1, body.theme-coral h3,
|
||||
body.theme-coral .item, body.theme-coral .bottom-nav, body.theme-coral .reader-menu,
|
||||
body.theme-coral .bookmark-panel, body.theme-coral .section-box,
|
||||
body.theme-coral .auto-scroll-btn, body.theme-coral .speed-btn, body.theme-coral .speed-btn.active,
|
||||
body.theme-coral .bookmark-btn, body.theme-coral .bookmark-btn.second,
|
||||
body.theme-coral .chapter-item-btn, body.theme-coral .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,70,70,0.35) 50%, rgba(255,90,90,0.2) 100%) !important;}
|
||||
body.theme-coral .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,90,90,0.3) 50%, rgba(255,70,70,0.15) 100%) !important;}
|
||||
body.theme-coral .progress-fill { background:linear-gradient(90deg, #ff5050, #ff8080) !important; box-shadow:0 0 6px rgba(255,70,70,0.4) !important; }
|
||||
body.theme-coral .loading-spinner { border-color:rgba(255,70,70,0.2) !important; border-top-color:#ff5050 !important; }
|
||||
body.theme-coral .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,70,70,0.35) 50%, rgba(255,90,90,0.2) 100%) !important; border-color:rgba(255,70,70,0.3) !important; color:#ffc0c0 !important; }
|
||||
/* coral 阅读页面UI */
|
||||
body.theme-coral .top-nav, body.theme-coral .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-coral .top-nav .current { color:#ff5050 !important; font-weight:700; }
|
||||
body.theme-coral .top-nav .split { color:rgba(220,150,150,0.4) !important; }
|
||||
body.theme-coral #reader, body.theme-coral #reader * { color:#ffc0c0 !important; }
|
||||
body.theme-coral .reader-menu { background:rgba(25,8,8,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-coral .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,70,70,0.35) 50%, rgba(255,90,90,0.2) 100%) !important; color:#ffc0c0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-coral .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,100,100,0.3) 100%) !important; color:#ffc0c0 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-coral .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,70,70,0.4) 50%, rgba(255,90,90,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(255,70,70,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-coral .bookmark-btn, body.theme-coral .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,70,70,0.35) 50%, rgba(255,90,90,0.2) 100%) !important; color:#ffc0c0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-coral .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,100,100,0.3) 100%) !important; color:#ffc0c0 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-coral .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,70,70,0.4) 50%, rgba(255,90,90,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(255,70,70,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-coral .bookmark-panel { background:rgba(25,8,8,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-coral .bm-header { color:#ffc0c0 !important; }
|
||||
body.theme-coral .bm-item { background:rgba(255,80,80,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#ffc0c0 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-coral .bm-item .book, body.theme-coral .bm-item .chap, body.theme-coral .bm-item .page { color:#ff8080 !important; }
|
||||
body.theme-coral .canvas-container { background:rgba(255,90,90,0.12) !important; }
|
||||
body.theme-coral .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(25,8,8,0.95) 40%) !important; }
|
||||
body.theme-coral .progress-track { background:rgba(255,80,80,0.15) !important; }
|
||||
body.theme-coral .progress-text { color:rgba(220,150,150,0.7) !important; }
|
||||
body.theme-coral .chapter-indicator { color:#ffc0c0 !important; }
|
||||
body.theme-coral .ebook-chapter { background:rgba(25,8,8,0.92) !important; color:#ffc0c0 !important; }
|
||||
body.theme-coral .ebook-chapter .chapter-title { color:#ff5050 !important; }
|
||||
body.theme-coral .menu-title, body.theme-coral .section-title { color:#ffc0c0 !important; }
|
||||
body.theme-coral .back-btn { color:#ffc0c0 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-coral .page-title { color:#ffc0c0 !important; }
|
||||
body.theme-coral .shelf-item { color:#ffc0c0 !important; }
|
||||
body.theme-coral h1, body.theme-coral h3, body.theme-coral h3 a { color:#ffc0c0 !important; }
|
||||
body.theme-coral .item a { color:#ffc0c0 !important; }
|
||||
body.theme-coral .book-chapter-item a { color:#ffc0c0 !important; }
|
||||
|
||||
/* mint 薄荷 */
|
||||
body.theme-mint { background:#000a06 !important; color:#b0ffd8 !important; }
|
||||
body.theme-mint .top-nav, body.theme-mint h1, body.theme-mint h3,
|
||||
body.theme-mint .item, body.theme-mint .bottom-nav, body.theme-mint .reader-menu,
|
||||
body.theme-mint .bookmark-panel, body.theme-mint .section-box,
|
||||
body.theme-mint .auto-scroll-btn, body.theme-mint .speed-btn, body.theme-mint .speed-btn.active,
|
||||
body.theme-mint .bookmark-btn, body.theme-mint .bookmark-btn.second,
|
||||
body.theme-mint .chapter-item-btn, body.theme-mint .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,190,120,0.35) 50%, rgba(40,210,140,0.2) 100%) !important;}
|
||||
body.theme-mint .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(40,210,140,0.3) 50%, rgba(30,190,120,0.15) 100%) !important;}
|
||||
body.theme-mint .progress-fill { background:linear-gradient(90deg, #20c080, #40e8a0) !important; box-shadow:0 0 6px rgba(30,190,120,0.4) !important; }
|
||||
body.theme-mint .loading-spinner { border-color:rgba(30,190,120,0.2) !important; border-top-color:#20c080 !important; }
|
||||
body.theme-mint .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,190,120,0.35) 50%, rgba(40,210,140,0.2) 100%) !important; border-color:rgba(30,190,120,0.3) !important; color:#b0ffd8 !important; }
|
||||
/* mint 阅读页面UI */
|
||||
body.theme-mint .top-nav, body.theme-mint .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-mint .top-nav .current { color:#20c080 !important; font-weight:700; }
|
||||
body.theme-mint .top-nav .split { color:rgba(120,210,170,0.4) !important; }
|
||||
body.theme-mint #reader, body.theme-mint #reader * { color:#b0ffd8 !important; }
|
||||
body.theme-mint .reader-menu { background:rgba(0,15,8,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-mint .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,190,120,0.35) 50%, rgba(40,210,140,0.2) 100%) !important; color:#b0ffd8 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-mint .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(40,210,150,0.3) 100%) !important; color:#b0ffd8 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-mint .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,190,120,0.4) 50%, rgba(40,210,140,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(30,190,120,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-mint .bookmark-btn, body.theme-mint .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,190,120,0.35) 50%, rgba(40,210,140,0.2) 100%) !important; color:#b0ffd8 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-mint .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(40,210,150,0.3) 100%) !important; color:#b0ffd8 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-mint .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(30,190,120,0.4) 50%, rgba(40,210,140,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(30,190,120,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-mint .bookmark-panel { background:rgba(0,15,8,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-mint .bm-header { color:#b0ffd8 !important; }
|
||||
body.theme-mint .bm-item { background:rgba(30,200,130,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#b0ffd8 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-mint .bm-item .book, body.theme-mint .bm-item .chap, body.theme-mint .bm-item .page { color:#60e8a8 !important; }
|
||||
body.theme-mint .canvas-container { background:rgba(40,210,140,0.12) !important; }
|
||||
body.theme-mint .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(0,15,8,0.95) 40%) !important; }
|
||||
body.theme-mint .progress-track { background:rgba(30,200,130,0.15) !important; }
|
||||
body.theme-mint .progress-text { color:rgba(120,210,170,0.7) !important; }
|
||||
body.theme-mint .chapter-indicator { color:#b0ffd8 !important; }
|
||||
body.theme-mint .ebook-chapter { background:rgba(0,15,8,0.92) !important; color:#b0ffd8 !important; }
|
||||
body.theme-mint .ebook-chapter .chapter-title { color:#20c080 !important; }
|
||||
body.theme-mint .menu-title, body.theme-mint .section-title { color:#b0ffd8 !important; }
|
||||
body.theme-mint .back-btn { color:#b0ffd8 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-mint .page-title { color:#b0ffd8 !important; }
|
||||
body.theme-mint .shelf-item { color:#b0ffd8 !important; }
|
||||
body.theme-mint h1, body.theme-mint h3, body.theme-mint h3 a { color:#b0ffd8 !important; }
|
||||
body.theme-mint .item a { color:#b0ffd8 !important; }
|
||||
body.theme-mint .book-chapter-item a { color:#b0ffd8 !important; }
|
||||
|
||||
/* 金灿灿 */
|
||||
body.theme-gold { background:#fff8e0 !important; color:#704e00 !important; }
|
||||
body.theme-gold .top-nav, body.theme-gold h1, body.theme-gold h3,
|
||||
body.theme-gold .item, body.theme-gold .bottom-nav, body.theme-gold .reader-menu,
|
||||
body.theme-gold .bookmark-panel, body.theme-gold .section-box,
|
||||
body.theme-gold .auto-scroll-btn, body.theme-gold .speed-btn, body.theme-gold .speed-btn.active,
|
||||
body.theme-gold .bookmark-btn, body.theme-gold .bookmark-btn.second,
|
||||
body.theme-gold .chapter-item-btn, body.theme-gold .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.7) 0%, rgba(255,215,80,0.55) 50%, rgba(230,180,30,0.35) 100%) !important;}
|
||||
body.theme-gold .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.65) 0%, rgba(255,225,120,0.45) 50%, rgba(240,195,60,0.25) 100%) !important;}
|
||||
body.theme-gold .progress-fill { background:linear-gradient(90deg, #fff, #ffd740) !important; box-shadow:0 0 6px rgba(255,200,40,0.5) !important; }
|
||||
body.theme-gold .loading-spinner { border-color:rgba(255,210,60,0.25) !important; border-top-color:#ffbc20 !important; }
|
||||
body.theme-gold .toast { background:linear-gradient(180deg, rgba(255,255,255,0.7) 0%, rgba(255,215,80,0.55) 50%, rgba(230,180,30,0.35) 100%) !important; border-color:rgba(255,200,40,0.4) !important; color:#5c3f00 !important; }
|
||||
/* 金灿灿亮面黄金UI - 明亮奢华 */
|
||||
body.theme-gold .top-nav, body.theme-gold .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-gold .top-nav .current { color:#b88600 !important; font-weight:700; }
|
||||
body.theme-gold .top-nav .split { color:rgba(92,63,0,0.4) !important; }
|
||||
body.theme-gold #reader, body.theme-gold #reader * { color:#6b4a00 !important; }
|
||||
body.theme-gold .reader-menu { background:rgba(255,248,220,0.92) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(180,130,20,0.15), inset 0 2px 4px rgba(255,255,255,0.6), inset 0 -2px 4px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-gold .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.7) 0%, rgba(255,215,80,0.55) 50%, rgba(230,180,30,0.35) 100%) !important; color:#5c3f00 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(200,150,30,0.15), inset 0 2px 4px rgba(255,255,255,0.6), inset 0 -2px 4px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-gold .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.65) 0%, rgba(255,225,120,0.45) 100%) !important; color:#5c3f00 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.6), inset 0 -1px 2px rgba(0,0,0,0.03) !important; }
|
||||
body.theme-gold .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.75) 0%, rgba(255,210,60,0.6) 50%, rgba(235,185,35,0.4) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(255,200,40,0.3), inset 0 2px 4px rgba(255,255,255,0.6), inset 0 -2px 4px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-gold .bookmark-btn, body.theme-gold .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.7) 0%, rgba(255,215,80,0.55) 50%, rgba(230,180,30,0.35) 100%) !important; color:#5c3f00 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(200,150,30,0.15), inset 0 2px 4px rgba(255,255,255,0.6), inset 0 -2px 4px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-gold .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.65) 0%, rgba(255,225,120,0.45) 100%) !important; color:#5c3f00 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.6), inset 0 -1px 2px rgba(0,0,0,0.03) !important; }
|
||||
body.theme-gold .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.75) 0%, rgba(255,210,60,0.6) 50%, rgba(235,185,35,0.4) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(255,200,40,0.3), inset 0 2px 4px rgba(255,255,255,0.6), inset 0 -2px 4px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-gold .bookmark-panel { background:rgba(255,248,220,0.92) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(180,130,20,0.15), inset 0 2px 4px rgba(255,255,255,0.6), inset 0 -2px 4px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-gold .bm-header { color:#5c3f00 !important; }
|
||||
body.theme-gold .bm-item { background:rgba(255,235,170,0.75) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#5c3f00 !important; box-shadow: 0 2px 8px rgba(180,130,20,0.1), inset 0 1px 3px rgba(255,255,255,0.6), inset 0 -1px 3px rgba(0,0,0,0.03) !important; }
|
||||
body.theme-gold .bm-item .book, body.theme-gold .bm-item .chap, body.theme-gold .page { color:#704e00 !important; }
|
||||
body.theme-gold .canvas-container { background:rgba(255,210,60,0.15) !important; }
|
||||
body.theme-gold .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(255,245,210,0.95) 40%) !important; }
|
||||
body.theme-gold .progress-track { background:rgba(255,200,40,0.2) !important; }
|
||||
body.theme-gold .progress-text { color:rgba(92,63,0,0.75) !important; }
|
||||
body.theme-gold .chapter-indicator { color:#5c3f00 !important; }
|
||||
body.theme-gold .ebook-chapter { background:rgba(255,252,235,0.96) !important; color:#6b4a00 !important; }
|
||||
body.theme-gold .ebook-chapter .chapter-title { color:#b88600 !important; }
|
||||
body.theme-gold .menu-title, body.theme-gold .section-title { color:#5c3f00 !important; }
|
||||
body.theme-gold .back-btn { color:#5c3f00 !important; background:rgba(255,255,255,0.65) !important; }
|
||||
body.theme-gold .page-title { color:#5c3f00 !important; }
|
||||
body.theme-gold .shelf-item { color:#6b4a00 !important; }
|
||||
body.theme-gold h1, body.theme-gold h3, body.theme-gold h3 a { color:#5c3f00 !important; }
|
||||
body.theme-gold .item a { color:#6b4a00 !important; }
|
||||
body.theme-gold .book-chapter-item a { color:#5c3f00 !important; }
|
||||
|
||||
/* 熔岩机甲 */
|
||||
body.theme-mecha { background:#1a1a1a !important; color:#ff7722 !important; }
|
||||
body.theme-mecha .top-nav, body.theme-mecha h1, body.theme-mecha h3,
|
||||
body.theme-mecha .item, body.theme-mecha .bottom-nav, body.theme-mecha .reader-menu,
|
||||
body.theme-mecha .bookmark-panel, body.theme-mecha .section-box,
|
||||
body.theme-mecha .auto-scroll-btn, body.theme-mecha .speed-btn, body.theme-mecha .speed-btn.active,
|
||||
body.theme-mecha .bookmark-btn, body.theme-mecha .bookmark-btn.second,
|
||||
body.theme-mecha .chapter-item-btn, body.theme-mecha .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.6) 50%, rgba(255,80,0,0.4) 100%) !important;}
|
||||
body.theme-mecha .book-chapter-item {background:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.5) 50%, rgba(255,80,0,0.3) 100%) !important;}
|
||||
body.theme-mecha .progress-fill { background:linear-gradient(90deg, #ff5500, #ff9933) !important; box-shadow:0 0 6px rgba(255,80,0,0.6) !important; }
|
||||
body.theme-mecha .loading-spinner { border-color:rgba(255,100,30,0.2) !important; border-top-color:#ff5500 !important; }
|
||||
body.theme-mecha .toast { background:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.6) 50%, rgba(255,80,0,0.4) 100%) !important; border-color:rgba(255,100,30,0.5) !important; color:#ffcc99 !important; }
|
||||
/* 熔岩机甲 - 阅读页面UI */
|
||||
body.theme-mecha .top-nav, body.theme-mecha .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-mecha .top-nav .current { color:#ff5500 !important; font-weight:700; }
|
||||
body.theme-mecha .top-nav .split { color:rgba(255,150,50,0.4) !important; }
|
||||
body.theme-mecha #reader, body.theme-mecha #reader * { color:#ffcc99 !important; }
|
||||
body.theme-mecha .reader-menu { background:rgba(40,40,40,0.9) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 0 2px 4px rgba(255,100,30,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-mecha .auto-scroll-btn { background:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.6) 50%, rgba(255,80,0,0.4) 100%) !important; color:#ffcc99 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(255,80,0,0.4), inset 0 2px 4px rgba(255,100,30,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-mecha .speed-btn { background:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.5) 100%) !important; color:#ffcc99 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,100,30,0.3), inset 0 -1px 2px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-mecha .speed-btn.active { background:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.7) 50%, rgba(255,80,0,0.5) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(255,80,0,0.6), inset 0 2px 4px rgba(255,100,30,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-mecha .bookmark-btn, body.theme-mecha .bookmark-btn.second { background:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.6) 50%, rgba(255,80,0,0.4) 100%) !important; color:#ffcc99 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(255,80,0,0.4), inset 0 2px 4px rgba(255,100,30,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-mecha .chapter-item-btn { background:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.5) 100%) !important; color:#ffcc99 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,100,30,0.3), inset 0 -1px 2px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-mecha .chapter-item-btn.active { background:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.7) 50%, rgba(255,80,0,0.5) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(255,80,0,0.6), inset 0 2px 4px rgba(255,100,30,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-mecha .bookmark-panel { background:rgba(40,40,40,0.9) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 0 2px 4px rgba(255,100,30,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-mecha .bm-header { color:#ffcc99 !important; }
|
||||
body.theme-mecha .bm-item { background:linear-gradient(180deg, rgba(80,80,80,0.8) 0%, rgba(255,100,30,0.6) 50%, rgba(255,80,0,0.4) 100%) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#ffcc99 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.3), inset 0 1px 3px rgba(255,100,30,0.3), inset 0 -1px 3px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-mecha .bm-item .book, body.theme-mecha .bm-item .chap, body.theme-mecha .bm-item .page { color:#ff9933 !important; }
|
||||
body.theme-mecha .canvas-container { background:rgba(255,100,30,0.15) !important; }
|
||||
body.theme-mecha .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(40,40,40,0.95) 40%) !important; }
|
||||
body.theme-mecha .progress-track { background:rgba(255,100,30,0.2) !important; }
|
||||
body.theme-mecha .progress-text { color:rgba(255,150,50,0.7) !important; }
|
||||
body.theme-mecha .chapter-indicator { color:#ffcc99 !important; }
|
||||
body.theme-mecha .ebook-chapter { background:rgba(50,50,50,0.95) !important; color:#ffcc99 !important; }
|
||||
body.theme-mecha .ebook-chapter .chapter-title { color:#ff5500 !important; }
|
||||
body.theme-mecha .menu-title, body.theme-mecha .section-title { color:#ffcc99 !important; }
|
||||
body.theme-mecha .back-btn { color:#ffcc99 !important; background:rgba(80,80,80,0.8) !important; }
|
||||
body.theme-mecha .page-title { color:#ffcc99 !important; }
|
||||
body.theme-mecha .shelf-item { color:#ffcc99 !important; }
|
||||
body.theme-mecha h1, body.theme-mecha h3, body.theme-mecha h3 a { color:#ffcc99 !important; }
|
||||
body.theme-mecha .item a { color:#ffcc99 !important; }
|
||||
body.theme-mecha .book-chapter-item a { color:#ffcc99 !important; }
|
||||
|
||||
/* 复古宫廷 */
|
||||
body.theme-royal { background:#4a1a1a !important; color:#e6c28a !important; }
|
||||
body.theme-royal .top-nav, body.theme-royal h1, body.theme-royal h3,
|
||||
body.theme-royal .item, body.theme-royal .bottom-nav, body.theme-royal .reader-menu,
|
||||
body.theme-royal .bookmark-panel, body.theme-royal .section-box,
|
||||
body.theme-royal .auto-scroll-btn, body.theme-royal .speed-btn, body.theme-royal .speed-btn.active,
|
||||
body.theme-royal .bookmark-btn, body.theme-royal .bookmark-btn.second,
|
||||
body.theme-royal .chapter-item-btn, body.theme-royal .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.5) 50%, rgba(180,120,60,0.3) 100%) !important;}
|
||||
body.theme-royal .book-chapter-item {background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.4) 50%, rgba(180,120,60,0.2) 100%) !important;}
|
||||
body.theme-royal .progress-fill { background:linear-gradient(90deg, #b8860b, #daa520) !important; box-shadow:0 0 6px rgba(218,165,32,0.5) !important; }
|
||||
body.theme-royal .loading-spinner { border-color:rgba(218,165,32,0.2) !important; border-top-color:#b8860b !important; }
|
||||
body.theme-royal .toast { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.5) 50%, rgba(180,120,60,0.3) 100%) !important; border-color:rgba(218,165,32,0.4) !important; color:#f0e6d2 !important; }
|
||||
/* 复古宫廷 - 阅读页面UI */
|
||||
body.theme-royal .top-nav, body.theme-royal .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-royal .top-nav .current { color:#daa520 !important; font-weight:700; }
|
||||
body.theme-royal .top-nav .split { color:rgba(230,194,138,0.4) !important; }
|
||||
body.theme-royal #reader, body.theme-royal #reader * { color:#f0e6d2 !important; }
|
||||
body.theme-royal .reader-menu { background:rgba(100,40,40,0.9) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .auto-scroll-btn { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.5) 50%, rgba(180,120,60,0.3) 100%) !important; color:#f0e6d2 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(218,165,32,0.3), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .speed-btn { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.4) 100%) !important; color:#f0e6d2 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -1px 2px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .speed-btn.active { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.6) 50%, rgba(180,120,60,0.4) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(218,165,32,0.5), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .bookmark-btn, body.theme-royal .bookmark-btn.second { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.5) 50%, rgba(180,120,60,0.3) 100%) !important; color:#f0e6d2 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(218,165,32,0.3), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .chapter-item-btn { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.4) 100%) !important; color:#f0e6d2 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -1px 2px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .chapter-item-btn.active { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.6) 50%, rgba(180,120,60,0.4) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(218,165,32,0.5), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .bookmark-panel { background:rgba(100,40,40,0.9) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .bm-header { color:#e6c28a !important; }
|
||||
body.theme-royal .bm-item { background:rgba(130,60,60,0.8) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#f0e6d2 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.3), inset 0 1px 3px rgba(218,165,32,0.3), inset 0 -1px 3px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .bm-item .book, body.theme-royal .bm-item .chap, body.theme-royal .bm-item .page { color:#daa520 !important; }
|
||||
body.theme-royal .canvas-container { background:rgba(218,165,32,0.15) !important; }
|
||||
body.theme-royal .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(100,40,40,0.95) 40%) !important; }
|
||||
body.theme-royal .progress-track { background:rgba(218,165,32,0.2) !important; }
|
||||
body.theme-royal .progress-text { color:rgba(230,194,138,0.7) !important; }
|
||||
body.theme-royal .chapter-indicator { color:#f0e6d2 !important; }
|
||||
body.theme-royal .ebook-chapter { background:rgba(120,50,50,0.95) !important; color:#f0e6d2 !important; }
|
||||
body.theme-royal .ebook-chapter .chapter-title { color:#daa520 !important; }
|
||||
body.theme-royal .menu-title, body.theme-royal .section-title { color:#e6c28a !important; }
|
||||
body.theme-royal .back-btn { color:#e6c28a !important; background:rgba(150,80,80,0.7) !important; }
|
||||
body.theme-royal .page-title { color:#e6c28a !important; }
|
||||
body.theme-royal .shelf-item { color:#e6c28a !important; }
|
||||
body.theme-royal h1, body.theme-royal h3, body.theme-royal h3 a { color:#e6c28a !important; }
|
||||
body.theme-royal .item a { color:#e6c28a !important; }
|
||||
body.theme-royal .book-chapter-item a { color:#e6c28a !important; }
|
||||
|
||||
/* 复古宫廷 */
|
||||
body.theme-royal { background:#4a1a1a !important; color:#e6c28a !important; }
|
||||
body.theme-royal .top-nav, body.theme-royal h1, body.theme-royal h3,
|
||||
body.theme-royal .item, body.theme-royal .bottom-nav, body.theme-royal .reader-menu,
|
||||
body.theme-royal .bookmark-panel, body.theme-royal .section-box,
|
||||
body.theme-royal .auto-scroll-btn, body.theme-royal .speed-btn, body.theme-royal .speed-btn.active,
|
||||
body.theme-royal .bookmark-btn, body.theme-royal .bookmark-btn.second,
|
||||
body.theme-royal .chapter-item-btn, body.theme-royal .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.5) 50%, rgba(180,120,60,0.3) 100%) !important;}
|
||||
body.theme-royal .book-chapter-item {background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.4) 50%, rgba(180,120,60,0.2) 100%) !important;}
|
||||
body.theme-royal .progress-fill { background:linear-gradient(90deg, #b8860b, #daa520) !important; box-shadow:0 0 6px rgba(218,165,32,0.5) !important; }
|
||||
body.theme-royal .loading-spinner { border-color:rgba(218,165,32,0.2) !important; border-top-color:#b8860b !important; }
|
||||
body.theme-royal .toast { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.5) 50%, rgba(180,120,60,0.3) 100%) !important; border-color:rgba(218,165,32,0.4) !important; color:#f0e6d2 !important; }
|
||||
/* 复古宫廷 - 阅读页面UI */
|
||||
body.theme-royal .top-nav, body.theme-royal .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-royal .top-nav .current { color:#daa520 !important; font-weight:700; }
|
||||
body.theme-royal .top-nav .split { color:rgba(230,194,138,0.4) !important; }
|
||||
body.theme-royal #reader, body.theme-royal #reader * { color:#f0e6d2 !important; }
|
||||
body.theme-royal .reader-menu { background:rgba(100,40,40,0.9) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .auto-scroll-btn { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.5) 50%, rgba(180,120,60,0.3) 100%) !important; color:#f0e6d2 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(218,165,32,0.3), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .speed-btn { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.4) 100%) !important; color:#f0e6d2 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -1px 2px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .speed-btn.active { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.6) 50%, rgba(180,120,60,0.4) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(218,165,32,0.5), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .bookmark-btn, body.theme-royal .bookmark-btn.second { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.5) 50%, rgba(180,120,60,0.3) 100%) !important; color:#f0e6d2 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(218,165,32,0.3), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .chapter-item-btn { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.4) 100%) !important; color:#f0e6d2 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -1px 2px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .chapter-item-btn.active { background:linear-gradient(180deg, rgba(150,80,80,0.7) 0%, rgba(200,150,80,0.6) 50%, rgba(180,120,60,0.4) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(218,165,32,0.5), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .bookmark-panel { background:rgba(100,40,40,0.9) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 2px 4px rgba(218,165,32,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .bm-header { color:#e6c28a !important; }
|
||||
body.theme-royal .bm-item { background:rgba(130,60,60,0.8) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#f0e6d2 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.3), inset 0 1px 3px rgba(218,165,32,0.3), inset 0 -1px 3px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-royal .bm-item .book, body.theme-royal .bm-item .chap, body.theme-royal .bm-item .page { color:#daa520 !important; }
|
||||
body.theme-royal .canvas-container { background:rgba(218,165,32,0.15) !important; }
|
||||
body.theme-royal .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(100,40,40,0.95) 40%) !important; }
|
||||
body.theme-royal .progress-track { background:rgba(218,165,32,0.2) !important; }
|
||||
body.theme-royal .progress-text { color:rgba(230,194,138,0.7) !important; }
|
||||
body.theme-royal .chapter-indicator { color:#f0e6d2 !important; }
|
||||
body.theme-royal .ebook-chapter { background:rgba(120,50,50,0.95) !important; color:#f0e6d2 !important; }
|
||||
body.theme-royal .ebook-chapter .chapter-title { color:#daa520 !important; }
|
||||
body.theme-royal .menu-title, body.theme-royal .section-title { color:#e6c28a !important; }
|
||||
body.theme-royal .back-btn { color:#e6c28a !important; background:rgba(150,80,80,0.7) !important; }
|
||||
body.theme-royal .page-title { color:#e6c28a !important; }
|
||||
body.theme-royal .shelf-item { color:#e6c28a !important; }
|
||||
body.theme-royal h1, body.theme-royal h3, body.theme-royal h3 a { color:#e6c28a !important; }
|
||||
body.theme-royal .item a { color:#e6c28a !important; }
|
||||
body.theme-royal .book-chapter-item a { color:#e6c28a !important; }
|
||||
|
||||
/* 橘黄暖阳 */
|
||||
body.theme-orangea { background:#f5f0e8 !important; color:#2d1f0e !important; }
|
||||
body.theme-orangea .top-nav, body.theme-orangea h1, body.theme-orangea h3,
|
||||
body.theme-orangea .item, body.theme-orangea .bottom-nav, body.theme-orangea .reader-menu,
|
||||
body.theme-orangea .bookmark-panel, body.theme-orangea .section-box,
|
||||
body.theme-orangea .auto-scroll-btn, body.theme-orangea .speed-btn, body.theme-orangea .speed-btn.active,
|
||||
body.theme-orangea .bookmark-btn, body.theme-orangea .bookmark-btn.second,
|
||||
body.theme-orangea .chapter-item-btn, body.theme-orangea .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.35) 50%, rgba(255,140,30,0.2) 100%) !important;}
|
||||
body.theme-orangea .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,180,100,0.3) 50%, rgba(255,160,60,0.15) 100%) !important;}
|
||||
body.theme-orangea .progress-fill { background:linear-gradient(90deg, #ff8c42, #ffaa5a) !important; box-shadow:0 0 6px rgba(255,140,66,0.4) !important; }
|
||||
body.theme-orangea .loading-spinner { border-color:rgba(255,140,66,0.2) !important; border-top-color:#ff8c42 !important; }
|
||||
body.theme-orangea .toast { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.35) 50%, rgba(255,140,30,0.2) 100%) !important; border-color:rgba(255,160,60,0.3) !important; color:#3d1f0e !important; }
|
||||
/* 橘黄暖阳 - 阅读页面UI */
|
||||
body.theme-orangea .top-nav, body.theme-orangea .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-orangea .top-nav .current { color:#b85c00 !important; font-weight:700; }
|
||||
body.theme-orangea .top-nav .split { color:rgba(60,30,10,0.4) !important; }
|
||||
body.theme-orangea #reader, body.theme-orangea #reader * { color:#3d1f0e !important; }
|
||||
body.theme-orangea .reader-menu { background:rgba(255,240,220,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orangea .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.35) 50%, rgba(255,140,30,0.2) 100%) !important; color:#3d1f0e !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orangea .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,180,100,0.3) 100%) !important; color:#3d1f0e !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-orangea .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.4) 50%, rgba(255,140,30,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(255,100,30,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orangea .bookmark-btn, body.theme-orangea .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.35) 50%, rgba(255,140,30,0.2) 100%) !important; color:#3d1f0e !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orangea .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,180,100,0.3) 100%) !important; color:#3d1f0e !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-orangea .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,160,60,0.4) 50%, rgba(255,140,30,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(255,100,30,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orangea .bookmark-panel { background:rgba(255,240,220,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-orangea .bm-header { color:#3d1f0e !important; }
|
||||
body.theme-orangea .bm-item { background:rgba(255,220,180,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#3d1f0e !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-orangea .bm-item .book, body.theme-orangea .bm-item .chap, body.theme-orangea .bm-item .page { color:#5a3a1a !important; }
|
||||
body.theme-orangea .canvas-container { background:rgba(255,180,100,0.12) !important; }
|
||||
body.theme-orangea .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(245,230,210,0.95) 40%) !important; }
|
||||
body.theme-orangea .progress-track { background:rgba(200,140,80,0.15) !important; }
|
||||
body.theme-orangea .progress-text { color:rgba(60,30,10,0.7) !important; }
|
||||
body.theme-orangea .chapter-indicator { color:#3d1f0e !important; }
|
||||
body.theme-orangea .ebook-chapter { background:rgba(255,248,240,0.92) !important; color:#3d1f0e !important; }
|
||||
body.theme-orangea .ebook-chapter .chapter-title { color:#b85c00 !important; }
|
||||
body.theme-orangea .menu-title, body.theme-orangea .section-title { color:#3d1f0e !important; }
|
||||
body.theme-orangea .back-btn { color:#3d1f0e !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-orangea .page-title { color:#3d1f0e !important; }
|
||||
body.theme-orangea .shelf-item { color:#3d1f0e !important; }
|
||||
body.theme-orangea h1, body.theme-orangea h3, body.theme-orangea h3 a { color:#3d1f0e !important; }
|
||||
body.theme-orangea .item a { color:#3d1f0e !important; }
|
||||
body.theme-orangea .book-chapter-item a { color:#3d1f0e !important; }
|
||||
|
||||
/* 翡翠宫廷 */
|
||||
body.theme-emeraldd { background:#0d2b1d !important; color:#c9b06c !important; }
|
||||
body.theme-emeraldd .top-nav, body.theme-emeraldd h1, body.theme-emeraldd h3,
|
||||
body.theme-emeraldd .item, body.theme-emeraldd .bottom-nav, body.theme-emeraldd .reader-menu,
|
||||
body.theme-emeraldd .bookmark-panel, body.theme-emeraldd .section-box,
|
||||
body.theme-emeraldd .auto-scroll-btn, body.theme-emeraldd .speed-btn, body.theme-emeraldd .speed-btn.active,
|
||||
body.theme-emeraldd .bookmark-btn, body.theme-emeraldd .bookmark-btn.second,
|
||||
body.theme-emeraldd .chapter-item-btn, body.theme-emeraldd .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(20,80,60,0.8) 0%, rgba(40,180,120,0.5) 50%, rgba(30,150,100,0.3) 100%) !important;}
|
||||
body.theme-emeraldd .book-chapter-item {background:linear-gradient(180deg, rgba(20,80,60,0.8) 0%, rgba(40,180,120,0.4) 50%, rgba(30,150,100,0.2) 100%) !important;}
|
||||
body.theme-emeraldd .progress-fill { background:linear-gradient(90deg, #00b86b, #00d88b) !important; box-shadow:0 0 6px rgba(0,184,107,0.5) !important; }
|
||||
body.theme-emeraldd .loading-spinner { border-color:rgba(0,184,107,0.2) !important; border-top-color:#00b86b !important; }
|
||||
body.theme-emeraldd .toast { background:linear-gradient(180deg, rgba(20,80,60,0.8) 0%, rgba(40,180,120,0.5) 50%, rgba(30,150,100,0.3) 100%) !important; border-color:rgba(0,184,107,0.4) !important; color:#e8e0d0 !important; }
|
||||
/* 翡翠宫廷 - 阅读页面UI */
|
||||
body.theme-emeraldd .top-nav, body.theme-emeraldd .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-emeraldd .top-nav .current { color:#ffd700 !important; font-weight:700; }
|
||||
body.theme-emeraldd .top-nav .split { color:rgba(201,176,108,0.4) !important; }
|
||||
body.theme-emeraldd #reader, body.theme-emeraldd #reader * { color:#e8e0d0 !important; }
|
||||
body.theme-emeraldd .reader-menu { background:rgba(15,60,45,0.9) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 2px 4px rgba(0,184,107,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-emeraldd .auto-scroll-btn { background:linear-gradient(180deg, rgba(20,80,60,0.8) 0%, rgba(40,180,120,0.5) 50%, rgba(30,150,100,0.3) 100%) !important; color:#e8e0d0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,184,107,0.3), inset 0 2px 4px rgba(0,184,107,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-emeraldd .speed-btn { background:linear-gradient(180deg, rgba(20,80,60,0.8) 0%, rgba(40,180,120,0.4) 100%) !important; color:#e8e0d0 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(0,184,107,0.3), inset 0 -1px 2px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-emeraldd .speed-btn.active { background:linear-gradient(180deg, rgba(20,80,60,0.8) 0%, rgba(40,180,120,0.6) 50%, rgba(30,150,100,0.4) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(0,184,107,0.5), inset 0 2px 4px rgba(0,184,107,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-emeraldd .bookmark-btn, body.theme-emeraldd .bookmark-btn.second { background:linear-gradient(180deg, rgba(20,80,60,0.8) 0%, rgba(40,180,120,0.5) 50%, rgba(30,150,100,0.3) 100%) !important; color:#e8e0d0 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,184,107,0.3), inset 0 2px 4px rgba(0,184,107,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-emeraldd .chapter-item-btn { background:linear-gradient(180deg, rgba(20,80,60,0.8) 0%, rgba(40,180,120,0.4) 100%) !important; color:#e8e0d0 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(0,184,107,0.3), inset 0 -1px 2px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-emeraldd .chapter-item-btn.active { background:linear-gradient(180deg, rgba(20,80,60,0.8) 0%, rgba(40,180,120,0.6) 50%, rgba(30,150,100,0.4) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(0,184,107,0.5), inset 0 2px 4px rgba(0,184,107,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-emeraldd .bookmark-panel { background:rgba(15,60,45,0.9) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 2px 4px rgba(0,184,107,0.3), inset 0 -2px 4px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-emeraldd .bm-header { color:#c9b06c !important; }
|
||||
body.theme-emeraldd .bm-item { background:rgba(30,100,75,0.8) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#e8e0d0 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.3), inset 0 1px 3px rgba(0,184,107,0.3), inset 0 -1px 3px rgba(0,0,0,0.2) !important; }
|
||||
body.theme-emeraldd .bm-item .book, body.theme-emeraldd .bm-item .chap, body.theme-emeraldd .bm-item .page { color:#ffd700 !important; }
|
||||
body.theme-emeraldd .canvas-container { background:rgba(0,184,107,0.15) !important; }
|
||||
body.theme-emeraldd .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(15,60,45,0.95) 40%) !important; }
|
||||
body.theme-emeraldd .progress-track { background:rgba(0,184,107,0.2) !important; }
|
||||
body.theme-emeraldd .progress-text { color:rgba(201,176,108,0.7) !important; }
|
||||
body.theme-emeraldd .chapter-indicator { color:#e8e0d0 !important; }
|
||||
body.theme-emeraldd .ebook-chapter { background:rgba(25,85,65,0.95) !important; color:#e8e0d0 !important; }
|
||||
body.theme-emeraldd .ebook-chapter .chapter-title { color:#ffd700 !important; }
|
||||
body.theme-emeraldd .menu-title, body.theme-emeraldd .section-title { color:#c9b06c !important; }
|
||||
body.theme-emeraldd .back-btn { color:#c9b06c !important; background:rgba(20,80,60,0.8) !important; }
|
||||
body.theme-emeraldd .page-title { color:#c9b06c !important; }
|
||||
body.theme-emeraldd .shelf-item { color:#c9b06c !important; }
|
||||
body.theme-emeraldd h1, body.theme-emeraldd h3, body.theme-emeraldd h3 a { color:#c9b06c !important; }
|
||||
body.theme-emeraldd .item a { color:#c9b06c !important; }
|
||||
body.theme-emeraldd .book-chapter-item a { color:#c9b06c !important; }
|
||||
|
||||
/* 森系童话 */
|
||||
body.theme-forest { background:#e8f5e9 !important; color:#2d4a35 !important; }
|
||||
body.theme-forest .top-nav, body.theme-forest h1, body.theme-forest h3,
|
||||
body.theme-forest .item, body.theme-forest .bottom-nav, body.theme-forest .reader-menu,
|
||||
body.theme-forest .bookmark-panel, body.theme-forest .section-box,
|
||||
body.theme-forest .auto-scroll-btn, body.theme-forest .speed-btn, body.theme-forest .speed-btn.active,
|
||||
body.theme-forest .bookmark-btn, body.theme-forest .bookmark-btn.second,
|
||||
body.theme-forest .chapter-item-btn, body.theme-forest .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(160,230,180,0.4) 50%, rgba(120,210,150,0.2) 100%) !important;}
|
||||
body.theme-forest .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(160,230,180,0.3) 50%, rgba(120,210,150,0.15) 100%) !important;}
|
||||
body.theme-forest .progress-fill { background:linear-gradient(90deg, #66bb6a, #81c784) !important; box-shadow:0 0 6px rgba(102,187,106,0.4) !important; }
|
||||
body.theme-forest .loading-spinner { border-color:rgba(102,187,106,0.2) !important; border-top-color:#66bb6a !important; }
|
||||
body.theme-forest .toast { background:linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(160,230,180,0.4) 50%, rgba(120,210,150,0.2) 100%) !important; border-color:rgba(120,210,150,0.3) !important; color:#2d4a35 !important; }
|
||||
/* 森系童话 - 阅读页面UI */
|
||||
body.theme-forest .top-nav, body.theme-forest .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-forest .top-nav .current { color:#4caf50 !important; font-weight:700; }
|
||||
body.theme-forest .top-nav .split { color:rgba(45,74,53,0.4) !important; }
|
||||
body.theme-forest #reader, body.theme-forest #reader * { color:#2d4a35 !important; }
|
||||
body.theme-forest .reader-menu { background:rgba(230,250,235,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forest .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(160,230,180,0.4) 50%, rgba(120,210,150,0.2) 100%) !important; color:#2d4a35 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forest .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(160,230,180,0.3) 100%) !important; color:#2d4a35 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-forest .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(160,230,180,0.5) 50%, rgba(120,210,150,0.3) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(102,187,106,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forest .bookmark-btn, body.theme-forest .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(160,230,180,0.4) 50%, rgba(120,210,150,0.2) 100%) !important; color:#2d4a35 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forest .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(160,230,180,0.3) 100%) !important; color:#2d4a35 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-forest .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(160,230,180,0.5) 50%, rgba(120,210,150,0.3) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(102,187,106,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forest .bookmark-panel { background:rgba(230,250,235,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forest .bm-header { color:#2d4a35 !important; }
|
||||
body.theme-forest .bm-item { background:rgba(200,240,210,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#2d4a35 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-forest .bm-item .book, body.theme-forest .bm-item .chap, body.theme-forest .bm-item .page { color:#4caf50 !important; }
|
||||
body.theme-forest .canvas-container { background:rgba(102,187,106,0.12) !important; }
|
||||
body.theme-forest .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(230,250,235,0.95) 40%) !important; }
|
||||
body.theme-forest .progress-track { background:rgba(102,187,106,0.15) !important; }
|
||||
body.theme-forest .progress-text { color:rgba(45,74,53,0.7) !important; }
|
||||
body.theme-forest .chapter-indicator { color:#2d4a35 !important; }
|
||||
body.theme-forest .ebook-chapter { background:rgba(240,255,245,0.92) !important; color:#2d4a35 !important; }
|
||||
body.theme-forest .ebook-chapter .chapter-title { color:#4caf50 !important; }
|
||||
body.theme-forest .menu-title, body.theme-forest .section-title { color:#2d4a35 !important; }
|
||||
body.theme-forest .back-btn { color:#2d4a35 !important; background:rgba(255,255,255,0.5) !important; }
|
||||
body.theme-forest .page-title { color:#2d4a35 !important; }
|
||||
body.theme-forest .shelf-item { color:#2d4a35 !important; }
|
||||
body.theme-forest h1, body.theme-forest h3, body.theme-forest h3 a { color:#2d4a35 !important; }
|
||||
body.theme-forest .item a { color:#2d4a35 !important; }
|
||||
body.theme-forest .book-chapter-item a { color:#2d4a35 !important; }
|
||||
|
||||
/* 暗夜紫晶 */
|
||||
body.theme-purplee { background:#1a1428 !important; color:#e8e0ff !important; }
|
||||
body.theme-purplee .top-nav, body.theme-purplee h1, body.theme-purplee h3,
|
||||
body.theme-purplee .item, body.theme-purplee .bottom-nav, body.theme-purplee .reader-menu,
|
||||
body.theme-purplee .bookmark-panel, body.theme-purplee .section-box,
|
||||
body.theme-purplee .auto-scroll-btn, body.theme-purplee .speed-btn, body.theme-purplee .speed-btn.active,
|
||||
body.theme-purplee .bookmark-btn, body.theme-purplee .bookmark-btn.second,
|
||||
body.theme-purplee .chapter-item-btn, body.theme-purplee .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(160,110,255,0.35) 50%, rgba(120,80,200,0.2) 100%) !important;}
|
||||
body.theme-purplee .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(140,100,220,0.3) 50%, rgba(100,70,180,0.15) 100%) !important;}
|
||||
body.theme-purplee .progress-fill { background:linear-gradient(90deg, #9c6aff, #c09dff) !important; box-shadow:0 0 6px rgba(156,106,255,0.4) !important; }
|
||||
body.theme-purplee .loading-spinner { border-color:rgba(156,106,255,0.2) !important; border-top-color:#9c6aff !important; }
|
||||
body.theme-purplee .toast { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(160,110,255,0.35) 50%, rgba(120,80,200,0.2) 100%) !important; border-color:rgba(160,110,255,0.3) !important; color:#f0e8ff !important; }
|
||||
/* 暗夜紫晶 - 阅读页面UI */
|
||||
body.theme-purplee .top-nav, body.theme-purplee .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-purplee .top-nav .current { color:#b886ff !important; font-weight:700; }
|
||||
body.theme-purplee .top-nav .split { color:rgba(240,230,255,0.4) !important; }
|
||||
body.theme-purplee #reader, body.theme-purplee #reader * { color:#f0e8ff !important; }
|
||||
body.theme-purplee .reader-menu { background:rgba(40,30,65,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purplee .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(160,110,255,0.35) 50%, rgba(120,80,200,0.2) 100%) !important; color:#f0e8ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purplee .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(140,100,220,0.3) 100%) !important; color:#f0e8ff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-purplee .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(160,110,255,0.4) 50%, rgba(120,80,200,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(156,106,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purplee .bookmark-btn, body.theme-purplee .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(160,110,255,0.35) 50%, rgba(120,80,200,0.2) 100%) !important; color:#f0e8ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purplee .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(140,100,220,0.3) 100%) !important; color:#f0e8ff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-purplee .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(160,110,255,0.4) 50%, rgba(120,80,200,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(156,106,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purplee .bookmark-panel { background:rgba(40,30,65,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-purplee .bm-header { color:#f0e8ff !important; }
|
||||
body.theme-purplee .bm-item { background:rgba(80,60,130,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#f0e8ff !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-purplee .bm-item .book, body.theme-purplee .bm-item .chap, body.theme-purplee .bm-item .page { color:#d4c2ff !important; }
|
||||
body.theme-purplee .canvas-container { background:rgba(156,106,255,0.12) !important; }
|
||||
body.theme-purplee .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(40,30,65,0.95) 40%) !important; }
|
||||
body.theme-purplee .progress-track { background:rgba(156,106,255,0.15) !important; }
|
||||
body.theme-purplee .progress-text { color:rgba(240,230,255,0.7) !important; }
|
||||
body.theme-purplee .chapter-indicator { color:#f0e8ff !important; }
|
||||
body.theme-purplee .ebook-chapter { background:rgba(30,20,50,0.92) !important; color:#f0e8ff !important; }
|
||||
body.theme-purplee .ebook-chapter .chapter-title { color:#b886ff !important; }
|
||||
body.theme-purplee .menu-title, body.theme-purplee .section-title { color:#f0e8ff !important; }
|
||||
body.theme-purplee .back-btn { color:#f0e8ff !important; background:rgba(255,255,255,0.1) !important; }
|
||||
body.theme-purplee .page-title { color:#f0e8ff !important; }
|
||||
body.theme-purplee .shelf-item { color:#f0e8ff !important; }
|
||||
body.theme-purplee h1, body.theme-purplee h3, body.theme-purplee h3 a { color:#f0e8ff !important; }
|
||||
body.theme-purplee .item a { color:#f0e8ff !important; }
|
||||
body.theme-purplee .book-chapter-item a { color:#f0e8ff !important; }
|
||||
|
||||
/* 深海幽蓝 */
|
||||
body.theme-ocean { background:#0f1c32 !important; color:#d0e8ff !important; }
|
||||
body.theme-ocean .top-nav, body.theme-ocean h1, body.theme-ocean h3,
|
||||
body.theme-ocean .item, body.theme-ocean .bottom-nav, body.theme-ocean .reader-menu,
|
||||
body.theme-ocean .bookmark-panel, body.theme-ocean .section-box,
|
||||
body.theme-ocean .auto-scroll-btn, body.theme-ocean .speed-btn, body.theme-ocean .speed-btn.active,
|
||||
body.theme-ocean .bookmark-btn, body.theme-ocean .bookmark-btn.second,
|
||||
body.theme-ocean .chapter-item-btn, body.theme-ocean .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(80,160,255,0.35) 50%, rgba(50,120,200,0.2) 100%) !important;}
|
||||
body.theme-ocean .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(70,140,230,0.3) 50%, rgba(40,100,180,0.15) 100%) !important;}
|
||||
body.theme-ocean .progress-fill { background:linear-gradient(90deg, #42a5ff, #7fc4ff) !important; box-shadow:0 0 6px rgba(66,165,255,0.4) !important; }
|
||||
body.theme-ocean .loading-spinner { border-color:rgba(66,165,255,0.2) !important; border-top-color:#42a5ff !important; }
|
||||
body.theme-ocean .toast { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(80,160,255,0.35) 50%, rgba(50,120,200,0.2) 100%) !important; border-color:rgba(80,160,255,0.3) !important; color:#e0f0ff !important; }
|
||||
/* 深海幽蓝 - 阅读页面UI */
|
||||
body.theme-ocean .top-nav, body.theme-ocean .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-ocean .top-nav .current { color:#64b5ff !important; font-weight:700; }
|
||||
body.theme-ocean .top-nav .split { color:rgba(220,240,255,0.4) !important; }
|
||||
body.theme-ocean #reader, body.theme-ocean #reader * { color:#e0f0ff !important; }
|
||||
body.theme-ocean .reader-menu { background:rgba(20,40,75,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-ocean .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(80,160,255,0.35) 50%, rgba(50,120,200,0.2) 100%) !important; color:#e0f0ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-ocean .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(70,140,230,0.3) 100%) !important; color:#e0f0ff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-ocean .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(80,160,255,0.4) 50%, rgba(50,120,200,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(66,165,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-ocean .bookmark-btn, body.theme-ocean .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(80,160,255,0.35) 50%, rgba(50,120,200,0.2) 100%) !important; color:#e0f0ff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-ocean .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(70,140,230,0.3) 100%) !important; color:#e0f0ff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-ocean .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(80,160,255,0.4) 50%, rgba(50,120,200,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(66,165,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-ocean .bookmark-panel { background:rgba(20,40,75,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-ocean .bm-header { color:#e0f0ff !important; }
|
||||
body.theme-ocean .bm-item { background:rgba(40,80,140,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#e0f0ff !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-ocean .bm-item .book, body.theme-ocean .bm-item .chap, body.theme-ocean .bm-item .page { color:#b8d8ff !important; }
|
||||
body.theme-ocean .canvas-container { background:rgba(66,165,255,0.12) !important; }
|
||||
body.theme-ocean .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(20,40,75,0.95) 40%) !important; }
|
||||
body.theme-ocean .progress-track { background:rgba(66,165,255,0.15) !important; }
|
||||
body.theme-ocean .progress-text { color:rgba(220,240,255,0.7) !important; }
|
||||
body.theme-ocean .chapter-indicator { color:#e0f0ff !important; }
|
||||
body.theme-ocean .ebook-chapter { background:rgba(15,30,55,0.92) !important; color:#e0f0ff !important; }
|
||||
body.theme-ocean .ebook-chapter .chapter-title { color:#64b5ff !important; }
|
||||
body.theme-ocean .menu-title, body.theme-ocean .section-title { color:#e0f0ff !important; }
|
||||
body.theme-ocean .back-btn { color:#e0f0ff !important; background:rgba(255,255,255,0.08) !important; }
|
||||
body.theme-ocean .page-title { color:#e0f0ff !important; }
|
||||
body.theme-ocean .shelf-item { color:#e0f0ff !important; }
|
||||
body.theme-ocean h1, body.theme-ocean h3, body.theme-ocean h3 a { color:#e0f0ff !important; }
|
||||
body.theme-ocean .item a { color:#e0f0ff !important; }
|
||||
body.theme-ocean .book-chapter-item a { color:#e0f0ff !important; }
|
||||
|
||||
/* 极光幻彩 */
|
||||
body.theme-aurora { background:#121a2f !important; color:#f0f8ff !important; }
|
||||
body.theme-aurora .top-nav, body.theme-aurora h1, body.theme-aurora h3,
|
||||
body.theme-aurora .item, body.theme-aurora .bottom-nav, body.theme-aurora .reader-menu,
|
||||
body.theme-aurora .bookmark-panel, body.theme-aurora .section-box,
|
||||
body.theme-aurora .auto-scroll-btn, body.theme-aurora .speed-btn, body.theme-aurora .speed-btn.active,
|
||||
body.theme-aurora .bookmark-btn, body.theme-aurora .bookmark-btn.second,
|
||||
body.theme-aurora .chapter-item-btn, body.theme-aurora .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(120,220,255,0.35) 50%, rgba(80,180,220,0.2) 100%) !important;}
|
||||
body.theme-aurora .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(100,200,235,0.3) 50%, rgba(70,160,200,0.15) 100%) !important;}
|
||||
body.theme-aurora .progress-fill { background:linear-gradient(90deg, #64e8ff, #a8f0ff) !important; box-shadow:0 0 6px rgba(100,232,255,0.4) !important; }
|
||||
body.theme-aurora .loading-spinner { border-color:rgba(100,232,255,0.2) !important; border-top-color:#64e8ff !important; }
|
||||
body.theme-aurora .toast { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(120,220,255,0.35) 50%, rgba(80,180,220,0.2) 100%) !important; border-color:rgba(120,220,255,0.3) !important; color:#f8ffff !important; }
|
||||
/* 极光幻彩 - 阅读页面UI */
|
||||
body.theme-aurora .top-nav, body.theme-aurora .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-aurora .top-nav .current { color:#86f2ff !important; font-weight:700; }
|
||||
body.theme-aurora .top-nav .split { color:rgba(240,255,255,0.4) !important; }
|
||||
body.theme-aurora #reader, body.theme-aurora #reader * { color:#f8ffff !important; }
|
||||
body.theme-aurora .reader-menu { background:rgba(25,40,75,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-aurora .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(120,220,255,0.35) 50%, rgba(80,180,220,0.2) 100%) !important; color:#f8ffff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-aurora .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(100,200,235,0.3) 100%) !important; color:#f8ffff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-aurora .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(120,220,255,0.4) 50%, rgba(80,180,220,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(100,232,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-aurora .bookmark-btn, body.theme-aurora .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(120,220,255,0.35) 50%, rgba(80,180,220,0.2) 100%) !important; color:#f8ffff !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-aurora .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(100,200,235,0.3) 100%) !important; color:#f8ffff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-aurora .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(120,220,255,0.4) 50%, rgba(80,180,220,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(100,232,255,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-aurora .bookmark-panel { background:rgba(25,40,75,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-aurora .bm-header { color:#f8ffff !important; }
|
||||
body.theme-aurora .bm-item { background:rgba(50,90,140,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#f8ffff !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-aurora .bm-item .book, body.theme-aurora .bm-item .chap, body.theme-aurora .bm-item .page { color:#c8f8ff !important; }
|
||||
body.theme-aurora .canvas-container { background:rgba(100,232,255,0.12) !important; }
|
||||
body.theme-aurora .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(25,40,75,0.95) 40%) !important; }
|
||||
body.theme-aurora .progress-track { background:rgba(100,232,255,0.15) !important; }
|
||||
body.theme-aurora .progress-text { color:rgba(240,255,255,0.7) !important; }
|
||||
body.theme-aurora .chapter-indicator { color:#f8ffff !important; }
|
||||
body.theme-aurora .ebook-chapter { background:rgba(18,26,47,0.92) !important; color:#f8ffff !important; }
|
||||
body.theme-aurora .ebook-chapter .chapter-title { color:#86f2ff !important; }
|
||||
body.theme-aurora .menu-title, body.theme-aurora .section-title { color:#f8ffff !important; }
|
||||
body.theme-aurora .back-btn { color:#f8ffff !important; background:rgba(255,255,255,0.1) !important; }
|
||||
body.theme-aurora .page-title { color:#f8ffff !important; }
|
||||
body.theme-aurora .shelf-item { color:#f8ffff !important; }
|
||||
body.theme-aurora h1, body.theme-aurora h3, body.theme-aurora h3 a { color:#f8ffff !important; }
|
||||
body.theme-aurora .item a { color:#f8ffff !important; }
|
||||
body.theme-aurora .book-chapter-item a { color:#f8ffff !important; }
|
||||
|
||||
/* 黑曜鎏金 */
|
||||
body.theme-blackgold { background:#101010 !important; color:#f8e8c8 !important; }
|
||||
body.theme-blackgold .top-nav, body.theme-blackgold h1, body.theme-blackgold h3,
|
||||
body.theme-blackgold .item, body.theme-blackgold .bottom-nav, body.theme-blackgold .reader-menu,
|
||||
body.theme-blackgold .bookmark-panel, body.theme-blackgold .section-box,
|
||||
body.theme-blackgold .auto-scroll-btn, body.theme-blackgold .speed-btn, body.theme-blackgold .speed-btn.active,
|
||||
body.theme-blackgold .bookmark-btn, body.theme-blackgold .bookmark-btn.second,
|
||||
body.theme-blackgold .chapter-item-btn, body.theme-blackgold .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,200,80,0.35) 50%, rgba(220,160,40,0.2) 100%) !important;}
|
||||
body.theme-blackgold .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(240,180,70,0.3) 50%, rgba(200,140,30,0.15) 100%) !important;}
|
||||
body.theme-blackgold .progress-fill { background:linear-gradient(90deg, #e6b850, #ffd070) !important; box-shadow:0 0 6px rgba(230,184,80,0.4) !important; }
|
||||
body.theme-blackgold .loading-spinner { border-color:rgba(230,184,80,0.2) !important; border-top-color:#e6b850 !important; }
|
||||
body.theme-blackgold .toast { background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,200,80,0.35) 50%, rgba(220,160,40,0.2) 100%) !important; border-color:rgba(255,200,80,0.3) !important; color:#f8e8c8 !important; }
|
||||
/* 黑曜鎏金 - 阅读页面UI */
|
||||
body.theme-blackgold .top-nav, body.theme-blackgold .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-blackgold .top-nav .current { color:#ffc864 !important; font-weight:700; }
|
||||
body.theme-blackgold .top-nav .split { color:rgba(248,232,200,0.4) !important; }
|
||||
body.theme-blackgold #reader, body.theme-blackgold #reader * { color:#f8e8c8 !important; }
|
||||
body.theme-blackgold .reader-menu { background:rgba(30,30,30,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blackgold .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,200,80,0.35) 50%, rgba(220,160,40,0.2) 100%) !important; color:#f8e8c8 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blackgold .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(240,180,70,0.3) 100%) !important; color:#f8e8c8 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-blackgold .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,200,80,0.4) 50%, rgba(220,160,40,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(230,184,80,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blackgold .bookmark-btn, body.theme-blackgold .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,200,80,0.35) 50%, rgba(220,160,40,0.2) 100%) !important; color:#f8e8c8 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blackgold .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(240,180,70,0.3) 100%) !important; color:#f8e8c8 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-blackgold .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,200,80,0.4) 50%, rgba(220,160,40,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(230,184,80,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blackgold .bookmark-panel { background:rgba(30,30,30,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-blackgold .bm-header { color:#f8e8c8 !important; }
|
||||
body.theme-blackgold .bm-item { background:rgba(60,50,30,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#f8e8c8 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-blackgold .bm-item .book, body.theme-blackgold .bm-item .chap, body.theme-blackgold .bm-item .page { color:#f0d8a8 !important; }
|
||||
body.theme-blackgold .canvas-container { background:rgba(230,184,80,0.12) !important; }
|
||||
body.theme-blackgold .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(30,30,30,0.95) 40%) !important; }
|
||||
body.theme-blackgold .progress-track { background:rgba(230,184,80,0.15) !important; }
|
||||
body.theme-blackgold .progress-text { color:rgba(248,232,200,0.7) !important; }
|
||||
body.theme-blackgold .chapter-indicator { color:#f8e8c8 !important; }
|
||||
body.theme-blackgold .ebook-chapter { background:rgba(16,16,16,0.92) !important; color:#f8e8c8 !important; }
|
||||
body.theme-blackgold .ebook-chapter .chapter-title { color:#ffc864 !important; }
|
||||
body.theme-blackgold .menu-title, body.theme-blackgold .section-title { color:#f8e8c8 !important; }
|
||||
body.theme-blackgold .back-btn { color:#f8e8c8 !important; background:rgba(255,255,255,0.05) !important; }
|
||||
body.theme-blackgold .page-title { color:#f8e8c8 !important; }
|
||||
body.theme-blackgold .shelf-item { color:#f8e8c8 !important; }
|
||||
body.theme-blackgold h1, body.theme-blackgold h3, body.theme-blackgold h3 a { color:#f8e8c8 !important; }
|
||||
body.theme-blackgold .item a { color:#f8e8c8 !important; }
|
||||
body.theme-blackgold .book-chapter-item a { color:#f8e8c8 !important; }
|
||||
|
||||
/* 星夜绯红 */
|
||||
body.theme-crimsonn { background:#1c0f1c !important; color:#ffe0f0 !important; }
|
||||
body.theme-crimsonn .top-nav, body.theme-crimsonn h1, body.theme-crimsonn h3,
|
||||
body.theme-crimsonn .item, body.theme-crimsonn .bottom-nav, body.theme-crimsonn .reader-menu,
|
||||
body.theme-crimsonn .bookmark-panel, body.theme-crimsonn .section-box,
|
||||
body.theme-crimsonn .auto-scroll-btn, body.theme-crimsonn .speed-btn, body.theme-crimsonn .speed-btn.active,
|
||||
body.theme-crimsonn .bookmark-btn, body.theme-crimsonn .bookmark-btn.second,
|
||||
body.theme-crimsonn .chapter-item-btn, body.theme-crimsonn .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(255,90,140,0.35) 50%, rgba(220,60,110,0.2) 100%) !important;}
|
||||
body.theme-crimsonn .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(240,80,130,0.3) 50%, rgba(200,50,100,0.15) 100%) !important;}
|
||||
body.theme-crimsonn .progress-fill { background:linear-gradient(90deg, #ff5a8c, #ff8cb0) !important; box-shadow:0 0 6px rgba(255,90,140,0.4) !important; }
|
||||
body.theme-crimsonn .loading-spinner { border-color:rgba(255,90,140,0.2) !important; border-top-color:#ff5a8c !important; }
|
||||
body.theme-crimsonn .toast { background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(255,90,140,0.35) 50%, rgba(220,60,110,0.2) 100%) !important; border-color:rgba(255,90,140,0.3) !important; color:#ffe8f4 !important; }
|
||||
/* 星夜绯红 - 阅读页面UI */
|
||||
body.theme-crimsonn .top-nav, body.theme-crimsonn .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-crimsonn .top-nav .current { color:#ff86a8 !important; font-weight:700; }
|
||||
body.theme-crimsonn .top-nav .split { color:rgba(255,232,244,0.4) !important; }
|
||||
body.theme-crimsonn #reader, body.theme-crimsonn #reader * { color:#ffe8f4 !important; }
|
||||
body.theme-crimsonn .reader-menu { background:rgba(50,25,50,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimsonn .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(255,90,140,0.35) 50%, rgba(220,60,110,0.2) 100%) !important; color:#ffe8f4 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimsonn .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.06) 0%, rgba(240,80,130,0.3) 100%) !important; color:#ffe8f4 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-crimsonn .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(255,90,140,0.4) 50%, rgba(220,60,110,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(255,90,140,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimsonn .bookmark-btn, body.theme-crimsonn .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(255,90,140,0.35) 50%, rgba(220,60,110,0.2) 100%) !important; color:#ffe8f4 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimsonn .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.06) 0%, rgba(240,80,130,0.3) 100%) !important; color:#ffe8f4 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-crimsonn .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.1) 0%, rgba(255,90,140,0.4) 50%, rgba(220,60,110,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(255,90,140,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimsonn .bookmark-panel { background:rgba(50,25,50,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-crimsonn .bm-header { color:#ffe8f4 !important; }
|
||||
body.theme-crimsonn .bm-item { background:rgba(100,50,80,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#ffe8f4 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-crimsonn .bm-item .book, body.theme-crimsonn .bm-item .chap, body.theme-crimsonn .bm-item .page { color:#f8d0e4 !important; }
|
||||
body.theme-crimsonn .canvas-container { background:rgba(255,90,140,0.12) !important; }
|
||||
body.theme-crimsonn .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(50,25,50,0.95) 40%) !important; }
|
||||
body.theme-crimsonn .progress-track { background:rgba(255,90,140,0.15) !important; }
|
||||
body.theme-crimsonn .progress-text { color:rgba(255,232,244,0.7) !important; }
|
||||
body.theme-crimsonn .chapter-indicator { color:#ffe8f4 !important; }
|
||||
body.theme-crimsonn .ebook-chapter { background:rgba(28,15,28,0.92) !important; color:#ffe8f4 !important; }
|
||||
body.theme-crimsonn .ebook-chapter .chapter-title { color:#ff86a8 !important; }
|
||||
body.theme-crimsonn .menu-title, body.theme-crimsonn .section-title { color:#ffe8f4 !important; }
|
||||
body.theme-crimsonn .back-btn { color:#ffe8f4 !important; background:rgba(255,255,255,0.06) !important; }
|
||||
body.theme-crimsonn .page-title { color:#ffe8f4 !important; }
|
||||
body.theme-crimsonn .shelf-item { color:#ffe8f4 !important; }
|
||||
body.theme-crimsonn h1, body.theme-crimsonn h3, body.theme-crimsonn h3 a { color:#ffe8f4 !important; }
|
||||
body.theme-crimsonn .item a { color:#ffe8f4 !important; }
|
||||
body.theme-crimsonn .book-chapter-item a { color:#ffe8f4 !important; }
|
||||
|
||||
/* 幽林雾影 */
|
||||
body.theme-forestfog { background:#121f18 !important; color:#e0f8e8 !important; }
|
||||
body.theme-forestfog .top-nav, body.theme-forestfog h1, body.theme-forestfog h3,
|
||||
body.theme-forestfog .item, body.theme-forestfog .bottom-nav, body.theme-forestfog .reader-menu,
|
||||
body.theme-forestfog .bookmark-panel, body.theme-forestfog .section-box,
|
||||
body.theme-forestfog .auto-scroll-btn, body.theme-forestfog .speed-btn, body.theme-forestfog .speed-btn.active,
|
||||
body.theme-forestfog .bookmark-btn, body.theme-forestfog .bookmark-btn.second,
|
||||
body.theme-forestfog .chapter-item-btn, body.theme-forestfog .chapter-item-btn.active {background-image:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(100,220,140,0.35) 50%, rgba(70,180,110,0.2) 100%) !important;}
|
||||
body.theme-forestfog .book-chapter-item {background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(90,200,130,0.3) 50%, rgba(60,160,100,0.15) 100%) !important;}
|
||||
body.theme-forestfog .progress-fill { background:linear-gradient(90deg, #50d888, #80e8b0) !important; box-shadow:0 0 6px rgba(80,216,136,0.4) !important; }
|
||||
body.theme-forestfog .loading-spinner { border-color:rgba(80,216,136,0.2) !important; border-top-color:#50d888 !important; }
|
||||
body.theme-forestfog .toast { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(100,220,140,0.35) 50%, rgba(70,180,110,0.2) 100%) !important; border-color:rgba(100,220,140,0.3) !important; color:#e8ffe8 !important; }
|
||||
/* 幽林雾影 - 阅读页面UI */
|
||||
body.theme-forestfog .top-nav, body.theme-forestfog .top-nav a { color:#3d1f0e !important; }
|
||||
body.theme-forestfog .top-nav .current { color:#68e8a0 !important; font-weight:700; }
|
||||
body.theme-forestfog .top-nav .split { color:rgba(232,255,232,0.4) !important; }
|
||||
body.theme-forestfog #reader, body.theme-forestfog #reader * { color:#e8ffe8 !important; }
|
||||
body.theme-forestfog .reader-menu { background:rgba(25,45,35,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forestfog .auto-scroll-btn { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(100,220,140,0.35) 50%, rgba(70,180,110,0.2) 100%) !important; color:#e8ffe8 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forestfog .speed-btn { background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(90,200,130,0.3) 100%) !important; color:#e8ffe8 !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-forestfog .speed-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(100,220,140,0.4) 50%, rgba(70,180,110,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; box-shadow: 0 4px 16px rgba(80,216,136,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forestfog .bookmark-btn, body.theme-forestfog .bookmark-btn.second { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(100,220,140,0.35) 50%, rgba(70,180,110,0.2) 100%) !important; color:#e8ffe8 !important; backdrop-filter:blur(12px) !important; -webkit-backdrop-filter:blur(12px) !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forestfog .chapter-item-btn { background:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(90,200,130,0.3) 100%) !important; color:#e8ffe8 !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -1px 2px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-forestfog .chapter-item-btn.active { background:linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(100,220,140,0.4) 50%, rgba(70,180,110,0.25) 100%) !important; color:#fff !important; backdrop-filter:blur(6px) !important; -webkit-backdrop-filter:blur(6px) !important; box-shadow: 0 4px 16px rgba(80,216,136,0.2), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forestfog .bookmark-panel { background:rgba(25,45,35,0.85) !important; backdrop-filter:blur(20px) !important; -webkit-backdrop-filter:blur(20px) !important; box-shadow: 0 8px 32px rgba(0,0,0,0.15), inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -2px 4px rgba(0,0,0,0.06) !important; }
|
||||
body.theme-forestfog .bm-header { color:#e8ffe8 !important; }
|
||||
body.theme-forestfog .bm-item { background:rgba(50,90,70,0.6) !important; backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; color:#e8ffe8 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.08), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -1px 3px rgba(0,0,0,0.04) !important; }
|
||||
body.theme-forestfog .bm-item .book, body.theme-forestfog .bm-item .chap, body.theme-forestfog .bm-item .page { color:#c8f8d8 !important; }
|
||||
body.theme-forestfog .canvas-container { background:rgba(80,216,136,0.12) !important; }
|
||||
body.theme-forestfog .progress-bar-wrap { background:linear-gradient(180deg, transparent, rgba(25,45,35,0.95) 40%) !important; }
|
||||
body.theme-forestfog .progress-track { background:rgba(80,216,136,0.15) !important; }
|
||||
body.theme-forestfog .progress-text { color:rgba(232,255,232,0.7) !important; }
|
||||
body.theme-forestfog .chapter-indicator { color:#e8ffe8 !important; }
|
||||
body.theme-forestfog .ebook-chapter { background:rgba(18,31,24,0.92) !important; color:#e8ffe8 !important; }
|
||||
body.theme-forestfog .ebook-chapter .chapter-title { color:#68e8a0 !important; }
|
||||
body.theme-forestfog .menu-title, body.theme-forestfog .section-title { color:#e8ffe8 !important; }
|
||||
body.theme-forestfog .back-btn { color:#e8ffe8 !important; background:rgba(255,255,255,0.08) !important; }
|
||||
body.theme-forestfog .page-title { color:#e8ffe8 !important; }
|
||||
body.theme-forestfog .shelf-item { color:#e8ffe8 !important; }
|
||||
body.theme-forestfog h1, body.theme-forestfog h3, body.theme-forestfog h3 a { color:#e8ffe8 !important; }
|
||||
body.theme-forestfog .item a { color:#e8ffe8 !important; }
|
||||
body.theme-forestfog .book-chapter-item a { color:#e8ffe8 !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>document.body.className='theme-'+(localStorage.getItem('reader_theme')||'orange');</script>
|
||||
<?php if ($isChapterPage): ?>
|
||||
<div class="top-nav">
|
||||
<a href="<?=$currentFile?>">🏠 首页</a>
|
||||
<a href="<?=$currentFile?>?book=<?=rawurlencode($book)?>"><?=htmlspecialchars(mb_substr($book,0,14))?></a>
|
||||
<span class="split">/</span>
|
||||
<span class="current"><?=htmlspecialchars(mb_substr($chapterTitle,0,16))?></span>
|
||||
<button id="themeBtn" style="cursor:pointer;background:none;border:none;font-size:20px;margin-left:auto;">🎨</button>
|
||||
</div>
|
||||
<div class="toast" id="toast"></div>
|
||||
<div class="loading-overlay" id="loadingOverlay" style="display:none">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
<div class="mask" id="mask"></div>
|
||||
<div class="reader-menu" id="menu">
|
||||
<div class="menu-title">
|
||||
阅读控制
|
||||
<span class="bm-close" id="closeMenuBtn" style="float:right;cursor:pointer;font-size:20px;">✕</span>
|
||||
</div>
|
||||
<div class="section-box">
|
||||
<div class="section-title">⚙️ 自动阅读</div>
|
||||
<button class="auto-scroll-btn" id="toggleScroll">▶️ 开始自动阅读</button>
|
||||
<div class="speed-row">
|
||||
<button class="speed-btn" data-speed="1.5">🐢 慢</button>
|
||||
<button class="speed-btn active" data-speed="3">⚡ 中</button>
|
||||
<button class="speed-btn" data-speed="8">💨 快</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-box">
|
||||
<div class="section-title">📌 书签功能</div>
|
||||
<button class="bookmark-btn" id="addBookmarkBtn">
|
||||
<span>🔖</span> 添加当前页书签
|
||||
</button>
|
||||
<button class="bookmark-btn second" id="openBookmarkBtn">
|
||||
<span>📋</span> 查看我的书签
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-box">
|
||||
<div class="section-title">📖 章节目录</div>
|
||||
<div class="chapter-grid" id="chapterGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bookmark-panel" id="bookmarkPanel">
|
||||
<div class="bm-header">
|
||||
<span>📋 我的书签</span>
|
||||
<span class="bm-close" id="closeBmPanel">✕</span>
|
||||
</div>
|
||||
<div id="bookmarkList" style="padding-top:4px;">
|
||||
<div style="color:#aaa;text-align:center;padding:20px;">暂无书签</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="reader"></div>
|
||||
<div class="progress-bar-wrap" id="progressBar" style="display:none">
|
||||
<div class="progress-track">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
<span class="progress-text" id="progressText">0 / 0</span>
|
||||
</div>
|
||||
<script>
|
||||
const mask = document.getElementById('mask');
|
||||
const menu = document.getElementById('menu');
|
||||
const toast = document.getElementById('toast');
|
||||
const bookmarkPanel = document.getElementById('bookmarkPanel');
|
||||
const bookmarkList = document.getElementById('bookmarkList');
|
||||
let menuOpen = false;
|
||||
|
||||
// 手势返回:第一次回章节目录,第二次回主页
|
||||
(function() {
|
||||
let chapterUrl = "<?=$currentFile?>?book=<?=rawurlencode($book)?>";
|
||||
<?php if($isEpub): ?>
|
||||
chapterUrl += "&chapter=<?=rawurlencode($chapter)?>";
|
||||
<?php endif; ?>
|
||||
let homeUrl = "<?=$currentFile?>";
|
||||
if (location.href.indexOf('&chapter=') > -1 || location.href.indexOf('&chap=') > -1) {
|
||||
history.replaceState(null, '', location.href);
|
||||
history.pushState(null, '', chapterUrl);
|
||||
history.pushState(null, '', homeUrl);
|
||||
}
|
||||
window.addEventListener('popstate', function(e) {
|
||||
// 已经到首页就不再跳
|
||||
if (location.href.indexOf('?book=') === -1 && location.href.indexOf('&chapter=') === -1) return;
|
||||
history.back();
|
||||
});
|
||||
})();
|
||||
let lastTap = 0;
|
||||
let savedPage = 1;
|
||||
document.getElementById('reader').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const now = Date.now();
|
||||
const delta = now - lastTap;
|
||||
lastTap = now;
|
||||
if (delta > 0 && delta < 300) {
|
||||
savedPage = getCurrentPage();
|
||||
menuOpen = !menuOpen;
|
||||
menu.classList.toggle('show', menuOpen);
|
||||
mask.classList.toggle('show', menuOpen);
|
||||
if (!menuOpen) closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
let speed = 6;
|
||||
const speedBtns = document.querySelectorAll('.speed-btn');
|
||||
speedBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
speedBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
speed = Number(btn.dataset.speed);
|
||||
if (isPlaying) {
|
||||
clearInterval(scrollTimer);
|
||||
scrollTimer = setInterval(() => { window.scrollBy(0, speed); updateProgressBar(); }, 30);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const BM_KEY = "bookmarks_v6";
|
||||
const CURRENT_BOOK = <?=json_encode($book, JSON_UNESCAPED_UNICODE) ?>;
|
||||
const CURRENT_CHAPTER = <?=json_encode($chapter, JSON_UNESCAPED_UNICODE) ?>;
|
||||
const CURRENT_CHAPTER_TITLE = <?=json_encode($chapterTitle, JSON_UNESCAPED_UNICODE) ?>;
|
||||
const CURRENT_CHAP = <?=$chap?>;
|
||||
const IS_EPUB = <?=$isEpub ? 'true' : 'false'?>;
|
||||
|
||||
function getBookmarks() {
|
||||
try { return JSON.parse(localStorage.getItem(BM_KEY) || "[]"); }
|
||||
catch (e) { return []; }
|
||||
}
|
||||
function saveBookmarks(list) {
|
||||
localStorage.setItem(BM_KEY, JSON.stringify(list));
|
||||
}
|
||||
function showToast(msg) {
|
||||
closeMenu();
|
||||
toast.innerText = msg;
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => toast.classList.remove('show'), 1500);
|
||||
}
|
||||
function getCurrentPage() {
|
||||
let els = document.querySelectorAll("#reader > img, #reader > canvas");
|
||||
let best = 1;
|
||||
let maxVisible = 0;
|
||||
let vh = window.innerHeight;
|
||||
els.forEach((el, i) => {
|
||||
let r = el.getBoundingClientRect();
|
||||
let visibleTop = Math.max(r.top, 0);
|
||||
let visibleBottom = Math.min(r.bottom, vh);
|
||||
let visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
||||
if (visibleHeight > maxVisible) {
|
||||
maxVisible = visibleHeight;
|
||||
best = i + 1;
|
||||
}
|
||||
});
|
||||
return best;
|
||||
}
|
||||
function updateProgressBar() {
|
||||
let wrap = document.getElementById('progressBar');
|
||||
let fill = document.getElementById('progressFill');
|
||||
let text = document.getElementById('progressText');
|
||||
if (!wrap || !fill || !text) return;
|
||||
let total = 0;
|
||||
<?php if(!$isPdf): ?>
|
||||
total = allImages.length;
|
||||
<?php else: ?>
|
||||
total = pdfDoc ? pdfDoc.numPages : 0;
|
||||
<?php endif; ?>
|
||||
if (total > 0) {
|
||||
wrap.style.display = 'flex';
|
||||
<?php if(!$isPdf): ?>
|
||||
// 用已加载图片数量计算进度
|
||||
let loaded = document.querySelectorAll('#reader > img').length;
|
||||
let current = Math.min(total, Math.max(1, loaded));
|
||||
let pct = Math.min(100, (loaded / total) * 100);
|
||||
<?php else: ?>
|
||||
// PDF 用已渲染 canvas 数量
|
||||
let loaded = renderedPages.size;
|
||||
let current = Math.min(total, Math.max(1, loaded));
|
||||
let pct = Math.min(100, (loaded / total) * 100);
|
||||
<?php endif; ?>
|
||||
fill.style.width = pct + '%';
|
||||
text.textContent = current + ' / ' + total;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("addBookmarkBtn").onclick = () => {
|
||||
let list = getBookmarks();
|
||||
let idx = list.findIndex(b => b.book === CURRENT_BOOK);
|
||||
let item = {
|
||||
book: CURRENT_BOOK,
|
||||
chapter: CURRENT_CHAPTER,
|
||||
chapterTitle: CURRENT_CHAPTER_TITLE,
|
||||
page: savedPage,
|
||||
chap: IS_EPUB ? CURRENT_CHAP : undefined,
|
||||
time: Date.now()
|
||||
};
|
||||
idx >= 0 ? list[idx] = item : list.push(item);
|
||||
saveBookmarks(list);
|
||||
showToast(`✅ 已保存:${CURRENT_CHAPTER_TITLE} 第${savedPage}页`);
|
||||
};
|
||||
|
||||
document.getElementById('closeMenuBtn').onclick = function() {
|
||||
menu.classList.remove('show');
|
||||
mask.classList.remove('show');
|
||||
menuOpen = false;
|
||||
};
|
||||
document.getElementById("openBookmarkBtn").onclick = () => {
|
||||
menu.classList.remove('show');
|
||||
mask.classList.remove('show');
|
||||
menuOpen = false;
|
||||
renderBookmarkList();
|
||||
bookmarkPanel.classList.add("show");
|
||||
};
|
||||
document.getElementById("closeBmPanel").onclick = () => {
|
||||
bookmarkPanel.classList.remove("show");
|
||||
};
|
||||
bookmarkPanel.onclick = (e) => {
|
||||
if (e.target === bookmarkPanel) bookmarkPanel.classList.remove("show");
|
||||
};
|
||||
|
||||
function renderBookmarkList() {
|
||||
let list = getBookmarks().sort((a, b) => b.time - a.time);
|
||||
if (list.length === 0) {
|
||||
bookmarkList.innerHTML = `<div style="color:#aaa;text-align:center;padding:20px;">暂无书签</div>`;
|
||||
return;
|
||||
}
|
||||
let html = "";
|
||||
list.forEach(b => {
|
||||
html += `
|
||||
<div class="bm-item" data-book="${b.book}" data-chapter="${b.chapter}" data-page="${b.page || 1}" data-chap="${b.chap || 0}">
|
||||
<span class="bm-del">🗑️</span>
|
||||
<div class="book">${b.book}</div>
|
||||
<div class="chap">${b.chapterTitle.substring(0,4)}</div>
|
||||
<div class="page">📄 第${b.page || 1}页</div>
|
||||
</div>`;
|
||||
});
|
||||
bookmarkList.innerHTML = html;
|
||||
document.querySelectorAll(".bm-item").forEach(item => {
|
||||
item.onclick = (e) => {
|
||||
if (e.target.classList.contains("bm-del")) return;
|
||||
let book = item.dataset.book;
|
||||
let chapter = item.dataset.chapter;
|
||||
let page = item.dataset.page || 1;
|
||||
let chap = item.dataset.chap || 0;
|
||||
sessionStorage.setItem("jump_to", JSON.stringify({page: page, chap: chap}));
|
||||
let chapParam = chap ? '&chap=' + chap : '';
|
||||
location.href = `<?=$currentFile?>?book=${encodeURIComponent(book)}&chapter=${encodeURIComponent(chapter)}${chapParam}`;
|
||||
bookmarkPanel.classList.remove("show");
|
||||
};
|
||||
});
|
||||
document.querySelectorAll(".bm-del").forEach(del => {
|
||||
del.onclick = (e) => {
|
||||
let item = del.closest(".bm-item");
|
||||
let book = item.dataset.book;
|
||||
let chapter = item.dataset.chapter;
|
||||
let list = getBookmarks().filter(b => !(b.book === book && b.chapter === chapter));
|
||||
saveBookmarks(list);
|
||||
renderBookmarkList();
|
||||
showToast("🗑️ 已删除");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function jumpToPage(page) {
|
||||
let els = document.querySelectorAll("#reader > img, #reader > canvas");
|
||||
let idx = page - 1;
|
||||
if (els[idx]) {
|
||||
els[idx].scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
} else {
|
||||
showToast("⌛ 加载中…");
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
let j = sessionStorage.getItem("jump_to");
|
||||
if (j) {
|
||||
sessionStorage.removeItem("jump_to");
|
||||
try {
|
||||
let data = JSON.parse(j);
|
||||
if (data.page && data.page > 1) {
|
||||
let targetPage = data.page;
|
||||
let overlay = document.getElementById('loadingOverlay');
|
||||
if (overlay) overlay.style.display = 'flex';
|
||||
function loadUntilTarget() {
|
||||
let els = document.querySelectorAll("#reader > img, #reader > canvas");
|
||||
if (els.length >= targetPage) {
|
||||
els[targetPage - 1].scrollIntoView({ behavior: "instant", block: "start" });
|
||||
updateProgressBar();
|
||||
setTimeout(() => {
|
||||
let rect = els[targetPage - 1].getBoundingClientRect();
|
||||
if (Math.abs(rect.top) > 10) {
|
||||
els[targetPage - 1].scrollIntoView({ behavior: "instant", block: "start" });
|
||||
}
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
let rect = els[targetPage - 1].getBoundingClientRect();
|
||||
if (Math.abs(rect.top) > 10) {
|
||||
els[targetPage - 1].scrollIntoView({ behavior: "instant", block: "start" });
|
||||
}
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
<?php if(!$isPdf): ?>
|
||||
if (typeof loadMore === 'function') loadMore();
|
||||
<?php else: ?>
|
||||
if (typeof window.pdfRender === 'function') window.pdfRender();
|
||||
<?php endif; ?>
|
||||
setTimeout(loadUntilTarget, 100);
|
||||
}
|
||||
loadUntilTarget();
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
};
|
||||
|
||||
function closeMenu() {
|
||||
menuOpen = false;
|
||||
menu.classList.remove('show');
|
||||
mask.classList.remove('show');
|
||||
lock = false;
|
||||
pageLoadedTime = Date.now();
|
||||
}
|
||||
mask.addEventListener('click', closeMenu);
|
||||
|
||||
let scrollTimer = null;
|
||||
let isPlaying = false;
|
||||
const toggleBtn = document.getElementById('toggleScroll');
|
||||
function toggleScroll() {
|
||||
if (isPlaying) {
|
||||
clearInterval(scrollTimer);
|
||||
toggleBtn.innerHTML = "▶️ 开始自动阅读";
|
||||
} else {
|
||||
scrollTimer = setInterval(() => window.scrollBy(0, speed), 30);
|
||||
toggleBtn.innerHTML = "⏸️ 暂停阅读";
|
||||
}
|
||||
isPlaying = !isPlaying;
|
||||
}
|
||||
// 恢复自动滚动状态
|
||||
if (sessionStorage.getItem('autoScroll') === '1') {
|
||||
sessionStorage.removeItem('autoScroll');
|
||||
let savedSpeed = sessionStorage.getItem('autoSpeed');
|
||||
if (savedSpeed) {
|
||||
speed = parseInt(savedSpeed);
|
||||
sessionStorage.removeItem('autoSpeed');
|
||||
speedBtns.forEach(b => {
|
||||
b.classList.remove('active');
|
||||
if (Number(b.dataset.speed) === speed) b.classList.add('active');
|
||||
});
|
||||
}
|
||||
setTimeout(() => toggleScroll(), 500);
|
||||
}
|
||||
toggleBtn.addEventListener('click', toggleScroll);
|
||||
|
||||
const currentChapter = "<?=$chapter?>";
|
||||
const grid = document.getElementById('chapterGrid');
|
||||
<?php if($isEpub && !empty($epubChapters)): ?>
|
||||
const epubChapters = <?=json_encode($epubChapters, JSON_UNESCAPED_UNICODE)?>;
|
||||
const currentChap = <?=$chap?>;
|
||||
epubChapters.forEach((ch, i) => {
|
||||
let btn = document.createElement('button');
|
||||
btn.className = 'chapter-item-btn';
|
||||
if (i === currentChap) btn.classList.add('active');
|
||||
btn.innerText = ch.title;
|
||||
grid.appendChild(btn);
|
||||
btn.onclick = () => {
|
||||
location.href = "<?=$currentFile.'?book='.rawurlencode($book)?>&chapter=<?=rawurlencode($chapter)?>&chap="+i;
|
||||
};
|
||||
});
|
||||
<?php else: ?>
|
||||
const chapters = [<?php foreach($allChapters as $ch) echo '"'.basename($ch).'",'; ?>];
|
||||
const currentIdx = <?=$currentIdx?>;
|
||||
const totalChapters = chapters.length;
|
||||
chapters.forEach((name, i) => {
|
||||
let btn = document.createElement('button');
|
||||
btn.className = 'chapter-item-btn';
|
||||
if (name === currentChapter) btn.classList.add('active');
|
||||
btn.innerText = name.replace('.pdf','').replace('.epub','').replace('第','').replace('话','');
|
||||
grid.appendChild(btn);
|
||||
btn.onclick = () => {
|
||||
location.href = "<?=$currentFile.'?book='.rawurlencode($book)?>&chapter="+encodeURIComponent(name);
|
||||
};
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
let lock = false;
|
||||
let pageLoadedTime = Date.now();
|
||||
let scrollDebounceTimer;
|
||||
window.addEventListener('scroll', () => {
|
||||
clearTimeout(scrollDebounceTimer);
|
||||
scrollDebounceTimer = setTimeout(() => {
|
||||
updateProgressBar();
|
||||
if (lock || menuOpen) return;
|
||||
const top = window.scrollY;
|
||||
const bottom = window.innerHeight + top;
|
||||
const total = document.body.scrollHeight;
|
||||
if (bottom >= total - 100) {
|
||||
lock = true;
|
||||
<?php if($isEpub): ?>
|
||||
if (CURRENT_CHAP >= <?=count($epubChapters)-1?>) {
|
||||
showToast("✅ 已到最终话");
|
||||
clearInterval(scrollTimer);
|
||||
isPlaying = false;
|
||||
toggleBtn.innerHTML = "▶️ 开始自动阅读";
|
||||
setTimeout(() => lock = false, 1200);
|
||||
return;
|
||||
}
|
||||
if (isPlaying) { sessionStorage.setItem('autoScroll', '1'); sessionStorage.setItem('autoSpeed', speed); }
|
||||
location.href = "<?=$currentFile.'?book='.rawurlencode($book)?>&chapter=<?=rawurlencode($chapter)?>&chap="+(CURRENT_CHAP+1);
|
||||
<?php else: ?>
|
||||
if (currentIdx >= totalChapters - 1) {
|
||||
showToast("✅ 已到最终话");
|
||||
clearInterval(scrollTimer);
|
||||
isPlaying = false;
|
||||
toggleBtn.innerHTML = "▶️ 开始自动阅读";
|
||||
setTimeout(() => lock = false, 1200);
|
||||
return;
|
||||
}
|
||||
if (isPlaying) { sessionStorage.setItem('autoScroll', '1'); sessionStorage.setItem('autoSpeed', speed); }
|
||||
location.href = "<?=$currentFile.'?book='.rawurlencode($book)?>&chapter="+encodeURIComponent(chapters[currentIdx+1]);
|
||||
<?php endif; ?>
|
||||
}
|
||||
if (top <= 30) {
|
||||
lock = true;
|
||||
<?php if($isEpub): ?>
|
||||
if (CURRENT_CHAP <= 0) {
|
||||
showToast("✅ 已是第一话");
|
||||
setTimeout(() => lock = false, 1200);
|
||||
return;
|
||||
}
|
||||
if (isPlaying) { sessionStorage.setItem('autoScroll', '1'); sessionStorage.setItem('autoSpeed', speed); }
|
||||
location.href = "<?=$currentFile.'?book='.rawurlencode($book)?>&chapter=<?=rawurlencode($chapter)?>&chap="+(CURRENT_CHAP-1);
|
||||
<?php else: ?>
|
||||
if (currentIdx <= 0) {
|
||||
showToast("✅ 已是第一话");
|
||||
setTimeout(() => lock = false, 1200);
|
||||
return;
|
||||
}
|
||||
if (isPlaying) { sessionStorage.setItem('autoScroll', '1'); sessionStorage.setItem('autoSpeed', speed); }
|
||||
location.href = "<?=$currentFile.'?book='.rawurlencode($book)?>&chapter="+encodeURIComponent(chapters[currentIdx-1]);
|
||||
<?php endif; ?>
|
||||
}
|
||||
}, 120);
|
||||
});
|
||||
|
||||
const BATCH = 8;
|
||||
const reader = document.getElementById('reader');
|
||||
let renderedPages = new Set();
|
||||
let pdfDoc = null;
|
||||
async function renderPage(n) {
|
||||
if (renderedPages.has(n)) return;
|
||||
renderedPages.add(n);
|
||||
let p = await pdfDoc.getPage(n);
|
||||
let vp = p.getViewport({scale:1.2});
|
||||
let c = document.createElement('canvas');
|
||||
c.width=vp.width; c.height=vp.height; c.style.width='100%';
|
||||
await p.render({canvasContext:c.getContext('2d'), viewport:vp}).promise;
|
||||
reader.appendChild(c);
|
||||
}
|
||||
|
||||
<?php if(!$isPdf): ?>
|
||||
const allImages = [
|
||||
<?php foreach($images as $img): ?>
|
||||
<?php if($isEpub): ?>"<?=$currentFile?>?action=epub_img&book=<?=rawurlencode($book)?>&chapter=<?=rawurlencode($chapter)?>&path=<?=implode('/', array_map('rawurlencode', explode('/', $img)))?>"<?php else: ?>"<?=$img?>"<?php endif; ?>,
|
||||
<?php endforeach; ?>
|
||||
];
|
||||
let idx=0,loading=false;
|
||||
function loadMore(){
|
||||
if(loading||idx>=allImages.length)return;
|
||||
loading=true;
|
||||
let end=Math.min(idx+BATCH,allImages.length);
|
||||
for(let i=idx;i<end;i++){
|
||||
let im=new Image(); im.src=allImages[i]; reader.appendChild(im);
|
||||
}
|
||||
idx=end; loading=false;
|
||||
}
|
||||
window.addEventListener('scroll',()=>{
|
||||
if(window.innerHeight + window.scrollY >= document.body.scrollHeight - 800) loadMore();
|
||||
});
|
||||
loadMore();
|
||||
<?php if($isEpub): ?>
|
||||
// 保证封面和纯文字章节也能触发滚动切换
|
||||
let spacer = document.createElement('div');
|
||||
spacer.style.cssText = 'height:200vh;pointer-events:none';
|
||||
document.getElementById('reader').appendChild(spacer);
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if(!$isEpub): ?>
|
||||
(async ()=>{
|
||||
pdfDoc = await pdfjsLib.getDocument("<?=$fileUrl?>").promise;
|
||||
let p=1,rendering=false;
|
||||
async function render(){
|
||||
if(rendering) return;
|
||||
rendering=true;
|
||||
let end = Math.min(p+BATCH, pdfDoc.numPages);
|
||||
while(p<=end){
|
||||
await renderPage(p); p++;
|
||||
}
|
||||
rendering=false;
|
||||
}
|
||||
window.pdfRender = render;
|
||||
window.addEventListener('scroll',()=>{
|
||||
if(window.innerHeight + window.scrollY >= document.body.scrollHeight - 800) render();
|
||||
});
|
||||
render();
|
||||
})();
|
||||
|
||||
// ===== 主题切换(阅读页面) =====
|
||||
document.getElementById('themeBtn').onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var p = document.getElementById('themePanelChapter');
|
||||
p.style.display = p.style.display === 'flex' ? 'none' : 'flex';
|
||||
};
|
||||
function setThemeChapter(name) {
|
||||
localStorage.setItem('reader_theme', name);
|
||||
var tp = document.getElementById('themePanelChapter');
|
||||
if (tp) tp.style.display = 'none';
|
||||
document.body.className = 'theme-' + name;
|
||||
}
|
||||
(function(){
|
||||
var saved = localStorage.getItem('reader_theme');
|
||||
document.body.className = 'theme-' + (saved || 'orange');
|
||||
})();
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
|
||||
<!-- 阅读页面主题面板 -->
|
||||
<div id="themePanelChapter" style="position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(255,255,255,0.15);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.25);border-radius:20px;padding:8px 10px;width:274px;z-index:10001;display:none;box-shadow:0 8px 32px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.3);width:274px;" onclick="event.stopPropagation()">
|
||||
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:7px;justify-content:center;">
|
||||
<button onclick="setThemeChapter('orange')" style="width:34px;height:34px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff8c42 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,140,66,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('blue')" style="width:34px;height:34px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #4a90d9 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(74,144,217,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('pink')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff9eb5 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,158,181,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('green')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #5aab8a 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(90,171,138,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('dark')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #1a1a2e 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(0,0,0,0.5),inset 0 -2px 4px rgba(0,0,0,0.3),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('purple')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #7a5aff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(122,90,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
|
||||
<button onclick="setThemeChapter('crimson')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #e01020 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(220,10,30,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('lava')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #ff5a00 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,80,0,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('bronze')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #c89830 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(200,150,40,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('emerald')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #00c060 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(0,190,90,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('teal')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #00b090 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(0,170,140,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('cobalt')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #2060ff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(30,90,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
|
||||
<button onclick="setThemeChapter('violet')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #9030ff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(140,40,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('amber')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #ffb000 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,170,0,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('magenta')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #e000a0 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(220,0,160,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('indigo')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #4a40d0 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(70,60,200,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('coral')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #ff5050 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,70,70,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setThemeChapter('mint')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #20c080 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(30,190,120,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
|
||||
<button onclick="setThemeChapter('gold')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ffd740 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,215,0,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('gold')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ffd740 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,215,0,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('mecha')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff5500 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,80,0,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('royal')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #daa520 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(218,165,32,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('orangea')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff8c42 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,140,66,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('emeraldd')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #00b86b 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(0,184,107,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('forest')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #66bb6a 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(102,187,106,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
|
||||
<button onclick="setThemeChapter('purplee')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #9c6aff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(156,106,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('ocean')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #42a5ff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(66,165,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('aurora')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #64e8ff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(100,232,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('blackgold')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #e6b850 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(230,184,80,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('crimsonn')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff5a8c 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,90,140,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setThemeChapter('forestfog')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #50d888 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(80,216,136,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<?php elseif($book): ?>
|
||||
<h3 style="display:flex;align-items:center;flex-wrap:wrap;gap:4px;padding-right:12px;">
|
||||
<a href="<?=$currentFile?>">🏠 首页</a>
|
||||
<?php
|
||||
$parts = explode('/', $book);
|
||||
$path = '';
|
||||
foreach ($parts as $i => $part) {
|
||||
$path .= ($i > 0 ? '/' : '') . $part;
|
||||
$display = htmlspecialchars(mb_substr($part, 0, 8));
|
||||
echo ' / <a href="'.$currentFile.'?book='.rawurlencode($path).'">'.$display.'</a>';
|
||||
}
|
||||
?></h3>
|
||||
<div class="mask" id="maskChapter" style="display:none"></div>
|
||||
<div class="bookmark-panel" id="bookmarkPanelChapter" style="display:none">
|
||||
<div class="bm-header">
|
||||
<span>📋 我的书签</span>
|
||||
<span class="bm-close" id="closeBmPanelChapter">✕</span>
|
||||
</div>
|
||||
<div id="bookmarkListChapter" style="padding-top:4px;">
|
||||
<div style="color:#aaa;text-align:center;padding:20px;">暂无书签</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="book-chapter-grid">
|
||||
<?php
|
||||
$files=scanDirectory($baseDir.'/'.$book);
|
||||
if($files){
|
||||
foreach($files as $f){
|
||||
$n=basename($f);
|
||||
if (is_dir($f)) {
|
||||
// 检查目录下是否有图片,没有图片就走中间层(PDF由下层扫描)
|
||||
$children = scanDirectory($f);
|
||||
$hasImages = false;
|
||||
foreach ($children as $child) {
|
||||
if (!is_dir($child)) {
|
||||
$ext = strtolower(pathinfo($child, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg','jpeg','png','webp'])) {
|
||||
$hasImages = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($hasImages) {
|
||||
$u = $currentFile.'?book='.rawurlencode($book).'&chapter='.rawurlencode($n);
|
||||
echo '<div class="book-chapter-item"><a href="'.$u.'">📁 '.htmlspecialchars($n).'</a></div>';
|
||||
} else {
|
||||
$u = $currentFile.'?book='.rawurlencode($book.'/'.$n);
|
||||
echo '<div class="book-chapter-item"><a class="level2" href="'.$u.'">📁 '.htmlspecialchars($n).'</a></div>';
|
||||
}
|
||||
} elseif (stripos($n, '.pdf') !== false) {
|
||||
$u = $currentFile.'?book='.rawurlencode($book).'&chapter='.rawurlencode($n);
|
||||
echo '<div class="book-chapter-item"><a href="'.$u.'">📄 '.htmlspecialchars($n).'</a></div>';
|
||||
} elseif (stripos($n, '.epub') !== false) {
|
||||
$u = $currentFile.'?book='.rawurlencode($book).'&chapter='.rawurlencode($n);
|
||||
echo '<div class="book-chapter-item"><a href="'.$u.'">📘 '.htmlspecialchars($n).'</a></div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo '<div style="grid-column:1/-1;text-align:center;padding:40px;">暂无章节</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<h1>📚 我的书架</h1>
|
||||
<div class="mask" id="maskHome" style="display:none"></div>
|
||||
<div class="bookmark-panel" id="bookmarkPanelHome" style="display:none">
|
||||
<div class="bm-header">
|
||||
<span>📋 我的书签</span>
|
||||
<span class="bm-close" id="closeBmPanelHome">✕</span>
|
||||
</div>
|
||||
<div id="bookmarkListHome" style="padding-top:4px;">
|
||||
<div style="color:#aaa;text-align:center;padding:20px;">暂无书签</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<?php
|
||||
$books=scanDirectory($baseDir);
|
||||
if($books){
|
||||
foreach($books as $b){
|
||||
if(is_dir($b)){
|
||||
$n=basename($b);
|
||||
echo '<div class="item"><a href="'.$currentFile.'?book='.rawurlencode($n).'">📖 '.htmlspecialchars($n).'</a></div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo '<div style="grid-column:1/-1;text-align:center;padding:30px;">请放入书籍文件夹</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if($book && !$isChapterPage): ?>
|
||||
<script>
|
||||
(function() {
|
||||
const BM_KEY = "bookmarks_v6";
|
||||
const currentFile = "<?=$currentFile?>";
|
||||
const mask = document.getElementById('maskChapter');
|
||||
const panel = document.getElementById('bookmarkPanelChapter');
|
||||
const list = document.getElementById('bookmarkListChapter');
|
||||
function getBookmarks() {
|
||||
try { return JSON.parse(localStorage.getItem(BM_KEY) || "[]"); }
|
||||
catch(e) { return []; }
|
||||
}
|
||||
function renderBookmarks() {
|
||||
let bookmarks = getBookmarks().sort((a,b) => b.time - a.time);
|
||||
if (bookmarks.length === 0) {
|
||||
list.innerHTML = '<div style="color:#aaa;text-align:center;padding:20px;">暂无书签</div>';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
bookmarks.forEach(b => {
|
||||
html += '<div class="bm-item" data-book="'+b.book+'" data-chapter="'+b.chapter+'" data-page="'+(b.page||1)+'" data-chap="'+(b.chap||0)+'">' +
|
||||
'<span class="bm-del">🗑️</span>' +
|
||||
'<div class="book">'+b.book+'</div>' +
|
||||
'<div class="chap">'+((b.chapterTitle||b.chapter).substring(0,4))+'</div>' +
|
||||
'<div class="page">📄 第'+(b.page||1)+'页</div></div>';
|
||||
});
|
||||
list.innerHTML = html;
|
||||
list.querySelectorAll('.bm-item').forEach(item => {
|
||||
item.onclick = function(e) {
|
||||
if (e.target.classList.contains('bm-del')) return;
|
||||
let bk = this.dataset.book, ch = this.dataset.chapter, pg = this.dataset.page||1, cp = this.dataset.chap||0;
|
||||
let cpParam = cp ? '&chap='+cp : '';
|
||||
sessionStorage.setItem("jump_to", JSON.stringify({page: pg, chap: cp}));
|
||||
location.href = currentFile+'?book='+encodeURIComponent(bk)+'&chapter='+encodeURIComponent(ch)+cpParam;
|
||||
};
|
||||
});
|
||||
list.querySelectorAll('.bm-del').forEach(del => {
|
||||
del.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
let item = del.closest('.bm-item');
|
||||
let bm = getBookmarks().filter(b => !(b.book===item.dataset.book && b.chapter===item.dataset.chapter));
|
||||
localStorage.setItem(BM_KEY, JSON.stringify(bm));
|
||||
renderBookmarks();
|
||||
};
|
||||
});
|
||||
}
|
||||
document.getElementById('chapterBookmarkBtn').onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
renderBookmarks();
|
||||
panel.style.display = 'block';
|
||||
mask.style.display = 'block';
|
||||
mask.onclick = function() { panel.style.display = 'none'; mask.style.display = 'none'; };
|
||||
};
|
||||
document.getElementById('closeBmPanelChapter').onclick = function() {
|
||||
panel.style.display = 'none'; mask.style.display = 'none';
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php if(!$isChapterPage): ?>
|
||||
<div class="bottom-nav">
|
||||
<a href="<?=$currentFile?>">🏠</a>
|
||||
<?php if(!$book): ?>
|
||||
<button id="bottomBookmarkBtn">🔖</button>
|
||||
<?php else: ?>
|
||||
<button id="chapterBookmarkBtn">🔖</button>
|
||||
<?php endif; ?>
|
||||
<button id="themeBtn" style="cursor:pointer;">🎨</button>
|
||||
</div>
|
||||
<?php if($book): ?>
|
||||
<script>
|
||||
(function(){
|
||||
var btn = document.getElementById('chapterBookmarkBtn');
|
||||
var panel = document.getElementById('bookmarkPanelChapter');
|
||||
var mask = document.getElementById('maskChapter');
|
||||
var list = document.getElementById('bookmarkListChapter');
|
||||
var BM_KEY = "bookmarks_v6";
|
||||
function getBookmarks() { try { return JSON.parse(localStorage.getItem(BM_KEY) || "[]"); } catch(e) { return []; } }
|
||||
function renderBookmarks() {
|
||||
var bookmarks = getBookmarks().sort((a,b) => b.time - a.time);
|
||||
if (bookmarks.length === 0) { list.innerHTML = '<div style="color:#aaa;text-align:center;padding:20px;">暂无书签</div>'; return; }
|
||||
var html = '';
|
||||
bookmarks.forEach(function(b) {
|
||||
html += '<div class="bm-item" data-book="'+b.book+'" data-chapter="'+b.chapter+'" data-page="'+(b.page||1)+'" data-chap="'+(b.chap||0)+'">' +
|
||||
'<span class="bm-del">🗑️</span><div class="book">'+b.book+'</div><div class="chap">'+(b.chapterTitle||b.chapter)+'</div><div class="page">📄 第'+(b.page||1)+'页</div></div>';
|
||||
});
|
||||
list.innerHTML = html;
|
||||
list.querySelectorAll('.bm-item').forEach(function(item) {
|
||||
item.onclick = function(e) {
|
||||
if (e.target.classList.contains('bm-del')) return;
|
||||
var bk = this.dataset.book, ch = this.dataset.chapter, pg = this.dataset.page||1, cp = this.dataset.chap||0;
|
||||
var cpParam = cp ? '&chap='+cp : '';
|
||||
sessionStorage.setItem("jump_to", JSON.stringify({page: pg, chap: cp}));
|
||||
location.href = '<?=$currentFile?>?book='+encodeURIComponent(bk)+'&chapter='+encodeURIComponent(ch)+cpParam;
|
||||
};
|
||||
});
|
||||
list.querySelectorAll('.bm-del').forEach(function(del) {
|
||||
del.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var item = del.closest('.bm-item');
|
||||
var bm = getBookmarks().filter(function(b) { return !(b.book===item.dataset.book && b.chapter===item.dataset.chapter); });
|
||||
localStorage.setItem(BM_KEY, JSON.stringify(bm));
|
||||
renderBookmarks();
|
||||
};
|
||||
});
|
||||
}
|
||||
btn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
renderBookmarks();
|
||||
panel.style.display = 'block';
|
||||
mask.style.display = 'block';
|
||||
mask.onclick = function() { panel.style.display = 'none'; mask.style.display = 'none'; };
|
||||
};
|
||||
document.getElementById('closeBmPanelChapter').onclick = function() {
|
||||
panel.style.display = 'none'; mask.style.display = 'none';
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if(!$book): ?>
|
||||
<script>
|
||||
(function() {
|
||||
const BM_KEY = "bookmarks_v6";
|
||||
const currentFile = "<?=$currentFile?>";
|
||||
const mask = document.getElementById('maskHome');
|
||||
const panel = document.getElementById('bookmarkPanelHome');
|
||||
const list = document.getElementById('bookmarkListHome');
|
||||
function getBookmarks() {
|
||||
try { return JSON.parse(localStorage.getItem(BM_KEY) || "[]"); }
|
||||
catch(e) { return []; }
|
||||
}
|
||||
function renderBookmarks() {
|
||||
let bookmarks = getBookmarks().sort((a,b) => b.time - a.time);
|
||||
if (bookmarks.length === 0) {
|
||||
list.innerHTML = '<div style="color:#aaa;text-align:center;padding:20px;">暂无书签</div>';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
bookmarks.forEach(b => {
|
||||
html +=
|
||||
'<div class="bm-item" data-book="' + b.book + '" data-chapter="' + b.chapter + '" data-page="' + (b.page||1) + '" data-chap="' + (b.chap||0) + '">' +
|
||||
'<span class="bm-del" style="pointer-events:none">🗑️</span>' +
|
||||
'<div class="book">' + b.book + '</div>' +
|
||||
'<div class="chap">' + ((b.chapterTitle || b.chapter).substring(0,4)) + '</div>' +
|
||||
'<div class="page">📄 第' + (b.page||1) + '页</div>' +
|
||||
'</div>';
|
||||
});
|
||||
list.innerHTML = html;
|
||||
list.querySelectorAll('.bm-item').forEach(item => {
|
||||
item.onclick = function(e) {
|
||||
if (e.target.classList.contains('bm-del')) return;
|
||||
let book = this.dataset.book;
|
||||
let chapter = this.dataset.chapter;
|
||||
let page = this.dataset.page || 1;
|
||||
let chap = this.dataset.chap || 0;
|
||||
let chapParam = chap ? '&chap=' + chap : '';
|
||||
sessionStorage.setItem("jump_to", JSON.stringify({page: page, chap: chap}));
|
||||
location.href = currentFile + '?book=' + encodeURIComponent(book) + '&chapter=' + encodeURIComponent(chapter) + chapParam;
|
||||
};
|
||||
});
|
||||
list.querySelectorAll('.bm-del').forEach(del => {
|
||||
del.style.pointerEvents = 'auto';
|
||||
del.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
let item = del.closest('.bm-item');
|
||||
let book = item.dataset.book;
|
||||
let chapter = item.dataset.chapter;
|
||||
let bm = getBookmarks().filter(b => !(b.book === book && b.chapter === chapter));
|
||||
localStorage.setItem(BM_KEY, JSON.stringify(bm));
|
||||
renderBookmarks();
|
||||
};
|
||||
});
|
||||
}
|
||||
if (location.hash === '#bookmark') {
|
||||
setTimeout(function() {
|
||||
renderBookmarks();
|
||||
panel.style.display = 'block';
|
||||
mask.style.display = 'block';
|
||||
mask.onclick = function() {
|
||||
panel.style.display = 'none';
|
||||
mask.style.display = 'none';
|
||||
};
|
||||
history.replaceState(null, '', location.pathname);
|
||||
}, 300);
|
||||
}
|
||||
var bmBtn = document.getElementById('bottomBookmarkBtn');
|
||||
if (bmBtn) {
|
||||
bmBtn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
renderBookmarks();
|
||||
panel.style.display = 'block';
|
||||
mask.style.display = 'block';
|
||||
mask.onclick = function() {
|
||||
panel.style.display = 'none';
|
||||
mask.style.display = 'none';
|
||||
};
|
||||
};
|
||||
}
|
||||
let lastTap = 0;
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.item') || e.target.closest('.book-chapter-item') || e.target.closest('.bookmark-panel') || e.target.closest('.bm-item')) return;
|
||||
const now = Date.now();
|
||||
const delta = now - lastTap;
|
||||
lastTap = now;
|
||||
if (delta > 0 && delta < 300) {
|
||||
renderBookmarks();
|
||||
panel.style.display = 'block';
|
||||
mask.style.display = 'block';
|
||||
mask.onclick = function() {
|
||||
panel.style.display = 'none';
|
||||
mask.style.display = 'none';
|
||||
};
|
||||
}
|
||||
});
|
||||
document.getElementById('closeBmPanelHome').onclick = function() {
|
||||
panel.style.display = 'none';
|
||||
mask.style.display = 'none';
|
||||
};
|
||||
// 委托删除事件
|
||||
document.getElementById('bookmarkListHome').addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('bm-del')) {
|
||||
e.stopPropagation();
|
||||
let item = e.target.closest('.bm-item');
|
||||
let book = item.dataset.book;
|
||||
let chapter = item.dataset.chapter;
|
||||
let bookmarks = getBookmarks().filter(b => !(b.book === book && b.chapter === chapter));
|
||||
localStorage.setItem(BM_KEY, JSON.stringify(bookmarks));
|
||||
renderBookmarks();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<div id="themePanel" style="position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(255,255,255,0.15);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.25);border-radius:20px;padding:8px 10px;z-index:10001;display:none;box-shadow:0 8px 32px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.3);" onclick="event.stopPropagation()">
|
||||
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:7px;justify-content:center;">
|
||||
<button onclick="setTheme('orange')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff8c42 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,140,66,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('blue')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #4a90d9 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(74,144,217,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('pink')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff9eb5 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,158,181,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('green')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #5aab8a 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(90,171,138,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('dark')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #1a1a2e 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(0,0,0,0.5),inset 0 -2px 4px rgba(0,0,0,0.3),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('purple')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #7a5aff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(122,90,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
|
||||
<button onclick="setTheme('crimson')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #e01020 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(220,10,30,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('lava')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #ff5a00 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,80,0,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('bronze')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #c89830 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(200,150,40,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('emerald')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #00c060 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(0,190,90,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('teal')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #00b090 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(0,170,140,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('cobalt')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #2060ff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(30,90,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
|
||||
<button onclick="setTheme('violet')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #9030ff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(140,40,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('amber')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #ffb000 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,170,0,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('magenta')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #e000a0 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(220,0,160,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('indigo')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #4a40d0 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(70,60,200,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('coral')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #ff5050 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,70,70,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
<button onclick="setTheme('mint')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.5), #20c080 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(30,190,120,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.3);"></button>
|
||||
|
||||
<button onclick="setTheme('gold')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ffd740 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,215,0,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('mecha')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff5500 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,80,0,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('royal')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #daa520 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(218,165,32,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('orangea')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff8c42 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,140,66,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('emeraldd')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #00b86b 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(0,184,107,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('forest')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #66bb6a 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(102,187,106,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
|
||||
<button onclick="setTheme('purplee')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #9c6aff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(156,106,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('ocean')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #42a5ff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(66,165,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('aurora')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #64e8ff 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(100,232,255,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('blackgold')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #e6b850 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(230,184,80,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('crimsonn')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #ff5a8c 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(255,90,140,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
<button onclick="setTheme('forestfog')" style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.7), #50d888 70%);border:none;cursor:pointer;box-shadow:0 3px 10px rgba(80,216,136,0.4),inset 0 -2px 4px rgba(0,0,0,0.2),inset 0 1px 3px rgba(255,255,255,0.5);"></button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('themeBtn').onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var p = document.getElementById('themePanel');
|
||||
p.style.display = p.style.display === 'flex' ? 'none' : 'flex';
|
||||
};
|
||||
function setTheme(name) {
|
||||
localStorage.setItem('reader_theme', name);
|
||||
var tp = document.getElementById('themePanel');
|
||||
if (tp) tp.style.display = 'none';
|
||||
document.body.className = 'theme-' + name;
|
||||
}
|
||||
(function(){
|
||||
var saved = localStorage.getItem('reader_theme');
|
||||
document.body.className = 'theme-' + (saved || 'orange');
|
||||
})();
|
||||
</script>
|
||||
</html>
|
||||
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private const HOST = 'https://fanqienovel.com';
|
||||
private const API_HOST = 'https://qkfqapi.vv9v.cn';
|
||||
private const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36';
|
||||
|
||||
private $startPage = 1;
|
||||
|
||||
public function init($extend = '') {
|
||||
$this->startPage = 1;
|
||||
}
|
||||
|
||||
public function homeContent($filter = []) {
|
||||
$url = self::HOST . '/api/author/book/category_list/v0/';
|
||||
$json = $this->fetchJson($url);
|
||||
|
||||
$classes = [];
|
||||
$filters = [];
|
||||
|
||||
// 默认"全部"分类
|
||||
$classes[] = [
|
||||
'type_name' => '全部',
|
||||
'type_id' => '-1'
|
||||
];
|
||||
|
||||
if (isset($json['data'])) {
|
||||
$grouped = [];
|
||||
foreach ($json['data'] as $item) {
|
||||
$label = $item['label'];
|
||||
if (!isset($grouped[$label])) {
|
||||
$grouped[$label] = ['names' => [], 'ids' => []];
|
||||
}
|
||||
$grouped[$label]['names'][] = $item['name'];
|
||||
$grouped[$label]['ids'][] = $item['category_id'];
|
||||
}
|
||||
|
||||
foreach ($grouped as $label => $data) {
|
||||
$classes[] = [
|
||||
'type_name' => $label,
|
||||
'type_id' => $label
|
||||
];
|
||||
|
||||
$filterItems = [];
|
||||
foreach ($data['names'] as $index => $name) {
|
||||
$filterItems[] = ['n' => $name, 'v' => $data['ids'][$index]];
|
||||
}
|
||||
|
||||
$filters[$label] = [
|
||||
[
|
||||
'key' => 'category_id',
|
||||
'name' => '筛选',
|
||||
'value' => $filterItems
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => (object)$filters
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
// url: /api/author/library/book_list/v0/?page_count=18&page_index=(fypage-1)&gender=1&category_id=fyclass&creation_status=-1&word_count=-1&book_type=-1&sort=0#fyfilter
|
||||
|
||||
$categoryId = $extend['category_id'] ?? '';
|
||||
|
||||
// 如果 tid 是 '-1' (全部),且没有选筛选,则可能需要默认值或者不做筛选
|
||||
// JS 逻辑:if (MY_CATE !== '-1') ... else input = input.split('#')[0]
|
||||
// 这里简化:构建 API 参数
|
||||
|
||||
$params = [
|
||||
'page_count' => 18,
|
||||
'page_index' => $pg - 1,
|
||||
'gender' => 1,
|
||||
'category_id' => $categoryId ?: '-1', // 默认 -1 ? JS 中如果是全部,input直接去掉了 category_id 参数?
|
||||
// 观察 JS:
|
||||
// if (MY_CATE !== '-1') { category_id = input.split('#')[1]; replace... }
|
||||
// 意思是如果不是全部,必须选筛选?
|
||||
// 实际上 API 支持 category_id 参数。
|
||||
'creation_status' => -1,
|
||||
'word_count' => -1,
|
||||
'book_type' => -1,
|
||||
'sort' => 0
|
||||
];
|
||||
|
||||
// 如果 tid 不是 '-1' 且没有 category_id (即直接点了一级分类但没点筛选)
|
||||
// JS 中 filters 定义了 value 是 category_id。
|
||||
// 如果用户只点了大类(如“都市”),tid="都市"。
|
||||
// 但 API 需要具体的 category_id(数字)。
|
||||
// JS 逻辑里 filters 的 key 是 '筛选',value 是数字 ID。
|
||||
// 如果 tid 是 '全部',category_id 应该传什么?
|
||||
// 抓包看:全部 -> category_id 不传或 -1。
|
||||
//
|
||||
// 修正:DS源里 filter_url 是 '{{fl.筛选}}',即 category_id 直接取值。
|
||||
// 如果 $categoryId 为空,且 $tid 不是 -1,说明用户只选了大类没选子类。
|
||||
// 这时应该怎么办?看 JS:
|
||||
// JS 的 filters 是必须选的吗?
|
||||
// 让我们假设如果 $tid 不是 -1,但 $categoryId 为空,我们可能无法请求,或者默认取该大类下的第一个?
|
||||
// 这里的 filters 构造里,value 就是 category_id。
|
||||
|
||||
if ($tid !== '-1' && empty($categoryId)) {
|
||||
// 尝试获取该分类下的第一个 ID?或者 API 支持直接传 label?
|
||||
// 实际上 fanqienovel API 需要数字 ID。
|
||||
// 简单起见,如果未选筛选,默认 -1 (全部)
|
||||
$params['category_id'] = -1;
|
||||
}
|
||||
|
||||
$url = self::HOST . '/api/author/library/book_list/v0/?' . http_build_query($params);
|
||||
$json = $this->fetchJson($url);
|
||||
|
||||
$videos = [];
|
||||
if (isset($json['data']['book_list'])) {
|
||||
foreach ($json['data']['book_list'] as $item) {
|
||||
$videos[] = [
|
||||
'vod_id' => $item['book_id'],
|
||||
'vod_name' => $this->decodeText($item['book_name']),
|
||||
'vod_pic' => 'http://p6-novel.byteimg.com/large/' . $item['thumb_uri'],
|
||||
'vod_remarks' => $this->decodeText($item['author']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($videos, $pg, 18, 18); // total 未知,假设无限
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = $ids[0];
|
||||
$url = self::HOST . "/page/$id";
|
||||
|
||||
// 模拟 PC UA
|
||||
$html = $this->fetch($url, ['headers' => ['User-Agent' => self::UA]]);
|
||||
|
||||
// 提取 window.__INITIAL_STATE__=
|
||||
if (preg_match('/window\.__INITIAL_STATE__=(.+?);<\/script>/s', $html, $matches) ||
|
||||
preg_match('/window\.__INITIAL_STATE__=(.+?)(?:;|$)/s', $html, $matches)) {
|
||||
$jsonStr = $matches[1];
|
||||
// 替换 undefined 为 null
|
||||
$jsonStr = str_replace('undefined', 'null', $jsonStr);
|
||||
$json = json_decode($jsonStr, true);
|
||||
|
||||
if (isset($json['page'])) {
|
||||
$info = $json['page'];
|
||||
$bookInfo = $info['bookInfo'] ?? $info; // 结构可能变动
|
||||
|
||||
// 书名等信息
|
||||
$vod = [
|
||||
'vod_id' => $id,
|
||||
'vod_name' => $info['bookName'] ?? '',
|
||||
'vod_pic' => $info['thumbUri'] ?? '',
|
||||
'vod_content' => $info['abstract'] ?? '',
|
||||
'vod_remarks' => $info['lastChapterTitle'] ?? '',
|
||||
'vod_director' => $info['author'] ?? '',
|
||||
'vod_play_from' => '番茄小说',
|
||||
];
|
||||
|
||||
// 章节列表
|
||||
$playList = [];
|
||||
$chapters = $info['chapterListWithVolume'] ?? [];
|
||||
|
||||
// chapterListWithVolume 是个二维数组 [[章节...], [章节...]]
|
||||
foreach ($chapters as $volume) {
|
||||
foreach ($volume as $chapter) {
|
||||
$title = $chapter['title'];
|
||||
$itemId = $chapter['itemId'];
|
||||
$playList[] = "$title$" . $itemId . '@' . $title;
|
||||
}
|
||||
}
|
||||
|
||||
$vod['vod_play_url'] = implode('#', $playList);
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
}
|
||||
return ['list' => []];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
// URL: /api/search?key=**&tab_type=3&offset=((fypage-1)*10)
|
||||
// HOST: API_HOST
|
||||
$offset = ($pg - 1) * 10;
|
||||
$url = self::API_HOST . "/api/search?key=" . urlencode($key) . "&tab_type=3&offset=$offset";
|
||||
|
||||
$json = $this->fetchJson($url);
|
||||
|
||||
$videos = [];
|
||||
// 寻找 search_tabs[5] -> tab_type=3 ? JS 中是 search_tabs[5] 但 API 参数是 tab_type=3
|
||||
// 遍历寻找 tab_type = 3 的 tab
|
||||
$targetData = [];
|
||||
if (isset($json['data']['search_tabs'])) {
|
||||
foreach ($json['data']['search_tabs'] as $tab) {
|
||||
// JS 取下标 5,我们严谨点判断
|
||||
// 或者 API 返回的结构里 tab_type 字段
|
||||
// 假设结构类似
|
||||
if (isset($tab['data'])) {
|
||||
// 检查第一条数据是否有 book_data 且是小说
|
||||
// 简单粗暴:合并所有 tab 的 data? 不,JS 明确是小说 tab
|
||||
// 暂时取第一个包含 book_data 的
|
||||
if (!empty($tab['data']) && isset($tab['data'][0]['book_data'])) {
|
||||
$targetData = $tab['data'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($targetData as $item) {
|
||||
if (isset($item['book_data'][0])) {
|
||||
$book = $item['book_data'][0];
|
||||
$videos[] = [
|
||||
'vod_id' => $book['book_id'],
|
||||
'vod_name' => $book['book_name'],
|
||||
'vod_pic' => $book['thumb_url'],
|
||||
'vod_remarks' => $book['author'],
|
||||
'vod_content' => $book['book_abstract'] ?? $book['abstract'] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($videos, $pg, 10, 10);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
// id: itemId@title
|
||||
$parts = explode('@', $id);
|
||||
$itemId = $parts[0];
|
||||
$title = $parts[1] ?? '';
|
||||
|
||||
// Handle Title$ItemId format
|
||||
if (strpos($itemId, '$') !== false) {
|
||||
$subParts = explode('$', $itemId);
|
||||
$itemId = $subParts[1];
|
||||
if (empty($title)) {
|
||||
$title = $subParts[0];
|
||||
}
|
||||
}
|
||||
|
||||
$url = self::API_HOST . "/api/content?tab=小说&item_id=$itemId";
|
||||
|
||||
// 随机 Cookie
|
||||
$cookie = $this->getFqCookie();
|
||||
$json = $this->fetchJson($url, ['headers' => ['Cookie' => $cookie]]);
|
||||
|
||||
$content = '';
|
||||
if (isset($json['data']['content'])) {
|
||||
$content = $json['data']['content'];
|
||||
}
|
||||
|
||||
// 构造 novel:// 协议返回,或者直接文本
|
||||
// DZ 风格播放器可能不支持 novel://,通常直接返回文本或 html
|
||||
// 如果是小说,通常返回 parse=0, url=text...
|
||||
// 这里模仿 JS 返回:novel://json_string
|
||||
// 还是直接返回文本内容比较通用?
|
||||
// 为了兼容,我们返回文本内容,如果客户端支持 novel:// 最好,不支持就直接显示
|
||||
// 现在的 PHP 爬虫通常返回 content-type: text/plain
|
||||
// 但 playerContent 需要返回标准结构
|
||||
|
||||
// 构造响应
|
||||
return [
|
||||
'parse' => 0,
|
||||
'playUrl' => '',
|
||||
'url' => $url, // 仅作记录
|
||||
'header' => (object)[],
|
||||
// 如果客户端支持直接显示文本内容,通常放在 header 或其他字段?
|
||||
// 实际上,播放接口返回 content 可能需要客户端特殊处理
|
||||
// 这里我们返回一个 data url 或者 模拟的 html
|
||||
// 参考 JS: return {parse: 0, url: 'novel://' + ret}
|
||||
'url' => 'novel://' . json_encode(['title' => $title, 'content' => $content], JSON_UNESCAPED_UNICODE)
|
||||
];
|
||||
}
|
||||
|
||||
private function getFqCookie() {
|
||||
$cookies = [
|
||||
'novel_web_id=78444872394737941004',
|
||||
'novel_web_id=69258894393744181011',
|
||||
'novel_web_id=77130880221809081001',
|
||||
'novel_web_id=64945771562463261001',
|
||||
'novel_web_id=78444872394737941004',
|
||||
'novel_web_id=0000000000004011402',
|
||||
'novel_web_id=0000000303614711402',
|
||||
'novel_web_id=0144211303614711401',
|
||||
'novel_web_id=0144211303614711402',
|
||||
'novel_web_id=0144211303614711403',
|
||||
'novel_web_id=0144211303614711406',
|
||||
'novel_web_id=7357767624615331361',
|
||||
'novel_web_id=7357767624615331362',
|
||||
'novel_web_id=7357767624615331365',
|
||||
];
|
||||
return $cookies[array_rand($cookies)];
|
||||
}
|
||||
|
||||
// 解密函数
|
||||
private function decodeText($text, $type = 0) {
|
||||
$charset = [];
|
||||
if ($type === 0) {
|
||||
// ... 巨大的数组 ...
|
||||
$charset = ['体', 'y', '十', '现', '快', '便', '话', '却', '月', '物', '水', '的', '放', '知', '爱', '万', '', '表', '风', '理', 'O', '老', '也', 'p', '常', '克', '平', '几', '最', '主', '她', 's', '将', '法', '情', 'o', '光', 'a', '我', '呢', 'J', '员', '太', '每', '望', '受', '教', 'w', '利', '军', '已', 'U', '人', '如', '变', '得', '要', '少', '斯', '门', '电', 'm', '男', '没', 'A', 'K', '国', '时', '中', '走', '么', '何', '口', '小', '向', '问', '轻', 'T', 'd', '神', '下', '间', '车', 'f', 'G', '度', 'D', '又', '大', '面', '远', '就', '写', 'j', '给', '通', '起', '实', 'E', '', '它', '去', 'S', '到', '道', '数', '吃', '们', '加', 'P', '是', '无', '把', '事', '西', '多', '界', '', '发', '新', '外', '活', '解', '孩', '只', '作', '前', 'Y', '尔', '经', '', 'u', '心', '告', '父', '等', 'Q', '民', '全', '这', '9', '果', '安', '', 'i', '母', '8', 'r', '说', '任', '先', '和', '地', 'C', '张', '战', '场', 'g', '像', 'c', 'q', '你', '使', '', '样', '总', '目', 'x', '性', '处', '音', '头', '', '应', '乐', '关', '能', '花', 'l', '当', '名', '手', '4', '重', '字', '声', '力', '友', '然', '生', '代', '内', '里', '本', '回', '真', '入', '师', '象', '', '0', '点', 'R', '亲', 'V', '种', '动', '英', '命', 'Z', 'h', 'X', '做', '特', '边', '高', '有', 'B', '为', '期', '自', '年', '马', '认', '出', '接', '至', 'H', '正', '方', '感', '所', '明', '者', '稜', 'F', '住', '学', '还', '分', '意', '更', '其', 'n', '但', '比', '觉', '以', '由', '死', '家', '让', '失', '士', 'L', '2', 'I', '金', '叫', '身', '报', '听', 'W', '再', '原', '山', '海', '白', '很', '见', '5', '直', '位', '第', '工', '个', '开', '岁', '好', '用', '都', '于', '可', '同', '3', '次', '四', '', '日', '信', '与', '女', '笑', '满', '并', '部', '什', '不', '从', '或', '机', '此', '', '了', '记', '三', 'e', '些', 'b', 'N', '夫', '会', '才', '儿', '眼', '两', '美', '被', '一', '公', '来', '立', 'z', '长', '对', '己', '看', 'k', '许', '因', '相', '色', '后', '往', '打', '结', '格', '过', '世', '气', '7', '子', '条', '在', '书', '之', '定', 'v', '拉', '成', '进', '带', '着', '东', '上', '想', '天', '他', '妈', '1', '文', '而', '路', '那', '别', '德', '6', 'M', 't', '行', '候', '难'];
|
||||
} else if ($type === 1) {
|
||||
$charset = ['', 's', '', '作', '口', '在', '他', '能', '并', 'B', '士', '4', 'U', '克', '才', '正', '们', '字', '声', '高', '全', '尔', '活', '者', '动', '其', '主', '报', '多', '望', '放', 'h', 'w', '次', '年', '', '中', '3', '特', '于', '十', '入', '要', '男', '同', 'G', '面', '分', '方', 'K', '什', '再', '教', '本', '己', '结', '1', '等', '世', 'N', '', '说', 'g', 'u', '期', 'Z', '外', '美', 'M', '行', '给', '9', '文', '将', '两', '许', '张', '友', '0', '英', '应', '向', '像', '此', '白', '安', '少', '何', '打', '气', '常', '定', '间', '花', '见', '孩', '它', '直', '风', '数', '使', '道', '第', '水', '已', '女', '山', '解', 'd', 'P', '的', '通', '关', '性', '叫', '儿', 'L', '妈', '问', '回', '神', '来', 'S', '', '四', '里', '前', '国', '些', 'O', 'v', 'l', 'A', '心', '平', '自', '无', '军', '光', '代', '是', '好', '却', 'c', '得', '种', '就', '意', '先', '立', 'z', '子', '过', 'Y', 'j', '表', '', '么', '所', '接', '了', '名', '金', '受', 'J', '满', '眼', '没', '部', '那', 'm', '每', '车', '度', '可', 'R', '斯', '经', '现', '门', '明', 'V', '如', '走', '命', 'y', '6', 'E', '战', '很', '上', 'f', '月', '西', '7', '长', '夫', '想', '话', '变', '海', '机', 'x', '到', 'W', '一', '成', '生', '信', '笑', '但', '父', '开', '内', '东', '马', '日', '小', '而', '后', '带', '以', '三', '几', '为', '认', 'X', '死', '员', '目', '位', '之', '学', '远', '人', '音', '呢', '我', 'q', '乐', '象', '重', '对', '个', '被', '别', 'F', '也', '书', '稜', 'D', '写', '还', '因', '家', '发', '时', 'i', '或', '住', '德', '当', 'o', 'I', '比', '觉', '然', '吃', '去', '公', 'a', '老', '亲', '情', '体', '太', 'b', '万', 'C', '电', '理', '', '失', '力', '更', '拉', '物', '着', '原', '她', '工', '实', '色', '感', '记', '看', '出', '相', '路', '大', '你', '候', '2', '和', '', '与', 'p', '样', '新', '只', '便', '最', '不', '进', 'T', 'r', '做', '格', '母', '总', '爱', '身', '师', '轻', '知', '往', '加', '从', '', '天', 'e', 'H', '', '听', '场', '由', '快', '边', '让', '把', '任', '8', '条', '头', '事', '至', '起', '点', '真', '手', '这', '难', '都', '界', '用', '法', 'n', '处', '下', '又', 'Q', '告', '地', '5', 'k', 't', '岁', '有', '会', '果', '利', '民'];
|
||||
} else if ($type === 2) {
|
||||
$charset = ['D', '在', '主', '特', '家', '军', '然', '表', '场', '4', '要', '只', 'v', '和', '?', '6', '别', '还', 'g', '现', '儿', '岁', '?', '?', '此', '象', '月', '3', '出', '战', '工', '相', 'o', '男', '直', '失', '世', 'F', '都', '平', '文', '什', 'V', 'O', '将', '真', 'T', '那', '当', '?', '会', '立', '些', 'u', '是', '十', '张', '学', '气', '大', '爱', '两', '命', '全', '后', '东', '性', '通', '被', '1', '它', '乐', '接', '而', '感', '车', '山', '公', '了', '常', '以', '何', '可', '话', '先', 'p', 'i', '叫', '轻', 'M', '士', 'w', '着', '变', '尔', '快', 'l', '个', '说', '少', '色', '里', '安', '花', '远', '7', '难', '师', '放', 't', '报', '认', '面', '道', 'S', '?', '克', '地', '度', 'I', '好', '机', 'U', '民', '写', '把', '万', '同', '水', '新', '没', '书', '电', '吃', '像', '斯', '5', '为', 'y', '白', '几', '日', '教', '看', '但', '第', '加', '候', '作', '上', '拉', '住', '有', '法', 'r', '事', '应', '位', '利', '你', '声', '身', '国', '问', '马', '女', '他', 'Y', '比', '父', 'x', 'A', 'H', 'N', 's', 'X', '边', '美', '对', '所', '金', '活', '回', '意', '到', 'z', '从', 'j', '知', '又', '内', '因', '点', 'Q', '三', '定', '8', 'R', 'b', '正', '或', '夫', '向', '德', '听', '更', '?', '得', '告', '并', '本', 'q', '过', '记', 'L', '让', '打', 'f', '人', '就', '者', '去', '原', '满', '体', '做', '经', 'K', '走', '如', '孩', 'c', 'G', '给', '使', '物', '?', '最', '笑', '部', '?', '员', '等', '受', 'k', '行', '一', '条', '果', '动', '光', '门', '头', '见', '往', '自', '解', '成', '处', '天', '能', '于', '名', '其', '发', '总', '母', '的', '死', '手', '入', '路', '进', '心', '来', 'h', '时', '力', '多', '开', '已', '许', 'd', '至', '由', '很', '界', 'n', '小', '与', 'Z', '想', '代', '么', '分', '生', '口', '再', '妈', '望', '次', '西', '风', '种', '带', 'J', '?', '实', '情', '才', '这', '?', 'E', '我', '神', '格', '长', '觉', '间', '年', '眼', '无', '不', '亲', '关', '结', '0', '友', '信', '下', '却', '重', '己', '老', '2', '音', '字', 'm', '呢', '明', '之', '前', '高', 'P', 'B', '目', '太', 'e', '9', '起', '稜', '她', '也', 'W', '用', '方', '子', '英', '每', '理', '便', '四', '数', '期', '中', 'C', '外', '样', 'a', '海', '们', '任'];
|
||||
}
|
||||
|
||||
// JS: _decodeText2
|
||||
// text = text.replace(reg, ($0, $1) => z[('0x' + $1) - 1000]);
|
||||
// reg = /%uE([0-9a-fA-F]{3})/gi
|
||||
// 58344 (decimal) = E3E8 (hex)
|
||||
// CODE_ST = 58344
|
||||
// index = charCode - 58344
|
||||
// JS charset array index logic:
|
||||
// z[('0x' + $1) - 1000] ???
|
||||
// JS code: z[('0x' + $1) - 1000]
|
||||
// If $1 is '3E8' (1000), then index is 0.
|
||||
// 'E3E8' -> $1='3E8'. 0x3E8 = 1000. 1000 - 1000 = 0.
|
||||
// So offset is indeed related to 0xE3E8.
|
||||
|
||||
// PHP Logic:
|
||||
// Iterate string, find characters in range [0xE3E8, 0xE55B] (approx)
|
||||
// Or use regex like JS.
|
||||
// In PHP, unicode characters can be matched or we can convert string to unicode code points.
|
||||
//
|
||||
// Better to use preg_replace_callback with unicode escape sequence?
|
||||
// But the input text might be normal UTF-8 string, not escaped %uXXXX.
|
||||
// JS's `_decodeText2` first calls `escape(text)`.
|
||||
// So we should do similar: convert string to unicode hex entities or iterate chars.
|
||||
|
||||
$result = '';
|
||||
$len = mb_strlen($text, 'UTF-8');
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$char = mb_substr($text, $i, 1, 'UTF-8');
|
||||
$code = mb_ord($char, 'UTF-8');
|
||||
|
||||
// CODE_ST = 58344 (0xE3E8)
|
||||
// CODE_ED = 58715 (0xE55B)
|
||||
if ($code >= 58344 && $code <= 58715) {
|
||||
$index = $code - 58344;
|
||||
if (isset($charset[$index])) {
|
||||
$result .= $charset[$index];
|
||||
} else {
|
||||
$result .= $char;
|
||||
}
|
||||
} else {
|
||||
$result .= $char;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行爬虫
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
/**
|
||||
* 番茄漫画 ᵈᶻ.php
|
||||
* 对应源: 番茄漫画[画].js
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private const HOST = 'https://qkfqapi.vv9v.cn';
|
||||
private const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36';
|
||||
|
||||
private $startPage = 1;
|
||||
|
||||
public function init($extend = '') {
|
||||
$this->startPage = 1;
|
||||
}
|
||||
|
||||
public function homeContent($filter = []) {
|
||||
$url = self::HOST . '/api/discover/style?tab=漫画';
|
||||
$json = $this->fetchJson($url);
|
||||
|
||||
$classes = [];
|
||||
if (isset($json['data']) && is_array($json['data'])) {
|
||||
foreach ($json['data'] as $item) {
|
||||
if (isset($item['url']) && trim($item['url'])) {
|
||||
$classes[] = [
|
||||
'type_id' => $item['url'], // URL作为ID
|
||||
'type_name' => $item['title'] ?? '未知分类',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => (object)[]
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
// tid 是类似 /api/discover?tab=漫画&type=7...&page=1 的URL
|
||||
// 需要替换 page 参数
|
||||
$url = self::HOST . $tid;
|
||||
if (strpos($tid, 'http') === 0) {
|
||||
$url = $tid;
|
||||
}
|
||||
|
||||
// 简单的页码替换逻辑:假设URL中包含 page=1
|
||||
// 如果包含 page=x,替换为 page=$pg
|
||||
if (preg_match('/page=\d+/', $url)) {
|
||||
$url = preg_replace('/page=\d+/', 'page=' . $pg, $url);
|
||||
} else {
|
||||
// 如果没有page参数,追加
|
||||
$sep = (strpos($url, '?') !== false) ? '&' : '?';
|
||||
$url .= $sep . 'page=' . $pg;
|
||||
}
|
||||
|
||||
$json = $this->fetchJson($url);
|
||||
$list = $this->parseList($json);
|
||||
|
||||
return $this->pageResult($list, $pg, 0, 10); // limit 10
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = $ids[0];
|
||||
$url = self::HOST . "/api/book?book_id=$id";
|
||||
$json = $this->fetchJson($url);
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $id,
|
||||
'vod_name' => '',
|
||||
'vod_pic' => '',
|
||||
'type_name' => '',
|
||||
'vod_year' => '',
|
||||
'vod_area' => '',
|
||||
'vod_remarks' => '',
|
||||
'vod_actor' => '',
|
||||
'vod_director' => '',
|
||||
'vod_content' => '',
|
||||
];
|
||||
|
||||
if (isset($json['data']['data'])) {
|
||||
$data = $json['data']['data'];
|
||||
$vod['vod_name'] = $data['book_name'] ?? '';
|
||||
$vod['type_name'] = $data['category'] ?? '';
|
||||
$vod['vod_pic'] = $data['thumb_url'] ?? '';
|
||||
$vod['vod_content'] = $data['abstract'] ?? '';
|
||||
$vod['vod_remarks'] = $data['sub_info'] ?? '';
|
||||
$vod['vod_director'] = $data['author'] ?? '';
|
||||
|
||||
// 章节列表
|
||||
// 这里需要获取章节列表,JS中使用了 jsonStr.parseX.data.data.chapterListWithVolume
|
||||
// 假设API返回结构一致
|
||||
$chapters = [];
|
||||
if (isset($data['chapterListWithVolume'])) {
|
||||
// 可能是嵌套数组,需要扁平化
|
||||
foreach ($data['chapterListWithVolume'] as $volume) {
|
||||
if (is_array($volume)) {
|
||||
foreach ($volume as $chapter) {
|
||||
$chapters[] = $chapter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果扁平化失败,尝试直接读取(视API返回而定)
|
||||
if (empty($chapters) && isset($data['chapter_list'])) {
|
||||
$chapters = $data['chapter_list'];
|
||||
}
|
||||
|
||||
$playUrls = [];
|
||||
foreach ($chapters as $ch) {
|
||||
$title = $ch['title'] ?? '未知章节';
|
||||
$itemId = $ch['itemId'] ?? $ch['item_id'] ?? '';
|
||||
$playUrls[] = "$title$$itemId@$title";
|
||||
}
|
||||
|
||||
$vod['vod_play_from'] = '番茄漫画';
|
||||
$vod['vod_play_url'] = implode('#', $playUrls);
|
||||
}
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$offset = ($pg - 1) * 10;
|
||||
$url = self::HOST . "/api/search?key=" . urlencode($key) . "&tab_type=8&offset=$offset";
|
||||
$json = $this->fetchJson($url);
|
||||
|
||||
$list = [];
|
||||
if (isset($json['data']['search_tabs'][3]['data'])) {
|
||||
$items = $json['data']['search_tabs'][3]['data'];
|
||||
foreach ($items as $it) {
|
||||
if (isset($it['book_data'][0])) {
|
||||
$book = $it['book_data'][0];
|
||||
$list[] = [
|
||||
'vod_id' => $book['book_id'] ?? '',
|
||||
'vod_name' => $book['book_name'] ?? '',
|
||||
'vod_pic' => $book['thumb_url'] ?? '',
|
||||
'vod_remarks' => $book['author'] ?? '',
|
||||
'vod_content' => $book['book_abstract'] ?? $book['abstract'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($list, $pg, 0, 10);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
// id: itemId@title
|
||||
$parts = explode('@', $id);
|
||||
$itemId = $parts[0];
|
||||
|
||||
$url = self::HOST . "/api/content?tab=漫画&item_id=$itemId&show_html=0";
|
||||
$cookie = $this->getFqCookie();
|
||||
$json = $this->fetchJson($url, ['headers' => ['Cookie' => $cookie]]);
|
||||
|
||||
$pics = [];
|
||||
if (isset($json['data']['images'])) {
|
||||
$images = $json['data']['images'];
|
||||
if (is_string($images)) {
|
||||
if (preg_match_all('/<img[^>]+src=[\'"]([^\'"]+)[\'"]/i', $images, $matches)) {
|
||||
$pics = $matches[1];
|
||||
}
|
||||
} elseif (is_array($images)) {
|
||||
foreach ($images as $img) {
|
||||
if (isset($img['src'])) {
|
||||
$pics[] = $img['src'];
|
||||
} elseif (isset($img['url'])) {
|
||||
$pics[] = $img['url'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($pics)) {
|
||||
return ['parse' => 0, 'url' => '', 'header' => (object)[]];
|
||||
}
|
||||
|
||||
// 漫画通常使用 pics:// 协议
|
||||
return ['parse' => 0, 'url' => 'pics://' . implode('&&', $pics), 'header' => (object)[]];
|
||||
}
|
||||
|
||||
// 辅助方法:解析列表
|
||||
private function parseList($json) {
|
||||
$list = [];
|
||||
$data = $json['data'] ?? [];
|
||||
if (isset($data['data'])) {
|
||||
$data = $data['data'];
|
||||
}
|
||||
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $item) {
|
||||
if ($item && (isset($item['book_name']) || isset($item['title']))) {
|
||||
$list[] = [
|
||||
'vod_id' => $item['book_id'] ?? $item['id'] ?? '',
|
||||
'vod_name' => $item['book_name'] ?? $item['title'] ?? '',
|
||||
'vod_pic' => $item['thumb_url'] ?? $item['cover'] ?? '',
|
||||
'vod_remarks' => $item['author'] ?? $item['category'] ?? '',
|
||||
'vod_content' => $item['abstract'] ?? $item['description'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function getFqCookie() {
|
||||
$cookies = [
|
||||
'novel_web_id=78444872394737941004',
|
||||
'novel_web_id=69258894393744181011',
|
||||
'novel_web_id=77130880221809081001',
|
||||
'novel_web_id=64945771562463261001',
|
||||
'novel_web_id=78444872394737941004',
|
||||
'novel_web_id=0000000000004011402',
|
||||
'novel_web_id=0000000303614711402',
|
||||
'novel_web_id=0144211303614711401',
|
||||
'novel_web_id=0144211303614711402',
|
||||
'novel_web_id=0144211303614711403',
|
||||
'novel_web_id=0144211303614711406',
|
||||
'novel_web_id=7357767624615331361',
|
||||
'novel_web_id=7357767624615331362',
|
||||
'novel_web_id=7357767624615331365',
|
||||
];
|
||||
return $cookies[array_rand($cookies)];
|
||||
}
|
||||
}
|
||||
|
||||
// 运行爬虫
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
/**
|
||||
* 绅士漫画
|
||||
*/
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
|
||||
protected const HOST = 'https://www.wn06.ru';
|
||||
|
||||
public function getName() {
|
||||
return "绅士漫画";
|
||||
}
|
||||
|
||||
public function init($extend = "") {
|
||||
$this->headers['Referer'] = self::HOST . '/';
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$classes = [
|
||||
["type_name" => "月榜", "type_id" => "rank_month"],
|
||||
["type_name" => "周榜", "type_id" => "rank_week"],
|
||||
["type_name" => "日榜", "type_id" => "rank_day"],
|
||||
["type_name" => "同人志", "type_id" => "1"],
|
||||
["type_name" => "韩漫", "type_id" => "20"],
|
||||
["type_name" => "单行本", "type_id" => "9"],
|
||||
["type_name" => "杂志&短篇", "type_id" => "10"]
|
||||
];
|
||||
|
||||
return ["class" => $classes, "filters" => (object)[]];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
return $this->categoryContent("rank_month", 1, [], []);
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
if ($tid == "rank_month") {
|
||||
$url = self::HOST . "/albums-favorite_ranking-page-{$pg}-type-month.html";
|
||||
} elseif ($tid == "rank_week") {
|
||||
$url = self::HOST . "/albums-favorite_ranking-page-{$pg}-type-week.html";
|
||||
} elseif ($tid == "rank_day") {
|
||||
$url = self::HOST . "/albums-favorite_ranking-page-{$pg}-type-day.html";
|
||||
} else {
|
||||
$url = self::HOST . "/albums-index-page-{$pg}-cate-{$tid}.html";
|
||||
}
|
||||
|
||||
$html = $this->fetch($url);
|
||||
|
||||
// Parse list items
|
||||
$items = $this->pdfa($html, '.gallary_wrap ul li');
|
||||
$videos = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$vid = $this->pd($item, '.info .title a&&href');
|
||||
$name = $this->pdfh($item, '.info .title a&&Text');
|
||||
$cover = $this->pd($item, '.pic_box img&&src');
|
||||
$info_text = $this->pdfh($item, '.info .info_col&&Text');
|
||||
$remark = "";
|
||||
if (preg_match('/(\d+)張圖片/', $info_text, $match)) {
|
||||
$remark = $match[1] . "页";
|
||||
}
|
||||
|
||||
$videos[] = [
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => $name,
|
||||
"vod_pic" => $cover,
|
||||
"vod_remarks" => $remark
|
||||
];
|
||||
}
|
||||
|
||||
return $this->pageResult($videos, $pg, 999999, 20);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$vid = $ids[0];
|
||||
$url = (strpos($vid, 'http') === 0) ? $vid : self::HOST . $vid;
|
||||
|
||||
$html = $this->fetch($url);
|
||||
|
||||
// Title
|
||||
$name = $this->pdfh($html, 'h2&&Text');
|
||||
if (empty($name)) $name = "未知";
|
||||
|
||||
// Cover
|
||||
$cover = $this->pd($html, '.uwthumb img&&src');
|
||||
if (empty($cover)) {
|
||||
$cover = $this->pd($html, '.cover img&&src');
|
||||
}
|
||||
|
||||
// Desc
|
||||
$desc = $this->pdfh($html, '.uwconn p||.info p&&Text');
|
||||
|
||||
// Pagination logic
|
||||
$max_page = 1;
|
||||
$aid = "";
|
||||
|
||||
if (preg_match('/aid-(\d+)/', $url, $match)) {
|
||||
$aid = $match[1];
|
||||
}
|
||||
|
||||
$paginator_links = $this->pdfa($html, '.paginator a');
|
||||
foreach ($paginator_links as $link) {
|
||||
$href = $this->pdfh($link, 'a&&href');
|
||||
|
||||
if (!$aid && preg_match('/aid-(\d+)/', $href, $m)) {
|
||||
$aid = $m[1];
|
||||
}
|
||||
|
||||
if (preg_match('/page-(\d+)/', $href, $m)) {
|
||||
$p = intval($m[1]);
|
||||
if ($p > $max_page) {
|
||||
$max_page = $p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$vod_play_url_list = [];
|
||||
if ($max_page == 1) {
|
||||
$vod_play_url_list[] = "第1页$" . $url;
|
||||
} else {
|
||||
for ($i = 1; $i <= $max_page; $i++) {
|
||||
if ($aid) {
|
||||
$page_url = self::HOST . "/photos-index-page-{$i}-aid-{$aid}.html";
|
||||
$vod_play_url_list[] = "第{$i}页$" . $page_url;
|
||||
} elseif ($i == 1) {
|
||||
$vod_play_url_list[] = "第1页$" . $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$play_url_str = implode("#", $vod_play_url_list);
|
||||
|
||||
return [
|
||||
"list" => [[
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => $name,
|
||||
"vod_pic" => $cover,
|
||||
"type_name" => "漫画",
|
||||
"vod_year" => "",
|
||||
"vod_area" => "",
|
||||
"vod_remarks" => "共{$max_page}页",
|
||||
"vod_actor" => "",
|
||||
"vod_director" => "",
|
||||
"vod_content" => $desc,
|
||||
"vod_play_from" => '阅读',
|
||||
"vod_play_url" => $play_url_str
|
||||
]]
|
||||
];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$url = self::HOST . "/search/?q=" . urlencode($key) . "&f=_all&s=create_time_DESC&syn=yes&page={$pg}";
|
||||
$html = $this->fetch($url);
|
||||
|
||||
$items = $this->pdfa($html, '.gallary_wrap ul li');
|
||||
$videos = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$vid = $this->pd($item, '.info .title a&&href');
|
||||
$name = $this->pdfh($item, '.info .title a&&Text');
|
||||
$cover = $this->pd($item, '.pic_box img&&src');
|
||||
|
||||
$info_text = $this->pdfh($item, '.info .info_col&&Text');
|
||||
$remark = "";
|
||||
if (preg_match('/(\d+)張圖片/', $info_text, $match)) {
|
||||
$remark = $match[1] . "页";
|
||||
}
|
||||
|
||||
$videos[] = [
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => $name,
|
||||
"vod_pic" => $cover,
|
||||
"vod_remarks" => $remark
|
||||
];
|
||||
}
|
||||
|
||||
return ["list" => $videos];
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$url = $id;
|
||||
$headers = $this->headers;
|
||||
$headers['Referer'] = $url;
|
||||
|
||||
$html = $this->fetch($url, ['headers' => $headers]);
|
||||
|
||||
$items = $this->pdfa($html, '.gallary_wrap.tb ul li');
|
||||
$img_info_list = [];
|
||||
$prefix_url = "";
|
||||
|
||||
foreach ($items as $item) {
|
||||
$seq = $this->pdfh($item, 'span.name.tb&&Text');
|
||||
$src = $this->pdfh($item, 'img&&src');
|
||||
|
||||
if (!$src) continue;
|
||||
|
||||
$ext = "jpg";
|
||||
$parts = explode('.', $src);
|
||||
if (count($parts) > 1) {
|
||||
$last = end($parts);
|
||||
$ext = explode('?', $last)[0];
|
||||
}
|
||||
|
||||
if (!$prefix_url && strpos($src, "wnimg1") !== false) {
|
||||
$last_slash = strrpos($src, '/');
|
||||
if ($last_slash !== false) {
|
||||
$prefix_url = substr($src, 0, $last_slash + 1);
|
||||
}
|
||||
}
|
||||
|
||||
$img_info_list[] = [
|
||||
"name" => $seq,
|
||||
"ext" => $ext,
|
||||
"raw_src" => $src
|
||||
];
|
||||
}
|
||||
|
||||
// Sort
|
||||
usort($img_info_list, function($a, $b) {
|
||||
$na = is_numeric($a['name']) ? intval($a['name']) : 0;
|
||||
$nb = is_numeric($b['name']) ? intval($b['name']) : 0;
|
||||
return $na <=> $nb;
|
||||
});
|
||||
|
||||
$final_images = [];
|
||||
foreach ($img_info_list as $item) {
|
||||
if ($prefix_url) {
|
||||
$full_url = "{$prefix_url}{$item['name']}.{$item['ext']}";
|
||||
} else {
|
||||
$full_url = $item['raw_src'];
|
||||
}
|
||||
|
||||
if (strpos($full_url, "tu.petatt.cn") !== false) continue;
|
||||
|
||||
if (strpos($full_url, '//') === 0) {
|
||||
$full_url = 'https:' . $full_url;
|
||||
}
|
||||
|
||||
$final_images[] = $full_url;
|
||||
}
|
||||
|
||||
$novel_data = implode("&&", $final_images);
|
||||
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => "pics://{$novel_data}",
|
||||
"header" => ""
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 自动运行
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,868 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $host = 'https://v.qq.com';
|
||||
private $apiHost = 'https://pbaccess.video.qq.com';
|
||||
|
||||
public function homeContent($filter) {
|
||||
$classes = [
|
||||
['type_id' => '100173', 'type_name' => '电影'],
|
||||
['type_id' => '100113', 'type_name' => '电视剧'],
|
||||
['type_id' => '100109', 'type_name' => '综艺'],
|
||||
['type_id' => '100105', 'type_name' => '纪录片'],
|
||||
['type_id' => '100119', 'type_name' => '动漫'],
|
||||
['type_id' => '100150', 'type_name' => '少儿'],
|
||||
['type_id' => '110755', 'type_name' => '短剧']
|
||||
];
|
||||
|
||||
$filters = [
|
||||
'100173' => [
|
||||
[
|
||||
'key' => 'iyear', 'name' => '年份', 'value' => [
|
||||
['n' => '全部', 'v' => '-1'], ['n' => '2025', 'v' => '2025'], ['n' => '2024', 'v' => '2024'],
|
||||
['n' => '2023', 'v' => '2023'], ['n' => '2022', 'v' => '2022'], ['n' => '2021', 'v' => '2021'],
|
||||
['n' => '2020', 'v' => '2020']
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'sort', 'name' => '排序', 'value' => [
|
||||
['n' => '综合', 'v' => '75'], ['n' => '最新', 'v' => '76'], ['n' => '最热', 'v' => '74']
|
||||
]
|
||||
]
|
||||
],
|
||||
'100113' => [
|
||||
[
|
||||
'key' => 'iyear', 'name' => '年份', 'value' => [
|
||||
['n' => '全部', 'v' => '-1'], ['n' => '2025', 'v' => '2025'], ['n' => '2024', 'v' => '2024'],
|
||||
['n' => '2023', 'v' => '2023'], ['n' => '2022', 'v' => '2022'], ['n' => '2021', 'v' => '2021'],
|
||||
['n' => '2020', 'v' => '2020']
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'sort', 'name' => '排序', 'value' => [
|
||||
['n' => '综合', 'v' => '75'], ['n' => '最新', 'v' => '76'], ['n' => '最热', 'v' => '74']
|
||||
]
|
||||
]
|
||||
],
|
||||
'100109' => [
|
||||
[
|
||||
'key' => 'sort', 'name' => '排序', 'value' => [
|
||||
['n' => '综合', 'v' => '75'], ['n' => '最新', 'v' => '76'], ['n' => '最热', 'v' => '74']
|
||||
]
|
||||
]
|
||||
],
|
||||
'100119' => [
|
||||
[
|
||||
'key' => 'sort', 'name' => '排序', 'value' => [
|
||||
['n' => '综合', 'v' => '75'], ['n' => '最新', 'v' => '76'], ['n' => '最热', 'v' => '74']
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => $filters
|
||||
];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
return ['list' => []];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$page = max(1, intval($pg));
|
||||
$offset = ($page - 1) * 21;
|
||||
|
||||
// 构建列表页URL(使用原始的页面URL结构)
|
||||
$url = $this->host . '/x/bu/pagesheet/list';
|
||||
$params = [
|
||||
'_all' => '1',
|
||||
'append' => '1',
|
||||
'channel' => $this->getChannelByTid($tid),
|
||||
'listpage' => '1',
|
||||
'offset' => $offset,
|
||||
'pagesize' => '21',
|
||||
'iarea' => '-1'
|
||||
];
|
||||
|
||||
// 添加排序参数
|
||||
if (isset($extend['sort']) && $extend['sort'] !== '-1') {
|
||||
$params['sort'] = $extend['sort'];
|
||||
} else {
|
||||
$params['sort'] = '75'; // 默认综合排序
|
||||
}
|
||||
|
||||
// 添加其他筛选参数
|
||||
if (isset($extend['iyear']) && $extend['iyear'] !== '-1') {
|
||||
$params['iyear'] = $extend['iyear'];
|
||||
}
|
||||
|
||||
$fullUrl = $url . '?' . http_build_query($params);
|
||||
|
||||
// 使用新的PHP解析逻辑
|
||||
$videos = $this->parseListPage($fullUrl);
|
||||
|
||||
return $this->pageResult($videos, $page, 99999, 21);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型ID获取频道名称
|
||||
*/
|
||||
private function getChannelByTid($tid) {
|
||||
$map = [
|
||||
'100173' => 'movie', // 电影
|
||||
'100113' => 'tv', // 电视剧
|
||||
'100109' => 'variety', // 综艺
|
||||
'100105' => 'doco', // 纪录片
|
||||
'100119' => 'cartoon', // 动漫
|
||||
'100150' => 'child', // 少儿
|
||||
'110755' => 'choice' // 短剧(用精选代替)
|
||||
];
|
||||
|
||||
return $map[$tid] ?? 'movie';
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析列表页(使用新的PHP逻辑)
|
||||
*/
|
||||
private function parseListPage($url) {
|
||||
$result = [];
|
||||
|
||||
try {
|
||||
// 1. 获取网页内容
|
||||
$html = $this->fetch($url);
|
||||
|
||||
// 2. 使用DOMDocument解析HTML
|
||||
libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$xpath = new DOMXPath($dom);
|
||||
|
||||
// 3. 查找所有列表项
|
||||
$listItems = $xpath->query('//div[contains(@class, "list_item")]');
|
||||
|
||||
foreach ($listItems as $item) {
|
||||
// 提取标题 (img的alt属性)
|
||||
$imgElements = $xpath->query('.//img', $item);
|
||||
$title = '';
|
||||
if ($imgElements->length > 0) {
|
||||
$node = $imgElements->item(0);
|
||||
if ($node instanceof DOMElement) {
|
||||
$title = $node->getAttribute('alt');
|
||||
$title = html_entity_decode($title, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
// 提取图片 (img的src属性)
|
||||
$pic = '';
|
||||
if ($imgElements->length > 0) {
|
||||
$node = $imgElements->item(0);
|
||||
if ($node instanceof DOMElement) {
|
||||
$pic = $node->getAttribute('src');
|
||||
// 确保图片URL完整
|
||||
if ($pic && !preg_match('/^https?:\/\//', $pic)) {
|
||||
$pic = $this->urlJoin($url, $pic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提取描述 (a标签的文本)
|
||||
$aElements = $xpath->query('.//a', $item);
|
||||
$desc = '';
|
||||
if ($aElements->length > 0) {
|
||||
$desc = trim($aElements->item(0)->textContent);
|
||||
$desc = html_entity_decode($desc, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
// 提取链接 (a标签的data-float属性)
|
||||
$link = '';
|
||||
if ($aElements->length > 0) {
|
||||
$node = $aElements->item(0);
|
||||
if ($node instanceof DOMElement) {
|
||||
$link = $node->getAttribute('data-float');
|
||||
// 处理链接,获取CID
|
||||
if ($link) {
|
||||
$cid = $this->extractCidFromUrl($link);
|
||||
if ($cid) {
|
||||
$link = $cid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果链接为空,尝试从其他属性提取
|
||||
if (empty($link) && $aElements->length > 0) {
|
||||
$node = $aElements->item(0);
|
||||
if ($node instanceof DOMElement) {
|
||||
$href = $node->getAttribute('href');
|
||||
if ($href) {
|
||||
$cid = $this->extractCidFromUrl($href);
|
||||
if ($cid) {
|
||||
$link = $cid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到结果数组
|
||||
if (!empty($link) && !empty($title)) {
|
||||
$result[] = [
|
||||
'vod_id' => $link,
|
||||
'vod_name' => $this->cleanText($title),
|
||||
'vod_pic' => $pic,
|
||||
'vod_remarks' => $this->cleanText($desc)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果DOM解析失败,尝试使用正则表达式
|
||||
if (empty($result)) {
|
||||
$result = $this->parseListWithRegex($html, $url);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log("解析列表页失败: " . $e->getMessage() . " URL: " . $url);
|
||||
// 尝试备用方法
|
||||
$result = $this->parseListWithRegex($html ?? '', $url);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用正则表达式解析列表页(备用方法)
|
||||
*/
|
||||
private function parseListWithRegex($html, $baseUrl) {
|
||||
$result = [];
|
||||
|
||||
// 匹配列表项
|
||||
$pattern = '/<div[^>]*class="[^"]*list_item[^"]*"[^>]*>(.*?)<\/div>/is';
|
||||
preg_match_all($pattern, $html, $itemMatches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($itemMatches as $item) {
|
||||
$itemHtml = $item[1];
|
||||
|
||||
// 提取标题
|
||||
preg_match('/<img[^>]*alt="([^"]*)"[^>]*>/i', $itemHtml, $titleMatch);
|
||||
$title = $titleMatch[1] ?? '';
|
||||
$title = html_entity_decode($title, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
// 提取图片
|
||||
preg_match('/<img[^>]*src="([^"]*)"[^>]*>/i', $itemHtml, $picMatch);
|
||||
$pic = $picMatch[1] ?? '';
|
||||
if ($pic && !preg_match('/^https?:\/\//', $pic)) {
|
||||
$pic = $this->urlJoin($baseUrl, $pic);
|
||||
}
|
||||
|
||||
// 提取链接
|
||||
preg_match('/<a[^>]*data-float="([^"]*)"[^>]*>/i', $itemHtml, $linkMatch);
|
||||
$link = $linkMatch[1] ?? '';
|
||||
if (empty($link)) {
|
||||
preg_match('/<a[^>]*href="([^"]*)"[^>]*>/i', $itemHtml, $hrefMatch);
|
||||
$link = $hrefMatch[1] ?? '';
|
||||
}
|
||||
|
||||
// 提取CID
|
||||
$cid = '';
|
||||
if ($link) {
|
||||
$cid = $this->extractCidFromUrl($link);
|
||||
}
|
||||
|
||||
// 提取描述
|
||||
preg_match('/<a[^>]*>(.*?)<\/a>/is', $itemHtml, $descMatch);
|
||||
$desc = $descMatch[1] ?? '';
|
||||
$desc = strip_tags($desc);
|
||||
$desc = html_entity_decode($desc, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
if (!empty($cid) && !empty($title)) {
|
||||
$result[] = [
|
||||
'vod_id' => $cid,
|
||||
'vod_name' => $this->cleanText($title),
|
||||
'vod_pic' => $pic,
|
||||
'vod_remarks' => $this->cleanText($desc)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取CID
|
||||
*/
|
||||
private function extractCidFromUrl($url) {
|
||||
// 处理多种URL格式
|
||||
$patterns = [
|
||||
'/\/cover\/([a-zA-Z0-9]+)\.html/', // /cover/CID.html
|
||||
'/\/([a-zA-Z0-9]+)\.html$/', // /CID.html
|
||||
'/cid=([a-zA-Z0-9]+)/', // cid=CID
|
||||
'/\/([a-zA-Z0-9]+)\//', // /CID/
|
||||
'/\/detail\/m\/([a-zA-Z0-9]+)\.html/' // /detail/m/CID.html
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $url, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* URL拼接辅助函数
|
||||
*/
|
||||
private function urlJoin($baseUrl, $relativePath) {
|
||||
if (empty($relativePath)) {
|
||||
return $relativePath;
|
||||
}
|
||||
|
||||
if (preg_match('/^https?:\/\//', $relativePath)) {
|
||||
return $relativePath;
|
||||
}
|
||||
|
||||
$baseParts = parse_url($baseUrl);
|
||||
$basePath = isset($baseParts['path']) ? dirname($baseParts['path']) : '/';
|
||||
|
||||
if (strpos($relativePath, '/') === 0) {
|
||||
// 绝对路径
|
||||
return $baseParts['scheme'] . '://' . $baseParts['host'] . $relativePath;
|
||||
} else {
|
||||
// 相对路径
|
||||
$newPath = rtrim($basePath, '/') . '/' . ltrim($relativePath, '/');
|
||||
return $baseParts['scheme'] . '://' . $baseParts['host'] . $newPath;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理文本,移除多余空格和换行
|
||||
*/
|
||||
private function cleanText($text) {
|
||||
if (empty($text)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = trim($text);
|
||||
$text = preg_replace('/\s+/', ' ', $text); // 替换多个空格为单个空格
|
||||
$text = preg_replace('/[\r\n]+/', ' ', $text); // 移除换行
|
||||
return $text;
|
||||
}
|
||||
|
||||
// ================== 以下是原有的搜索逻辑,完全保持不变 ==================
|
||||
|
||||
public function detailContent($ids) {
|
||||
$videoId = is_array($ids) ? $ids[0] : $ids;
|
||||
|
||||
// 获取视频基本信息
|
||||
$infoUrl = $this->apiHost . '/trpc.universal_backend_service.page_server_rpc.PageServer/GetPageData';
|
||||
|
||||
$infoBody = [
|
||||
"page_params" => [
|
||||
"req_from" => "web",
|
||||
"cid" => $videoId,
|
||||
"vid" => "",
|
||||
"lid" => "",
|
||||
"page_type" => "detail_operation",
|
||||
"page_id" => "detail_page_introduction"
|
||||
],
|
||||
"has_cache" => 1
|
||||
];
|
||||
|
||||
$infoResponse = $this->fetch($infoUrl . '?video_appid=3000010&vplatform=2&vversion_name=8.2.96', [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($infoBody),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer: ' . $this->host . '/',
|
||||
'Origin: ' . $this->host
|
||||
]
|
||||
]);
|
||||
|
||||
$infoJson = json_decode($infoResponse, true);
|
||||
$infoData = $infoJson['data'] ?? [];
|
||||
|
||||
// 提取视频详情
|
||||
$vod = [
|
||||
'vod_id' => $videoId,
|
||||
'vod_name' => '',
|
||||
'type_name' => '',
|
||||
'vod_actor' => '',
|
||||
'vod_year' => '',
|
||||
'vod_content' => '',
|
||||
'vod_remarks' => '',
|
||||
'vod_pic' => '',
|
||||
'vod_play_from' => '腾讯视频',
|
||||
'vod_play_url' => ''
|
||||
];
|
||||
|
||||
// 提取基本信息
|
||||
if (isset($infoData['module_list_datas'][0]['module_datas'][0]['item_data_lists']['item_datas'][0])) {
|
||||
$detailData = $infoData['module_list_datas'][0]['module_datas'][0]['item_data_lists']['item_datas'][0];
|
||||
$itemParams = $detailData['item_params'] ?? [];
|
||||
|
||||
$vod['vod_name'] = $itemParams['title'] ?? '';
|
||||
$vod['type_name'] = $itemParams['sub_genre'] ?? '';
|
||||
$vod['vod_year'] = $itemParams['year'] ?? '';
|
||||
$vod['vod_content'] = $itemParams['cover_description'] ?? '';
|
||||
$vod['vod_remarks'] = $itemParams['holly_online_time'] ?? $itemParams['hotval'] ?? '';
|
||||
$vod['vod_pic'] = $itemParams['image_url'] ?? '';
|
||||
|
||||
// 提取演员信息
|
||||
if (isset($detailData['sub_items']['star_list']['item_datas'])) {
|
||||
$actors = [];
|
||||
foreach ($detailData['sub_items']['star_list']['item_datas'] as $star) {
|
||||
$actors[] = $star['item_params']['name'] ?? '';
|
||||
}
|
||||
$vod['vod_actor'] = implode(',', $actors);
|
||||
}
|
||||
}
|
||||
|
||||
// 方法1:使用分页获取所有剧集
|
||||
$playUrls = $this->getAllEpisodes($videoId);
|
||||
|
||||
// 方法2:如果方法1失败,尝试备用方法
|
||||
if (empty($playUrls)) {
|
||||
$playUrls = $this->getEpisodesByTab($videoId);
|
||||
}
|
||||
|
||||
// 方法3:如果还是没有剧集,可能是电影
|
||||
if (empty($playUrls) && !empty($videoId)) {
|
||||
$playUrls[] = "正片\${$videoId}";
|
||||
}
|
||||
|
||||
$vod['vod_play_url'] = implode('#', $playUrls);
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$page = max(1, intval($pg));
|
||||
$videos = [];
|
||||
|
||||
// 使用原有的搜索逻辑
|
||||
$searchData = $this->vodSearch($key, $page - 1); // JavaScript代码中页码从0开始
|
||||
|
||||
if (!empty($searchData)) {
|
||||
foreach ($searchData as $item) {
|
||||
$videos[] = [
|
||||
'vod_id' => $item['id'] ?? '',
|
||||
'vod_name' => $item['title'] ?? '',
|
||||
'vod_pic' => $item['img'] ?? '',
|
||||
'vod_remarks' => $item['desc'] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总页数(假设每页30条)
|
||||
$total = count($videos) > 0 ? 999 : 0;
|
||||
$limit = 30;
|
||||
|
||||
return $this->pageResult($videos, $page, $total, $limit);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
// 解析播放地址格式:cid@vid 或 cid
|
||||
if (strpos($id, '@') !== false) {
|
||||
$parts = explode('@', $id);
|
||||
$cid = $parts[0];
|
||||
$vid = $parts[1];
|
||||
$url = "{$this->host}/x/cover/{$cid}/{$vid}.html";
|
||||
} else {
|
||||
// 只有cid,可能是电影
|
||||
$url = "{$this->host}/x/cover/{$id}.html";
|
||||
}
|
||||
|
||||
return [
|
||||
'parse' => 1,
|
||||
'jx' => 1,
|
||||
'play_parse' => true,
|
||||
'parse_type' => '壳子超级解析',
|
||||
'parse_source' => '腾讯视频',
|
||||
'url' => $url,
|
||||
'header' => json_encode([
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer' => $this->host,
|
||||
'Origin' => $this->host
|
||||
], JSON_UNESCAPED_UNICODE)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行搜索(原有的搜索逻辑)
|
||||
*/
|
||||
private function vodSearch($keyword, $page = 0) {
|
||||
$url = 'https://pbaccess.video.qq.com/trpc.videosearch.mobile_search.MultiTerminalSearch/MbSearch?vplatform=2';
|
||||
|
||||
$body = json_encode([
|
||||
"version" => "25042201",
|
||||
"clientType" => 1,
|
||||
"filterValue" => "",
|
||||
"uuid" => "B1E50847-D25F-4C4B-BBA0-36F0093487F6",
|
||||
"retry" => 0,
|
||||
"query" => $keyword,
|
||||
"pagenum" => $page,
|
||||
"isPrefetch" => true,
|
||||
"pagesize" => 30,
|
||||
"queryFrom" => 0,
|
||||
"searchDatakey" => "",
|
||||
"transInfo" => "",
|
||||
"isneedQc" => true,
|
||||
"preQid" => "",
|
||||
"adClientInfo" => "",
|
||||
"extraInfo" => [
|
||||
"isNewMarkLabel" => "1",
|
||||
"multi_terminal_pc" => "1",
|
||||
"themeType" => "1",
|
||||
"sugRelatedIds" => "{}",
|
||||
"appVersion" => ""
|
||||
]
|
||||
]);
|
||||
|
||||
$response = $this->fetch($url, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.139 Safari/537.36',
|
||||
'Content-Type: application/json',
|
||||
'Origin: https://v.qq.com',
|
||||
'Referer: https://v.qq.com/'
|
||||
]
|
||||
]);
|
||||
|
||||
return $this->parseSearchResult($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析搜索结果(原有的搜索逻辑)
|
||||
*/
|
||||
private function parseSearchResult($html) {
|
||||
$d = [];
|
||||
$seenIds = [];
|
||||
|
||||
try {
|
||||
$json = json_decode($html, true);
|
||||
|
||||
// 处理normalList
|
||||
if (isset($json['data']['normalList']['itemList'])) {
|
||||
$this->processItemList($json['data']['normalList']['itemList'], $d, $seenIds);
|
||||
}
|
||||
|
||||
// 处理areaBoxList
|
||||
if (isset($json['data']['areaBoxList'])) {
|
||||
foreach ($json['data']['areaBoxList'] as $box) {
|
||||
if (isset($box['itemList'])) {
|
||||
$this->processItemList($box['itemList'], $d, $seenIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("搜索解析出错: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $d;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理项目列表(原有的搜索逻辑)
|
||||
*/
|
||||
private function processItemList($itemList, &$d, &$seenIds) {
|
||||
$nonMainContentKeywords = [
|
||||
':', '#', '特辑', '"', '剪辑', '片花', '独家', '专访', '纯享',
|
||||
'制作', '幕后', '宣传', 'MV', '主题曲', '插曲', '彩蛋',
|
||||
'精彩', '集锦', '盘点', '回顾', '解说', '评测', '反应', 'reaction'
|
||||
];
|
||||
|
||||
foreach ($itemList as $it) {
|
||||
if (isset($it['doc']['id'], $it['videoInfo']['title'])) {
|
||||
$itemId = $it['doc']['id'];
|
||||
$videoInfo = $it['videoInfo'];
|
||||
$title = $videoInfo['title'] ?? '';
|
||||
|
||||
// 检查是否主要内容
|
||||
if (!$this->isMainContent($title, $nonMainContentKeywords)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否为QQ平台
|
||||
if (!$this->isQQPlatform($videoInfo['playSites'] ?? [])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 去重检查
|
||||
if (in_array($itemId, $seenIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seenIds[] = $itemId;
|
||||
|
||||
$d[] = [
|
||||
'id' => $itemId,
|
||||
'title' => $title,
|
||||
'img' => $videoInfo['imgUrl'] ?? '',
|
||||
'desc' => $videoInfo['secondLine'] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否主要内容(原有的搜索逻辑)
|
||||
*/
|
||||
private function isMainContent($title, $nonMainContentKeywords) {
|
||||
if (empty($title)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否包含HTML标签(如<em>)
|
||||
if (strpos($title, '<') !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否包含非主要内容关键词
|
||||
foreach ($nonMainContentKeywords as $keyword) {
|
||||
if (strpos($title, $keyword) !== false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为QQ平台(原有的搜索逻辑)
|
||||
*/
|
||||
private function isQQPlatform($playSites) {
|
||||
if (empty($playSites) || !is_array($playSites)) {
|
||||
return true; // 如果没有平台信息,默认保留
|
||||
}
|
||||
|
||||
foreach ($playSites as $site) {
|
||||
if (isset($site['enName']) && strtolower($site['enName']) === 'qq') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ================== 其他辅助方法保持不变 ==================
|
||||
|
||||
private function buildFilterParams($params) {
|
||||
$result = [];
|
||||
foreach ($params as $key => $value) {
|
||||
if ($value !== '-1' && $value !== '' && $value !== null) {
|
||||
$result[] = "{$key}={$value}";
|
||||
}
|
||||
}
|
||||
return empty($result) ? 'sort=75' : implode('&', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法1:分页获取所有剧集
|
||||
*/
|
||||
private function getAllEpisodes($videoId) {
|
||||
$allEpisodes = [];
|
||||
$pageSize = 50;
|
||||
$pageNum = 1;
|
||||
$hasMore = true;
|
||||
|
||||
while ($hasMore) {
|
||||
$episodeUrl = $this->apiHost . '/trpc.video_detail_svr.video_detail_svr.VideoDetail/GetEpisodeList';
|
||||
|
||||
$episodeBody = [
|
||||
"cid" => $videoId,
|
||||
"vid" => "",
|
||||
"req_from" => "web",
|
||||
"page_context" => "",
|
||||
"page_size" => $pageSize,
|
||||
"page_num" => $pageNum,
|
||||
"order" => 1
|
||||
];
|
||||
|
||||
$episodeResponse = $this->fetch($episodeUrl . '?video_appid=3000010&vplatform=2&vversion_name=8.2.96', [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($episodeBody),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer: ' . $this->host . '/',
|
||||
'Origin: ' . $this->host
|
||||
]
|
||||
]);
|
||||
|
||||
$episodeJson = json_decode($episodeResponse, true);
|
||||
$episodeData = $episodeJson['data'] ?? [];
|
||||
|
||||
if (isset($episodeData['item_data_lists']['item_datas'])) {
|
||||
$episodes = $episodeData['item_data_lists']['item_datas'];
|
||||
|
||||
if (!empty($episodes)) {
|
||||
foreach ($episodes as $item) {
|
||||
$itemId = $item['item_id'] ?? '';
|
||||
$itemParams = $item['item_params'] ?? [];
|
||||
|
||||
if (!empty($itemId)) {
|
||||
$title = $itemParams['title'] ?? $itemParams['subtitle'] ?? "第" . ($itemParams['order'] ?? '?') . "集";
|
||||
$allEpisodes[] = "{$title}\${$videoId}@{$itemId}";
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否还有更多页
|
||||
$hasMore = isset($episodeData['has_more']) && $episodeData['has_more'] == 1;
|
||||
$pageNum++;
|
||||
} else {
|
||||
$hasMore = false;
|
||||
}
|
||||
} else {
|
||||
$hasMore = false;
|
||||
}
|
||||
|
||||
// 防止无限循环
|
||||
if ($pageNum > 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $allEpisodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法2:通过tab标签获取所有剧集
|
||||
*/
|
||||
private function getEpisodesByTab($videoId) {
|
||||
$allEpisodes = [];
|
||||
|
||||
$tabUrl = $this->apiHost . '/trpc.universal_backend_service.page_server_rpc.PageServer/GetPageData';
|
||||
|
||||
$tabBody = [
|
||||
"page_params" => [
|
||||
"req_from" => "web_vsite",
|
||||
"page_id" => "vsite_episode_list",
|
||||
"page_type" => "detail_operation",
|
||||
"id_type" => "1",
|
||||
"cid" => $videoId,
|
||||
"vid" => "",
|
||||
"lid" => "",
|
||||
"page_context" => "",
|
||||
"detail_page_type" => "1"
|
||||
],
|
||||
"has_cache" => 1
|
||||
];
|
||||
|
||||
$tabResponse = $this->fetch($tabUrl . '?video_appid=3000010&vplatform=2&vversion_name=8.2.96', [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($tabBody),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer: ' . $this->host . '/',
|
||||
'Origin: ' . $this->host
|
||||
]
|
||||
]);
|
||||
|
||||
$tabJson = json_decode($tabResponse, true);
|
||||
$tabData = $tabJson['data'] ?? [];
|
||||
|
||||
if (isset($tabData['module_list_datas'])) {
|
||||
foreach ($tabData['module_list_datas'] as $module) {
|
||||
if (isset($module['module_datas'])) {
|
||||
foreach ($module['module_datas'] as $moduleData) {
|
||||
// 获取tab信息
|
||||
$moduleParams = $moduleData['module_params'] ?? [];
|
||||
$tabsJson = $moduleParams['tabs'] ?? '[]';
|
||||
$tabs = json_decode($tabsJson, true) ?: [];
|
||||
|
||||
// 处理每个tab的剧集
|
||||
foreach ($tabs as $tab) {
|
||||
$tabContext = $tab['page_context'] ?? '';
|
||||
if (!empty($tabContext)) {
|
||||
$tabEpisodes = $this->getEpisodesByTabContext($videoId, $tabContext);
|
||||
$allEpisodes = array_merge($allEpisodes, $tabEpisodes);
|
||||
}
|
||||
}
|
||||
|
||||
// 同时获取当前tab的剧集
|
||||
if (isset($moduleData['item_data_lists']['item_datas'])) {
|
||||
foreach ($moduleData['item_data_lists']['item_datas'] as $item) {
|
||||
$itemId = $item['item_id'] ?? '';
|
||||
$itemParams = $item['item_params'] ?? [];
|
||||
|
||||
if (!empty($itemId)) {
|
||||
$title = $itemParams['union_title'] ?? $itemParams['title'] ?? "第" . ($itemParams['order'] ?? '?') . "集";
|
||||
$allEpisodes[] = "{$title}\${$videoId}@{$itemId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $allEpisodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定tab上下文的所有剧集
|
||||
*/
|
||||
private function getEpisodesByTabContext($videoId, $pageContext) {
|
||||
$episodes = [];
|
||||
|
||||
$url = $this->apiHost . '/trpc.universal_backend_service.page_server_rpc.PageServer/GetPageData';
|
||||
|
||||
$body = [
|
||||
"page_params" => [
|
||||
"req_from" => "web_vsite",
|
||||
"page_id" => "vsite_episode_list",
|
||||
"page_type" => "detail_operation",
|
||||
"id_type" => "1",
|
||||
"cid" => $videoId,
|
||||
"vid" => "",
|
||||
"lid" => "",
|
||||
"page_context" => $pageContext,
|
||||
"detail_page_type" => "1"
|
||||
],
|
||||
"has_cache" => 1
|
||||
];
|
||||
|
||||
$response = $this->fetch($url . '?video_appid=3000010&vplatform=2&vversion_name=8.2.96', [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($body),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer: ' . $this->host . '/',
|
||||
'Origin: ' . $this->host
|
||||
]
|
||||
]);
|
||||
|
||||
$json = json_decode($response, true);
|
||||
$data = $json['data'] ?? [];
|
||||
|
||||
if (isset($data['module_list_datas'])) {
|
||||
foreach ($data['module_list_datas'] as $module) {
|
||||
if (isset($module['module_datas'])) {
|
||||
foreach ($module['module_datas'] as $moduleData) {
|
||||
if (isset($moduleData['item_data_lists']['item_datas'])) {
|
||||
foreach ($moduleData['item_data_lists']['item_datas'] as $item) {
|
||||
$itemId = $item['item_id'] ?? '';
|
||||
$itemParams = $item['item_params'] ?? [];
|
||||
|
||||
if (!empty($itemId)) {
|
||||
$title = $itemParams['union_title'] ?? $itemParams['title'] ?? "第" . ($itemParams['order'] ?? '?') . "集";
|
||||
$episodes[] = "{$title}\${$videoId}@{$itemId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $episodes;
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $HOST;
|
||||
private $HEADERS;
|
||||
private $IMGHOST;
|
||||
|
||||
public function init($extend = '') {
|
||||
$this->HOST = 'https://api.ztcgi.com';
|
||||
$this->HEADERS = ['User-Agent: Mozilla/5.0 (Linux; Android 9; V2196A Build/PQ3A.190705.08211809; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Mobile Safari/537.36'];
|
||||
$this->IMGHOST = 'https://img1.vbwus.com';
|
||||
$this->preprocess();
|
||||
}
|
||||
|
||||
private function preprocess() {
|
||||
try {
|
||||
$res = json_decode($this->fetch($this->HOST . '/api/appAuthConfig', [], $this->HEADERS), true);
|
||||
if (isset($res['data']['imgDomain'])) {
|
||||
$this->IMGHOST = 'https://' . $res['data']['imgDomain'];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
// 生成分类列表
|
||||
$classNames = explode('&', '电影&电视剧&动漫&短剧&综艺');
|
||||
$classUrls = explode('&', '1&2&3&67&4');
|
||||
$classes = [];
|
||||
$filterObj = [];
|
||||
|
||||
// 过滤器配置
|
||||
$filterConfig = [
|
||||
"1" => [
|
||||
["key"=>"cateId","name"=>"分类","value"=>[["n"=>"全部","v"=>"1"],["n"=>"首推","v"=>"5"],["n"=>"动作","v"=>"6"],["n"=>"喜剧","v"=>"7"],["n"=>"战争","v"=>"8"],["n"=>"恐怖","v"=>"9"],["n"=>"剧情","v"=>"10"],["n"=>"爱情","v"=>"11"],["n"=>"科幻","v"=>"12"],["n"=>"动画","v"=>"13"]]],
|
||||
["key"=>"area","name"=>"地區","value"=>[["n"=>"全部","v"=>"0"],["n"=>"国产","v"=>"1"],["n"=>"中国香港","v"=>"3"],["n"=>"中国台湾","v"=>"6"],["n"=>"美国","v"=>"5"],["n"=>"韩国","v"=>"18"],["n"=>"日本","v"=>"2"]]],
|
||||
["key"=>"year","name"=>"年代","value"=>[["n"=>"全部","v"=>"0"],["n"=>"2025","v"=>"107"],["n"=>"2024","v"=>"119"],["n"=>"2023","v"=>"153"],["n"=>"2022","v"=>"101"],["n"=>"2021","v"=>"118"],["n"=>"2020","v"=>"16"],["n"=>"2019","v"=>"7"],["n"=>"2018","v"=>"2"],["n"=>"2017","v"=>"3"],["n"=>"2016","v"=>"22"]]],
|
||||
["key"=>"sort","name"=>"排序","value"=>[["n"=>"热门","v"=>"hot"],["n"=>"评分","v"=>"rating"],["n"=>"更新","v"=>"update"]]]
|
||||
],
|
||||
"2" => [
|
||||
["key"=>"cateId","name"=>"分类","value"=>[["n"=>"全部","v"=>"2"],["n"=>"首推","v"=>"14"],["n"=>"国产","v"=>"15"],["n"=>"港台","v"=>"16"],["n"=>"日韩","v"=>"17"],["n"=>"海外","v"=>"18"]]],
|
||||
["key"=>"area","name"=>"地區","value"=>[["n"=>"全部","v"=>"0"],["n"=>"国产","v"=>"1"],["n"=>"中国香港","v"=>"3"],["n"=>"中国台湾","v"=>"6"],["n"=>"美国","v"=>"5"],["n"=>"韩国","v"=>"18"],["n"=>"日本","v"=>"2"]]],
|
||||
["key"=>"year","name"=>"年代","value"=>[["n"=>"全部","v"=>"0"],["n"=>"2025","v"=>"107"],["n"=>"2024","v"=>"119"],["n"=>"2023","v"=>"153"],["n"=>"2022","v"=>"101"],["n"=>"2021","v"=>"118"],["n"=>"2020","v"=>"16"],["n"=>"2019","v"=>"7"],["n"=>"2018","v"=>"2"],["n"=>"2017","v"=>"3"],["n"=>"2016","v"=>"22"]]],
|
||||
["key"=>"sort","name"=>"排序","value"=>[["n"=>"热门","v"=>"hot"],["n"=>"评分","v"=>"rating"],["n"=>"更新","v"=>"update"]]]
|
||||
],
|
||||
"3" => [
|
||||
["key"=>"cateId","name"=>"分类","value"=>[["n"=>"全部","v"=>"3"],["n"=>"首推","v"=>"19"],["n"=>"海外","v"=>"20"],["n"=>"日本","v"=>"21"],["n"=>"国产","v"=>"22"]]],
|
||||
["key"=>"area","name"=>"地區","value"=>[["n"=>"全部","v"=>"0"],["n"=>"国产","v"=>"1"],["n"=>"中国香港","v"=>"3"],["n"=>"中国台湾","v"=>"6"],["n"=>"美国","v"=>"5"],["n"=>"韩国","v"=>"18"],["n"=>"日本","v"=>"2"]]],
|
||||
["key"=>"year","name"=>"年代","value"=>[["n"=>"全部","v"=>"0"],["n"=>"2025","v"=>"107"],["n"=>"2024","v"=>"119"],["n"=>"2023","v"=>"153"],["n"=>"2022","v"=>"101"],["n"=>"2021","v"=>"118"],["n"=>"2020","v"=>"16"],["n"=>"2019","v"=>"7"],["n"=>"2018","v"=>"2"],["n"=>"2017","v"=>"3"],["n"=>"2016","v"=>"22"]]],
|
||||
["key"=>"sort","name"=>"排序","value"=>[["n"=>"热门","v"=>"hot"],["n"=>"评分","v"=>"rating"],["n"=>"更新","v"=>"update"]]]
|
||||
],
|
||||
"4" => [
|
||||
["key"=>"cateId","name"=>"分类","value"=>[["n"=>"全部","v"=>"4"],["n"=>"首推","v"=>"23"],["n"=>"国产","v"=>"24"],["n"=>"海外","v"=>"25"],["n"=>"港台","v"=>"26"]]],
|
||||
["key"=>"area","name"=>"地區","value"=>[["n"=>"全部","v"=>"0"],["n"=>"国产","v"=>"1"],["n"=>"中国香港","v"=>"3"],["n"=>"中国台湾","v"=>"6"],["n"=>"美国","v"=>"5"],["n"=>"韩国","v"=>"18"],["n"=>"日本","v"=>"2"]]],
|
||||
["key"=>"year","name"=>"年代","value"=>[["n"=>"全部","v"=>"0"],["n"=>"2025","v"=>"107"],["n"=>"2024","v"=>"119"],["n"=>"2023","v"=>"153"],["n"=>"2022","v"=>"101"],["n"=>"2021","v"=>"118"],["n"=>"2020","v"=>"16"],["n"=>"2019","v"=>"7"],["n"=>"2018","v"=>"2"],["n"=>"2017","v"=>"3"],["n"=>"2016","v"=>"22"]]],
|
||||
["key"=>"sort","name"=>"排序","value"=>[["n"=>"热门","v"=>"hot"],["n"=>"评分","v"=>"rating"],["n"=>"更新","v"=>"update"]]]
|
||||
],
|
||||
"67" => [
|
||||
["key"=>"cateId","name"=>"分类","value"=>[["n"=>"全部","v"=>"67"],["n"=>"言情","v"=>"70"],["n"=>"爱情","v"=>"71"],["n"=>"战神","v"=>"72"],["n"=>"古代","v"=>"73"],["n"=>"萌娃","v"=>"74"],["n"=>"神医","v"=>"75"],["n"=>"玄幻","v"=>"76"],["n"=>"重生","v"=>"77"],["n"=>"激情","v"=>"79"],["n"=>"时尚","v"=>"82"],["n"=>"剧情演绎","v"=>"83"],["n"=>"影视","v"=>"84"],["n"=>"人文社科","v"=>"85"],["n"=>"二次元","v"=>"86"],["n"=>"明星八卦","v"=>"87"],["n"=>"随拍","v"=>"88"],["n"=>"个人管理","v"=>"89"],["n"=>"音乐","v"=>"90"],["n"=>"汽车","v"=>"91"],["n"=>"休闲","v"=>"92"],["n"=>"校园教育","v"=>"93"],["n"=>"游戏","v"=>"94"],["n"=>"科普","v"=>"95"],["n"=>"科技","v"=>"96"],["n"=>"时政社会","v"=>"97"],["n"=>"萌宠","v"=>"98"],["n"=>"体育","v"=>"99"],["n"=>"穿越","v"=>"80"],["n"=>"","v"=>"81"],["n"=>"闪婚","v"=>"112"]]],
|
||||
["key"=>"sort","name"=>"排序","value"=>[["n"=>"全部","v"=>""],["n"=>"最新","v"=>"update"],["n"=>"最热","v"=>"hot"]]]
|
||||
]
|
||||
];
|
||||
|
||||
for ($i = 0; $i < count($classNames); $i++) {
|
||||
$typeId = $classUrls[$i];
|
||||
$classes[] = [
|
||||
'type_id' => $typeId,
|
||||
'type_name' => $classNames[$i]
|
||||
];
|
||||
|
||||
if (isset($filterConfig[$typeId])) {
|
||||
$filterObj[$typeId] = $filterConfig[$typeId];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取首页推荐 (保持原有逻辑)
|
||||
$homeUrl = $this->HOST . '/api/dyTag/hand_data?category_id=88';
|
||||
$homeData = json_decode($this->fetch($homeUrl, [], $this->HEADERS), true);
|
||||
$list = [];
|
||||
|
||||
if (isset($homeData['data']['20'])) {
|
||||
foreach ($homeData['data']['20'] as $item) {
|
||||
$list[] = [
|
||||
'vod_id' => $item['id'],
|
||||
'vod_name' => $item['title'],
|
||||
'vod_pic' => $this->IMGHOST . $item['path'],
|
||||
'vod_remarks' => $item['mask'] . ' ⭐' . $item['score']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => $filterObj,
|
||||
'list' => $list
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
// 构建请求URL
|
||||
$url = $this->HOST . '/api/crumb/list?page=' . $pg . '&type=0&limit=24';
|
||||
|
||||
// 处理过滤器
|
||||
$filterUrl = 'area=' . ($filter['area'] ?? '0') . '&sort=' . ($filter['sort'] ?? 'update') . '&year=' . ($filter['year'] ?? '0') . '&category_id=' . ($filter['cateId'] ?? $tid);
|
||||
$url .= '&' . $filterUrl;
|
||||
|
||||
// 处理短剧特殊情况
|
||||
if ($tid == 67) {
|
||||
$url = str_replace('/api/crumb/list', '/api/crumb/shortList', $url);
|
||||
}
|
||||
|
||||
$data = json_decode($this->fetch($url, [], $this->HEADERS), true);
|
||||
$list = [];
|
||||
$total = 0;
|
||||
|
||||
if (isset($data['data'])) {
|
||||
// 检查API响应中是否包含total信息
|
||||
if (is_array($data['data']) && count($data['data']) > 0) {
|
||||
// 对于API没有直接返回total的情况,我们假设总共有大量数据
|
||||
// 这里使用一个较大的值来确保分页正常工作
|
||||
$total = 1000;
|
||||
|
||||
foreach ($data['data'] as $item) {
|
||||
$isShort = $tid == 67;
|
||||
$imgUrl = $this->IMGHOST . ($isShort ? ($item['cover_image'] ?? $item['path']) : ($item['thumbnail'] ?? $item['path']));
|
||||
|
||||
// 短剧需要在vod_id中附加类型信息,以便detailContent方法识别
|
||||
$vodId = $isShort ? ($item['id'] . '@67') : $item['id'];
|
||||
|
||||
$list[] = [
|
||||
'vod_id' => $vodId,
|
||||
'vod_name' => $item['title'],
|
||||
'vod_pic' => $imgUrl,
|
||||
'vod_remarks' => ($item['mask'] ?? '') . ' ⭐' . ($item['score'] ?? '0')
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($list, $pg, $total, 24);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
$tid = '';
|
||||
if (strpos($id, '@') !== false) {
|
||||
list($id, $tid) = explode('@', $id);
|
||||
}
|
||||
|
||||
$isShort = $tid == 67;
|
||||
$detailPath = $isShort ? '/api/detail?vid=' . $id : '/api/video/detailv2?id=' . $id;
|
||||
|
||||
$detailUrl = $this->HOST . $detailPath;
|
||||
$data = json_decode($this->fetch($detailUrl, [], $this->HEADERS), true);
|
||||
$item = $data['data'];
|
||||
|
||||
$playFrom = [];
|
||||
$playUrls = [];
|
||||
|
||||
if ($isShort) {
|
||||
// 短剧可能有不同的数据结构,尝试多种方式获取播放列表
|
||||
$playlist = [];
|
||||
|
||||
// 尝试从playlist字段获取
|
||||
if (isset($item['playlist']) && is_array($item['playlist'])) {
|
||||
$playlist = $item['playlist'];
|
||||
}
|
||||
// 尝试从video_list字段获取
|
||||
elseif (isset($item['video_list']) && is_array($item['video_list'])) {
|
||||
$playlist = $item['video_list'];
|
||||
}
|
||||
// 尝试从episodes字段获取
|
||||
elseif (isset($item['episodes']) && is_array($item['episodes'])) {
|
||||
$playlist = $item['episodes'];
|
||||
}
|
||||
|
||||
if (count($playlist) > 0) {
|
||||
$playFrom[] = '短剧';
|
||||
$urls = [];
|
||||
foreach ($playlist as $ep) {
|
||||
// 处理不同的数据结构
|
||||
$title = $ep['title'] ?? $ep['episode_title'] ?? ($ep['episode'] ?? '第1集');
|
||||
$url = $ep['url'] ?? $ep['video_url'] ?? $ep['play_url'] ?? '';
|
||||
|
||||
// 过滤无效地址和ftp协议
|
||||
if (!empty($url) && stripos($url, 'ftp://') !== 0) {
|
||||
$urls[] = $title . '$' . $url;
|
||||
}
|
||||
}
|
||||
if (!empty($urls)) {
|
||||
$playUrls[] = implode('#', $urls);
|
||||
}
|
||||
} else {
|
||||
// 尝试直接从item中获取单个播放地址(针对单集短剧)
|
||||
$url = $item['url'] ?? $item['video_url'] ?? $item['play_url'] ?? '';
|
||||
if (!empty($url) && stripos($url, 'ftp://') !== 0) {
|
||||
$playFrom[] = '短剧';
|
||||
$playUrls[] = '全集$' . $url;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isset($item['source_list_source'])) {
|
||||
foreach ($item['source_list_source'] as $src) {
|
||||
$name = $src['name'] == '常规线路' ? '边下边播线路' : $src['name'];
|
||||
|
||||
$urls = [];
|
||||
foreach ($src['source_list'] as $ep) {
|
||||
$url = $ep['url'];
|
||||
// 过滤ftp协议的地址,只保留http/https协议
|
||||
if (stripos($url, 'ftp://') === 0) {
|
||||
continue;
|
||||
}
|
||||
$urls[] = ($ep['source_name'] ?? $ep['weight']) . '$' . $url;
|
||||
}
|
||||
if (!empty($urls)) {
|
||||
$playFrom[] = $name;
|
||||
$playUrls[] = implode('#', $urls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从source_list字段获取(备用方案)
|
||||
if (empty($playUrls) && isset($item['source_list'])) {
|
||||
foreach ($item['source_list'] as $src) {
|
||||
$name = $src['name'] ?? '默认线路';
|
||||
|
||||
$urls = [];
|
||||
foreach ($src['source'] as $ep) {
|
||||
$url = $ep['url'];
|
||||
if (stripos($url, 'ftp://') !== 0) {
|
||||
$urls[] = ($ep['name'] ?? $ep['title']) . '$' . $url;
|
||||
}
|
||||
}
|
||||
if (!empty($urls)) {
|
||||
$playFrom[] = $name;
|
||||
$playUrls[] = implode('#', $urls);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'list' => [[
|
||||
'vod_id' => $id,
|
||||
'vod_name' => $item['title'],
|
||||
'vod_pic' => $this->IMGHOST . ($isShort ? ($item['cover_image'] ?? $item['path']) : ($item['thumbnail'] ?? $item['path'])),
|
||||
'vod_year' => $item['year'] ?? '',
|
||||
'vod_area' => $item['area'] ?? '',
|
||||
'vod_remarks' => $item['update_cycle'] ?? $item['mask'] ?? '',
|
||||
'vod_actor' => implode('/', array_column($item['actors'] ?? [], 'name')),
|
||||
'vod_director' => implode('/', array_column($item['directors'] ?? [], 'name')),
|
||||
'vod_content' => $item['description'] ?? '',
|
||||
'vod_play_from' => implode('$$$', $playFrom),
|
||||
'vod_play_url' => implode('$$$', $playUrls)
|
||||
]]
|
||||
];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$searchUrl = $this->HOST . '/api/v2/search/videoV2?key=' . urlencode($key) . '&page=' . $pg;
|
||||
$data = json_decode($this->fetch($searchUrl, [], $this->HEADERS), true);
|
||||
$list = [];
|
||||
$total = 0;
|
||||
|
||||
if (isset($data['data'])) {
|
||||
if (is_array($data['data']) && count($data['data']) > 0) {
|
||||
// 对于搜索结果,同样使用较大值确保分页正常
|
||||
$total = 1000;
|
||||
|
||||
foreach ($data['data'] as $item) {
|
||||
// 检查是否为短剧,根据category_id判断
|
||||
$isShort = isset($item['category_id']) && $item['category_id'] == 67;
|
||||
$vodId = $isShort ? ($item['id'] . '@67') : $item['id'];
|
||||
|
||||
$list[] = [
|
||||
'vod_id' => $vodId,
|
||||
'vod_name' => $item['title'],
|
||||
'vod_pic' => $this->IMGHOST . $item['thumbnail'],
|
||||
'vod_remarks' => $item['mask'] . ' ⭐' . $item['score']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->pageResult($list, $pg, $total, 20);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
try {
|
||||
// 优化视频播放,使用更高效的处理方式
|
||||
// 检查是否为m3u8格式(流媒体格式)
|
||||
if (stripos($id, '.m3u8') !== false) {
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => $id,
|
||||
'header' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1',
|
||||
'Referer' => $id,
|
||||
'Accept' => '*/*',
|
||||
'Connection' => 'keep-alive',
|
||||
'Origin' => parse_url($id, PHP_URL_SCHEME) . '://' . parse_url($id, PHP_URL_HOST)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 检查是否为mp4等直接视频格式
|
||||
$videoExtensions = ['mp4', 'flv', 'avi', 'wmv', 'mov', 'webm'];
|
||||
$path = parse_url($id, PHP_URL_PATH) ?? '';
|
||||
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
|
||||
if (in_array($extension, $videoExtensions)) {
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => $id,
|
||||
'header' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1',
|
||||
'Referer' => $id,
|
||||
'Range' => 'bytes=0-', // 支持断点续传
|
||||
'Accept-Ranges' => 'bytes'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 默认返回原始地址
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => $id,
|
||||
'header' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1',
|
||||
'Referer' => $id
|
||||
]
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
// 发生错误时返回原始地址
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => $id,
|
||||
'header' => ['User-Agent' => 'Mozilla/5.0']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
error_reporting(0);
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private const SOURCES = [
|
||||
's1' => ['name' => '🔞滴滴', 'api' => 'https://api.ddapi.cc/api.php/provide/vod'],
|
||||
's2' => ['name' => '🔞鸡坤', 'api' => 'https://jkunzyapi.com/api.php/provide/vod'],
|
||||
's3' => ['name' => '🔞TG资源', 'api' => 'https://tgzyz.pp.ua/api.php/provide/vod'],
|
||||
's4' => ['name' => '🔞越南', 'api' => 'https://vnzyz.com/api.php/provide/vod'],
|
||||
's5' => ['name' => '🔞奥斯卡', 'api' => 'https://aosikazy4.com/api.php/provide/vod'],
|
||||
's6' => ['name' => '🔞X细胞', 'api' => 'https://www.xxibaozyw.com/api.php/provide/vod'],
|
||||
's7' => ['name' => '🔞大奶子', 'api' => 'https://apidanaizi.com/api.php/provide/vod'],
|
||||
's8' => ['name' => '🔞精品X', 'api' => 'https://www.jingpinx.com/api.php/provide/vod'],
|
||||
's9' => ['name' => '🔞老色p', 'api' => 'https://apilsbzy1.com/api.php/provide/vod'],
|
||||
's10' => ['name' => '🔞番号', 'api' => 'http://fhapi9.com/api.php/provide/vod'],
|
||||
's11' => ['name' => '🔞黄色仓库', 'api' => 'https://hsckzy888.com/api.php/provide/vod/from/hsckm3u8/at/json'],
|
||||
's12' => ['name' => '🔞百花', 'api' => 'https://bhziyuan.com/api.php/provide/vod/'],
|
||||
's13' => ['name' => '🔞辣椒', 'api' => 'https://apilj.com/api.php/provide/vod'],
|
||||
's14' => ['name' => '🔞155', 'api' => 'https://155api.com/api.php/provide/vod'],
|
||||
's15' => ['name' => '🔞杏吧', 'api' => 'https://xingba111.com/api.php/provide/vod/'],
|
||||
's16' => ['name' => '🔞玉兔', 'api' => 'https://apiyutu.com/api.php/provide/vod'],
|
||||
's17' => ['name' => '🔞AIvin', 'api' => 'http://lbapiby.com/api.php/provide/vod/at/json'],
|
||||
's18' => ['name' => '🔞乐播', 'api' => 'https://lbapi9.com/api.php/provide/vod'],
|
||||
's19' => ['name' => '🔞奶香香', 'api' => 'https://naixxzy.com/api.php/provide/vod'],
|
||||
's20' => ['name' => '🔞森林', 'api' => 'https://slapibf.com/api.php/provide/vod'],
|
||||
's21' => ['name' => '🔞番茄', 'api' => 'https://fqzy.me//api.php/provide/vod/'],
|
||||
's22' => ['name' => '🔞鲨鱼', 'api' => 'https://shayuapi.com/api.php/provide/vod'],
|
||||
's23' => ['name' => '🔞91麻豆', 'api' => 'http://91md.me/api.php/provide/vod'],
|
||||
's24' => ['name' => '🔞CK百货', 'api' => 'https://ckbh1.xyz/api.php/provide/vod/'],
|
||||
's25' => ['name' => '🔞桃花', 'api' => 'https://thzy1.me/api.php/provide/vod/'],
|
||||
's26' => ['name' => '🔞豆豆', 'api' => 'https://doudouzy.com/api.php/provide/vod/'],
|
||||
's27' => ['name' => '🔞色猫', 'api' => 'https://api.maozyapi.com/inc/apijson_vod.php'],
|
||||
's28' => ['name' => '🔞黑料X', 'api' => 'https://www.heiliaozyapi.com/api.php/provide/vod/'],
|
||||
's29' => ['name' => '🔞香蕉', 'api' => 'https://www.xiangjiaozyw.com/api.php/provide/vod/'],
|
||||
's30' => ['name' => '🔞百万', 'api' => 'https://api.bwzyz.com/api.php/provide/vod/at/json'],
|
||||
's31' => ['name' => '🔞souav', 'api' => 'https://api.souavzy.vip/api.php/provide/vod'],
|
||||
's32' => ['name' => '🔞淫水机', 'api' => 'https://www.xrbsp.com/api/json.php'],
|
||||
's33' => ['name' => '🔞白嫖', 'api' => 'https://www.kxgav.com/api/json.php'],
|
||||
's34' => ['name' => '🔞美少女', 'api' => 'https://www.msnii.com/api/json.php'],
|
||||
's35' => ['name' => '🔞色南国', 'api' => 'https://api.sexnguon.com/api.php/provide/vod'],
|
||||
's36' => ['name' => '🔞香奶儿', 'api' => 'https://www.gdlsp.com/api/json.php'],
|
||||
's37' => ['name' => '🔞黄AV', 'api' => 'https://www.pgxdy.com/api/json.php'],
|
||||
's38' => ['name' => '🇹极速', 'api' => 'https://jszyapi.com/api.php/provide/vod'],
|
||||
's39' => ['name' => '📺红牛3', 'api' => 'https://www.hongniuzy3.com/api.php/provide/vod'],
|
||||
's40' => ['name' => '🌊海洋', 'api' => 'http://www.seacms.org/api.php/provide/vod']
|
||||
|
||||
];
|
||||
|
||||
public function getName() { return "影视+专属全网聚合"; }
|
||||
public function init($extend = "") {}
|
||||
|
||||
private function buildUrl($url, $query) {
|
||||
return strpos($url, '?') !== false ? $url . '&' . $query : $url . '?' . $query;
|
||||
}
|
||||
|
||||
private function setCurlOpts($ch, $url, $timeout = 10) {
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_ENCODING, '');
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36');
|
||||
}
|
||||
|
||||
private function cleanItem($item, $sourceKey, $sourceName, $isDetail = false) {
|
||||
if (!$isDetail) {
|
||||
$item['vod_id'] = $sourceKey . '@@' . $item['vod_id'];
|
||||
}
|
||||
$item['vod_remarks'] = $sourceName . " | " . ($item['vod_remarks'] ?? '');
|
||||
|
||||
if (!empty($item['vod_play_from'])) {
|
||||
$froms = explode('$$$', $item['vod_play_from']);
|
||||
foreach ($froms as &$f) {
|
||||
$f = $sourceName . '-' . $f;
|
||||
}
|
||||
$item['vod_play_from'] = implode('$$$', $froms);
|
||||
}
|
||||
|
||||
unset($item['vod_down_from']);
|
||||
unset($item['vod_down_url']);
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function homeContent($filter = []) {
|
||||
$classes = [];
|
||||
$filters = [];
|
||||
$mh = curl_multi_init();
|
||||
$ch_list = [];
|
||||
|
||||
foreach (self::SOURCES as $key => $source) {
|
||||
$classes[] = ['type_id' => $key, 'type_name' => $source['name']];
|
||||
$ch = curl_init();
|
||||
$this->setCurlOpts($ch, $this->buildUrl($source['api'], "ac=list"), 4);
|
||||
curl_multi_add_handle($mh, $ch);
|
||||
$ch_list[$key] = $ch;
|
||||
}
|
||||
|
||||
$active = null;
|
||||
do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
while ($active && $mrc == CURLM_OK) {
|
||||
if (curl_multi_select($mh) != -1) {
|
||||
do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($ch_list as $key => $ch) {
|
||||
$response = curl_multi_getcontent($ch);
|
||||
curl_multi_remove_handle($mh, $ch);
|
||||
curl_close($ch);
|
||||
|
||||
$res = json_decode($response, true);
|
||||
$filterValues = [['n' => '全部(最新)', 'v' => '']];
|
||||
|
||||
if (isset($res['class'])) {
|
||||
foreach ($res['class'] as $c) {
|
||||
$filterValues[] = ['n' => $c['type_name'], 'v' => $c['type_id']];
|
||||
}
|
||||
}
|
||||
$filters[$key] = [['key' => 'cateId', 'name' => '分类', 'value' => $filterValues]];
|
||||
}
|
||||
curl_multi_close($mh);
|
||||
return ['class' => $classes, 'filters' => $filters, 'list' => []];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
if (!isset(self::SOURCES[$tid])) return ['list' => []];
|
||||
$source = self::SOURCES[$tid];
|
||||
if (!is_array($extend)) $extend = [];
|
||||
$realTid = isset($extend['cateId']) ? $extend['cateId'] : '';
|
||||
|
||||
$query = "ac=detail&pg={$pg}";
|
||||
if ($realTid !== '') $query .= "&t={$realTid}";
|
||||
|
||||
$ch = curl_init();
|
||||
$this->setCurlOpts($ch, $this->buildUrl($source['api'], $query), 10);
|
||||
$html = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$res = json_decode($html, true);
|
||||
$list = [];
|
||||
if (isset($res['list'])) {
|
||||
foreach ($res['list'] as $item) {
|
||||
$list[] = $this->cleanItem($item, $tid, $source['name'], false);
|
||||
}
|
||||
}
|
||||
return ['list' => $list, 'page' => $res['page'] ?? $pg, 'pagecount' => $res['pagecount'] ?? 0, 'limit' => $res['limit'] ?? 20, 'total' => $res['total'] ?? 0];
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
if (strpos($id, '@@') === false) return ['list' => []];
|
||||
|
||||
list($sourceKey, $realId) = explode('@@', $id);
|
||||
if (!isset(self::SOURCES[$sourceKey])) return ['list' => []];
|
||||
|
||||
$source = self::SOURCES[$sourceKey];
|
||||
$ch = curl_init();
|
||||
$this->setCurlOpts($ch, $this->buildUrl($source['api'], "ac=detail&ids={$realId}"), 10);
|
||||
$html = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$res = json_decode($html, true);
|
||||
$list = [];
|
||||
if (isset($res['list'])) {
|
||||
foreach ($res['list'] as $item) {
|
||||
$cleaned = $this->cleanItem($item, $sourceKey, $source['name'], true);
|
||||
$cleaned['vod_id'] = $id;
|
||||
$list[] = $cleaned;
|
||||
}
|
||||
}
|
||||
return ['list' => $list];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$keyword = urlencode($key);
|
||||
$list = [];
|
||||
$maxPageCount = 0;
|
||||
|
||||
$mh = curl_multi_init();
|
||||
$ch_list = [];
|
||||
foreach (self::SOURCES as $sourceKey => $source) {
|
||||
$ch = curl_init();
|
||||
$this->setCurlOpts($ch, $this->buildUrl($source['api'], "ac=detail&wd={$keyword}&pg={$pg}"), 6);
|
||||
curl_multi_add_handle($mh, $ch);
|
||||
$ch_list[$sourceKey] = $ch;
|
||||
}
|
||||
|
||||
$active = null;
|
||||
do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
while ($active && $mrc == CURLM_OK) {
|
||||
if (curl_multi_select($mh) != -1) {
|
||||
do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($ch_list as $sourceKey => $ch) {
|
||||
$response = curl_multi_getcontent($ch);
|
||||
curl_multi_remove_handle($mh, $ch);
|
||||
curl_close($ch);
|
||||
|
||||
$source = self::SOURCES[$sourceKey];
|
||||
$res = json_decode($response, true);
|
||||
if (isset($res['list'])) {
|
||||
foreach ($res['list'] as $item) {
|
||||
$list[] = $this->cleanItem($item, $sourceKey, $source['name'], false);
|
||||
}
|
||||
if (isset($res['pagecount']) && $res['pagecount'] > $maxPageCount) {
|
||||
$maxPageCount = $res['pagecount'];
|
||||
}
|
||||
}
|
||||
}
|
||||
curl_multi_close($mh);
|
||||
return ['list' => $list, 'page' => $pg, 'pagecount' => $maxPageCount ?: $pg, 'limit' => 40, 'total' => 9999];
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
return [
|
||||
"parse" => 0,
|
||||
"url" => $id,
|
||||
"header" => [
|
||||
"User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,627 @@
|
||||
<?php
|
||||
/**
|
||||
* 裤佬.php - 影视+专属全网聚合
|
||||
* 完全独立版本,不依赖外部文件
|
||||
*/
|
||||
|
||||
error_reporting(0);
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
// ==================== HtmlParser 类 ====================
|
||||
class HtmlParser {
|
||||
|
||||
/**
|
||||
* Parse HTML and return array of OuterHTML strings
|
||||
*/
|
||||
public function pdfa($html, $rule) {
|
||||
if (empty($html) || empty($rule)) return [];
|
||||
$doc = $this->getDom($html);
|
||||
$xpath = new DOMXPath($doc);
|
||||
|
||||
$xpathQuery = $this->parseRuleToXpath($rule);
|
||||
$nodes = $xpath->query($xpathQuery);
|
||||
|
||||
$res = [];
|
||||
if ($nodes) {
|
||||
foreach ($nodes as $node) {
|
||||
$res[] = $doc->saveHTML($node);
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML and return single value (Text, Html, or Attribute)
|
||||
*/
|
||||
public function pdfh($html, $rule, $baseUrl = '') {
|
||||
if (empty($html) || empty($rule)) return '';
|
||||
$doc = $this->getDom($html);
|
||||
$xpath = new DOMXPath($doc);
|
||||
|
||||
$option = '';
|
||||
if (strpos($rule, '&&') !== false) {
|
||||
$parts = explode('&&', $rule);
|
||||
$option = array_pop($parts);
|
||||
$rule = implode('&&', $parts);
|
||||
}
|
||||
|
||||
$xpathQuery = $this->parseRuleToXpath($rule);
|
||||
$nodes = $xpath->query($xpathQuery);
|
||||
|
||||
if ($nodes && $nodes->length > 0) {
|
||||
if ($option === 'Text') {
|
||||
$text = '';
|
||||
foreach ($nodes as $node) {
|
||||
$text .= $node->textContent;
|
||||
}
|
||||
return $this->parseText($text);
|
||||
}
|
||||
|
||||
$node = $nodes->item(0);
|
||||
return $this->formatOutput($doc, $node, $option, $baseUrl);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML and return URL (auto joined)
|
||||
*/
|
||||
public function pd($html, $rule, $baseUrl = '') {
|
||||
$res = $this->pdfh($html, $rule, $baseUrl);
|
||||
return $this->urlJoin($baseUrl, $res);
|
||||
}
|
||||
|
||||
// --- Helper Methods ---
|
||||
|
||||
private function parseText($text) {
|
||||
$text = preg_replace('/[\s]+/u', "\n", $text);
|
||||
$text = preg_replace('/\n+/', "\n", $text);
|
||||
$text = trim($text);
|
||||
$text = str_replace("\n", ' ', $text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
private function parseRuleToXpath($rule) {
|
||||
$rule = str_replace('&&', ' ', $rule);
|
||||
$parts = explode(' ', $rule);
|
||||
$xpathParts = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (empty($part)) continue;
|
||||
$xpathParts[] = $this->transSingleSelector($part);
|
||||
}
|
||||
|
||||
return '//' . implode('//', $xpathParts);
|
||||
}
|
||||
|
||||
private function transSingleSelector($selector) {
|
||||
$position = null;
|
||||
if (preg_match('/:eq\((-?\d+)\)/', $selector, $matches)) {
|
||||
$idx = intval($matches[1]);
|
||||
$selector = str_replace($matches[0], '', $selector);
|
||||
if ($idx >= 0) {
|
||||
$position = $idx + 1;
|
||||
} else {
|
||||
$offset = abs($idx) - 1;
|
||||
$position = "last()" . ($offset > 0 ? "-$offset" : "");
|
||||
}
|
||||
}
|
||||
|
||||
$tag = '*';
|
||||
$conditions = [];
|
||||
|
||||
if (preg_match('/#([\w-]+)/', $selector, $m)) {
|
||||
$conditions[] = '@id="' . $m[1] . '"';
|
||||
$selector = str_replace($m[0], '', $selector);
|
||||
}
|
||||
|
||||
if (preg_match_all('/\.([\w-]+)/', $selector, $m)) {
|
||||
foreach ($m[1] as $cls) {
|
||||
$conditions[] = 'contains(concat(" ", normalize-space(@class), " "), " ' . $cls . ' ")';
|
||||
}
|
||||
$selector = preg_replace('/\.[\w-]+/', '', $selector);
|
||||
}
|
||||
|
||||
if (!empty($selector)) {
|
||||
$tag = $selector;
|
||||
}
|
||||
|
||||
$xpath = $tag;
|
||||
if (!empty($conditions)) {
|
||||
$xpath .= '[' . implode(' and ', $conditions) . ']';
|
||||
}
|
||||
if ($position !== null) {
|
||||
$xpath .= '[' . $position . ']';
|
||||
}
|
||||
|
||||
return $xpath;
|
||||
}
|
||||
|
||||
private function formatOutput($doc, $node, $option, $baseUrl) {
|
||||
if ($option === 'Text') {
|
||||
return $this->parseText($node->textContent);
|
||||
} elseif ($option === 'Html') {
|
||||
return $doc->saveHTML($node);
|
||||
} elseif ($option) {
|
||||
return $node->getAttribute($option);
|
||||
}
|
||||
return $doc->saveHTML($node);
|
||||
}
|
||||
|
||||
private function getDom($html) {
|
||||
$doc = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
if (!empty($html) && mb_detect_encoding($html, 'UTF-8', true) === false) {
|
||||
$html = mb_convert_encoding($html, 'UTF-8', 'GBK, BIG5');
|
||||
}
|
||||
$html = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . $html;
|
||||
$doc->loadHTML($html);
|
||||
libxml_clear_errors();
|
||||
return $doc;
|
||||
}
|
||||
|
||||
private function urlJoin($baseUrl, $relativeUrl) {
|
||||
if (empty($relativeUrl)) return '';
|
||||
if (preg_match('#^https?://#', $relativeUrl)) return $relativeUrl;
|
||||
if (empty($baseUrl)) return $relativeUrl;
|
||||
|
||||
$parts = parse_url($baseUrl);
|
||||
$scheme = isset($parts['scheme']) ? $parts['scheme'] . '://' : 'http://';
|
||||
$host = isset($parts['host']) ? $parts['host'] : '';
|
||||
|
||||
if (substr($relativeUrl, 0, 1) == '/') {
|
||||
return $scheme . $host . $relativeUrl;
|
||||
}
|
||||
|
||||
$path = isset($parts['path']) ? $parts['path'] : '/';
|
||||
$dir = rtrim(dirname($path), '/\\');
|
||||
if ($dir === '/' || $dir === '\\') $dir = '';
|
||||
|
||||
return $scheme . $host . $dir . '/' . $relativeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== BaseSpider 抽象类 ====================
|
||||
abstract class BaseSpider {
|
||||
|
||||
protected $headers = [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9',
|
||||
];
|
||||
|
||||
protected $htmlParser;
|
||||
|
||||
public function __construct() {
|
||||
$this->htmlParser = new HtmlParser();
|
||||
}
|
||||
|
||||
public function init($extend = '') {}
|
||||
public function homeContent($filter) { return ['class' => []]; }
|
||||
public function homeVideoContent() { return ['list' => []]; }
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
return ['list' => [], 'page' => $pg, 'pagecount' => 1, 'limit' => 20, 'total' => 0];
|
||||
}
|
||||
public function detailContent($ids) { return ['list' => []]; }
|
||||
public function searchContent($key, $quick = false, $pg = 1) { return ['list' => []]; }
|
||||
public function playerContent($flag, $id, $vipFlags = []) { return ['parse' => 0, 'url' => '', 'header' => []]; }
|
||||
public function localProxy($params) { return null; }
|
||||
public function action($action, $value) { return ''; }
|
||||
|
||||
protected function pdfa($html, $rule) {
|
||||
return $this->htmlParser->pdfa($html, $rule);
|
||||
}
|
||||
|
||||
protected function pdfh($html, $rule, $baseUrl = '') {
|
||||
return $this->htmlParser->pdfh($html, $rule, $baseUrl);
|
||||
}
|
||||
|
||||
protected function pd($html, $rule, $baseUrl = '') {
|
||||
if (empty($baseUrl)) {
|
||||
$baseUrl = $this->tryGetHost();
|
||||
}
|
||||
return $this->htmlParser->pd($html, $rule, $baseUrl);
|
||||
}
|
||||
|
||||
private function tryGetHost() {
|
||||
try {
|
||||
$ref = new ReflectionClass($this);
|
||||
if ($ref->hasProperty('HOST')) {
|
||||
$prop = $ref->getProperty('HOST');
|
||||
if (PHP_VERSION_ID < 80100) {
|
||||
$prop->setAccessible(true);
|
||||
}
|
||||
$val = $prop->getValue($this);
|
||||
if (!empty($val)) return $val;
|
||||
}
|
||||
if ($ref->hasConstant('HOST')) {
|
||||
return $ref->getConstant('HOST');
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function pageResult($list, $pg, $total = 0, $limit = 20) {
|
||||
$pg = max(1, intval($pg));
|
||||
$count = count($list);
|
||||
if ($total > 0) {
|
||||
$pagecount = ceil($total / $limit);
|
||||
} else {
|
||||
if ($count < $limit) {
|
||||
$pagecount = $pg;
|
||||
$total = ($pg - 1) * $limit + $count;
|
||||
} else {
|
||||
$pagecount = 9999;
|
||||
$total = 99999;
|
||||
}
|
||||
}
|
||||
return [
|
||||
'list' => $list,
|
||||
'page' => $pg,
|
||||
'pagecount' => intval($pagecount),
|
||||
'limit' => intval($limit),
|
||||
'total' => intval($total)
|
||||
];
|
||||
}
|
||||
|
||||
protected function fetch($url, $options = [], $headers = []) {
|
||||
if (isset($options['headers'])) {
|
||||
$headers = array_merge($headers, $options['headers']);
|
||||
unset($options['headers']);
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
$customHeaders = [];
|
||||
foreach ($headers as $k => $v) {
|
||||
if (is_numeric($k)) {
|
||||
$parts = explode(':', $v, 2);
|
||||
if (count($parts) === 2) {
|
||||
$key = trim($parts[0]);
|
||||
$value = trim($parts[1]);
|
||||
$customHeaders[$key] = $value;
|
||||
}
|
||||
} else {
|
||||
$customHeaders[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$finalHeadersMap = array_merge($this->headers, $customHeaders);
|
||||
$mergedHeaders = [];
|
||||
foreach ($finalHeadersMap as $k => $v) {
|
||||
if ($v === "") {
|
||||
$mergedHeaders[] = $k . ";";
|
||||
} else {
|
||||
$mergedHeaders[] = "$k: $v";
|
||||
}
|
||||
}
|
||||
|
||||
$defaultOptions = [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_HTTPHEADER => $mergedHeaders,
|
||||
];
|
||||
|
||||
if (isset($options['body'])) {
|
||||
$defaultOptions[CURLOPT_POST] = true;
|
||||
$defaultOptions[CURLOPT_POSTFIELDS] = $options['body'];
|
||||
unset($options['body']);
|
||||
}
|
||||
|
||||
if (isset($options['cookie'])) {
|
||||
$defaultOptions[CURLOPT_COOKIE] = $options['cookie'];
|
||||
unset($options['cookie']);
|
||||
}
|
||||
|
||||
foreach ($options as $k => $v) {
|
||||
$defaultOptions[$k] = $v;
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, $defaultOptions);
|
||||
$result = curl_exec($ch);
|
||||
if (is_resource($ch)) curl_close($ch);
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function fetchJson($url, $options = []) {
|
||||
$resp = $this->fetch($url, $options);
|
||||
return json_decode($resp, true) ?: [];
|
||||
}
|
||||
|
||||
public function run() {
|
||||
$ac = $_GET['ac'] ?? '';
|
||||
$t = $_GET['t'] ?? '';
|
||||
$pg = $_GET['pg'] ?? '1';
|
||||
$wd = $_GET['wd'] ?? '';
|
||||
$ids = $_GET['ids'] ?? '';
|
||||
$play = $_GET['play'] ?? '';
|
||||
$flag = $_GET['flag'] ?? '';
|
||||
$filter = isset($_GET['filter']) && $_GET['filter'] === 'true';
|
||||
$extend = $_GET['ext'] ?? '';
|
||||
if (!empty($extend) && is_string($extend)) {
|
||||
$decoded = json_decode(base64_decode($extend), true);
|
||||
if (is_array($decoded)) $extend = $decoded;
|
||||
}
|
||||
$action = $_GET['action'] ?? '';
|
||||
$value = $_GET['value'] ?? '';
|
||||
|
||||
$this->init($extend);
|
||||
|
||||
try {
|
||||
if ($ac === 'action') {
|
||||
echo json_encode($this->action($action, $value), JSON_UNESCAPED_UNICODE);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($ac === 'play' || !empty($play)) {
|
||||
$playId = !empty($play) ? $play : ($_GET['id'] ?? '');
|
||||
echo json_encode($this->playerContent($flag, $playId), JSON_UNESCAPED_UNICODE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($wd)) {
|
||||
echo json_encode($this->searchContent($wd, false, $pg), JSON_UNESCAPED_UNICODE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($ids) && !empty($ac)) {
|
||||
$idList = explode(',', $ids);
|
||||
echo json_encode($this->detailContent($idList), JSON_UNESCAPED_UNICODE);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($t !== '' && !empty($ac)) {
|
||||
$filterData = [];
|
||||
echo json_encode($this->categoryContent($t, $pg, $filterData, $extend), JSON_UNESCAPED_UNICODE);
|
||||
return;
|
||||
}
|
||||
|
||||
$homeData = $this->homeContent($filter);
|
||||
$videoData = $this->homeVideoContent();
|
||||
$result = ['class' => $homeData['class'] ?? []];
|
||||
if (isset($videoData['list'])) $result['list'] = $videoData['list'];
|
||||
if (isset($homeData['list']) && !empty($homeData['list'])) $result['list'] = $homeData['list'];
|
||||
if (isset($homeData['filters'])) $result['filters'] = $homeData['filters'];
|
||||
|
||||
echo json_encode($result, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['code' => 500, 'msg' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['code' => 500, 'msg' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Spider 类 ====================
|
||||
class Spider extends BaseSpider {
|
||||
private const SOURCES = [
|
||||
's1' => ['name' => '🔞滴滴', 'api' => 'https://api.ddapi.cc/api.php/provide/vod'],
|
||||
's2' => ['name' => '🔞鸡坤', 'api' => 'https://jkunzyapi.com/api.php/provide/vod'],
|
||||
's3' => ['name' => '🔞TG资源', 'api' => 'https://tgzyz.pp.ua/api.php/provide/vod'],
|
||||
's4' => ['name' => '🔞越南', 'api' => 'https://vnzyz.com/api.php/provide/vod'],
|
||||
's5' => ['name' => '🔞奥斯卡', 'api' => 'https://aosikazy4.com/api.php/provide/vod'],
|
||||
's6' => ['name' => '🔞X细胞', 'api' => 'https://www.xxibaozyw.com/api.php/provide/vod'],
|
||||
's7' => ['name' => '🔞大奶子', 'api' => 'https://apidanaizi.com/api.php/provide/vod'],
|
||||
's8' => ['name' => '🔞精品X', 'api' => 'https://www.jingpinx.com/api.php/provide/vod'],
|
||||
's9' => ['name' => '🔞老色p', 'api' => 'https://apilsbzy1.com/api.php/provide/vod'],
|
||||
's10' => ['name' => '🔞番号', 'api' => 'http://fhapi9.com/api.php/provide/vod'],
|
||||
's11' => ['name' => '🔞黄色仓库', 'api' => 'https://hsckzy888.com/api.php/provide/vod/from/hsckm3u8/at/json'],
|
||||
's12' => ['name' => '🔞百花', 'api' => 'https://bhziyuan.com/api.php/provide/vod/'],
|
||||
's13' => ['name' => '🔞辣椒', 'api' => 'https://apilj.com/api.php/provide/vod'],
|
||||
's14' => ['name' => '🔞155', 'api' => 'https://155api.com/api.php/provide/vod'],
|
||||
's15' => ['name' => '🔞杏吧', 'api' => 'https://xingba111.com/api.php/provide/vod/'],
|
||||
's16' => ['name' => '🔞玉兔', 'api' => 'https://apiyutu.com/api.php/provide/vod'],
|
||||
's17' => ['name' => '🔞AIvin', 'api' => 'http://lbapiby.com/api.php/provide/vod/at/json'],
|
||||
's18' => ['name' => '🔞乐播', 'api' => 'https://lbapi9.com/api.php/provide/vod'],
|
||||
's19' => ['name' => '🔞奶香香', 'api' => 'https://naixxzy.com/api.php/provide/vod'],
|
||||
's20' => ['name' => '🔞森林', 'api' => 'https://slapibf.com/api.php/provide/vod'],
|
||||
's21' => ['name' => '🔞番茄', 'api' => 'https://fqzy.me//api.php/provide/vod/'],
|
||||
's22' => ['name' => '🔞鲨鱼', 'api' => 'https://shayuapi.com/api.php/provide/vod'],
|
||||
's23' => ['name' => '🔞91麻豆', 'api' => 'http://91md.me/api.php/provide/vod'],
|
||||
's24' => ['name' => '🔞CK百货', 'api' => 'https://ckbh1.xyz/api.php/provide/vod/'],
|
||||
's25' => ['name' => '🔞桃花', 'api' => 'https://thzy1.me/api.php/provide/vod/'],
|
||||
's26' => ['name' => '🔞豆豆', 'api' => 'https://doudouzy.com/api.php/provide/vod/'],
|
||||
's27' => ['name' => '🔞色猫', 'api' => 'https://api.maozyapi.com/inc/apijson_vod.php'],
|
||||
's28' => ['name' => '🔞黑料X', 'api' => 'https://www.heiliaozyapi.com/api.php/provide/vod/'],
|
||||
's29' => ['name' => '🔞香蕉', 'api' => 'https://www.xiangjiaozyw.com/api.php/provide/vod/'],
|
||||
's30' => ['name' => '🔞百万', 'api' => 'https://api.bwzyz.com/api.php/provide/vod/at/json'],
|
||||
's31' => ['name' => '🔞souav', 'api' => 'https://api.souavzy.vip/api.php/provide/vod'],
|
||||
's32' => ['name' => '🔞淫水机', 'api' => 'https://www.xrbsp.com/api/json.php'],
|
||||
's33' => ['name' => '🔞白嫖', 'api' => 'https://www.kxgav.com/api/json.php'],
|
||||
's34' => ['name' => '🔞美少女', 'api' => 'https://www.msnii.com/api/json.php'],
|
||||
's35' => ['name' => '🔞色南国', 'api' => 'https://api.sexnguon.com/api.php/provide/vod'],
|
||||
's36' => ['name' => '🔞香奶儿', 'api' => 'https://www.gdlsp.com/api/json.php'],
|
||||
's37' => ['name' => '🔞黄AV', 'api' => 'https://www.pgxdy.com/api/json.php'],
|
||||
's38' => ['name' => '🇹极速', 'api' => 'https://jszyapi.com/api.php/provide/vod'],
|
||||
's39' => ['name' => '📺红牛3', 'api' => 'https://www.hongniuzy3.com/api.php/provide/vod'],
|
||||
's40' => ['name' => '🌊海洋', 'api' => 'http://www.seacms.org/api.php/provide/vod']
|
||||
];
|
||||
|
||||
public function getName() { return "影视+专属全网聚合"; }
|
||||
public function init($extend = "") {}
|
||||
|
||||
private function buildUrl($url, $query) {
|
||||
return strpos($url, '?') !== false ? $url . '&' . $query : $url . '?' . $query;
|
||||
}
|
||||
|
||||
private function setCurlOpts($ch, $url, $timeout = 10) {
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_ENCODING, '');
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36');
|
||||
}
|
||||
|
||||
private function cleanItem($item, $sourceKey, $sourceName, $isDetail = false) {
|
||||
if (!$isDetail) {
|
||||
$item['vod_id'] = $sourceKey . '@@' . $item['vod_id'];
|
||||
}
|
||||
$item['vod_remarks'] = $sourceName . " | " . ($item['vod_remarks'] ?? '');
|
||||
|
||||
if (!empty($item['vod_play_from'])) {
|
||||
$froms = explode('$$$', $item['vod_play_from']);
|
||||
foreach ($froms as &$f) {
|
||||
$f = $sourceName . '-' . $f;
|
||||
}
|
||||
$item['vod_play_from'] = implode('$$$', $froms);
|
||||
}
|
||||
|
||||
unset($item['vod_down_from']);
|
||||
unset($item['vod_down_url']);
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function homeContent($filter = []) {
|
||||
$classes = [];
|
||||
$filters = [];
|
||||
$mh = curl_multi_init();
|
||||
$ch_list = [];
|
||||
|
||||
foreach (self::SOURCES as $key => $source) {
|
||||
$classes[] = ['type_id' => $key, 'type_name' => $source['name']];
|
||||
$ch = curl_init();
|
||||
$this->setCurlOpts($ch, $this->buildUrl($source['api'], "ac=list"), 4);
|
||||
curl_multi_add_handle($mh, $ch);
|
||||
$ch_list[$key] = $ch;
|
||||
}
|
||||
|
||||
$active = null;
|
||||
do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
while ($active && $mrc == CURLM_OK) {
|
||||
if (curl_multi_select($mh) != -1) {
|
||||
do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($ch_list as $key => $ch) {
|
||||
$response = curl_multi_getcontent($ch);
|
||||
curl_multi_remove_handle($mh, $ch);
|
||||
curl_close($ch);
|
||||
|
||||
$res = json_decode($response, true);
|
||||
$filterValues = [['n' => '全部(最新)', 'v' => '']];
|
||||
|
||||
if (isset($res['class'])) {
|
||||
foreach ($res['class'] as $c) {
|
||||
$filterValues[] = ['n' => $c['type_name'], 'v' => $c['type_id']];
|
||||
}
|
||||
}
|
||||
$filters[$key] = [['key' => 'cateId', 'name' => '分类', 'value' => $filterValues]];
|
||||
}
|
||||
curl_multi_close($mh);
|
||||
return ['class' => $classes, 'filters' => $filters, 'list' => []];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
if (!isset(self::SOURCES[$tid])) return ['list' => []];
|
||||
$source = self::SOURCES[$tid];
|
||||
if (!is_array($extend)) $extend = [];
|
||||
$realTid = isset($extend['cateId']) ? $extend['cateId'] : '';
|
||||
|
||||
$query = "ac=detail&pg={$pg}";
|
||||
if ($realTid !== '') $query .= "&t={$realTid}";
|
||||
|
||||
$ch = curl_init();
|
||||
$this->setCurlOpts($ch, $this->buildUrl($source['api'], $query), 10);
|
||||
$html = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$res = json_decode($html, true);
|
||||
$list = [];
|
||||
if (isset($res['list'])) {
|
||||
foreach ($res['list'] as $item) {
|
||||
$list[] = $this->cleanItem($item, $tid, $source['name'], false);
|
||||
}
|
||||
}
|
||||
return ['list' => $list, 'page' => $res['page'] ?? $pg, 'pagecount' => $res['pagecount'] ?? 0, 'limit' => $res['limit'] ?? 20, 'total' => $res['total'] ?? 0];
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
if (strpos($id, '@@') === false) return ['list' => []];
|
||||
|
||||
list($sourceKey, $realId) = explode('@@', $id);
|
||||
if (!isset(self::SOURCES[$sourceKey])) return ['list' => []];
|
||||
|
||||
$source = self::SOURCES[$sourceKey];
|
||||
$ch = curl_init();
|
||||
$this->setCurlOpts($ch, $this->buildUrl($source['api'], "ac=detail&ids={$realId}"), 10);
|
||||
$html = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$res = json_decode($html, true);
|
||||
$list = [];
|
||||
if (isset($res['list'])) {
|
||||
foreach ($res['list'] as $item) {
|
||||
$cleaned = $this->cleanItem($item, $sourceKey, $source['name'], true);
|
||||
$cleaned['vod_id'] = $id;
|
||||
$list[] = $cleaned;
|
||||
}
|
||||
}
|
||||
return ['list' => $list];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$keyword = urlencode($key);
|
||||
$list = [];
|
||||
$maxPageCount = 0;
|
||||
|
||||
$mh = curl_multi_init();
|
||||
$ch_list = [];
|
||||
foreach (self::SOURCES as $sourceKey => $source) {
|
||||
$ch = curl_init();
|
||||
$this->setCurlOpts($ch, $this->buildUrl($source['api'], "ac=detail&wd={$keyword}&pg={$pg}"), 6);
|
||||
curl_multi_add_handle($mh, $ch);
|
||||
$ch_list[$sourceKey] = $ch;
|
||||
}
|
||||
|
||||
$active = null;
|
||||
do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
while ($active && $mrc == CURLM_OK) {
|
||||
if (curl_multi_select($mh) != -1) {
|
||||
do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($ch_list as $sourceKey => $ch) {
|
||||
$response = curl_multi_getcontent($ch);
|
||||
curl_multi_remove_handle($mh, $ch);
|
||||
curl_close($ch);
|
||||
|
||||
$source = self::SOURCES[$sourceKey];
|
||||
$res = json_decode($response, true);
|
||||
if (isset($res['list'])) {
|
||||
foreach ($res['list'] as $item) {
|
||||
$list[] = $this->cleanItem($item, $sourceKey, $source['name'], false);
|
||||
}
|
||||
if (isset($res['pagecount']) && $res['pagecount'] > $maxPageCount) {
|
||||
$maxPageCount = $res['pagecount'];
|
||||
}
|
||||
}
|
||||
}
|
||||
curl_multi_close($mh);
|
||||
return ['list' => $list, 'page' => $pg, 'pagecount' => $maxPageCount ?: $pg, 'limit' => 40, 'total' => 9999];
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
return [
|
||||
"parse" => 0,
|
||||
"url" => $id,
|
||||
"header" => [
|
||||
"User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 运行
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
|
||||
public function getName() {
|
||||
return "酷爱漫画";
|
||||
}
|
||||
|
||||
public function init($extend = "") {
|
||||
// pass
|
||||
}
|
||||
|
||||
public function isVideoFormat($url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function manualVideoCheck() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getHeader() {
|
||||
return [
|
||||
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.203 Safari/537.36",
|
||||
"Referer" => "https://www.kuimh.com/"
|
||||
];
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$classes = [
|
||||
["type_name" => "国产", "type_id" => "1"],
|
||||
["type_name" => "日本", "type_id" => "2"],
|
||||
["type_name" => "韩国", "type_id" => "3"],
|
||||
["type_name" => "欧美", "type_id" => "5"],
|
||||
["type_name" => "其他", "type_id" => "7"],
|
||||
["type_name" => "日韩", "type_id" => "8"]
|
||||
];
|
||||
|
||||
$tags = ["全部", "恋爱", "古风", "校园", "奇幻", "大女主", "治愈", "穿越", "励志", "爆笑", "萌系", "玄幻", "日常", "都市", "彩虹", "灵异", "悬疑", "少年"];
|
||||
$tagValues = [];
|
||||
foreach ($tags as $t) {
|
||||
$tagValues[] = ["n" => $t, "v" => $t];
|
||||
}
|
||||
$filterConfig = [
|
||||
"key" => "tag",
|
||||
"name" => "题材",
|
||||
"value" => $tagValues
|
||||
];
|
||||
|
||||
$statusConfig = [
|
||||
"key" => "end",
|
||||
"name" => "状态",
|
||||
"value" => [
|
||||
["n" => "全部", "v" => "-1"],
|
||||
["n" => "连载", "v" => "0"],
|
||||
["n" => "完结", "v" => "1"]
|
||||
]
|
||||
];
|
||||
|
||||
$filters = [];
|
||||
foreach ($classes as $c) {
|
||||
$filters[$c['type_id']] = [$filterConfig, $statusConfig];
|
||||
}
|
||||
|
||||
return ["class" => $classes, "filters" => $filters];
|
||||
}
|
||||
|
||||
public function homeVideoContent() {
|
||||
return $this->categoryContent("1", 1, [], []);
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$tag = urlencode($extend['tag'] ?? '全部');
|
||||
$end = $extend['end'] ?? '-1';
|
||||
$url = "https://www.kuimh.com/booklist?tag={$tag}&area={$tid}&end={$end}&page={$pg}";
|
||||
|
||||
try {
|
||||
$html = $this->fetch($url, ['headers' => $this->getHeader()]);
|
||||
$items = $this->pdfa($html, '.mh-item');
|
||||
|
||||
$videos = [];
|
||||
foreach ($items as $item) {
|
||||
$vid = $this->pd($item, 'a&&href');
|
||||
$style = $this->pd($item, 'p&&style');
|
||||
$cover = "";
|
||||
if (preg_match('/url\((.*?)\)/', $style, $matches)) {
|
||||
$cover = $matches[1];
|
||||
}
|
||||
|
||||
// 尝试提取名称,Python逻辑是取第二个a标签,这里简化
|
||||
$name = $this->pd($item, 'a:eq(1)&&Text');
|
||||
if (!$name) {
|
||||
$name = $this->pd($item, '.title a&&Text');
|
||||
}
|
||||
if (!$name) {
|
||||
$name = $this->pd($item, 'a&&title');
|
||||
}
|
||||
if (!$name) {
|
||||
$name = $this->pd($item, 'Text');
|
||||
}
|
||||
|
||||
$videos[] = [
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => trim($name),
|
||||
"vod_pic" => $cover,
|
||||
"vod_remarks" => ""
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
"list" => $videos,
|
||||
"page" => $pg,
|
||||
"pagecount" => 9999,
|
||||
"limit" => 30,
|
||||
"total" => 999999
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return ["list" => []];
|
||||
}
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$vid = $ids[0];
|
||||
$url = (strpos($vid, 'http') === 0) ? $vid : "https://www.kuimh.com{$vid}";
|
||||
|
||||
try {
|
||||
$html = $this->fetch($url, ['headers' => $this->getHeader()]);
|
||||
|
||||
$name = $this->pd($html, '.info h1&&Text');
|
||||
$cover = $this->pd($html, '.cover img&&src');
|
||||
$desc = $this->pd($html, '.content p&&Text');
|
||||
|
||||
$chapterList = $this->pdfa($html, '.mCustomScrollBox li a');
|
||||
if (empty($chapterList)) {
|
||||
$chapterList = $this->pdfa($html, '#detail-list-select li a');
|
||||
}
|
||||
|
||||
$vodPlayUrlList = [];
|
||||
foreach ($chapterList as $chapter) {
|
||||
$chapterName = $this->pd($chapter, 'a&&Text');
|
||||
$chapterHref = $this->pd($chapter, 'a&&href');
|
||||
|
||||
if (!$chapterHref) continue;
|
||||
|
||||
$vodPlayUrlList[] = "{$chapterName}\${$chapterHref}";
|
||||
}
|
||||
|
||||
$playUrlStr = implode("#", $vodPlayUrlList);
|
||||
|
||||
return [
|
||||
"list" => [[
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => $name,
|
||||
"vod_pic" => $cover,
|
||||
"type_name" => "漫画",
|
||||
"vod_year" => "",
|
||||
"vod_area" => "",
|
||||
"vod_remarks" => "",
|
||||
"vod_actor" => "",
|
||||
"vod_director" => "",
|
||||
"vod_content" => $desc,
|
||||
"vod_play_from" => '阅读',
|
||||
"vod_play_url" => $playUrlStr
|
||||
]]
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return ["list" => []];
|
||||
}
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$key = urlencode($key);
|
||||
$url = "https://www.kuimh.com/search?keyword={$key}&page={$pg}";
|
||||
|
||||
try {
|
||||
$html = $this->fetch($url, ['headers' => $this->getHeader()]);
|
||||
$items = $this->pdfa($html, '.mh-item');
|
||||
|
||||
$videos = [];
|
||||
foreach ($items as $item) {
|
||||
$vid = $this->pd($item, 'a&&href');
|
||||
$style = $this->pd($item, 'p&&style');
|
||||
$cover = "";
|
||||
if (preg_match('/url\((.*?)\)/', $style, $matches)) {
|
||||
$cover = $matches[1];
|
||||
}
|
||||
|
||||
$name = $this->pd($item, '.title a&&title');
|
||||
if (!$name) $name = $this->pd($item, 'a&&title');
|
||||
if (!$name) $name = $this->pd($item, 'Text');
|
||||
|
||||
$videos[] = [
|
||||
"vod_id" => $vid,
|
||||
"vod_name" => trim($name),
|
||||
"vod_pic" => $cover,
|
||||
"vod_remarks" => ""
|
||||
];
|
||||
}
|
||||
return ['list' => $videos];
|
||||
} catch (Exception $e) {
|
||||
return ['list' => []];
|
||||
}
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$url = (strpos($id, 'http') === 0) ? $id : "https://www.kuimh.com{$id}";
|
||||
$headers = $this->getHeader();
|
||||
$headers['Referer'] = $url;
|
||||
|
||||
try {
|
||||
$html = $this->fetch($url, ['headers' => $headers]);
|
||||
|
||||
$imageList = [];
|
||||
|
||||
// 1. DOM 解析
|
||||
$imgs = $this->pdfa($html, '.comicpage img');
|
||||
if (empty($imgs)) {
|
||||
$imgs = $this->pdfa($html, '.comiclist img');
|
||||
}
|
||||
|
||||
foreach ($imgs as $img) {
|
||||
$src = $this->pd($img, 'data-echo');
|
||||
if (!$src) $src = $this->pd($img, 'data-src');
|
||||
if (!$src) $src = $this->pd($img, 'data-original');
|
||||
if (!$src) $src = $this->pd($img, 'src');
|
||||
|
||||
if ($src) $imageList[] = $src;
|
||||
}
|
||||
|
||||
// 2. data-echo 全局查找
|
||||
if (empty($imageList)) {
|
||||
$allLazyImgs = $this->pdfa($html, 'img[data-echo]');
|
||||
foreach ($allLazyImgs as $img) {
|
||||
$src = $this->pd($img, 'data-echo');
|
||||
if ($src && !in_array($src, $imageList)) {
|
||||
$imageList[] = $src;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 正则兜底
|
||||
if (empty($imageList)) {
|
||||
if (preg_match_all('/(https?:\/\/[^"\'\\\\]+\.(?:jpg|png|jpeg|webp))/', $html, $matches)) {
|
||||
foreach ($matches[1] as $m) {
|
||||
$imageList[] = $m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 过滤与去重
|
||||
$uniqueImages = [];
|
||||
foreach ($imageList as $i) {
|
||||
if (in_array($i, $uniqueImages)) continue;
|
||||
if (strpos($i, "grey.gif") !== false) continue;
|
||||
if (strpos($i, "logo") !== false) continue;
|
||||
if (strpos($i, "icon") !== false) continue;
|
||||
if (strpos($i, "tu.petatt.cn") !== false) continue;
|
||||
|
||||
$uniqueImages[] = $i;
|
||||
}
|
||||
|
||||
$novelData = implode("&&", $uniqueImages);
|
||||
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => "pics://{$novelData}",
|
||||
"header" => ""
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return ["parse" => 0, "url" => "", "header" => ""];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $HOST = 'https://m.jiabaide.cn';
|
||||
private $UA = 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36';
|
||||
|
||||
/**
|
||||
* 核心签名算法:sha1(md5(query_string))
|
||||
*/
|
||||
private function getSignedHeaders($params) {
|
||||
$t = (string)(time() * 1000); // 毫秒时间戳
|
||||
$params['key'] = 'cb808529bae6b6be45ecfab29a4889bc';
|
||||
$params['t'] = $t;
|
||||
|
||||
// 构建 QueryString
|
||||
$query = [];
|
||||
foreach ($params as $k => $v) {
|
||||
$query[] = "$k=$v";
|
||||
}
|
||||
$queryStr = implode('&', $query);
|
||||
|
||||
// 签名逻辑:SHA1(MD5(str))
|
||||
$sign = sha1(md5($queryStr));
|
||||
|
||||
return [
|
||||
'User-Agent: ' . $this->UA,
|
||||
'Referer: ' . $this->HOST,
|
||||
't: ' . $t,
|
||||
'sign: ' . $sign
|
||||
];
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
// 5. 首页 (获取分类与筛选)
|
||||
$typeUrl = $this->HOST . '/api/mw-movie/anonymous/get/filer/type';
|
||||
$typeRes = $this->fetch($typeUrl, [], $this->getSignedHeaders([]));
|
||||
$typeArr = json_decode($typeRes, true)['data'] ?? [];
|
||||
|
||||
$classes = [];
|
||||
foreach ($typeArr as $item) {
|
||||
$classes[] = ['type_id' => (string)$item['typeId'], 'type_name' => $item['typeName']];
|
||||
}
|
||||
|
||||
// 获取筛选
|
||||
$filterUrl = $this->HOST . '/api/mw-movie/anonymous/v1/get/filer/list';
|
||||
$filterRes = $this->fetch($filterUrl, [], $this->getSignedHeaders([]));
|
||||
$filterData = json_decode($filterRes, true)['data'] ?? [];
|
||||
|
||||
$filters = [];
|
||||
$nameMap = [
|
||||
'typeList' => ['key' => 'type', 'name' => '类型'],
|
||||
'plotList' => ['key' => 'class', 'name' => '剧情'],
|
||||
'districtList' => ['key' => 'area', 'name' => '地区'],
|
||||
'languageList' => ['key' => 'lang', 'name' => '语言'],
|
||||
'yearList' => ['key' => 'year', 'name' => '年份']
|
||||
];
|
||||
|
||||
foreach ($classes as $cls) {
|
||||
$tid = $cls['type_id'];
|
||||
$fRow = [];
|
||||
foreach ($nameMap as $apiKey => $cfg) {
|
||||
if (!isset($filterData[$tid][$apiKey])) continue;
|
||||
$values = [['n' => '全部', 'v' => '']];
|
||||
foreach ($filterData[$tid][$apiKey] as $v) {
|
||||
$values[] = [
|
||||
'n' => $v['itemText'],
|
||||
'v' => ($apiKey === 'typeList') ? $v['itemValue'] : $v['itemText']
|
||||
];
|
||||
}
|
||||
$fRow[] = ['key' => $cfg['key'], 'name' => $cfg['name'], 'value' => $values];
|
||||
}
|
||||
// 增加排序
|
||||
$fRow[] = [
|
||||
'key' => 'by', 'name' => '排序',
|
||||
'value' => [
|
||||
['n' => '最近更新', 'v' => '1'],
|
||||
['n' => '添加时间', 'v' => '2'],
|
||||
['n' => '人气高低', 'v' => '3'],
|
||||
['n' => '评分高低', 'v' => '4']
|
||||
]
|
||||
];
|
||||
$filters[$tid] = $fRow;
|
||||
}
|
||||
|
||||
// 首页推荐
|
||||
$hotUrl = $this->HOST . '/api/mw-movie/anonymous/home/hotSearch';
|
||||
$hotRes = $this->fetch($hotUrl, [], $this->getSignedHeaders([]));
|
||||
$hotVods = json_decode($hotRes, true)['data'] ?? [];
|
||||
$list = [];
|
||||
foreach (array_slice($hotVods, 0, 20) as $it) {
|
||||
$list[] = [
|
||||
'vod_id' => $it['vodId'],
|
||||
'vod_name' => $it['vodName'],
|
||||
'vod_pic' => $it['vodPic'],
|
||||
'vod_remarks' => $it['vodRemarks']
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => $filters,
|
||||
'list' => $list
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$params = [
|
||||
'area' => $extend['area'] ?? '',
|
||||
'lang' => $extend['lang'] ?? '',
|
||||
'pageNum' => $pg,
|
||||
'pageSize' => '30',
|
||||
'sort' => $extend['by'] ?? '1',
|
||||
'sortBy' => '1',
|
||||
'type' => $extend['type'] ?? '',
|
||||
'type1' => $tid,
|
||||
'v_class' => $extend['class'] ?? '',
|
||||
'year' => $extend['year'] ?? '',
|
||||
];
|
||||
|
||||
$apiUrl = $this->HOST . '/api/mw-movie/anonymous/video/list?' . http_build_query($params);
|
||||
$res = $this->fetch($apiUrl, [], $this->getSignedHeaders($params));
|
||||
$json = json_decode($res, true);
|
||||
|
||||
$list = [];
|
||||
if (isset($json['data']['list'])) {
|
||||
foreach ($json['data']['list'] as $it) {
|
||||
$list[] = [
|
||||
'vod_id' => $it['vodId'],
|
||||
'vod_name' => $it['vodName'],
|
||||
'vod_pic' => $it['vodPic'],
|
||||
'vod_remarks' => $it['vodRemarks'] . '_' . $it['vodDoubanScore']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$total = $json['data']['total'] ?? 0;
|
||||
return $this->pageResult($list, $pg, $total, 30);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
$params = ['id' => $id];
|
||||
$apiUrl = $this->HOST . '/api/mw-movie/anonymous/video/detail?' . http_build_query($params);
|
||||
$res = $this->fetch($apiUrl, [], $this->getSignedHeaders($params));
|
||||
$json = json_decode($res, true);
|
||||
$kvod = $json['data'] ?? null;
|
||||
|
||||
if (!$kvod) {
|
||||
return ['list' => []];
|
||||
}
|
||||
|
||||
$episodes = [];
|
||||
if (!empty($kvod['episodeList'])) {
|
||||
foreach ($kvod['episodeList'] as $it) {
|
||||
// 存入格式:名字$ID@NID
|
||||
$episodes[] = $it['name'] . '$' . $kvod['vodId'] . '@' . $it['nid'];
|
||||
}
|
||||
}
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $kvod['vodId'],
|
||||
'vod_name' => $kvod['vodName'],
|
||||
'vod_pic' => $kvod['vodPic'],
|
||||
'type_name' => $kvod['vodClass'],
|
||||
'vod_remarks' => $kvod['vodRemarks'],
|
||||
'vod_content' => trim(strip_tags($kvod['vodContent'] ?? '')),
|
||||
'vod_play_from' => '金牌线路',
|
||||
'vod_play_url' => implode('#', $episodes)
|
||||
];
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$page = max(1, intval($pg));
|
||||
$params = [
|
||||
'keyword' => $key,
|
||||
'pageNum' => $pg,
|
||||
'pageSize' => '30'
|
||||
];
|
||||
$apiUrl = $this->HOST . '/api/mw-movie/anonymous/video/searchByWordPageable?' . http_build_query($params);
|
||||
$res = $this->fetch($apiUrl, [], $this->getSignedHeaders($params));
|
||||
$json = json_decode($res, true);
|
||||
|
||||
$list = [];
|
||||
if (isset($json['data']['list'])) {
|
||||
foreach ($json['data']['list'] as $it) {
|
||||
$list[] = [
|
||||
'vod_id' => $it['vodId'],
|
||||
'vod_name' => $it['vodName'],
|
||||
'vod_pic' => $it['vodPic'],
|
||||
'vod_remarks' => $it['vodRemarks']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$total = $json['data']['total'] ?? 0;
|
||||
return $this->pageResult($list, $pg, $total, 30);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
// 格式: vodId@nid
|
||||
list($sid, $nid) = explode('@', $id);
|
||||
$params = [
|
||||
'clientType' => '3',
|
||||
'id' => $sid,
|
||||
'nid' => $nid
|
||||
];
|
||||
$apiUrl = $this->HOST . '/api/mw-movie/anonymous/v2/video/episode/url?' . http_build_query($params);
|
||||
$res = $this->fetch($apiUrl, [], $this->getSignedHeaders($params));
|
||||
$json = json_decode($res, true);
|
||||
|
||||
$playUrl = "";
|
||||
if (!empty($json['data']['list'])) {
|
||||
// 取第一个清晰度的 URL
|
||||
$playUrl = $json['data']['list'][0]['url'];
|
||||
}
|
||||
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => $playUrl,
|
||||
'header' => ['User-Agent' => $this->UA]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
|
||||
private const AES_KEY = '242ccb8230d709e1';
|
||||
private const SIGN_KEY = 'd3dGiJc651gSQ8w1';
|
||||
private const APP_ID = 'com.kmxs.reader';
|
||||
|
||||
private const BASE_HEADERS = [
|
||||
"app-version" => "51110",
|
||||
"platform" => "android",
|
||||
"reg" => "0",
|
||||
"AUTHORIZATION" => "",
|
||||
"application-id" => self::APP_ID,
|
||||
"net-env" => "1",
|
||||
"channel" => "unknown",
|
||||
"qm-params" => ""
|
||||
];
|
||||
|
||||
public function init($extend = "") {
|
||||
parent::init($extend);
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return "阅读助手";
|
||||
}
|
||||
|
||||
private function getSign($params) {
|
||||
ksort($params);
|
||||
$signStr = "";
|
||||
foreach ($params as $k => $v) {
|
||||
$signStr .= "{$k}={$v}";
|
||||
}
|
||||
$signStr .= self::SIGN_KEY;
|
||||
return md5($signStr);
|
||||
}
|
||||
|
||||
private function getHeaders($params) {
|
||||
$headers = self::BASE_HEADERS;
|
||||
$headers['sign'] = $this->getSign($params);
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function getApiUrl($path, &$params, $domainType = "bc") {
|
||||
$baseUrl = ($domainType == "bc") ? "https://api-bc.wtzw.com" : "https://api-ks.wtzw.com";
|
||||
if (strpos($path, "search") !== false) {
|
||||
$baseUrl = "https://api-bc.wtzw.com";
|
||||
}
|
||||
|
||||
// PHP headers logic is separate from URL params in fetch
|
||||
// But the sign is calculated on params.
|
||||
// And requests in Python sends params in query string.
|
||||
// So we need to construct URL with query string.
|
||||
// Also sign must be in headers.
|
||||
|
||||
// Wait, Python code:
|
||||
// params['sign'] = self.get_sign(params) -> This adds sign to params!
|
||||
// headers['sign'] = self.get_sign(headers) -> This adds sign to headers (based on headers)!
|
||||
|
||||
// Let's re-read Python code carefully.
|
||||
/*
|
||||
def get_sign(self, params):
|
||||
# sorts params and md5
|
||||
|
||||
def get_headers(self):
|
||||
headers = self.BASE_HEADERS.copy()
|
||||
headers['sign'] = self.get_sign(headers) <-- Sign of HEADERS
|
||||
return headers
|
||||
|
||||
def get_api_url(self, path, params, domain_type="bc"):
|
||||
params['sign'] = self.get_sign(params) <-- Sign of PARAMS
|
||||
...
|
||||
return url, params
|
||||
*/
|
||||
|
||||
// So we have TWO signatures: one in params (signing params) and one in headers (signing headers).
|
||||
|
||||
// Params signing
|
||||
$params['sign'] = $this->getSign($params);
|
||||
|
||||
// Build query string
|
||||
$queryString = http_build_query($params);
|
||||
|
||||
return "{$baseUrl}{$path}?{$queryString}";
|
||||
}
|
||||
|
||||
private function getRequestHeaders() {
|
||||
// Headers signing
|
||||
$headers = self::BASE_HEADERS;
|
||||
$headers['sign'] = $this->getSign($headers);
|
||||
$headers['User-Agent'] = "okhttp/3.12.1";
|
||||
|
||||
// Format for fetch
|
||||
// fetch expects array Key => Value
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function decryptContent($base64Content) {
|
||||
try {
|
||||
$encryptedBytes = base64_decode($base64Content);
|
||||
if (strlen($encryptedBytes) < 16) {
|
||||
return "数据长度不足";
|
||||
}
|
||||
|
||||
$iv = substr($encryptedBytes, 0, 16);
|
||||
$ciphertext = substr($encryptedBytes, 16);
|
||||
|
||||
// aes-128-cbc
|
||||
$decrypted = openssl_decrypt($ciphertext, 'aes-128-cbc', self::AES_KEY, OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
if ($decrypted === false) {
|
||||
return "解密失败";
|
||||
}
|
||||
|
||||
return trim($decrypted);
|
||||
} catch (Exception $e) {
|
||||
return "解密错误: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function homeContent($filter = []) {
|
||||
$cats = [
|
||||
["type_name" => "玄幻奇幻", "type_id" => "1|202"],
|
||||
["type_name" => "都市人生", "type_id" => "1|203"],
|
||||
["type_name" => "武侠仙侠", "type_id" => "1|205"],
|
||||
["type_name" => "历史军事", "type_id" => "1|56"],
|
||||
["type_name" => "科幻末世", "type_id" => "1|64"],
|
||||
["type_name" => "游戏竞技", "type_id" => "1|75"],
|
||||
["type_name" => "现代言情", "type_id" => "2|1"],
|
||||
["type_name" => "古代言情", "type_id" => "2|2"],
|
||||
["type_name" => "幻想言情", "type_id" => "2|4"],
|
||||
["type_name" => "婚恋情感", "type_id" => "2|6"],
|
||||
["type_name" => "悬疑推理", "type_id" => "3|262"]
|
||||
];
|
||||
return ['class' => $cats, 'filters' => (object)[]];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$parts = explode("|", $tid);
|
||||
$gender = $parts[0] ?? "1";
|
||||
$catId = $parts[1] ?? "202";
|
||||
|
||||
$params = [
|
||||
'gender' => $gender,
|
||||
'category_id' => $catId,
|
||||
'need_filters' => '1',
|
||||
'page' => $pg,
|
||||
'need_category' => '1'
|
||||
];
|
||||
|
||||
$url = $this->getApiUrl("/api/v4/category/get-list", $params, "bc");
|
||||
$headers = $this->getRequestHeaders();
|
||||
|
||||
try {
|
||||
$json = $this->fetchJson($url, ['headers' => $headers]);
|
||||
|
||||
$bookList = [];
|
||||
if (isset($json['data']['books'])) {
|
||||
$bookList = $json['data']['books'];
|
||||
} elseif (isset($json['books'])) {
|
||||
$bookList = $json['books'];
|
||||
}
|
||||
|
||||
$videos = [];
|
||||
foreach ($bookList as $book) {
|
||||
$pic = $book['image_link'] ?? '';
|
||||
if (strpos($pic, 'http://') === 0) {
|
||||
$pic = str_replace('http://', 'https://', $pic);
|
||||
}
|
||||
|
||||
$videos[] = [
|
||||
"vod_id" => (string)($book['id'] ?? ''),
|
||||
"vod_name" => $book['title'] ?? '',
|
||||
"vod_pic" => $pic,
|
||||
"vod_remarks" => $book['author'] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
return ['list' => $videos, 'page' => $pg, 'pagecount' => 999, 'limit' => 20, 'total' => 9999];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return ['list' => []];
|
||||
}
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$bid = $ids[0];
|
||||
$headers = $this->getRequestHeaders();
|
||||
|
||||
$detailParams = ['id' => $bid, 'imei_ip' => '2937357107', 'teeny_mode' => '0'];
|
||||
$detailUrl = $this->getApiUrl("/api/v4/book/detail", $detailParams, "bc");
|
||||
|
||||
$vod = ["vod_id" => $bid, "vod_name" => "获取中...", "vod_play_from" => "阅读助手"];
|
||||
|
||||
try {
|
||||
$json = $this->fetchJson($detailUrl, ['headers' => $headers]);
|
||||
|
||||
if (isset($json['data']['book'])) {
|
||||
$bookInfo = $json['data']['book'];
|
||||
$vod["vod_name"] = $bookInfo['title'] ?? '';
|
||||
|
||||
$pic = $bookInfo['image_link'] ?? '';
|
||||
if (strpos($pic, 'http://') === 0) {
|
||||
$pic = str_replace('http://', 'https://', $pic);
|
||||
}
|
||||
$vod["vod_pic"] = $pic;
|
||||
|
||||
$vod["type_name"] = $bookInfo['category_name'] ?? '';
|
||||
$vod["vod_remarks"] = ($bookInfo['words_num'] ?? '') . "字";
|
||||
$vod["vod_actor"] = $bookInfo['author'] ?? '';
|
||||
$vod["vod_content"] = $bookInfo['intro'] ?? '';
|
||||
}
|
||||
|
||||
// Get Chapters
|
||||
$chapterParams = ['id' => $bid];
|
||||
$chapterUrl = $this->getApiUrl("/api/v1/chapter/chapter-list", $chapterParams, "ks");
|
||||
|
||||
$jsonC = $this->fetchJson($chapterUrl, ['headers' => $headers]);
|
||||
|
||||
$lists = [];
|
||||
if (isset($jsonC['data']['chapter_lists'])) {
|
||||
$lists = $jsonC['data']['chapter_lists'];
|
||||
}
|
||||
|
||||
$chapterList = [];
|
||||
foreach ($lists as $item) {
|
||||
$cid = (string)$item['id'];
|
||||
$cname = str_replace(["@@", "$"], ["-", ""], $item['title']);
|
||||
$urlCode = "{$bid}@@{$cid}@@{$cname}";
|
||||
$chapterList[] = "{$cname}\${$urlCode}";
|
||||
}
|
||||
|
||||
$vod['vod_play_url'] = implode("#", $chapterList);
|
||||
return ["list" => [$vod]];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$vod["vod_content"] = "Error: " . $e->getMessage();
|
||||
return ["list" => [$vod]];
|
||||
}
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$params = ['gender' => '3', 'imei_ip' => '2937357107', 'page' => $pg, 'wd' => $key];
|
||||
$url = $this->getApiUrl("/api/v5/search/words", $params, "bc");
|
||||
$headers = $this->getRequestHeaders();
|
||||
|
||||
try {
|
||||
$json = $this->fetchJson($url, ['headers' => $headers]);
|
||||
|
||||
$videos = [];
|
||||
if (isset($json['data']['books'])) {
|
||||
foreach ($json['data']['books'] as $book) {
|
||||
$videos[] = [
|
||||
"vod_id" => (string)$book['id'],
|
||||
"vod_name" => $book['original_title'],
|
||||
"vod_pic" => $book['image_link'],
|
||||
"vod_remarks" => $book['original_author']
|
||||
];
|
||||
}
|
||||
}
|
||||
return ['list' => $videos, 'page' => $pg];
|
||||
} catch (Exception $e) {
|
||||
return ['list' => [], 'page' => $pg];
|
||||
}
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
try {
|
||||
$parts = explode("@@", $id);
|
||||
$bid = $parts[0];
|
||||
$cid = $parts[1];
|
||||
$title = isset($parts[2]) ? $parts[2] : "";
|
||||
|
||||
$params = ['id' => $bid, 'chapterId' => $cid];
|
||||
$url = $this->getApiUrl("/api/v1/chapter/content", $params, "ks");
|
||||
$headers = $this->getRequestHeaders();
|
||||
|
||||
$json = $this->fetchJson($url, ['headers' => $headers]);
|
||||
|
||||
$content = "";
|
||||
if (isset($json['data']['content'])) {
|
||||
if (!$title && isset($json['data']['title'])) {
|
||||
$title = $json['data']['title'];
|
||||
}
|
||||
$content = $this->decryptContent($json['data']['content']);
|
||||
} else {
|
||||
$msg = $json['msg'] ?? '未知错误';
|
||||
$content = "加载失败: {$msg}";
|
||||
}
|
||||
|
||||
if (!$title) $title = "章节正文";
|
||||
|
||||
$resultData = [
|
||||
'title' => $title,
|
||||
'content' => $content
|
||||
];
|
||||
|
||||
$ret = json_encode($resultData, JSON_UNESCAPED_UNICODE);
|
||||
$finalUrl = "novel://{$ret}";
|
||||
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => $finalUrl,
|
||||
"header" => ""
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$errData = [
|
||||
'title' => "错误",
|
||||
'content' => "发生异常: " . $e->getMessage()
|
||||
];
|
||||
return [
|
||||
"parse" => 0,
|
||||
"playUrl" => "",
|
||||
"url" => "novel://" . json_encode($errData, JSON_UNESCAPED_UNICODE),
|
||||
"header" => ""
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $host = 'http://ldys.sq1005.top';
|
||||
private $publicKey = "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoYt0BP77U+DM08BiI/QbSRIfxijXo85BTPqIM1Ow8BNwhLETzRIZ+dEwdWDbydG/PspgBAfRpGaYVdJYtvaC2JnoO8+Ik6qMWojfEJxSFLa0Pb0A892tun4gsxoEMjcreZ+YGyaBxAfqX0BSMfdrOgIYaZQjYrw9TRLlUT31QoQIDAQAB\n-----END PUBLIC KEY-----";
|
||||
private $privateKey = "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCquQQ5r6+yJI8CDFkXRp8vUsdD45ov8EP12ooLs56ca2DQXaSNGS9910bAPVA9chkp0mKIvKqjAsHz5Tl9EeNPblarGEeJUIxpxZtiSqNTpvtiD/TjhpzuHYic7RAfQ/h7p/ypE8ymU42pYjsB5t26Mv6XgkLV+jzrSf73HlCuS0iMyLmt6zz3Mw9izM13EpB8iFLtfbbYymycKTx4RAmPQLwhNGex/AlUIYxXP4R2yyaa4W6mEtc6aME2QuzJFxPgP3HJ9NBx/LWVn4skxWjZ7zg+VRQRHnjyVaSLu3Z5gN5ITWCyE32qaHJa6WBahZj5jWhRyAG1bQ+xKJa8lBL5AgMBAAECggEAUwv9SjJ0PSwbhNuM2w23kcWquROWhYtTA91zGY4esehqB/IFgb2mpIh8Gje5OKqwIu/8jpd4SiOlRYdUF8sD0DfUYRZGdj2AkFNX6tBz8tVfo6wvbB6naA1lzzBij1L5JO3qsjS3cJFkb+kg2yP66AC2Z+0tpfk8eRhdtshAZwfcd1DEGt1uAvYL1eaUK9HRvpt9lPeGcHERDl2hBd4uyaF0K1O+zF9y59nYbTySWPxRZq3sFEE85xRMlstD7YZi7W2gKvMFRD4/FKmrZ3m7aKJRITtyKOyyPcYmepNv3Qv7kk59Pg38n2WWQ0Ra/bCH3E48YNCnQvZMpitkTfJhoQKBgQDbnROOYTP8OTJ6f/qhoGjxeO3x1VOaOp8l0x7b0SCfoqNGS0Cyiqj72BmJtPMPqSTjn6MmNzqbg1KOdhXyzNozs+i5ccW1M56j96mr5I/Z0FpE3oyIHNfDDBlf9M8YQqEF9oYxniYYft9oapO7cRQkHER6qpvnHTavwlv4m78CXwKBgQDHAjs2YlpKDdI1lcbZJCc7TwtH+Pd2bUki8YXafWNcPhITQHbOZjr310eK1QJC6GJncjkOqbX7yv3ivvTO35FZTQhuA1xEG1P00FG8bE0tHYPIwQHi9y0eA5cieMdo8E6XYria1mw/3fqSQEsfZyJlR32JQIoGAipM8iO1X2nZpwKBgDkMFIhnt5lNQk+P7wsNIDWZtDWdtJnboHuy29E+Abt2A/O+mI/IdRz2hau/1WO8DFkUnszOi+rZshhPlGP90rCbi1igtTrcrdjp/KkqNjPea5R4OwkgdOu1uOG0NheXNzzVTQaWjk7Opjn5dWa7eP/oV+GFb/oZHJuLYVizHGsBAoGADA7rjZEKDYCm4w5PPSr+oY5ZjaPdQrS+gLqHtMRyN82fBMGcMUdqfUfzEstzVqCEDeaS5HuOBlK3bXzKkppjUTjksN3NQmcxgBz7RuJ9DqXCLXDcb2cwuafYCYOt+YLOEEgwDVm+t2P44dG5e46hO+fICH/7nP+WlpD5buz4GfMCgYB57r3g/6hi9WUDnfc7ZAzWMqR0EhJVYKYy+KFEtdIPzhkkIHq5RASe88E9kzoGoZFdb3tIjvGZWcHerirrqWkMsuQtP/Qi0zjieid5tAPj+r4kbiCVTw0E0jnmPBzGInQi7lpeTTKnG1fbyS5lBS+WmHfIuzpECgCkxhaT+LJJkg==\n-----END PRIVATE KEY-----";
|
||||
|
||||
private $deviceId = '';
|
||||
private $token = '';
|
||||
|
||||
protected function getHeaders() {
|
||||
if (empty($this->deviceId)) {
|
||||
$this->deviceId = $this->generateDid();
|
||||
}
|
||||
if (empty($this->token)) {
|
||||
$this->token = $this->getToken();
|
||||
}
|
||||
|
||||
return [
|
||||
'User-Agent' => 'okhttp/4.12.0',
|
||||
'client' => 'app',
|
||||
'deviceType' => 'Android',
|
||||
'deviceId' => $this->deviceId,
|
||||
'token' => $this->token,
|
||||
'Content-Type' => 'application/json'
|
||||
];
|
||||
}
|
||||
|
||||
private function generateDid() {
|
||||
$hex = '0123456789abcdef';
|
||||
$did = '';
|
||||
for ($i = 0; $i < 16; $i++) {
|
||||
$did .= $hex[mt_rand(0, 15)];
|
||||
}
|
||||
return $did;
|
||||
}
|
||||
|
||||
private function getToken() {
|
||||
$url = $this->host . '/api/v1/app/user/visitorInfo';
|
||||
$headers = [
|
||||
'User-Agent' => 'okhttp/4.12.0',
|
||||
'client' => 'app',
|
||||
'deviceType' => 'Android',
|
||||
'deviceId' => $this->deviceId
|
||||
];
|
||||
|
||||
$jsonStr = $this->fetch($url, [], $headers);
|
||||
$json = json_decode($jsonStr, true);
|
||||
|
||||
if (isset($json['code']) && $json['code'] === 200 && isset($json['data']['token'])) {
|
||||
return $json['data']['token'];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function rsaEncrypt($data) {
|
||||
if (openssl_public_encrypt($data, $encrypted, $this->publicKey, OPENSSL_PKCS1_PADDING)) {
|
||||
return base64_encode($encrypted);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function rsaDecrypt($data) {
|
||||
$decoded = base64_decode($data);
|
||||
|
||||
$keyRes = openssl_pkey_get_private($this->privateKey);
|
||||
$details = openssl_pkey_get_details($keyRes);
|
||||
$keySize = ceil($details['bits'] / 8); // 128 for 1024 bit
|
||||
|
||||
$result = '';
|
||||
$chunks = str_split($decoded, $keySize);
|
||||
|
||||
foreach ($chunks as $chunk) {
|
||||
if (openssl_private_decrypt($chunk, $decrypted, $this->privateKey, OPENSSL_PKCS1_PADDING)) {
|
||||
$result .= $decrypted;
|
||||
} else {
|
||||
// error_log("Decrypt failed for chunk");
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
$url = $this->host . '/api/v1/app/screen/screenType';
|
||||
$jsonStr = $this->fetch($url, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_HTTPHEADER => $this->formatHeaders($this->getHeaders())
|
||||
]);
|
||||
|
||||
$json = json_decode($jsonStr, true);
|
||||
$classes = [];
|
||||
$filterObj = [];
|
||||
|
||||
if (isset($json['data'])) {
|
||||
foreach ($json['data'] as $mainCate) {
|
||||
$typeId = (string)$mainCate['id'];
|
||||
$classes[] = [
|
||||
'type_id' => $typeId,
|
||||
'type_name' => $mainCate['name']
|
||||
];
|
||||
|
||||
$filters = [];
|
||||
if (isset($mainCate['children'])) {
|
||||
foreach ($mainCate['children'] as $subCate) {
|
||||
$filterType = '';
|
||||
switch ($subCate['name']) {
|
||||
case '类型': $filterType = 'type'; break;
|
||||
case '地区': $filterType = 'area'; break;
|
||||
case '年份': $filterType = 'year'; break;
|
||||
}
|
||||
|
||||
if ($filterType) {
|
||||
$values = [['n' => '全部', 'v' => '']];
|
||||
foreach ($subCate['children'] as $item) {
|
||||
$values[] = ['n' => $item['name'], 'v' => $item['name']];
|
||||
}
|
||||
$filters[] = [
|
||||
'key' => $filterType,
|
||||
'name' => $subCate['name'],
|
||||
'value' => $values
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filters[] = [
|
||||
'key' => 'sort',
|
||||
'name' => '排序',
|
||||
'value' => [
|
||||
['n' => '最新', 'v' => 'NEWEST'],
|
||||
['n' => '人气', 'v' => 'POPULARITY'],
|
||||
['n' => '评分', 'v' => 'COLLECT'],
|
||||
['n' => '热搜', 'v' => 'HOT']
|
||||
]
|
||||
];
|
||||
|
||||
$filterObj[$typeId] = $filters;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => $classes,
|
||||
'filters' => $filterObj
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$url = $this->host . '/api/v1/app/screen/screenMovie';
|
||||
|
||||
$condition = [
|
||||
'classify' => $extend['type'] ?? '',
|
||||
'region' => $extend['area'] ?? '',
|
||||
'sreecnTypeEnum' => $extend['sort'] ?? 'NEWEST',
|
||||
'typeId' => $tid,
|
||||
'year' => $extend['year'] ?? ''
|
||||
];
|
||||
|
||||
$params = [
|
||||
'condition' => $condition,
|
||||
'pageNum' => (int)$pg,
|
||||
'pageSize' => 40
|
||||
];
|
||||
|
||||
$jsonStr = $this->fetch($url, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($params),
|
||||
CURLOPT_HTTPHEADER => $this->formatHeaders($this->getHeaders())
|
||||
]);
|
||||
|
||||
$json = json_decode($jsonStr, true);
|
||||
$videos = [];
|
||||
|
||||
if (isset($json['data']['records'])) {
|
||||
foreach ($json['data']['records'] as $item) {
|
||||
$videos[] = [
|
||||
'vod_id' => $item['id'] . '*' . $item['typeId'],
|
||||
'vod_name' => $item['name'],
|
||||
'vod_pic' => $item['cover'],
|
||||
'vod_remarks' => $item['totalEpisode'] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$total = $json['data']['total'] ?? 0;
|
||||
return $this->pageResult($videos, $pg, $total, 40);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
$parts = explode('*', $id);
|
||||
$vodId = (int)$parts[0];
|
||||
$typeId = $parts[1] ?? '';
|
||||
|
||||
// 1. 获取基本详情
|
||||
$detailUrl = $this->host . '/api/v1/app/play/movieDesc';
|
||||
$detailParams = ['id' => $vodId, 'typeId' => $typeId];
|
||||
|
||||
$detailRes = $this->fetch($detailUrl, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($detailParams),
|
||||
CURLOPT_HTTPHEADER => $this->formatHeaders($this->getHeaders())
|
||||
]);
|
||||
|
||||
$detailJson = json_decode($detailRes, true);
|
||||
$detailData = $detailJson['data'] ?? [];
|
||||
|
||||
// 2. 获取播放列表 (加密)
|
||||
$playReqPayload = json_encode([
|
||||
'id' => $vodId,
|
||||
'source' => 0,
|
||||
'typeId' => $typeId
|
||||
]);
|
||||
|
||||
$playParams = ['key' => $this->rsaEncrypt($playReqPayload)];
|
||||
|
||||
$playDataRes = $this->fetch($this->host . '/api/v1/app/play/movieDetails', [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($playParams),
|
||||
CURLOPT_HTTPHEADER => $this->formatHeaders($this->getHeaders())
|
||||
]);
|
||||
|
||||
$playJson = json_decode($playDataRes, true);
|
||||
$playDataEnc = $playJson['data'] ?? '';
|
||||
|
||||
$decryptedDataStr = $this->rsaDecrypt($playDataEnc);
|
||||
|
||||
$decryptedData = json_decode($decryptedDataStr, true);
|
||||
|
||||
$shows = [];
|
||||
$playUrls = [];
|
||||
|
||||
if (isset($decryptedData['moviePlayerList'])) {
|
||||
foreach ($decryptedData['moviePlayerList'] as $player) {
|
||||
// 3. 获取具体集数 (加密)
|
||||
$episodePayload = json_encode([
|
||||
'id' => $vodId,
|
||||
'source' => 0,
|
||||
'typeId' => $typeId,
|
||||
'playerId' => $player['id']
|
||||
]);
|
||||
|
||||
$episodeParams = ['key' => $this->rsaEncrypt($episodePayload)];
|
||||
|
||||
$episodeRes = $this->fetch($this->host . '/api/v1/app/play/movieDetails', [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($episodeParams),
|
||||
CURLOPT_HTTPHEADER => $this->formatHeaders($this->getHeaders())
|
||||
]);
|
||||
|
||||
$episodeJson = json_decode($episodeRes, true);
|
||||
$episodeDataEnc = $episodeJson['data'] ?? '';
|
||||
|
||||
$episodeDecStr = $this->rsaDecrypt($episodeDataEnc);
|
||||
$episodeDecData = json_decode($episodeDecStr, true);
|
||||
|
||||
$urls = [];
|
||||
if (isset($episodeDecData['episodeList'])) {
|
||||
foreach ($episodeDecData['episodeList'] as $ep) {
|
||||
$param = [
|
||||
'id' => $vodId,
|
||||
'typeId' => $typeId,
|
||||
'playerId' => $player['id'],
|
||||
'episodeId' => $ep['id']
|
||||
];
|
||||
// 封装参数到URL中
|
||||
$urls[] = $ep['episode'] . '$' . json_encode($param);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($urls)) {
|
||||
$shows[] = $player['moviePlayerName'];
|
||||
$playUrls[] = implode('#', $urls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $id,
|
||||
'vod_name' => $detailData['name'] ?? '',
|
||||
'vod_pic' => $detailData['cover'] ?? '',
|
||||
'vod_year' => $detailData['year'] ?? '',
|
||||
'vod_area' => $detailData['area'] ?? '',
|
||||
'vod_remarks' => $detailData['totalEpisode'] ?? '',
|
||||
'vod_actor' => $detailData['star'] ?? '',
|
||||
'vod_content' => $detailData['introduce'] ?? '',
|
||||
'vod_play_from' => implode('$$$', $shows),
|
||||
'vod_play_url' => implode('$$$', $playUrls)
|
||||
];
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$url = $this->host . '/api/v1/app/search/searchMovie';
|
||||
$params = [
|
||||
'condition' => ['value' => $key],
|
||||
'pageNum' => (int)$pg,
|
||||
'pageSize' => 40
|
||||
];
|
||||
|
||||
$jsonStr = $this->fetch($url, [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($params),
|
||||
CURLOPT_HTTPHEADER => $this->formatHeaders($this->getHeaders())
|
||||
]);
|
||||
|
||||
$json = json_decode($jsonStr, true);
|
||||
$videos = [];
|
||||
|
||||
if (isset($json['data']['records'])) {
|
||||
foreach ($json['data']['records'] as $item) {
|
||||
$videos[] = [
|
||||
'vod_id' => $item['id'] . '*' . $item['typeId'],
|
||||
'vod_name' => $item['name'],
|
||||
'vod_pic' => $item['cover'],
|
||||
'vod_remarks' => $item['totalEpisode'] ?? ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$total = $json['data']['total'] ?? 0;
|
||||
return $this->pageResult($videos, $pg, $total, 40);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
// $id 是 detailContent 中封装的 JSON 参数
|
||||
$param = json_decode($id, true);
|
||||
|
||||
$urlPayload = json_encode([
|
||||
'id' => $param['id'],
|
||||
'source' => 0,
|
||||
'typeId' => $param['typeId'],
|
||||
'playerId' => $param['playerId'],
|
||||
'episodeId' => $param['episodeId']
|
||||
]);
|
||||
|
||||
$urlParams = ['key' => $this->rsaEncrypt($urlPayload)];
|
||||
|
||||
$postData = $this->fetch($this->host . '/api/v1/app/play/movieDetails', [
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => json_encode($urlParams),
|
||||
CURLOPT_HTTPHEADER => $this->formatHeaders($this->getHeaders())
|
||||
]);
|
||||
|
||||
$json = json_decode($postData, true);
|
||||
$encryptedUrl = $json['data'] ?? '';
|
||||
|
||||
$decryptedUrlDataStr = $this->rsaDecrypt($encryptedUrl);
|
||||
$playerUrlData = json_decode($decryptedUrlDataStr, true);
|
||||
$playerUrl = $playerUrlData['url'] ?? '';
|
||||
|
||||
// 最后一步分析 URL
|
||||
$analysisUrl = $this->host . '/api/v1/app/play/analysisMovieUrl?playerUrl=' . urlencode($playerUrl) . '&playerId=' . $param['playerId'];
|
||||
|
||||
$analysisRes = $this->fetch($analysisUrl, [
|
||||
CURLOPT_HTTPHEADER => $this->formatHeaders($this->getHeaders())
|
||||
]);
|
||||
|
||||
$analysisJson = json_decode($analysisRes, true);
|
||||
$finalUrl = $analysisJson['data'] ?? '';
|
||||
|
||||
return [
|
||||
'parse' => 0,
|
||||
'url' => $finalUrl,
|
||||
'header' => [
|
||||
'User-Agent' => 'okhttp/4.12.0'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 辅助方法:将关联数组 headers 转换为 curl 需要的格式
|
||||
private function formatHeaders($headers) {
|
||||
$formatted = [];
|
||||
foreach ($headers as $k => $v) {
|
||||
$formatted[] = "$k: $v";
|
||||
}
|
||||
return $formatted;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行爬虫
|
||||
(new Spider())->run();
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/spider.php';
|
||||
|
||||
class Spider extends BaseSpider {
|
||||
private $HOST = 'https://www.mqtv.cc';
|
||||
private $KEY = 'Mcxos@mucho!nmme';
|
||||
private $UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36';
|
||||
|
||||
/**
|
||||
* 对应 JS 中的 encodeData 和 decodeData (XOR + Base64)
|
||||
*/
|
||||
private function mq_xor_codec($data, $key, $is_decode = false) {
|
||||
if ($is_decode) {
|
||||
$data = base64_decode($data);
|
||||
} else {
|
||||
$data = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
$data = base64_encode($data);
|
||||
}
|
||||
|
||||
$res = '';
|
||||
$keyLen = strlen($key);
|
||||
for ($i = 0; $i < strlen($data); $i++) {
|
||||
$res .= $data[$i] ^ $key[$i % $keyLen];
|
||||
}
|
||||
|
||||
if ($is_decode) {
|
||||
return json_decode(base64_decode($res), true);
|
||||
} else {
|
||||
return urlencode(base64_encode($res));
|
||||
}
|
||||
}
|
||||
|
||||
private function getHeaders($referer = '/') {
|
||||
return [
|
||||
'User-Agent: ' . $this->UA,
|
||||
'Referer: ' . $this->HOST . $referer,
|
||||
'X-Requested-With: XMLHttpRequest'
|
||||
];
|
||||
}
|
||||
|
||||
// 获取页面 PageID 并生成 Token
|
||||
private function getToken($path, $ref = '/') {
|
||||
$html = $this->fetch($this->HOST . $path, [], $this->getHeaders($ref));
|
||||
preg_match("/window\.pageid\s?=\s?'(.*?)';/i", $html, $m);
|
||||
$pageId = $m[1] ?? "";
|
||||
return $this->mq_xor_codec($pageId, $this->KEY);
|
||||
}
|
||||
|
||||
public function homeContent($filter) {
|
||||
// 5. 首页 (homeVod)
|
||||
$token = $this->getToken('/');
|
||||
$apiUrl = $this->HOST . "/libs/VodList.api.php?home=index&token=$token";
|
||||
$resp = json_decode($this->fetch($apiUrl, [], $this->getHeaders()), true);
|
||||
$list = [];
|
||||
if (isset($resp['data']['movie'])) {
|
||||
foreach ($resp['data']['movie'] as $section) {
|
||||
foreach ($section['show'] as $v) {
|
||||
$list[] = [
|
||||
'vod_id' => $v['url'],
|
||||
'vod_name' => $v['title'],
|
||||
'vod_pic' => $v['img'],
|
||||
'vod_remarks' => $v['remark']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => [
|
||||
['type_id' => '/type/movie', 'type_name' => '电影'],
|
||||
['type_id' => '/type/tv', 'type_name' => '电视剧'],
|
||||
['type_id' => '/type/va', 'type_name' => '综艺'],
|
||||
['type_id' => '/type/ct', 'type_name' => '动漫']
|
||||
],
|
||||
'list' => array_slice($list, 0, 30)
|
||||
];
|
||||
}
|
||||
|
||||
public function categoryContent($tid, $pg = 1, $filter = [], $extend = []) {
|
||||
$typeKey = explode('/', trim($tid, '/'))[1] ?? 'movie';
|
||||
$token = $this->getToken($tid);
|
||||
$apiUrl = $this->HOST . "/libs/VodList.api.php?type=$typeKey&rank=rankhot&page=$pg&token=$token";
|
||||
|
||||
$resp = json_decode($this->fetch($apiUrl, [], $this->getHeaders($tid)), true);
|
||||
$list = [];
|
||||
if (isset($resp['data'])) {
|
||||
foreach ($resp['data'] as $v) {
|
||||
$list[] = [
|
||||
'vod_id' => $v['url'],
|
||||
'vod_name' => $v['title'],
|
||||
'vod_pic' => $v['img'],
|
||||
'vod_remarks' => $v['remark']
|
||||
];
|
||||
}
|
||||
}
|
||||
return $this->pageResult($list, $pg);
|
||||
}
|
||||
|
||||
public function detailContent($ids) {
|
||||
$id = is_array($ids) ? $ids[0] : $ids;
|
||||
$pathParts = explode('/', trim($id, '/'));
|
||||
$realId = end($pathParts);
|
||||
$token = $this->getToken($id);
|
||||
|
||||
$apiUrl = $this->HOST . "/libs/VodInfo.api.php?type=ct&id=$realId&token=$token";
|
||||
$json = json_decode($this->fetch($apiUrl, [], $this->getHeaders($id)), true);
|
||||
$data = $json['data'];
|
||||
|
||||
// 处理解析线路
|
||||
$parsesArr = [];
|
||||
foreach (($data['playapi'] ?? []) as $p) {
|
||||
if (isset($p['url'])) {
|
||||
$parsesArr[] = (strpos($p['url'], '//') === 0) ? "https:" . $p['url'] : $p['url'];
|
||||
}
|
||||
}
|
||||
$parsesStr = implode(',', $parsesArr);
|
||||
|
||||
$playFrom = [];
|
||||
$playUrls = [];
|
||||
foreach (($data['playinfo'] ?? []) as $site) {
|
||||
$playFrom[] = $site['cnsite'];
|
||||
$urls = [];
|
||||
foreach ($site['player'] as $ep) {
|
||||
// 将解析接口封装在 URL 后面,供 play 阶段调用
|
||||
$urls[] = $ep['no'] . '$' . $ep['url'] . '@' . $parsesStr;
|
||||
}
|
||||
$playUrls[] = implode('#', $urls);
|
||||
}
|
||||
|
||||
$vod = [
|
||||
'vod_id' => $id,
|
||||
'vod_name' => $data['title'],
|
||||
'vod_pic' => $data['img'],
|
||||
'vod_remarks' => $data['remark'],
|
||||
'vod_year' => $data['year'],
|
||||
'vod_area' => $data['area'],
|
||||
'vod_actor' => $data['actor'],
|
||||
'vod_director' => $data['director'],
|
||||
'vod_content' => $data['content'] ?? '',
|
||||
'vod_play_from' => implode('$$$', $playFrom),
|
||||
'vod_play_url' => implode('$$$', $playUrls)
|
||||
];
|
||||
|
||||
return ['list' => [$vod]];
|
||||
}
|
||||
|
||||
public function searchContent($key, $quick = false, $pg = 1) {
|
||||
$path = '/search/' . urlencode($key);
|
||||
$token = $this->getToken($path);
|
||||
$apiUrl = $this->HOST . "/libs/VodList.api.php?search=" . urlencode($key) . "&token=$token";
|
||||
|
||||
$resp = json_decode($this->fetch($apiUrl, [], $this->getHeaders($path)), true);
|
||||
$data = $this->mq_xor_codec($resp['data'], $this->KEY, true); // 搜索数据需要解密
|
||||
|
||||
$list = [];
|
||||
if (isset($data['vod_all'])) {
|
||||
foreach ($data['vod_all'] as $item) {
|
||||
foreach ($item['show'] as $v) {
|
||||
$list[] = [
|
||||
'vod_id' => $v['url'],
|
||||
'vod_name' => $v['title'],
|
||||
'vod_pic' => $v['img'],
|
||||
'vod_remarks' => $v['remark']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->pageResult($list, $pg);
|
||||
}
|
||||
|
||||
public function playerContent($flag, $id, $vipFlags = []) {
|
||||
$parts = explode('@', $id);
|
||||
$rawUrl = $parts[0];
|
||||
$parses = isset($parts[1]) ? explode(',', $parts[1]) : [];
|
||||
|
||||
// 默认返回第一个解析地址配合嗅探,模拟 JS 中的逻辑
|
||||
$finalUrl = $rawUrl;
|
||||
if (!empty($parses)) {
|
||||
$finalUrl = $parses[0] . $rawUrl;
|
||||
}
|
||||
|
||||
return [
|
||||
'parse' => 1,
|
||||
'url' => $finalUrl,
|
||||
'header' => ['User-Agent' => $this->UA]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
(new Spider())->run();
|
||||
Reference in New Issue
Block a user