3247 lines
209 KiB
HTML
3247 lines
209 KiB
HTML
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"><title>片库 · 4K</title><!-- ES5 兼容引导层(文档第一个内联脚本,必须 ES5):polyfill + 特性检测 + App 标记。见开发文档 15.5 --><script>
|
||
(function () {
|
||
"use strict";
|
||
var D = document, root = D.documentElement;
|
||
if (window.fongmiBridge || window.fm || window.fongmi) root.className += " fm-native";
|
||
if (window.fongmiClient && window.fongmiClient.isLeanback) root.className += " tv-mode";
|
||
|
||
if (!Element.prototype.matches) Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
||
if (!Element.prototype.closest) Element.prototype.closest = function (sel) {
|
||
var el = this; while (el && el.nodeType === 1) { if (el.matches(sel)) return el; el = el.parentNode; } return null;
|
||
};
|
||
function addRC(p) { if (p && !p.replaceChildren) p.replaceChildren = function () {
|
||
while (this.firstChild) this.removeChild(this.firstChild);
|
||
for (var i = 0; i < arguments.length; i++) this.appendChild(arguments[i]);
|
||
}; }
|
||
addRC(Element.prototype); if (typeof DocumentFragment !== "undefined") addRC(DocumentFragment.prototype);
|
||
if (!Object.values) Object.values = function (o) { var r = [], k; for (k in o) if (Object.prototype.hasOwnProperty.call(o, k)) r.push(o[k]); return r; };
|
||
if (!Object.entries) Object.entries = function (o) { var r = [], k; for (k in o) if (Object.prototype.hasOwnProperty.call(o, k)) r.push([k, o[k]]); return r; };
|
||
if (!Array.from) Array.from = function (a) { var r = [], i, n = a ? a.length : 0; if (typeof n !== "number") n = 0; for (i = 0; i < n; i++) r.push(a[i]); return r; };
|
||
if (!Array.prototype.includes) Array.prototype.includes = function (v) { return this.indexOf(v) !== -1; };
|
||
if (!Array.prototype.flat) Array.prototype.flat = function () { return this.reduce(function (a, c) { return a.concat(Array.isArray(c) ? c.flat() : c); }, []); };
|
||
if (window.NodeList && NodeList.prototype && !NodeList.prototype.forEach) NodeList.prototype.forEach = Array.prototype.forEach;
|
||
if (window.HTMLCollection && HTMLCollection.prototype && !HTMLCollection.prototype.forEach) HTMLCollection.prototype.forEach = Array.prototype.forEach;
|
||
if (!String.prototype.includes) String.prototype.includes = function (s) { return this.indexOf(s) !== -1; };
|
||
if (!String.prototype.startsWith) String.prototype.startsWith = function (s) { return this.lastIndexOf(s, 0) === 0; };
|
||
if (!String.prototype.endsWith) String.prototype.endsWith = function (s) { var i = this.length - s.length; return i >= 0 && this.indexOf(s, i) === i; };
|
||
if (typeof Promise !== "undefined" && !Promise.prototype["finally"]) Promise.prototype["finally"] = function (cb) {
|
||
return this.then(function (v) { return Promise.resolve(cb()).then(function () { return v; }); },
|
||
function (e) { return Promise.resolve(cb()).then(function () { throw e; }); });
|
||
};
|
||
|
||
var supports = (window.CSS && CSS.supports) ? function (p, v) { try { return CSS.supports(p, v); } catch (e) { return false; } } : function () { return false; };
|
||
function flexGapOk() {
|
||
var f = D.createElement("div");
|
||
f.style.cssText = "display:flex;flex-direction:column;position:absolute;visibility:hidden;width:1px;gap:1px;";
|
||
var c1 = D.createElement("div"), c2 = D.createElement("div");
|
||
c1.style.height = "1px"; c2.style.height = "1px";
|
||
f.appendChild(c1); f.appendChild(c2);
|
||
(D.body || root).appendChild(f); var ok = f.scrollHeight === 3; f.parentNode.removeChild(f); return ok;
|
||
}
|
||
function aspectOk() {
|
||
if (!supports("aspect-ratio", "1 / 1")) return false;
|
||
var b = D.createElement("div"); b.style.cssText = "position:absolute;visibility:hidden;width:50px;aspect-ratio:1/1;";
|
||
(D.body || root).appendChild(b); var ok = b.offsetHeight >= 40; b.parentNode.removeChild(b); return ok;
|
||
}
|
||
function detect() {
|
||
if (!flexGapOk()) root.className += " no-layout-gap";
|
||
if (!(supports("width", "min(1px,2px)") && supports("width", "clamp(1px,2px,3px)"))) root.className += " no-css-functions";
|
||
if (!aspectOk()) root.className += " no-aspect-ratio";
|
||
}
|
||
if (D.body) detect(); else D.addEventListener("DOMContentLoaded", detect);
|
||
})();
|
||
</script><style>
|
||
/* ============================================================
|
||
片库 · 4K 原盘影视下载 — home-cinema 暗金质感
|
||
page 透明(壁纸透出)+ 半透明面板承载文字
|
||
signature:按画质分组的资源行(scene 发布名 + 体积 + 复制/下载)
|
||
语法/兼容:仅基础选择器,无 :is/:has/:focus-visible;无 clamp/min/max;
|
||
inset 前置 top/left/right/bottom 兜底;动效在 TV / reduce-motion 关闭
|
||
============================================================ */
|
||
:root {
|
||
--bg-fallback: #05050d;
|
||
|
||
--panel-rgb: 16, 14, 32;
|
||
--panel: rgba(var(--panel-rgb), .72);
|
||
--panel-soft: rgba(var(--panel-rgb), .52);
|
||
--panel-strong: rgba(var(--panel-rgb), .92);
|
||
--line: rgba(255, 43, 214, .18);
|
||
--line-soft: rgba(255, 43, 214, .09);
|
||
--line-strong: rgba(0, 240, 255, .35);
|
||
|
||
--ink: #eafcff;
|
||
--ink-soft: #9fb8c9;
|
||
--ink-faint: #5d6f8a;
|
||
|
||
--gold: #ff2bd6;
|
||
--gold-deep: #c200a3;
|
||
--gold-lite: #ff7af0;
|
||
--gold-edge: #00f0ff;
|
||
--gold-rgb: 255, 43, 214;
|
||
--on-gold: #08020a;
|
||
--gold-soft: rgba(var(--gold-rgb), .16);
|
||
--cyan: #00f0ff;
|
||
--slate: #8fb6c9;
|
||
--green: #39ff8f;
|
||
--good: #39ff8f; --warn: #ffe156; --bad: #ff3864; --gray: #5d6f8a;
|
||
--neon-glow: 0 0 6px rgba(255,43,214,.7), 0 0 16px rgba(0,240,255,.35);
|
||
|
||
--safe-top: var(--fm-safe-top, 0px);
|
||
--safe-bottom: var(--fm-safe-bottom, 0px);
|
||
--safe-left: var(--fm-safe-left, 0px);
|
||
--safe-right: var(--fm-safe-right, 0px);
|
||
|
||
--r-sm: 8px; --r: 12px; --r-lg: 16px;
|
||
--maxw: 1180px;
|
||
--gut: 14px; /* 统一“到边框”和“卡片间”的间距,避免忽宽忽窄 */
|
||
--ease: cubic-bezier(.22, .61, .36, 1);
|
||
--display: "Audiowide", "Orbitron", "PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei", "Source Han Sans SC", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||
--mono: "Share Tech Mono", "SFMono-Regular", "JetBrains Mono", "Roboto Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace;
|
||
}
|
||
@import url("https://fonts.googleapis.com/css2?family=Audiowide&family=Orbitron:wght@500;700;900&family=Share+Tech+Mono&display=swap");
|
||
/* 备选主题色(点左上角 P 循环切换):只改强调色变量,全站随之换色 */
|
||
html[data-theme="rose"] {
|
||
--gold: #ff2d78; --gold-deep: #cc0055; --gold-lite: #ff80b0; --gold-edge: #f7ff00; --gold-rgb: 255, 45, 120; --on-gold: #1a0010;
|
||
--cyan: #f7ff00; --line: rgba(255,45,120,.18); --line-soft: rgba(255,45,120,.09); --line-strong: rgba(247,255,0,.35);
|
||
}
|
||
html[data-theme="ocean"] {
|
||
--gold: #00e5ff; --gold-deep: #00a3c4; --gold-lite: #7af7ff; --gold-edge: #ff6b00; --gold-rgb: 0, 229, 255; --on-gold: #001016;
|
||
--cyan: #ff6b00; --line: rgba(0,229,255,.18); --line-soft: rgba(0,229,255,.09); --line-strong: rgba(255,107,0,.35);
|
||
}
|
||
html[data-theme="matrix"] {
|
||
--gold: #00ff41; --gold-deep: #00b32c; --gold-lite: #7dffaa; --gold-edge: #ff003c; --gold-rgb: 0, 255, 65; --on-gold: #001a08;
|
||
--cyan: #ff4060; --line: rgba(0,255,65,.18); --line-soft: rgba(0,255,65,.09); --line-strong: rgba(255,64,96,.35);
|
||
}
|
||
html[data-theme="reactor"] {
|
||
--gold: #ff8c00; --gold-deep: #cc6400; --gold-lite: #ffbc60; --gold-edge: #00cfff; --gold-rgb: 255, 140, 0; --on-gold: #1a0500;
|
||
--cyan: #00cfff; --line: rgba(255,140,0,.18); --line-soft: rgba(255,140,0,.09); --line-strong: rgba(0,207,255,.35);
|
||
}
|
||
html[data-theme="void"] {
|
||
--gold: #bf00ff; --gold-deep: #8800cc; --gold-lite: #df80ff; --gold-edge: #aaff00; --gold-rgb: 191, 0, 255; --on-gold: #080014;
|
||
--cyan: #aaff00; --line: rgba(191,0,255,.18); --line-soft: rgba(191,0,255,.09); --line-strong: rgba(170,255,0,.35);
|
||
}
|
||
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
||
html, body { margin: 0; padding: 0; min-height: 100%; background: transparent; color: var(--ink); font-family: var(--display); font-size: 15px; line-height: 1.5; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
|
||
html:not(.fm-native), html:not(.fm-native) body { background: var(--bg-fallback); }
|
||
img { display: block; max-width: 100%; }
|
||
button { font-family: inherit; color: inherit; cursor: pointer; }
|
||
input { font-family: inherit; }
|
||
.hidden { display: none !important; }
|
||
|
||
.app { position: relative; min-height: 100vh; min-height: var(--fm-web-height, 100vh); background: transparent; padding-left: var(--safe-left); padding-right: var(--safe-right); }
|
||
html.fm-native .app { background: linear-gradient(180deg, rgba(5,4,14,.6), rgba(5,4,14,.4) 28%, rgba(5,4,14,.55)); }
|
||
/* 赛博朋克霓虹氛围光 + 网格 + 扫描线(低成本,TV 关闭) */
|
||
.app::before { content: ""; position: fixed; top: 0; left: 0; right: 0; bottom: auto; height: 560px; pointer-events: none; z-index: 0;
|
||
will-change: transform; transform: translateZ(0);
|
||
background:
|
||
radial-gradient(ellipse 80% 60% at 10% -5%, color-mix(in srgb, var(--cyan) 28%, transparent), color-mix(in srgb, var(--cyan) 0%, transparent) 55%),
|
||
radial-gradient(ellipse 80% 60% at 90% -5%, rgba(var(--gold-rgb),.28), rgba(var(--gold-rgb),0) 55%),
|
||
radial-gradient(ellipse 40% 30% at 50% 0%, rgba(255,255,255,.04), transparent 60%); }
|
||
html:not(.tv-mode) body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
||
will-change: transform; transform: translateZ(0);
|
||
background-image:
|
||
linear-gradient(color-mix(in srgb, var(--cyan) 6%, transparent) 1px, transparent 1px),
|
||
linear-gradient(90deg, color-mix(in srgb, var(--cyan) 6%, transparent) 1px, transparent 1px),
|
||
linear-gradient(color-mix(in srgb, var(--cyan) 2%, transparent) 1px, transparent 1px),
|
||
linear-gradient(90deg, color-mix(in srgb, var(--cyan) 2%, transparent) 1px, transparent 1px),
|
||
linear-gradient(180deg, color-mix(in srgb, var(--gold) 7%, transparent) 0%, transparent 35%);
|
||
background-size: 120px 120px, 120px 120px, 24px 24px, 24px 24px, 100% 100%; }
|
||
html:not(.tv-mode) body::after { content: ""; position: fixed; left: 0; right: 0; top: 0; height: 2px; z-index: 0; pointer-events: none;
|
||
will-change: transform, top;
|
||
background: linear-gradient(90deg, transparent, var(--cyan), var(--gold), transparent);
|
||
box-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 80%, transparent);
|
||
animation: scanline 5.5s linear infinite; }
|
||
@keyframes scanline { 0% { top: -2px; opacity: 0; } 8% { opacity: 1; } 92% { opacity: 1; } 100% { top: 100%; opacity: 0; } }
|
||
.tv-mode .app::before { display: none; }
|
||
|
||
/* ── 赛博进度条 ── */
|
||
#cybar {
|
||
position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 200;
|
||
pointer-events: none; opacity: 0; transition: opacity .18s;
|
||
}
|
||
#cybar.active { opacity: 1; }
|
||
#cybar-track {
|
||
position: absolute; top: 0; left: 0; height: 100%; width: 0%;
|
||
background: linear-gradient(90deg, var(--gold), var(--cyan));
|
||
box-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 90%, transparent), 0 0 20px rgba(var(--gold-rgb),.6);
|
||
transition: width .28s cubic-bezier(.4,0,.2,1);
|
||
}
|
||
/* 分段闪烁节点 */
|
||
#cybar-segs {
|
||
position: absolute; top: 0; left: 0; right: 0; height: 100%;
|
||
display: flex; align-items: center;
|
||
}
|
||
.cybar-seg {
|
||
flex: 1; height: 100%; position: relative;
|
||
}
|
||
.cybar-seg::after {
|
||
content: ""; position: absolute; right: 0; top: -1px; bottom: -1px; width: 1px;
|
||
background: color-mix(in srgb, var(--cyan) 30%, transparent);
|
||
}
|
||
/* 进度条顶部方块光标 */
|
||
#cybar-cursor {
|
||
position: absolute; top: -2px; height: 7px; width: 4px;
|
||
background: #fff;
|
||
box-shadow: 0 0 8px var(--cyan), 0 0 18px rgba(var(--gold-rgb),.8);
|
||
transform: translateX(-2px);
|
||
transition: left .28s cubic-bezier(.4,0,.2,1);
|
||
animation: cybarCursorBlink .55s step-end infinite;
|
||
}
|
||
@keyframes cybarCursorBlink { 0%, 60% { opacity: 1; } 61%, 100% { opacity: .3; } }
|
||
/* 进度条右侧文字标签 */
|
||
#cybar-label {
|
||
position: absolute; right: 8px; top: 5px;
|
||
font-family: var(--mono); font-size: 8px; letter-spacing: 1.5px;
|
||
color: color-mix(in srgb, var(--cyan) 70%, transparent);
|
||
text-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 80%, transparent);
|
||
white-space: nowrap; pointer-events: none; opacity: 0;
|
||
transition: opacity .18s;
|
||
}
|
||
#cybar.active #cybar-label { opacity: 1; }
|
||
/* 完成时闪烁消失 */
|
||
@keyframes cybarDone {
|
||
0% { opacity: 1; filter: brightness(1); }
|
||
30% { opacity: 1; filter: brightness(2.5); }
|
||
55% { opacity: .6; filter: brightness(1.5); }
|
||
80% { opacity: .3; }
|
||
100% { opacity: 0; }
|
||
}
|
||
#cybar.done { animation: cybarDone .55s var(--ease) forwards; }
|
||
|
||
/* ── Typewriter 光标 ── */
|
||
.tw-cursor {
|
||
display: inline-block; width: 1ch; color: var(--cyan);
|
||
animation: cursor-blink 0.9s step-end infinite;
|
||
margin-left: 1px;
|
||
}
|
||
|
||
/* ── 数字翻牌 ── */
|
||
@keyframes numRoll {
|
||
0% { transform: translateY(-80%); opacity: 0; filter: blur(3px); }
|
||
60% { filter: blur(0); }
|
||
100% { transform: translateY(0); opacity: 1; filter: blur(0); }
|
||
}
|
||
.num-roll { display: inline-block; animation: numRoll .42s var(--ease) both; }
|
||
|
||
/* ── 文字乱码还原 ── */
|
||
@keyframes glitchReveal {
|
||
0% { opacity: 0; }
|
||
15% { opacity: 1; }
|
||
100% { opacity: 1; }
|
||
}
|
||
.scramble-target { display: inline; }
|
||
.wrap { position: relative; z-index: 1; width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gut); }
|
||
|
||
/* ---------------- 顶栏 ---------------- */
|
||
.topbar { position: sticky; top: 0; z-index: 20; padding-top: calc(var(--safe-top) + 12px); padding-bottom: 12px; background: linear-gradient(to bottom, rgba(4,2,12,.97), rgba(4,2,12,.7) 72%, rgba(4,2,12,0)); backdrop-filter: blur(14px); transition: padding .25s var(--ease), background .25s var(--ease), box-shadow .25s var(--ease); }
|
||
.topbar::before { content: ""; display: none; }
|
||
/* 实时状态文字 — 由 JS 驱动 */
|
||
/* 状态栏移到首页搜索框上方 */
|
||
.home-status-bar { display: flex; align-items: center; gap: 10px; margin: 14px 0 0; padding: 6px 12px; font-family: var(--mono); font-size: 9px; letter-spacing: 2px; color: color-mix(in srgb, var(--cyan) 50%, transparent); border-top: 1px solid color-mix(in srgb, var(--cyan) 12%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--cyan) 8%, transparent); background: color-mix(in srgb, var(--cyan) 3%, transparent); position: relative; overflow: hidden; flex-wrap: nowrap; height: 26px; }
|
||
.home-status-bar::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: linear-gradient(to bottom, var(--cyan), var(--gold)); box-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 60%, transparent); }
|
||
.home-status-bar::after { content: ""; position: absolute; right: 0; top: 0; bottom: 0; width: 2px; background: linear-gradient(to bottom, var(--gold), var(--cyan)); box-shadow: 0 0 6px rgba(var(--gold-rgb),.6); }
|
||
.home-status-indicator { width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px var(--green); flex-shrink: 0; animation: statusPulse 1.8s ease-in-out infinite; }
|
||
@keyframes statusPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
|
||
.home-status-text { flex: 1 1 auto; text-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 60%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; font-variant-numeric: tabular-nums; }
|
||
.home-status-right { flex-shrink: 0; color: color-mix(in srgb, var(--gold) 40%, transparent); text-shadow: 0 0 8px rgba(var(--gold-rgb),.4); white-space: nowrap; }
|
||
.topbar::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: linear-gradient(90deg, transparent 2%, var(--cyan) 20%, var(--gold) 80%, transparent 98%); box-shadow: 0 0 10px rgba(var(--gold-rgb),.5), 0 0 3px color-mix(in srgb, var(--cyan) 80%, transparent); opacity: 1; }
|
||
/* 向下滚动后头部收缩、玻璃感更强(更省空间、更聚焦) */
|
||
.topbar.shrink { padding-top: calc(var(--safe-top) + 6px); padding-bottom: 8px; background: linear-gradient(to bottom, rgba(6,7,9,.98), rgba(6,7,9,.9)); box-shadow: 0 6px 20px rgba(0,0,0,.45); }
|
||
.topbar.shrink .brand .logo { width: 28px; height: 28px; font-size: 16px; }
|
||
.topbar.shrink .searchbox { height: 38px; }
|
||
.topbar .wrap { display: flex; align-items: center; }
|
||
.brand { display: flex; align-items: center; flex: 0 1 auto; min-width: 0; margin-right: 16px; text-decoration: none; overflow: visible; }
|
||
.brand .logo { cursor: pointer; width: 34px; height: 34px; border-radius: 0; background: transparent; border: 1px solid rgba(var(--gold-rgb),.7); box-shadow: 0 0 8px rgba(var(--gold-rgb),.4), 0 0 20px rgba(var(--gold-rgb),.15), inset 0 0 8px rgba(var(--gold-rgb),.08); display: flex; align-items: center; justify-content: center; color: var(--gold); font-family: var(--mono); font-weight: 900; font-size: 16px; margin-right: 10px; position: relative; transition: width .25s var(--ease), height .25s var(--ease), font-size .25s var(--ease), transform .2s var(--ease); animation: logoPulse 2.6s ease-in-out infinite; }
|
||
.brand .logo::before { content: ""; position: absolute; top: -3px; left: -3px; width: 8px; height: 8px; border-top: 1px solid var(--cyan); border-left: 1px solid var(--cyan); }
|
||
.brand .logo::after { content: ""; position: absolute; bottom: -3px; right: -3px; width: 8px; height: 8px; border-bottom: 1px solid var(--cyan); border-right: 1px solid var(--cyan); }
|
||
@keyframes logoPulse {
|
||
0%, 100% { box-shadow: 0 0 8px rgba(var(--gold-rgb),.4), 0 0 20px rgba(var(--gold-rgb),.15), inset 0 0 8px rgba(var(--gold-rgb),.08); }
|
||
50% { box-shadow: 0 0 14px rgba(var(--gold-rgb),.8), 0 0 32px rgba(var(--gold-rgb),.3), inset 0 0 12px rgba(var(--gold-rgb),.12); }
|
||
}
|
||
.tv-mode .brand .logo { animation: none; }
|
||
.brand .logo:active { transform: scale(.88) rotate(-12deg); }
|
||
.brand b { font-family: "Audiowide", var(--display); font-size: 21px; font-weight: 400; line-height: 1.5; letter-spacing: 1.5px; color: var(--ink); white-space: nowrap; overflow: visible; text-transform: uppercase;
|
||
text-shadow: 1.5px 0 0 color-mix(in srgb, var(--cyan) 85%, transparent), -1.5px 0 0 color-mix(in srgb, var(--gold) 85%, transparent), 0 0 14px color-mix(in srgb, var(--cyan) 50%, transparent), 0 0 26px rgba(var(--gold-rgb),.35); }
|
||
.searchbox { position: relative; flex: 1 1 auto; min-width: 0; display: flex; align-items: center; height: 42px; padding: 0 4px 0 16px; background: var(--panel-strong); border: 1px solid color-mix(in srgb, var(--cyan) 30%, transparent); border-radius: 4px; transition: border-color .2s var(--ease), background .2s var(--ease), height .25s var(--ease), box-shadow .2s var(--ease); box-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 12%, transparent); }
|
||
.searchbox:focus-within { border-color: var(--cyan); background: var(--panel-strong); box-shadow: 0 0 0 1px color-mix(in srgb, var(--cyan) 40%, transparent), 0 0 10px color-mix(in srgb, var(--cyan) 30%, transparent); }
|
||
.searchbox svg { width: 17px; height: 17px; flex: 0 0 auto; fill: var(--cyan); }
|
||
.searchbox input { flex: 1 1 auto; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; color: var(--ink); font-family: var(--mono); font-size: 14px; padding: 0 10px; }
|
||
.searchbox input::placeholder { color: color-mix(in srgb, var(--cyan) 50%, transparent); }
|
||
.searchbox .go { flex: 0 0 auto; height: 32px; padding: 0 16px; border: 1px solid var(--gold); border-radius: 2px; background: transparent; color: var(--gold); font-family: var(--mono); font-weight: 800; font-size: 11px; letter-spacing: 2.5px; transition: all .15s var(--ease); text-transform: uppercase; position: relative; overflow: hidden; }
|
||
.searchbox .go::before { content: ""; position: absolute; inset: 0; background: var(--gold); opacity: 0; transition: opacity .15s; }
|
||
.searchbox .go:hover { color: var(--on-gold); box-shadow: 0 0 16px rgba(var(--gold-rgb),.5); }
|
||
.searchbox .go:hover::before { opacity: 1; }
|
||
.searchbox .go:active { transform: scale(.96); }
|
||
|
||
/* 搜索输入联想下拉 */
|
||
.suggest { position: absolute; top: calc(100% + 8px); left: 0; right: 0; z-index: 40; background: var(--panel-strong); border: 1px solid var(--line-strong); border-radius: 14px; box-shadow: 0 16px 44px rgba(0,0,0,.55); max-height: 62vh; overflow-y: auto; -webkit-overflow-scrolling: touch; display: none; }
|
||
.suggest.show { display: block; }
|
||
.suggest .sg { display: flex; align-items: center; gap: 11px; padding: 8px 12px; border-bottom: 1px solid var(--line-soft); }
|
||
.no-layout-gap .suggest .sg > * { margin-right: 11px; }
|
||
.suggest .sg:last-child { border-bottom: 0; }
|
||
.suggest .sg:active, .suggest .sg.focus { background: rgba(255,255,255,.07); }
|
||
.suggest .sg .pp { flex: 0 0 auto; width: 38px; height: 54px; border-radius: 6px; overflow: hidden; background: #15161c; }
|
||
.suggest .sg .pp img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||
.suggest .sg .pp .ph { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: rgba(255,255,255,.7); padding: 3px; text-align: center; line-height: 1.1; }
|
||
.suggest .sg .tx { flex: 1 1 auto; min-width: 0; }
|
||
.suggest .sg .tt { font-size: 14px; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.suggest .sg .mt { font-size: 11.5px; color: var(--ink-faint); margin-top: 3px; }
|
||
.suggest .sg.kwrow .kwic { display: flex; align-items: center; justify-content: center; font-size: 17px; background: transparent; }
|
||
.suggest .sg.kwrow .tt { color: var(--ink-soft); }
|
||
|
||
/* ---------------- 公告条 ---------------- */
|
||
.notice { display: flex; align-items: center; gap: 10px; margin: 14px 0 2px; padding: 10px 12px; background: color-mix(in srgb, var(--gold) 4%, transparent); border: 1px solid color-mix(in srgb, var(--gold) 25%, transparent); border-left: 3px solid var(--gold); border-radius: 0; color: var(--ink-soft); font-size: 12px; font-family: var(--mono); line-height: 1.5; letter-spacing: .3px; position: relative; }
|
||
.notice::before { content: "// NOTICE"; position: absolute; top: -9px; left: 12px; background: var(--bg-fallback); padding: 0 6px; font-size: 9px; color: color-mix(in srgb, var(--gold) 50%, transparent); letter-spacing: 2px; }
|
||
.no-layout-gap .notice > * { margin-right: 10px; }
|
||
.notice .ic { flex: 0 0 auto; width: 18px; height: 18px; border-radius: 0; background: transparent; border: 1px solid rgba(var(--gold-rgb),.5); color: var(--gold); font-weight: 900; font-size: 11px; font-family: var(--mono); display: flex; align-items: center; justify-content: center; }
|
||
.notice .x { margin-left: auto; flex: 0 0 auto; background: transparent; border: 1px solid rgba(var(--gold-rgb),.2); border-radius: 2px; color: rgba(var(--gold-rgb),.5); font-size: 15px; line-height: 1; padding: 1px 6px; font-family: var(--mono); transition: color .2s, border-color .2s, background .2s; }
|
||
.notice .x:hover { color: var(--gold); border-color: rgba(var(--gold-rgb),.5); background: rgba(var(--gold-rgb),.06); }
|
||
.notice .x:active { color: var(--ink); }
|
||
|
||
/* ---------------- 分类导航 ---------------- */
|
||
.nav { display: flex; gap: 8px; overflow-x: auto; overflow-y: visible; padding: 18px 4px 10px; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
||
.nav::-webkit-scrollbar { display: none; }
|
||
.no-layout-gap .nav > * { margin-right: 8px; }
|
||
.navtab { flex: 0 0 auto; border: 0; border-bottom: 2px solid color-mix(in srgb, var(--cyan) 12%, transparent); background: transparent; color: color-mix(in srgb, var(--cyan) 40%, transparent); height: 35px; padding: 0 16px; border-radius: 0; font-size: 11px; font-family: var(--mono); font-weight: 700; letter-spacing: 2px; text-transform: uppercase; white-space: nowrap; transition: color .2s, border-color .2s, box-shadow .2s; position: relative; }
|
||
.navtab.on { color: var(--cyan); border-bottom-color: var(--cyan); text-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 80%, transparent), 0 0 20px color-mix(in srgb, var(--cyan) 40%, transparent); box-shadow: 0 4px 12px color-mix(in srgb, var(--cyan) 20%, transparent); }
|
||
.navtab:focus { outline: 0; }
|
||
.navtab.focus { box-shadow: 0 0 0 1px var(--cyan); }
|
||
|
||
/* ---------------- 区块标题 ---------------- */
|
||
.section-head { display: flex; align-items: center; justify-content: space-between; margin: 20px 0 12px; }
|
||
.section-head h2 { position: relative; margin: 0; padding-left: 16px; font-size: 14px; font-family: var(--mono); font-weight: 700; letter-spacing: 2.5px; text-transform: uppercase; color: var(--cyan); text-shadow: 0 0 10px color-mix(in srgb, var(--cyan) 50%, transparent), 0 0 24px color-mix(in srgb, var(--cyan) 20%, transparent); }
|
||
.section-head h2::before { content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 8px; height: 2px; background: var(--cyan); box-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 80%, transparent); }
|
||
.section-head h2::after { content: ""; position: absolute; left: 2px; top: 50%; transform: translateY(-50%) translateY(-4px); width: 2px; height: 8px; background: var(--cyan); box-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 80%, transparent); }
|
||
|
||
/* ---------------- 海报网格 ----------------
|
||
直接照搬 emby 文档的方案:窄屏固定 3 列;≥720px 起换成 auto-fill + minmax,
|
||
按目标卡宽(132px/156px)自动算列数,列数随宽度平滑变化,
|
||
横屏只是"宽度变大",会被同一套规则自动接住,不需要再单独写 orientation 断点 */
|
||
.grid { display: block; }
|
||
@supports (display: grid) {
|
||
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gut); }
|
||
@media (min-width: 720px) {
|
||
.grid { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); }
|
||
}
|
||
@media (min-width: 1180px) {
|
||
.grid { grid-template-columns: repeat(auto-fill, minmax(156px, 1fr)); }
|
||
}
|
||
}
|
||
/* 无 flex/grid gap 支持时的兜底(极少数老内核才会触发):固定 3 列 */
|
||
.no-layout-gap .grid { display: flex; flex-wrap: wrap; gap: 0; margin: 0 calc(var(--gut) / -2); font-size: 0; }
|
||
.no-layout-gap .grid > .card { display: block; width: calc(33.3% - var(--gut)); margin: 0 calc(var(--gut) / 2) var(--gut); font-size: 15px; }
|
||
|
||
/* 分类切换动效:内容按方向滑入 + 淡入(fwd 从右进,back 从左进) */
|
||
@keyframes gridSwapFwd { from { opacity: 0; transform: translateX(28px); } to { opacity: 1; transform: translateX(0); } }
|
||
@keyframes gridSwapBack { from { opacity: 0; transform: translateX(-28px); } to { opacity: 1; transform: translateX(0); } }
|
||
.grid.gswap-fwd { animation: gridSwapFwd .34s var(--ease); }
|
||
.grid.gswap-back { animation: gridSwapBack .34s var(--ease); }
|
||
/* 卡片首次/筛选/续载时逐个淡入(交错),动画结束由 JS 摘掉 .cin 以恢复按压反馈 */
|
||
@keyframes cardIn { from { opacity: 0; transform: translateY(8px); clip-path: inset(0 0 100% 0); } 40% { clip-path: inset(0 0 0% 0); } to { opacity: 1; transform: none; clip-path: inset(0 0 0% 0); } }
|
||
.card.cin { animation: cardIn .38s var(--ease) both; }
|
||
.tv-mode .card.cin { animation: none; }
|
||
.tv-mode .grid.gswap-fwd, .tv-mode .grid.gswap-back { animation: none; }
|
||
|
||
.card { position: relative; text-align: left; padding: 0; background: rgba(8,10,18,.85); border: 1px solid color-mix(in srgb, var(--cyan) 15%, transparent); border-radius: 0; overflow: hidden; transition: transform .16s var(--ease), box-shadow .2s var(--ease), border-color .2s; }
|
||
.card::before { content: ""; position: absolute; top: -1px; left: -1px; width: 12px; height: 12px; border-top: 2px solid color-mix(in srgb, var(--cyan) 60%, transparent); border-left: 2px solid color-mix(in srgb, var(--cyan) 60%, transparent); z-index: 3; pointer-events: none; }
|
||
.card::after { content: ""; position: absolute; bottom: -1px; right: -1px; width: 12px; height: 12px; border-bottom: 2px solid rgba(var(--gold-rgb),.4); border-right: 2px solid rgba(var(--gold-rgb),.4); z-index: 3; pointer-events: none; }
|
||
.card:hover { border-color: color-mix(in srgb, var(--cyan) 35%, transparent); box-shadow: 0 0 18px color-mix(in srgb, var(--cyan) 12%, transparent), 0 4px 24px rgba(0,0,0,.5); }
|
||
.card:hover::before { border-color: var(--cyan); }
|
||
.card:hover::after { border-color: var(--gold); }
|
||
.card:active { transform: scale(.975); border-color: var(--cyan); box-shadow: 0 0 24px color-mix(in srgb, var(--cyan) 40%, transparent); }
|
||
.card .poster { position: relative; width: 100%; background: #0d0f16; background-image: radial-gradient(85% 65% at 26% -8%, rgba(var(--gold-rgb), .08), rgba(var(--gold-rgb), 0) 60%), linear-gradient(150deg, #1a1b24 0%, #0e0f16 55%, #080910 100%); aspect-ratio: 2 / 3; }
|
||
.card .poster::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; inset: 0;
|
||
background-image: repeating-linear-gradient(0deg, color-mix(in srgb, var(--cyan) 2%, transparent) 0px, color-mix(in srgb, var(--cyan) 2%, transparent) 1px, transparent 1px, transparent 3px);
|
||
pointer-events: none; z-index: 1; opacity: 0; transition: opacity .2s; }
|
||
.card:hover .poster::before { opacity: 1; }
|
||
.card .poster::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; top: auto; height: 50%; z-index: 2; background: linear-gradient(to top, rgba(4,5,10,.85) 0%, rgba(4,5,10,.3) 50%, rgba(4,5,10,0) 100%); pointer-events: none; }
|
||
.no-aspect-ratio .card .poster { height: 0; padding-top: 150%; }
|
||
.card .poster .ph, .card .poster img { position: absolute; top: 0; left: 0; right: 0; bottom: 0; inset: 0; width: 100%; height: 100%; }
|
||
.card .poster img { object-fit: cover; }
|
||
.card .poster .ph { display: flex; align-items: center; justify-content: center; text-align: center; padding: 10px; color: rgba(255,255,255,.85); font-weight: 800; font-size: 14px; line-height: 1.3; text-shadow: 0 1px 4px rgba(0,0,0,.5); font-family: var(--mono); }
|
||
.qtag { position: absolute; z-index: 2; top: 7px; left: 7px; font-size: 9px; font-family: var(--mono); font-weight: 900; letter-spacing: 1.5px; padding: 2px 6px; border-radius: 0; color: var(--on-gold); background: var(--gold); box-shadow: 0 0 10px rgba(var(--gold-rgb),.7); text-transform: uppercase; }
|
||
.qtag.q4k { background: transparent; color: var(--cyan); border: 1px solid var(--cyan); box-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 50%, transparent), inset 0 0 4px color-mix(in srgb, var(--cyan) 10%, transparent); }
|
||
.qtag.qbd { background: transparent; color: var(--gold); border: 1px solid var(--gold); box-shadow: 0 0 8px rgba(var(--gold-rgb),.5); }
|
||
.qtag.qweb { background: transparent; color: #8fb6c9; border: 1px solid rgba(143,182,201,.4); box-shadow: none; }
|
||
.remark { position: absolute; z-index: 2; right: 7px; bottom: 7px; font-size: 9px; font-family: var(--mono); font-weight: 700; letter-spacing: 1px; color: rgba(255,255,255,.7); background: rgba(4,6,10,.8); border: 1px solid rgba(255,255,255,.1); padding: 2px 6px; border-radius: 0; }
|
||
.yeartag { position: absolute; z-index: 2; left: 7px; bottom: 7px; font-size: 9px; font-family: var(--mono); font-weight: 700; color: color-mix(in srgb, var(--cyan) 70%, transparent); background: color-mix(in srgb, var(--cyan) 6%, transparent); border: 1px solid color-mix(in srgb, var(--cyan) 20%, transparent); padding: 2px 6px; border-radius: 0; }
|
||
.card .meta { padding: 5px 9px 6px; background: color-mix(in srgb, var(--cyan) 1%, transparent); position: relative; overflow: hidden; }
|
||
.card .meta::before { content: ""; position: absolute; inset: 0; background: repeating-linear-gradient(0deg, color-mix(in srgb, var(--cyan) 4%, transparent) 0px, color-mix(in srgb, var(--cyan) 4%, transparent) 1px, transparent 1px, transparent 3px); pointer-events: none; z-index: 0; }
|
||
.card .meta .t { font-size: 13px; font-weight: 700; font-family: var(--mono); color: var(--ink); line-height: 18px; height: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: color .2s, text-shadow .2s; letter-spacing: .5px; position: relative; z-index: 1; text-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 55%, transparent), 0 0 14px color-mix(in srgb, var(--cyan) 25%, transparent); }
|
||
.card .meta .sub { display: flex; align-items: center; gap: 6px; margin-top: 5px; height: 17px; white-space: nowrap; overflow: hidden; position: relative; z-index: 1; }
|
||
.no-layout-gap .card .meta .sub > * { margin-right: 6px; }
|
||
.card .meta .sub .yr { font-size: 11px; font-family: var(--mono); color: color-mix(in srgb, var(--cyan) 35%, transparent); flex: 0 0 auto; }
|
||
.rate { display: inline-flex; align-items: center; height: 15px; line-height: 1; white-space: nowrap; font-family: var(--mono); font-size: 9px; font-weight: 800; border-radius: 0; padding: 0 4px; flex: 0 0 auto; letter-spacing: .4px; border: 1px solid; }
|
||
.rate.im { background: transparent; color: var(--gold); border-color: rgba(var(--gold-rgb),.35); text-shadow: 0 0 6px rgba(var(--gold-rgb),.5); }
|
||
.rate.db { background: transparent; color: var(--green); border-color: rgba(57,255,143,.3); text-shadow: 0 0 6px rgba(57,255,143,.4); }
|
||
.card:focus { outline: 0; }
|
||
.card.focus { box-shadow: 0 0 0 1px var(--cyan), 0 0 20px color-mix(in srgb, var(--cyan) 20%, transparent); border-color: transparent; transform: translateY(-2px); }
|
||
.card.focus .meta .t { color: var(--cyan); text-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 50%, transparent); }
|
||
|
||
/* 品牌副标(窄屏隐藏) */
|
||
|
||
/* ---------------- 筛选器 ---------------- */
|
||
.filters { margin: 4px 0 14px; overflow: hidden; }
|
||
.filters.collapsed { display: none; }
|
||
.filterrow { display: flex; align-items: center; gap: 8px; overflow-x: auto; padding: 5px 0; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
||
.filterrow::-webkit-scrollbar { display: none; }
|
||
.no-layout-gap .filterrow > * { margin-right: 8px; }
|
||
.filterrow .flabel { flex: 0 0 auto; font-size: 12px; color: var(--ink-faint); font-weight: 700; width: 40px; }
|
||
.fpill { flex: 0 0 auto; height: 28px; padding: 0 12px; border-radius: 2px; border: 1px solid color-mix(in srgb, var(--cyan) 15%, transparent); background: color-mix(in srgb, var(--cyan) 3%, transparent); color: color-mix(in srgb, var(--cyan) 45%, transparent); font-size: 11px; font-family: var(--mono); font-weight: 700; letter-spacing: 1px; text-transform: uppercase; white-space: nowrap; transition: color .2s, background .2s, border-color .2s, box-shadow .2s; }
|
||
.fpill.on { background: color-mix(in srgb, var(--cyan) 10%, transparent); border-color: color-mix(in srgb, var(--cyan) 50%, transparent); color: var(--cyan); box-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 20%, transparent), inset 0 0 8px color-mix(in srgb, var(--cyan) 5%, transparent); text-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 60%, transparent); }
|
||
.fpill:focus { outline: 0; }
|
||
.fpill.focus { box-shadow: 0 0 0 1px var(--gold); }
|
||
.ftools { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; }
|
||
.ftools .res { font-size: 12px; color: var(--ink-faint); }
|
||
.fclear { font-size: 11px; font-family: var(--mono); letter-spacing: 1.5px; text-transform: uppercase; color: color-mix(in srgb, var(--gold) 50%, transparent); background: transparent; border: 1px solid color-mix(in srgb, var(--gold) 20%, transparent); border-radius: 2px; height: 26px; padding: 0 12px; transition: color .2s, border-color .2s, box-shadow .2s; }
|
||
.fclear:hover { color: var(--gold); border-color: rgba(var(--gold-rgb),.4); box-shadow: 0 0 8px rgba(var(--gold-rgb),.15); }
|
||
.fclear:focus { outline: 0; } .fclear.focus { box-shadow: 0 0 0 1px var(--gold); }
|
||
|
||
/* 筛选 收起/展开 按钮(放在区块标题右侧,不占用额外整行高度) */
|
||
.ftoggle { flex: 0 0 auto; display: flex; align-items: center; gap: 5px; height: 28px; padding: 0 12px; border-radius: 2px; border: 1px solid color-mix(in srgb, var(--cyan) 20%, transparent); background: color-mix(in srgb, var(--cyan) 3%, transparent); color: color-mix(in srgb, var(--cyan) 50%, transparent); font-size: 11px; font-family: var(--mono); font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; transition: color .2s, background .2s, border-color .2s, box-shadow .2s; }
|
||
.ftoggle:hover { color: var(--cyan); border-color: color-mix(in srgb, var(--cyan) 40%, transparent); background: color-mix(in srgb, var(--cyan) 6%, transparent); }
|
||
.no-layout-gap .ftoggle > * { margin-right: 5px; }
|
||
.ftoggle-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 2px; background: var(--gold); color: var(--on-gold); font-size: 10px; font-weight: 900; font-family: var(--mono); }
|
||
.ftoggle-count.hidden { display: none; }
|
||
.ftoggle-chev { width: 14px; height: 14px; flex: 0 0 auto; fill: currentColor; transition: transform .2s var(--ease); transform: rotate(-90deg); }
|
||
.ftoggle.open .ftoggle-chev { transform: rotate(0deg); }
|
||
.ftoggle:focus { outline: 0; } .ftoggle.focus { box-shadow: 0 0 0 1px var(--gold); }
|
||
|
||
|
||
|
||
/* 画质角标(片库 用语:4K / Dolby / HDR / Web) */
|
||
.qtag.tDolby { background: transparent; color: #ff9d4d; border: 1px solid rgba(255,157,77,.5); box-shadow: 0 0 8px rgba(255,157,77,.3); }
|
||
.qtag.tHDR { background: transparent; color: #c9a6ff; border: 1px solid rgba(201,166,255,.4); box-shadow: 0 0 8px rgba(201,166,255,.2); }
|
||
.qtag.t4K { background: transparent; color: var(--cyan); border: 1px solid color-mix(in srgb, var(--cyan) 50%, transparent); box-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 30%, transparent); }
|
||
.qtag.tWeb { background: transparent; color: #8fb6c9; border: 1px solid rgba(143,182,201,.3); }
|
||
.qtag.tHD { background: transparent; color: #aaa; border: 1px solid rgba(170,170,170,.25); }
|
||
.qtag.tStar { background: transparent; color: var(--gold); border: 1px solid rgba(var(--gold-rgb),.4); text-shadow: 0 0 6px rgba(var(--gold-rgb),.5); }
|
||
.rate.st { background: transparent; color: var(--gold); border: 1px solid rgba(var(--gold-rgb),.35); }
|
||
|
||
/* 卡片中英双标题 */
|
||
.card .meta .en { font-size: 11px; line-height: 15px; height: 15px; color: var(--ink-faint); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.card .meta .gen { font-size: 11px; line-height: 15px; height: 15px; color: var(--ink-faint); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.loadmore { display: block; width: 100%; max-width: 360px; margin: 18px auto 6px; height: 44px; border: 1px solid color-mix(in srgb, var(--cyan) 25%, transparent); background: transparent; color: color-mix(in srgb, var(--cyan) 60%, transparent); border-radius: 2px; font-size: 11px; font-family: var(--mono); font-weight: 700; letter-spacing: 3px; text-transform: uppercase; transition: all .2s var(--ease); position: relative; overflow: hidden; }
|
||
.loadmore::before { content: ""; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--cyan) 6%, transparent), transparent); transition: left .5s var(--ease); }
|
||
.loadmore:hover { color: var(--cyan); border-color: color-mix(in srgb, var(--cyan) 50%, transparent); box-shadow: 0 0 18px color-mix(in srgb, var(--cyan) 15%, transparent), inset 0 0 18px color-mix(in srgb, var(--cyan) 4%, transparent); }
|
||
.loadmore:hover::before { left: 100%; }
|
||
.loadmore:active { background: color-mix(in srgb, var(--cyan) 5%, transparent); }
|
||
.loadmore:focus { outline: 0; } .loadmore.focus { box-shadow: 0 0 0 1px var(--gold); border-color: transparent; }
|
||
|
||
/* ---------------- 状态 / 骨架 ---------------- */
|
||
.state { text-align: center; color: var(--ink-faint); padding: 44px 16px; }
|
||
.state b { display: block; color: var(--ink-soft); font-size: 15px; margin-bottom: 6px; }
|
||
.state button { margin-top: 12px; background: transparent; border: 1px solid color-mix(in srgb, var(--cyan) 25%, transparent); color: var(--cyan); border-radius: 2px; height: 36px; padding: 0 18px; font-family: var(--mono); font-size: 11px; letter-spacing: 2px; text-transform: uppercase; transition: all .2s; }
|
||
.state button:hover { background: color-mix(in srgb, var(--cyan) 6%, transparent); box-shadow: 0 0 12px color-mix(in srgb, var(--cyan) 15%, transparent); }
|
||
.skel { border-radius: 10px; background: linear-gradient(100deg, rgba(255,255,255,.03), rgba(255,255,255,.16), rgba(255,255,255,.03)); background-size: 220% 100%; animation: shimmer 1.2s linear infinite; }
|
||
.skel.poster-skel { aspect-ratio: 2/3; background-color: #15161c; }
|
||
.no-aspect-ratio .skel.poster-skel { height: 0; padding-top: 150%; }
|
||
@keyframes shimmer { 0% { background-position: 120% 0; } 100% { background-position: -120% 0; } }
|
||
|
||
|
||
/* ---------------- 详情浮层 ---------------- */
|
||
body.detail-active .app { pointer-events: none; }
|
||
.sheet { position: fixed; top: 0; left: 0; right: 0; bottom: 0; inset: 0; z-index: 60; overflow-y: auto; -webkit-overflow-scrolling: touch; background: #08080f; transform: translateX(100%); transition: transform .26s cubic-bezier(.4,0,.2,1); will-change: transform; overscroll-behavior: contain; }
|
||
.sheet.active { transform: translateX(0); }
|
||
html.fm-native .sheet { background: #08080f; }
|
||
.sheet { background: #08080f; }
|
||
.sheet .hero { position: relative; height: 60vh; min-height: 360px; max-height: 600px; overflow: hidden; }
|
||
/* 模糊底:铺满整块作氛围背景(在主图之下),主图始终 cover 铺满裁切、不留白 */
|
||
.sheet .hero .blur { position: absolute; inset: 0; background-size: cover; background-position: center; filter: blur(34px) saturate(1.2); transform: scale(1.25); opacity: .5; }
|
||
.sheet .hero .bg { position: absolute; top: 0; left: 0; right: 0; bottom: 0; inset: 0; background-size: cover; background-position: center; background-repeat: no-repeat; background-color: #15161c; }
|
||
.sheet .hero .bg::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; inset: 0;
|
||
background:
|
||
linear-gradient(to bottom, rgba(11,12,15,0) 0, rgba(11,12,15,0) 40%, rgba(11,12,15,.88) 80%, #0b0c0f 100%),
|
||
linear-gradient(180deg, color-mix(in srgb, var(--cyan) 6%, transparent) 0%, transparent 20%),
|
||
repeating-linear-gradient(0deg, color-mix(in srgb, var(--cyan) 2%, transparent) 0px, color-mix(in srgb, var(--cyan) 2%, transparent) 1px, transparent 1px, transparent 4px); }
|
||
/* 顶部暖金氛围光,与首页 .app::before 同源,让进入详情页时光感连贯不断裂 */
|
||
.sheet .hero .heroglow { position: absolute; top: 0; left: 0; right: 0; height: 46%; z-index: 1; pointer-events: none; background: radial-gradient(120% 100% at 50% -16%, rgba(var(--gold-rgb), .22), rgba(var(--gold-rgb), 0) 68%); }
|
||
.tv-mode .sheet .hero .heroglow { display: none; }
|
||
.sheet .hero .topctl { position: relative; z-index: 3; display: flex; justify-content: flex-end; padding: calc(var(--safe-top) + 14px) 18px 0; }
|
||
.iconbtn { width: 40px; height: 40px; border-radius: 2px; border: 1px solid color-mix(in srgb, var(--cyan) 30%, transparent); background: rgba(0,12,16,.7); backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; transition: all .15s var(--ease); }
|
||
.iconbtn:hover { border-color: var(--cyan); background: color-mix(in srgb, var(--cyan) 8%, transparent); box-shadow: 0 0 12px color-mix(in srgb, var(--cyan) 30%, transparent); }
|
||
.iconbtn:active { transform: scale(.92); }
|
||
.iconbtn svg { width: 19px; height: 19px; fill: var(--cyan); }
|
||
.iconbtn:focus { outline: 0; }
|
||
.iconbtn.focus { box-shadow: 0 0 0 1px var(--gold); }
|
||
|
||
/* 大图底部叠加的标题区 */
|
||
.sheet .herofg { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; padding: 0 18px 10px; max-width: var(--maxw); margin: 0 auto; text-align: center; display: flex; flex-direction: column; align-items: center; }
|
||
.hero-logo { margin-bottom: 8px; max-width: 92%; }
|
||
.hero-logo img { max-height: 120px; max-width: 100%; width: auto; object-fit: contain; filter: drop-shadow(0 2px 16px rgba(0,0,0,.8)) drop-shadow(0 0 32px rgba(0,0,0,.6)); }
|
||
.hero-logo.has-logo ~ h1 { display: none !important; }
|
||
.sheet .herofg h1 { margin: 0 0 8px; font-size: 32px; font-weight: 900; color: #fff; line-height: 1.12; letter-spacing: 1px; text-shadow: 2px 0 color-mix(in srgb, var(--cyan) 50%, transparent), -2px 0 color-mix(in srgb, var(--gold) 50%, transparent), 0 0 20px rgba(255,255,255,.3); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-align: center; }
|
||
/* ── h1 glitch 入场 ── */
|
||
@keyframes detailH1Glitch {
|
||
0% { opacity:0; transform: translateX(0); clip-path: inset(0 0 100% 0); text-shadow: 6px 0 color-mix(in srgb, var(--cyan) 90%, transparent), -6px 0 color-mix(in srgb, var(--gold) 90%, transparent); }
|
||
8% { opacity:1; clip-path: inset(60% 0 0 0); transform: translateX(-4px); text-shadow: 6px 0 color-mix(in srgb, var(--cyan) 90%, transparent), -6px 0 color-mix(in srgb, var(--gold) 90%, transparent); }
|
||
16% { clip-path: inset(20% 0 40% 0); transform: translateX(3px); text-shadow: 4px 0 color-mix(in srgb, var(--cyan) 70%, transparent), -4px 0 color-mix(in srgb, var(--gold) 70%, transparent); }
|
||
24% { clip-path: inset(80% 0 0 0); transform: translateX(-2px); }
|
||
32% { clip-path: inset(0 0 0 0); transform: translateX(0); text-shadow: 2px 0 color-mix(in srgb, var(--cyan) 50%, transparent), -2px 0 color-mix(in srgb, var(--gold) 50%, transparent), 0 0 20px rgba(255,255,255,.3); }
|
||
100% { clip-path: inset(0 0 0 0); transform: translateX(0); text-shadow: 2px 0 color-mix(in srgb, var(--cyan) 50%, transparent), -2px 0 color-mix(in srgb, var(--gold) 50%, transparent), 0 0 20px rgba(255,255,255,.3); }
|
||
}
|
||
.sheet .herofg h1.glitch-enter { animation: detailH1Glitch .55s var(--ease) both; }
|
||
@media (prefers-reduced-motion: reduce) { .sheet .herofg h1.glitch-enter { animation: none; opacity: 1; } }
|
||
.sheet .herofg .en { color: rgba(255,255,255,.6); font-size: 12px; font-family: var(--mono); letter-spacing: 1px; margin-bottom: 12px; text-shadow: 0 1px 6px rgba(0,0,0,.6); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; text-align: center; }
|
||
.sheet .herofg .chips { display: flex; flex-wrap: wrap; gap: 7px; justify-content: center; }
|
||
.no-layout-gap .sheet .herofg .chips > * { margin: 0 7px 7px 0; }
|
||
|
||
.sheet .body { position: relative; z-index: 2; padding: 4px 18px 64px; max-width: var(--maxw); margin: 0 auto; background: #0b0c0f; }
|
||
/* 详情页网格背景已移除 */
|
||
.sheet .body > * { position: relative; z-index: 1; }
|
||
.chip { font-size: 10px; color: color-mix(in srgb, var(--cyan) 85%, transparent); background: color-mix(in srgb, var(--cyan) 8%, transparent); border: 1px solid color-mix(in srgb, var(--cyan) 30%, transparent); padding: 2px 8px; border-radius: 3px; backdrop-filter: blur(4px); font-family: var(--mono); letter-spacing: .4px; font-weight: 700; text-transform: uppercase; }
|
||
@media (min-width: 720px) { .sheet .hero { height: 60vh; } .sheet .herofg h1 { font-size: 40px; } }
|
||
/* 横屏/矮屏(如手机横置):min-height 360 会把大图撑得过高,几乎占满屏;此处压低 */
|
||
@media (max-height: 560px) { .sheet .hero { height: 72vh; min-height: 190px; max-height: none; } .sheet .herofg { padding-bottom: 12px; } .sheet .herofg h1 { font-size: 24px; margin-bottom: 5px; } .sheet .herofg .en { margin-bottom: 7px; } }
|
||
.tv-mode .sheet .hero { height: 66vh; }
|
||
.tv-mode .sheet .herofg h1 { font-size: 44px; }
|
||
|
||
/* ── 海报轮播 ── */
|
||
/* ── hero 轮播背景层 ── */
|
||
.hero-bg-blur { position: absolute; top: 0; left: 0; right: 0; bottom: 0; inset: 0; z-index: 0; background-size: cover; background-position: center; filter: blur(34px) saturate(1.2); transform: scale(1.25); opacity: .5; }
|
||
.hero-bg { position: absolute; top: 0; left: 0; right: 0; bottom: 0; inset: 0; z-index: 1; background-size: cover; background-position: center; background-repeat: no-repeat; background-color: #15161c; transition: opacity .6s ease; }
|
||
.hero-bg::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; inset: 0; z-index: 2;
|
||
background: linear-gradient(to bottom, rgba(11,12,15,0) 0, rgba(11,12,15,0) 55%, rgba(11,12,15,.68) 84%, #0b0c0f 100%); }
|
||
.hero-bg-next { z-index: 0; opacity: 0; transition: none; }
|
||
|
||
/* 操作区:播放按钮居中大展示 */
|
||
.actions { display: flex; flex-direction: column; align-items: center; gap: 10px; margin: 8px 0 9px; position: relative; }
|
||
/* SYSTEM READY 标签改为 inline 不占布局空间 */
|
||
.actions::before { content: "SYSTEM READY ▶"; position: absolute; top: 0; left: 50%; transform: translate(-50%, -100%); margin-top: -4px; font-family: var(--mono); font-size: 9px; letter-spacing: 2px; color: color-mix(in srgb, var(--cyan) 30%, transparent); white-space: nowrap; pointer-events: none; }
|
||
.no-layout-gap .actions > * { margin-right: 0; }
|
||
/* 主播放按钮:宽大居中,赛博卡片样式 */
|
||
.actbtn.primary {
|
||
width: 160px; height: 42px; flex: none;
|
||
border: 1px solid rgba(var(--gold-rgb),.85); border-top-color: rgba(255,255,255,.5); border-radius: 0;
|
||
background: linear-gradient(100deg, rgba(40,4,30,.9) 0%, rgba(var(--gold-rgb),.22) 55%, rgba(var(--gold-rgb),.42) 100%);
|
||
color: #fff; font-size: 12px; font-weight: 900; font-family: var(--mono);
|
||
letter-spacing: 4px; text-transform: uppercase;
|
||
display: flex; align-items: center; justify-content: center; gap: 9px;
|
||
position: relative; overflow: visible;
|
||
clip-path: polygon(14px 0%, 100% 0%, calc(100% - 14px) 100%, 0% 100%);
|
||
user-select: none; -webkit-user-select: none;
|
||
box-shadow: 0 0 14px rgba(var(--gold-rgb),.55), 0 0 38px rgba(var(--gold-rgb),.35), 0 0 70px rgba(var(--gold-rgb),.18), inset 0 1px 0 rgba(255,255,255,.15), inset 0 0 18px rgba(var(--gold-rgb),.15);
|
||
text-shadow: 0 0 14px rgba(var(--gold-rgb),1), 0 0 6px rgba(255,255,255,.8);
|
||
transition: all .2s var(--ease);
|
||
}
|
||
.actbtn.primary::before { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,.12), transparent); transition: left .5s var(--ease); left: -100%; }
|
||
.actbtn.primary:hover { box-shadow: 0 0 18px rgba(var(--gold-rgb),.7), 0 0 50px rgba(var(--gold-rgb),.5), 0 0 90px rgba(var(--gold-rgb),.25), inset 0 1px 0 rgba(255,255,255,.2), inset 0 0 24px rgba(var(--gold-rgb),.2); border-color: var(--gold-lite); }
|
||
.actbtn.primary:hover::before { left: 100%; }
|
||
/* 隐藏旧的 actbtn(非 primary)— 由 JS 用新结构替代,CSS 先预隐藏 */
|
||
.actbtn:not(.primary) { display: none; }
|
||
/* ── PLAY 开机动效 ── */
|
||
@keyframes playPowerOn {
|
||
0% { transform: scaleX(.3) scaleY(.6); opacity: 0; border-color: transparent; box-shadow: none; color: transparent; }
|
||
25% { transform: scaleX(1) scaleY(.6); opacity: 1; border-color: var(--gold); box-shadow: 0 0 24px rgba(var(--gold-rgb),.7); color: transparent; }
|
||
55% { transform: scaleX(1) scaleY(1); border-color: var(--gold); box-shadow: 0 0 32px rgba(var(--gold-rgb),.5); color: transparent; }
|
||
75% { color: transparent; }
|
||
85% { color: var(--gold); opacity: 1; }
|
||
100% { transform: scaleX(1) scaleY(1); opacity: 1; }
|
||
}
|
||
.actbtn.primary.power-on { animation: playPowerOn .52s var(--ease) both; }
|
||
@media (prefers-reduced-motion: reduce) { .actbtn.primary.power-on { animation: none; } }
|
||
|
||
/* 元数据 */
|
||
.metablk { margin: 18px 0 6px; background: color-mix(in srgb, var(--cyan) 3%, transparent); border: 1px solid color-mix(in srgb, var(--cyan) 20%, transparent); border-radius: 4px; padding: 0 16px; position: relative; overflow: hidden; }
|
||
.metablk::before { content: "// DATA BLOCK"; position: absolute; top: 8px; right: 12px; font-family: var(--mono); font-size: 10px; color: color-mix(in srgb, var(--cyan) 25%, transparent); letter-spacing: 1px; }
|
||
.metablk::after { content: ""; position: absolute; top: 0; left: 0; width: 3px; height: 100%; background: linear-gradient(to bottom, var(--cyan), var(--gold)); box-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 50%, transparent); }
|
||
.metarow { display: flex; padding: 11px 0; border-bottom: 1px solid color-mix(in srgb, var(--cyan) 8%, transparent); font-size: 13px; position: relative; }
|
||
.metarow:last-child { border-bottom: 0; }
|
||
.metarow::before { content: ">"; position: absolute; left: -14px; color: color-mix(in srgb, var(--cyan) 30%, transparent); font-family: var(--mono); font-size: 11px; }
|
||
.metarow .k { flex: 0 0 62px; color: var(--cyan); font-family: var(--mono); font-weight: 700; letter-spacing: .3px; text-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 40%, transparent); }
|
||
.metarow .v { flex: 1 1 auto; min-width: 0; color: var(--ink); line-height: 1.55; letter-spacing: .3px; text-shadow: 0 0 10px color-mix(in srgb, var(--cyan) 8%, transparent); }
|
||
/* 评分 + 元数据 合并一行 */
|
||
.infobar { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px; margin: 13px 0 9px; }
|
||
.infobar .drating { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; justify-content: center; }
|
||
.infobar .metablk { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; justify-content: center; }
|
||
/* 评分标签行 */
|
||
.drating { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin: 0; }
|
||
.no-layout-gap .drating > * { margin: 0 6px 6px 0; }
|
||
/* 评分标签:与 .mtag 统一风格 */
|
||
.drating .rtag {
|
||
font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: .8px;
|
||
padding: 4px 10px; white-space: nowrap; text-transform: uppercase;
|
||
border: 1px solid; border-radius: 0;
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
line-height: 1.2;
|
||
}
|
||
.drating .rtag.st {
|
||
color: #ffe156; border-color: rgba(255,225,86,.35);
|
||
background: rgba(255,225,86,.06);
|
||
}
|
||
.drating .rtag.im {
|
||
color: var(--gold); border-color: rgba(var(--gold-rgb),.35);
|
||
background: rgba(var(--gold-rgb),.06);
|
||
}
|
||
.drating .rtag.db {
|
||
color: var(--green); border-color: rgba(57,255,143,.3);
|
||
background: rgba(57,255,143,.05);
|
||
}
|
||
.metatags { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 0; }
|
||
.no-layout-gap .metatags > * { margin: 0 8px 8px 0; }
|
||
.mtag { font-size: 11px; font-weight: 700; font-family: var(--mono); letter-spacing: .8px; text-transform: uppercase; color: color-mix(in srgb, var(--cyan) 50%, transparent); background: color-mix(in srgb, var(--cyan) 4%, transparent); border: 1px solid color-mix(in srgb, var(--cyan) 15%, transparent); padding: 4px 10px; border-radius: 0; white-space: nowrap; line-height: 1.2; }
|
||
.mtag .ml { color: color-mix(in srgb, var(--cyan) 30%, transparent); font-weight: 700; margin-right: 5px; }
|
||
.mtag.key { color: var(--gold); background: rgba(var(--gold-rgb),.06); border-color: rgba(var(--gold-rgb),.3); text-shadow: 0 0 6px rgba(var(--gold-rgb),.3); }
|
||
|
||
.blk-title { position: relative; padding-left: 12px; font-size: 16px; font-weight: 800; margin: 24px 0 12px; text-shadow: 0 1px 3px rgba(0,0,0,.5); font-family: var(--mono); letter-spacing: 1.5px; text-transform: uppercase; }
|
||
.blk-title::before { content: ""; position: absolute; left: 0; top: 2px; bottom: 2px; width: 4px; border-radius: 3px; background: linear-gradient(to bottom, var(--gold-lite), var(--gold-deep)); box-shadow: 0 0 8px rgba(var(--gold-rgb),.6); }
|
||
.blk-title::after { content: " _"; color: var(--cyan); opacity: .7; animation: cursor-blink 1.2s step-end infinite; }
|
||
@keyframes cursor-blink { 0%, 100% { opacity: .7; } 50% { opacity: 0; } }
|
||
@keyframes blkGlitch {
|
||
0% { text-shadow: 0 1px 3px rgba(0,0,0,.5); transform: translateX(0); clip-path: none; }
|
||
10% { text-shadow: 3px 0 color-mix(in srgb, var(--cyan) 90%, transparent), -3px 0 rgba(var(--gold-rgb),.9); transform: translateX(-3px); clip-path: inset(20% 0 50% 0); }
|
||
20% { text-shadow: -3px 0 color-mix(in srgb, var(--cyan) 90%, transparent), 3px 0 rgba(var(--gold-rgb),.9); transform: translateX(3px); clip-path: inset(55% 0 10% 0); }
|
||
30% { text-shadow: 2px 0 color-mix(in srgb, var(--cyan) 70%, transparent), -2px 0 rgba(var(--gold-rgb),.7); transform: translateX(-1px); clip-path: inset(0 0 0 0); }
|
||
40% { text-shadow: 0 1px 3px rgba(0,0,0,.5); transform: translateX(0); clip-path: none; }
|
||
100% { text-shadow: 0 1px 3px rgba(0,0,0,.5); transform: translateX(0); clip-path: none; }
|
||
}
|
||
.blk-title.glitch-fire { animation: blkGlitch .32s steps(1) both; }
|
||
@media (prefers-reduced-motion: reduce) { .blk-title.glitch-fire { animation: none; } }
|
||
.desc-box { position: relative; }
|
||
.desc { color: rgba(200,220,240,.75); font-size: 13px; line-height: 1.7; background: rgba(4,8,18,.85); border: 1px solid color-mix(in srgb, var(--cyan) 22%, transparent); border-radius: 0; padding: 12px 14px; position: relative; font-family: var(--mono); letter-spacing: .4px; box-shadow: inset 0 0 24px color-mix(in srgb, var(--cyan) 3%, transparent); }
|
||
.desc::before { content: "// SYNOPSIS"; position: absolute; top: -9px; left: 12px; background: #0b0c0f; padding: 0 6px; font-size: 9px; color: color-mix(in srgb, var(--cyan) 40%, transparent); letter-spacing: 1.5px; }
|
||
.desc::after { content: ""; position: absolute; top: 0; right: 0; width: 32px; height: 32px; background: linear-gradient(225deg, color-mix(in srgb, var(--cyan) 6%, transparent) 0%, transparent 60%); pointer-events: none; }
|
||
.desc.clamp { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.desc.clamp.open { display: block; -webkit-line-clamp: unset; overflow: visible; }
|
||
.no-css-functions .desc.clamp { max-height: 64px; overflow: hidden; }
|
||
.no-css-functions .desc.clamp.open { max-height: none; }
|
||
.desc-toggle { display: block; margin: 6px auto 0; background: transparent; border: 0; color: color-mix(in srgb, var(--cyan) 55%, transparent); font-family: var(--mono); font-size: 10.5px; letter-spacing: 1.5px; text-transform: uppercase; padding: 2px 8px; }
|
||
.desc-toggle:hover { color: var(--cyan); }
|
||
.desc-toggle.hidden { display: none; }
|
||
|
||
/* 剧照 */
|
||
.stills { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
||
.stills::-webkit-scrollbar { display: none; }
|
||
.no-layout-gap .stills > * { margin-right: 10px; }
|
||
.stills .s { flex: 0 0 auto; width: 230px; aspect-ratio: 16/9; border-radius: 0; overflow: hidden; background: #0d0f16; border: 1px solid color-mix(in srgb, var(--cyan) 12%, transparent); transition: border-color .2s, box-shadow .2s; }
|
||
.stills .s:hover { border-color: color-mix(in srgb, var(--cyan) 35%, transparent); box-shadow: 0 0 14px color-mix(in srgb, var(--cyan) 12%, transparent); }
|
||
.stills .s img { width: 100%; height: 100%; object-fit: cover; }
|
||
|
||
/* 演员阵容:横向滚动条带,每个 chip = 圆形霓虹头像(左)+ 姓名/角色(右) */
|
||
.castrow { display: flex; align-items: flex-start; gap: 14px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
||
.castrow::-webkit-scrollbar { display: none; }
|
||
.no-layout-gap .castrow > * { margin-right: 14px; }
|
||
.castchip { flex: 0 0 auto; width: 64px; display: flex; flex-direction: column; align-items: center; gap: 7px; cursor: pointer; }
|
||
.castchip:active .cav { transform: scale(.92); }
|
||
.cav { flex: 0 0 auto; width: 44px; height: 44px; border-radius: 3px; overflow: hidden; background: #0d0f16; border: 1px solid rgba(var(--gold-rgb),.75); box-shadow: 0 0 8px rgba(var(--gold-rgb),.45), 0 0 18px rgba(var(--gold-rgb),.15), inset 0 0 8px rgba(var(--gold-rgb),.08); position: relative; }
|
||
.cav::before { content: ""; position: absolute; top: -3px; left: -3px; width: 8px; height: 8px; border-top: 1px solid var(--cyan); border-left: 1px solid var(--cyan); }
|
||
.cav::after { content: ""; position: absolute; bottom: -3px; right: -3px; width: 8px; height: 8px; border-bottom: 1px solid var(--cyan); border-right: 1px solid var(--cyan); }
|
||
.castchip:hover .cav { box-shadow: 0 0 12px rgba(var(--gold-rgb),.6), 0 0 26px rgba(var(--gold-rgb),.25), inset 0 0 10px rgba(var(--gold-rgb),.1); }
|
||
.cav img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||
.cph { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: var(--gold); font-family: var(--mono); font-weight: 900; font-size: 17px; text-shadow: 0 0 8px rgba(var(--gold-rgb),.7); background: rgba(var(--gold-rgb),.06); }
|
||
/* 无头像时用人物剪影背景 */
|
||
.cav.cav-noimg {
|
||
background: rgba(var(--gold-rgb),.06);
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 44 44'%3E%3Ccircle cx='22' cy='16' r='8' fill='rgba(255%2C43%2C214%2C0.22)'/%3E%3Cellipse cx='22' cy='38' rx='13' ry='9' fill='rgba(255%2C43%2C214%2C0.15)'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
background-size: 100% 100%;
|
||
}
|
||
.ctx { width: 100%; min-width: 0; display: flex; flex-direction: column; align-items: center; line-height: 1.3; text-align: center; }
|
||
.cname { width: 100%; font-size: 11.5px; font-weight: 700; font-family: var(--mono); color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; letter-spacing: .5px; text-shadow: 0 0 8px rgba(255,255,255,.15); }
|
||
.crole { width: 100%; font-size: 10px; color: color-mix(in srgb, var(--cyan) 65%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; letter-spacing: .5px; text-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 40%, transparent); }
|
||
.crole.role-dir { color: var(--gold); text-shadow: 0 0 5px rgba(var(--gold-rgb),.4); }
|
||
/* 选中演员高亮 */
|
||
.castchip.filmo-active .cav { border-color: var(--cyan); box-shadow: 0 0 12px color-mix(in srgb, var(--cyan) 60%, transparent), 0 0 26px color-mix(in srgb, var(--cyan) 25%, transparent); }
|
||
|
||
/* 演员相关作品:点头像后内联展开在 castrow 下方 */
|
||
.cast-filmo-wrap { margin: 8px 0 4px; overflow: hidden; transition: max-height .35s cubic-bezier(.22,.61,.36,1); max-height: 0; }
|
||
.cast-filmo-wrap.open { max-height: 600px; }
|
||
.cast-filmo-inner { padding: 10px 0 4px; }
|
||
.cast-filmo-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid color-mix(in srgb, var(--cyan) 10%, transparent); }
|
||
.cast-filmo-name { font-size: 13px; font-weight: 800; font-family: var(--mono); color: var(--ink); letter-spacing: .5px; }
|
||
.cast-filmo-close { margin-left: auto; width: 26px; height: 26px; border: 1px solid color-mix(in srgb, var(--cyan) 20%, transparent); background: transparent; color: color-mix(in srgb, var(--cyan) 60%, transparent); font-size: 16px; line-height: 1; border-radius: 2px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||
.cast-filmo-close:hover { color: var(--cyan); border-color: var(--cyan); }
|
||
.cast-filmo-loading { text-align: center; color: var(--ink-faint); font-family: var(--mono); font-size: 11px; letter-spacing: 2px; padding: 20px 0; }
|
||
/* 横向单排滚动,卡片复用首页 .card 同款样式(角标/星标/年份/标题),固定宽度以适配横滚条带
|
||
.fg-cell 为通用单元格规则,演员关联作品(filmogrid-inline)和猜你喜欢(reco)共用,保证卡片尺寸/字体完全一致 */
|
||
.filmogrid-inline, .reco { display: flex; gap: var(--gut); overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; padding-bottom: 4px; }
|
||
.filmogrid-inline::-webkit-scrollbar, .reco::-webkit-scrollbar { display: none; }
|
||
.fg-cell { flex: 0 0 auto; width: 132px; }
|
||
.fg-cell .card { width: 100%; }
|
||
|
||
/* 资源分组 */
|
||
.resgroup { margin-bottom: 18px; }
|
||
.resgroup .gh { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; font-size: 13px; font-weight: 800; font-family: var(--mono); letter-spacing: 1px; text-transform: uppercase; color: color-mix(in srgb, var(--cyan) 60%, transparent); }
|
||
.resgroup .gh::before { content: "//"; color: color-mix(in srgb, var(--cyan) 30%, transparent); margin-right: 2px; }
|
||
.no-layout-gap .resgroup .gh > * { margin-right: 8px; }
|
||
.gtag { font-size: 12px; font-weight: 900; letter-spacing: .4px; padding: 3px 10px; border-radius: 6px; color: var(--on-gold); }
|
||
.gtag.原盘 { background: linear-gradient(95deg, var(--gold-lite), var(--gold)); }
|
||
.gtag.4K, .gtag.中字4K { background: linear-gradient(95deg, #8bd9ff, var(--cyan)); color: #04121f; }
|
||
.gtag.1080P { background: var(--slate); color: #14161c; }
|
||
.gtag.其它 { background: #6f6a5e; color: #fff; }
|
||
.resgroup .gh .cnt { color: var(--ink-faint); font-weight: 600; font-size: 12px; }
|
||
|
||
/* 资源画质类别:并排标签(原盘 / 4K / 中字4K…),点选切换下方框内容 */
|
||
.restabs { display: flex; gap: 8px; overflow-x: auto; padding: 2px 0 12px; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
||
.restabs::-webkit-scrollbar { display: none; }
|
||
.no-layout-gap .restabs > * { margin-right: 8px; }
|
||
.restab { flex: 0 0 auto; display: flex; align-items: center; gap: 7px; height: 34px; padding: 0 14px; border-radius: 3px; border: 1px solid color-mix(in srgb, var(--cyan) 15%, transparent); background: color-mix(in srgb, var(--cyan) 3%, transparent); color: var(--ink-soft); font-size: 12px; font-weight: 800; font-family: var(--mono); letter-spacing: .8px; text-transform: uppercase; white-space: nowrap; transition: color .2s, background .2s, border-color .2s, box-shadow .2s; }
|
||
.no-layout-gap .restab > * { margin-right: 7px; }
|
||
.restab .rt-cnt { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 17px; padding: 0 5px; border-radius: 999px; background: rgba(255,255,255,.08); color: var(--ink-faint); font-size: 11px; font-weight: 800; }
|
||
.restab:focus { outline: 0; }
|
||
.restab.focus { box-shadow: 0 0 0 2px var(--gold); }
|
||
/* 选中态:用各画质代表色,呼应卡片角标 */
|
||
.restab.on { border-color: transparent; }
|
||
.restab.on .rt-cnt { background: rgba(0,0,0,.18); }
|
||
.restab.on[data-tier="原盘"] { background: linear-gradient(95deg, var(--gold-lite), var(--gold)); color: var(--on-gold); }
|
||
.restab.on[data-tier="原盘"] .rt-cnt { color: rgba(42,28,0,.7); }
|
||
.restab.on[data-tier="4K"], .restab.on[data-tier="中字4K"] { background: linear-gradient(95deg, #8bd9ff, var(--cyan)); color: #04121f; }
|
||
.restab.on[data-tier="4K"] .rt-cnt, .restab.on[data-tier="中字4K"] .rt-cnt { color: rgba(4,18,31,.72); }
|
||
.restab.on[data-tier="1080P"] { background: var(--slate); color: #14161c; }
|
||
.restab.on[data-tier="1080P"] .rt-cnt { color: rgba(20,22,28,.65); }
|
||
.restab.on[data-tier="720P"], .restab.on[data-tier="其它"] { background: #6f6a5e; color: #fff; }
|
||
.restab.on[data-tier="720P"] .rt-cnt, .restab.on[data-tier="其它"] .rt-cnt { background: rgba(255,255,255,.2); color: #fff; }
|
||
.resbox { display: block; }
|
||
|
||
.resitem { display: flex; align-items: center; gap: 12px; min-height: 58px; padding: 9px 13px; margin-bottom: 8px; background: rgba(8,10,16,.9); border: 1px solid color-mix(in srgb, var(--cyan) 12%, transparent); border-left: 3px solid var(--cyan); border-radius: 4px; transition: box-shadow .2s, border-color .2s, background .2s; position: relative; overflow: hidden; }
|
||
.resitem::before { content: ""; position: absolute; top: 0; left: -100%; right: auto; width: 100%; height: 1px; background: linear-gradient(90deg, transparent, var(--cyan), rgba(var(--gold-rgb),.8), transparent); opacity: 0; transition: opacity .15s; }
|
||
.resitem:hover::before { opacity: 1; animation: resitemSweep .45s var(--ease) forwards; }
|
||
@keyframes resitemSweep { 0% { left: -100%; opacity: 1; } 100% { left: 100%; opacity: .3; } }
|
||
@media (prefers-reduced-motion: reduce) { .resitem:hover::before { animation: none; left: 0; opacity: .7; width: 100%; } }
|
||
.resitem::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(90deg, color-mix(in srgb, var(--cyan) 2%, transparent), transparent 30%); pointer-events: none; }
|
||
.resitem:hover { background: color-mix(in srgb, var(--cyan) 4%, transparent); border-color: color-mix(in srgb, var(--cyan) 30%, transparent); box-shadow: 0 0 16px color-mix(in srgb, var(--cyan) 10%, transparent), inset 0 0 20px color-mix(in srgb, var(--cyan) 3%, transparent); }
|
||
.no-layout-gap .resitem > * { margin-right: 12px; }
|
||
.resitem.focus { box-shadow: 0 0 0 2px var(--gold); border-color: transparent; }
|
||
.dot { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 2px; background: var(--gray); box-shadow: 0 0 6px rgba(93,111,138,.6); }
|
||
.dot.ok { background: var(--good); box-shadow: 0 0 7px rgba(57,255,143,.85), 0 0 14px rgba(57,255,143,.4); animation: dotPulse 2s ease-in-out infinite; }
|
||
.dot.bad { background: var(--bad); box-shadow: 0 0 7px rgba(255,56,100,.85); } .dot.locked { background: var(--warn); box-shadow: 0 0 7px rgba(255,225,86,.85); }
|
||
.dot.uncertain { background: #6f8aa5; } .dot.unsupported { background: #555048; box-shadow: none; }
|
||
@keyframes dotPulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
|
||
.resitem .ri { flex: 1 1 auto; min-width: 0; overflow: hidden; }
|
||
.resitem .ri .tags { display: flex; flex-wrap: wrap; gap: 5px; }
|
||
.no-layout-gap .resitem .ri .tags > * { margin: 0 5px 5px 0; }
|
||
/* ── 赛博标签卡片:每类标签独立颜色方块,带斜切角和霓虹边光 ── */
|
||
.btag {
|
||
font-family: var(--mono); font-size: 10px; font-weight: 800; line-height: 1;
|
||
padding: 3px 8px; white-space: nowrap; letter-spacing: 1px; text-transform: uppercase;
|
||
position: relative; display: inline-flex; align-items: center;
|
||
border-radius: 0;
|
||
/* 默认:青色 */
|
||
color: var(--cyan); background: color-mix(in srgb, var(--cyan) 7%, transparent);
|
||
border: 1px solid color-mix(in srgb, var(--cyan) 30%, transparent);
|
||
box-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 12%, transparent), inset 0 0 4px color-mix(in srgb, var(--cyan) 4%, transparent);
|
||
clip-path: polygon(4px 0%, 100% 0%, calc(100% - 4px) 100%, 0% 100%);
|
||
}
|
||
/* 网盘类型:品红/金 */
|
||
.btag.disk {
|
||
color: var(--gold); background: rgba(var(--gold-rgb),.08);
|
||
border-color: rgba(var(--gold-rgb),.5); font-weight: 900;
|
||
box-shadow: 0 0 8px rgba(var(--gold-rgb),.2), inset 0 0 6px rgba(var(--gold-rgb),.06);
|
||
text-shadow: 0 0 8px rgba(var(--gold-rgb),.5);
|
||
clip-path: polygon(4px 0%, 100% 0%, calc(100% - 4px) 100%, 0% 100%);
|
||
}
|
||
/* 提取码:紫色 */
|
||
.btag.pw {
|
||
color: #c9a6ff; background: rgba(201,166,255,.07);
|
||
border-color: rgba(201,166,255,.4);
|
||
box-shadow: 0 0 6px rgba(201,166,255,.15);
|
||
clip-path: polygon(4px 0%, 100% 0%, calc(100% - 4px) 100%, 0% 100%);
|
||
}
|
||
/* 画质类标签独立配色 */
|
||
.btag[class*="4K"],.btag:not(.disk):not(.pw)[data-b="4K"] { color: var(--cyan); border-color: color-mix(in srgb, var(--cyan) 50%, transparent); background: color-mix(in srgb, var(--cyan) 10%, transparent); box-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 20%, transparent); }
|
||
.btag-4k { color: var(--cyan); border-color: color-mix(in srgb, var(--cyan) 50%, transparent); background: color-mix(in srgb, var(--cyan) 10%, transparent); box-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 20%, transparent); }
|
||
.btag-hdr { color: #c9a6ff; border-color: rgba(201,166,255,.45); background: rgba(201,166,255,.07); box-shadow: 0 0 6px rgba(201,166,255,.15); }
|
||
.btag-dv { color: #ff9d4d; border-color: rgba(255,157,77,.45); background: rgba(255,157,77,.07); box-shadow: 0 0 6px rgba(255,157,77,.15); }
|
||
.btag-atmos { color: #39ff8f; border-color: rgba(57,255,143,.4); background: rgba(57,255,143,.06); box-shadow: 0 0 6px rgba(57,255,143,.12); }
|
||
.btag-cn { color: #ffe156; border-color: rgba(255,225,86,.4); background: rgba(255,225,86,.06); box-shadow: 0 0 5px rgba(255,225,86,.12); }
|
||
.btag-1080 { color: #8fb6c9; border-color: rgba(143,182,201,.4); background: rgba(143,182,201,.06); }
|
||
.btag-remux { color: var(--gold); border-color: rgba(var(--gold-rgb),.5); background: rgba(var(--gold-rgb),.08); box-shadow: 0 0 6px rgba(var(--gold-rgb),.15); font-weight: 900; }
|
||
.btag-web { color: rgba(143,182,201,.7); border-color: rgba(143,182,201,.25); background: rgba(143,182,201,.04); }
|
||
.btag-h265 { color: #7af7ff; border-color: rgba(122,247,255,.3); background: rgba(122,247,255,.05); }
|
||
.btag-h264 { color: color-mix(in srgb, var(--cyan) 40%, transparent); border-color: color-mix(in srgb, var(--cyan) 18%, transparent); background: color-mix(in srgb, var(--cyan) 3%, transparent); }
|
||
.btag-720 { color: #5d6f8a; border-color: rgba(93,111,138,.3); background: rgba(93,111,138,.05); }
|
||
.btag-hibit { color: #ff6ad5; border-color: rgba(255,106,213,.45); background: rgba(255,106,213,.07); box-shadow: 0 0 6px rgba(255,106,213,.15); font-weight: 900; }
|
||
.resitem .size { flex: 0 0 auto; font-family: var(--mono); font-size: 12px; font-weight: 800; color: var(--cyan); background: color-mix(in srgb, var(--cyan) 6%, transparent); border: 1px solid color-mix(in srgb, var(--cyan) 25%, transparent); padding: 3px 8px; border-radius: 0; white-space: nowrap; letter-spacing: .5px; text-shadow: 0 0 8px color-mix(in srgb, var(--cyan) 50%, transparent); }
|
||
.resitem .ops { flex: 0 0 auto; display: flex; gap: 6px; }
|
||
.no-layout-gap .resitem .ops > * { margin-right: 6px; }
|
||
.resitem .op { font-family: var(--mono); font-size: 11px; font-weight: 800; letter-spacing: 1.5px; text-transform: uppercase; border-radius: 2px; height: 30px; padding: 0 12px; border: 1px solid color-mix(in srgb, var(--cyan) 20%, transparent); background: transparent; color: color-mix(in srgb, var(--cyan) 60%, transparent); transition: all .15s var(--ease); }
|
||
.resitem .op:hover { color: var(--cyan); border-color: color-mix(in srgb, var(--cyan) 50%, transparent); background: color-mix(in srgb, var(--cyan) 6%, transparent); box-shadow: 0 0 10px color-mix(in srgb, var(--cyan) 15%, transparent); }
|
||
.resitem .op:active { transform: scale(.95); }
|
||
.resitem .op.play {
|
||
background: linear-gradient(135deg, rgba(var(--gold-rgb),.14) 0%, rgba(var(--gold-rgb),.04) 100%);
|
||
border: 1px solid var(--gold);
|
||
color: var(--gold); font-weight: 900; letter-spacing: 2px;
|
||
box-shadow: 0 0 12px rgba(var(--gold-rgb),.3), inset 0 0 10px rgba(var(--gold-rgb),.06);
|
||
text-shadow: 0 0 10px rgba(var(--gold-rgb),.7);
|
||
clip-path: polygon(6px 0%, 100% 0%, calc(100% - 6px) 100%, 0% 100%);
|
||
position: relative; overflow: hidden;
|
||
}
|
||
.resitem .op.play::before {
|
||
content: ""; position: absolute; inset: 0;
|
||
background: linear-gradient(90deg, transparent, rgba(var(--gold-rgb),.12), transparent);
|
||
left: -100%; transition: left .45s var(--ease);
|
||
}
|
||
.resitem .op.play:hover { background: rgba(var(--gold-rgb),.2); border-color: var(--gold-lite); box-shadow: 0 0 22px rgba(var(--gold-rgb),.5), inset 0 0 16px rgba(var(--gold-rgb),.08); color: var(--gold-lite); }
|
||
.resitem .op.play:hover::before { left: 100%; }
|
||
.resitem .op.play:active { background: rgba(var(--gold-rgb),.25); }
|
||
.resmore { width: 100%; height: 36px; margin: 2px 0 4px; border: 1px dashed color-mix(in srgb, var(--cyan) 20%, transparent); background: transparent; color: color-mix(in srgb, var(--cyan) 40%, transparent); border-radius: 2px; font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; transition: color .2s, border-color .2s, box-shadow .2s; }
|
||
.resmore:hover { color: var(--cyan); border-color: color-mix(in srgb, var(--cyan) 40%, transparent); box-shadow: 0 0 10px color-mix(in srgb, var(--cyan) 10%, transparent); }
|
||
.resmore:active { color: var(--cyan); }
|
||
.resmore:focus { outline: 0; }
|
||
.resmore.focus { box-shadow: 0 0 0 1px var(--cyan); border-color: transparent; }
|
||
.resitem .op:focus { outline: 0; }
|
||
.resitem .op.focus { box-shadow: 0 0 0 2px var(--cyan); }
|
||
|
||
/* 提示条 */
|
||
.toast { position: fixed; left: 50%; bottom: calc(var(--safe-bottom) + 26px); transform: translateX(-50%); z-index: 90; background: rgba(2,6,14,.95); border: 1px solid color-mix(in srgb, var(--cyan) 40%, transparent); border-left: 3px solid var(--cyan); color: var(--cyan); font-size: 12px; font-family: var(--mono); letter-spacing: 1px; padding: 10px 18px; border-radius: 0; box-shadow: 0 0 20px color-mix(in srgb, var(--cyan) 20%, transparent), 0 8px 28px rgba(0,0,0,.6); opacity: 0; pointer-events: none; transition: opacity .22s var(--ease); max-width: 80%; }
|
||
.toast.show { opacity: 1; }
|
||
|
||
/* 剧照/海报大图查看器 */
|
||
.imgviewer { position: fixed; top: 0; left: 0; right: 0; bottom: 0; inset: 0; z-index: 95; background: rgba(3,2,8,.96); display: none; align-items: center; justify-content: center; padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left); }
|
||
.imgviewer.show { display: flex; }
|
||
.imgviewer .ivstage { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||
.imgviewer img { max-width: 92vw; max-height: 88vh; object-fit: contain; box-shadow: 0 0 40px rgba(0,0,0,.6); }
|
||
.imgviewer .ivclose { position: absolute; top: calc(var(--safe-top) + 14px); right: calc(var(--safe-right) + 14px); width: 36px; height: 36px; border-radius: 50%; border: 1px solid color-mix(in srgb, var(--cyan) 40%, transparent); background: rgba(2,6,14,.7); color: var(--ink); display: flex; align-items: center; justify-content: center; z-index: 2; }
|
||
.imgviewer .ivclose svg { width: 16px; height: 16px; fill: currentColor; }
|
||
.imgviewer .ivnav { position: absolute; top: 50%; transform: translateY(-50%); width: 42px; height: 42px; border-radius: 50%; border: 1px solid color-mix(in srgb, var(--cyan) 30%, transparent); background: rgba(2,6,14,.6); color: var(--ink); display: flex; align-items: center; justify-content: center; z-index: 2; }
|
||
.imgviewer .ivnav svg { width: 18px; height: 18px; fill: currentColor; }
|
||
.imgviewer .ivprev { left: calc(var(--safe-left) + 10px); }
|
||
.imgviewer .ivnext { right: calc(var(--safe-right) + 10px); }
|
||
.imgviewer .ivcount { position: absolute; bottom: calc(var(--safe-bottom) + 14px); left: 50%; transform: translateX(-50%); font-family: var(--mono); font-size: 11px; letter-spacing: 1px; color: rgba(255,255,255,.6); z-index: 2; }
|
||
.tv-mode .imgviewer .ivnav, .tv-mode .imgviewer .ivclose { display: none; }
|
||
.hero { cursor: pointer; }
|
||
.stills .s { cursor: pointer; }
|
||
|
||
/* 回到顶部悬浮按钮:滚动一段后淡入,点按平滑回顶;详情页内隐藏 */
|
||
.backtop { position: fixed; right: 16px; bottom: calc(var(--safe-bottom) + 78px); z-index: 88; width: 42px; height: 42px; border-radius: 2px; border: 1px solid color-mix(in srgb, var(--cyan) 35%, transparent); background: rgba(4,8,14,.85); color: var(--cyan); font-size: 18px; font-weight: 800; line-height: 1; font-family: var(--mono); box-shadow: 0 0 12px color-mix(in srgb, var(--cyan) 15%, transparent), inset 0 0 8px color-mix(in srgb, var(--cyan) 3%, transparent); opacity: 0; transform: translateY(12px) scale(.9); pointer-events: none; transition: opacity .24s var(--ease), transform .24s var(--ease); }
|
||
.backtop.show { opacity: 1; transform: none; pointer-events: auto; }
|
||
.backtop:hover { border-color: var(--cyan); box-shadow: 0 0 20px color-mix(in srgb, var(--cyan) 30%, transparent); }
|
||
.backtop:active { transform: scale(.9); }
|
||
body.detail-active .backtop { opacity: 0 !important; pointer-events: none !important; }
|
||
.tv-mode .backtop { display: none; }
|
||
/* 随机一部:常驻右下角,点一下从当前列表随便打开一部 */
|
||
.dice { position: fixed; right: 16px; bottom: calc(var(--safe-bottom) + 22px); z-index: 88; width: 42px; height: 42px; border-radius: 2px; border: 1px solid rgba(var(--gold-rgb),.35); background: rgba(8,4,14,.85); font-size: 20px; line-height: 1; box-shadow: 0 0 12px rgba(var(--gold-rgb),.15), inset 0 0 8px rgba(var(--gold-rgb),.03); transition: transform .25s var(--ease), opacity .22s var(--ease), box-shadow .2s; }
|
||
.dice:hover { border-color: var(--gold); box-shadow: 0 0 20px rgba(var(--gold-rgb),.3); }
|
||
.dice:active { transform: scale(.88) rotate(-20deg); }
|
||
body.detail-active .dice { opacity: 0; pointer-events: none; }
|
||
.tv-mode .dice { display: none; }
|
||
/* 卡片按压反馈:轻微下沉,触感更明确 */
|
||
.card:active { transform: scale(.975); }
|
||
/* 猜你喜欢:横向单排,卡片尺寸/字体与演员关联作品共用 .fg-cell 规则(见上方 .filmogrid-inline, .reco) */
|
||
.no-layout-gap .filmogrid-inline > *, .no-layout-gap .reco > * { margin-right: var(--gut); }
|
||
|
||
/* ---------------- TV 大屏 ---------------- */
|
||
.tv-mode { font-size: 17px; }
|
||
.tv-mode .wrap { max-width: none; padding: 0 52px; }
|
||
.tv-mode .app { padding-top: 10px; padding-bottom: 27px; }
|
||
.tv-mode .brand b { font-size: 26px; }
|
||
.tv-mode .section-head h2 { font-size: 23px; }
|
||
@supports (display: grid) { .tv-mode .grid { grid-template-columns: repeat(6, 1fr); } }
|
||
.tv-mode.no-layout-gap .grid > .card { width: calc(16.6% - var(--gut)); }
|
||
.tv-mode .card .meta .t { font-size: 18px; }
|
||
.tv-mode .searchbox input { font-size: 18px; }
|
||
/* TV 关闭动效,优先稳定帧率 */
|
||
.tv-mode .card, .tv-mode .actbtn, .tv-mode .op, .tv-mode .navtab, .tv-mode .skel, .tv-mode .iconbtn { transition: none; animation: none; }
|
||
.tv-mode .card.focus { transform: none; }
|
||
|
||
/* 降级:去掉 calc 之外不需要的;no-css-functions 不影响本表(未用 clamp/min/max) */
|
||
.no-css-functions .app { padding-left: 0; padding-right: 0; }
|
||
|
||
/* 尊重系统“减少动态效果” */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
* { animation: none !important; transition: none !important; }
|
||
.card:active, .actbtn:active, .op:active, .iconbtn:active, .card.focus { transform: none !important; }
|
||
}
|
||
|
||
/* ── 赛博朋克增强 ── */
|
||
.card:hover { box-shadow: 0 0 0 1px color-mix(in srgb, var(--cyan) 35%, transparent), 0 0 18px color-mix(in srgb, var(--cyan) 15%, transparent), 0 6px 24px rgba(0,0,0,.5); }
|
||
.qtag { text-shadow: 0 0 8px currentColor; }
|
||
.blk-title { text-shadow: 0 0 12px color-mix(in srgb, var(--cyan) 60%, transparent), 0 0 28px color-mix(in srgb, var(--cyan) 25%, transparent); }
|
||
.navtab:hover { color: var(--cyan); text-shadow: 0 0 10px color-mix(in srgb, var(--cyan) 80%, transparent); border-bottom-color: color-mix(in srgb, var(--cyan) 40%, transparent); }
|
||
.notice { box-shadow: inset 4px 0 14px rgba(var(--gold-rgb),.06), 0 0 18px rgba(var(--gold-rgb),.04); }
|
||
.fpill.on { box-shadow: 0 0 8px rgba(var(--gold-rgb),.4), inset 0 0 6px rgba(var(--gold-rgb),.08); }
|
||
@keyframes skelGlow { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
|
||
.skel { background: linear-gradient(90deg, rgba(255,255,255,.04) 25%, rgba(0,240,255,.07) 50%, rgba(255,255,255,.04) 75%) !important; background-size: 200% 100% !important; animation: skelGlow 2.2s linear infinite !important; }
|
||
.searchbox:focus-within::after { content: ""; position: absolute; top: -1px; left: 10%; right: 10%; height: 1px; background: var(--cyan); box-shadow: 0 0 8px var(--cyan); pointer-events: none; }
|
||
</style></head><body><!-- 赛博进度条 --><div id="cybar"><div id="cybar-segs"></div><div id="cybar-track"></div><div id="cybar-cursor"></div><div id="cybar-label">LOADING</div></div><div class="app" id="app"><header class="topbar"><div class="wrap"><a class="brand" href="#home" id="brand"><span class="logo" id="themeBtn" title="点击切换主题色">搜</span><b>片库</b></a><div class="searchbox"><svg viewbox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 1 0-.7.7l.27.28v.79l5 5 1.49-1.49-5-5zm-6 0A4.5 4.5 0 1 1 14 9.5 4.5 4.5 0 0 1 9.5 14z" /></svg><input id="q" type="search" inputmode="search" placeholder="" autocomplete="off" spellcheck="false"><button class="go focusable" id="goBtn" data-fk="go">搜索</button><div class="suggest" id="suggest"></div></div></div></header><main class="wrap" id="home"><div class="home-status-bar"><span class="home-status-indicator"></span><span class="home-status-text" id="topbarStatus">NET_LINK::ACTIVE</span><span class="home-status-right">片库</span></div><div class="notice" id="notice"><span class="ic">!</span><span>为方便观看尽量提供网盘资源,没有 4K 的也会提供蓝光原盘版本,保证清晰度。</span><button class="x focusable" id="noticeX" data-fk="noticex" aria-label="关闭">×</button></div><nav class="nav" id="nav"></nav><section><div class="section-head"><h2 id="listTitle">TG 4K Movies</h2><button class="ftoggle focusable" id="ftoggle" data-fk="ftoggle" aria-label="展开或收起筛选"><span>筛选</span><span class="ftoggle-count hidden" id="ftoggleCount"></span><svg class="ftoggle-chev" viewbox="0 0 24 24"><path d="M7 10l5 5 5-5z" /></svg></button></div><div class="filters" id="filters"></div><div class="grid" id="grid"></div><div id="listState"></div></section></main></div><!-- 详情 --><div class="sheet" id="sheet" aria-hidden="true"><div class="hero"><div class="hero-bg-blur" id="heroBgBlur"></div><div class="hero-bg" id="heroBgMain"></div><div class="hero-bg hero-bg-next" id="heroBgNext"></div><div class="heroglow"></div><div class="herofg"><div class="hero-logo hidden" id="dLogo"></div><h1 id="dTitle">—</h1><div class="chips" id="dChips"></div></div></div><div class="body"><div class="actions"><button class="actbtn primary focusable" id="actPlay" data-fk="act-play"><svg viewbox="0 0 24 24" style="width:13px;height:13px;fill:currentColor;flex-shrink:0"><path d="M8 5v14l11-7z" /></svg>PLAY ▸</button></div><div class="infobar" id="infoBar"><div class="drating" id="dRating"></div><div class="metablk" id="metablk"></div></div><div class="blk-title">剧情简介</div><div class="desc-box"><div class="desc clamp" id="dDesc"></div><button class="desc-toggle hidden" id="descToggle" type="button">展开</button></div><div id="castWrap" class="hidden"><div class="blk-title">演员阵容</div><div class="castrow" id="cast"></div></div><div class="blk-title" id="resTitle">在线资源</div><div id="resState"></div><div id="resgroups"></div><div id="stillsWrap" class="hidden"><div class="blk-title">影视剧照</div><div class="stills" id="stills"></div></div><div id="recoWrap" class="hidden"><div class="blk-title">猜你喜欢</div><div class="reco" id="reco"></div></div></div></div><div class="toast" id="toast"></div><button class="dice" id="diceBtn" aria-label="随便看看">🎲</button><button class="backtop" id="backTop" aria-label="回到顶部">↑</button><div class="imgviewer" id="imgViewer" aria-hidden="true"><div class="ivstage"><button class="ivnav ivprev" id="ivPrev" aria-label="上一张"><svg viewbox="0 0 24 24"><path d="M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z" /></svg></button><img id="ivImg" src="" alt=""><button class="ivnav ivnext" id="ivNext" aria-label="下一张"><svg viewbox="0 0 24 24"><path d="M8.59 16.59 10 18l6-6-6-6-1.41 1.41L13.17 12z" /></svg></button><button class="ivclose" id="ivClose" aria-label="关闭"><svg viewbox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 7 19l5.59-5.59L17.59 19 19 17.59 13.41 12z" /></svg></button><div class="ivcount" id="ivCount"></div></div></div><script>
|
||
/* ============================================================
|
||
片库 WebHome —— 业务代码(ES2017 基线:禁止 ?. ?? ||= &&= ??= 等)
|
||
通过 fm SDK 工作:fm.req 取数据、fm.res 加载图片、fm.pan.play 推送播放、
|
||
fm.pan.check 检网盘有效性、fm.search 聚合搜索、fm.cache 持久化
|
||
============================================================ */
|
||
"use strict";
|
||
|
||
var CONFIG = {
|
||
/* 可选:片库 风格目录 JSON 接口。留空用内置演示数据,首屏仍可渲染。
|
||
约定:?cat=分类key -> { list:[ {id,title,en,year,genre,country,director,cast,imdb,quality,remark,poster,backdrop,desc,stills:[],resources:[{tier,name,size,url,type,password}]} ] } */
|
||
catalogApi: "",
|
||
/* 接口走 api.tmdb.org(同 nostr-emby 文档方案):国内大部分地区不需要代理即可直连,
|
||
比经典域名 api.themoviedb.org 稳定得多(后者常被污染)。图片走 image.tmdb.org,同样直连不经代理;
|
||
尺寸分级参考 emby:网格小图按角色封顶到较高档位,主视觉图给最高档位 */
|
||
tmdb: { apiKey: "d7040155454e7fdf547c4d889ebbcca7", lang: "zh-CN", apiBase: "https://api.tmdb.org/3", imageBase: "https://images.tmdb.org/t/p/", listSize: "w780", heroSize: "original", stillSize: "original", castSize: "w185" },
|
||
imageHeaders: {},
|
||
pansou: { apiBase: "https://so.252035.xyz", diskTypes: ["quark","aliyun","baidu","uc","tianyi","xunlei","123","115","mobile"] }
|
||
};
|
||
|
||
var DISK_LABEL = { quark:"夸克", aliyun:"阿里", baidu:"百度", uc:"UC", tianyi:"天翼", xunlei:"迅雷", "123":"123云盘", "115":"115", mobile:"移动云", magnet:"磁力", ed2k:"电驴", thunder:"迅雷链", others:"其它" };
|
||
|
||
var COUNTRY = { US:"美国", CN:"中国大陆", HK:"中国香港", TW:"中国台湾", JP:"日本", KR:"韩国", GB:"英国", FR:"法国", DE:"德国", IT:"意大利", ES:"西班牙", IN:"印度", TH:"泰国", RU:"俄罗斯", CA:"加拿大", AU:"澳大利亚", SE:"瑞典", DK:"丹麦", NO:"挪威", BR:"巴西", IE:"爱尔兰", NZ:"新西兰", BE:"比利时", NL:"荷兰", MX:"墨西哥" };
|
||
|
||
/* 分类即查询定义:name 显示名,其余字段驱动 TMDB discover
|
||
tv:剧集 genres:类型(多类型取并集 OR)
|
||
sort:"vote" 走高分排序 minVotes/maxVotes:票数门槛 yearLte:年代上限
|
||
originCC:制片地区默认值(国家码数组,OR 取并集),仅在用户未手选地区时生效;
|
||
该分类同时会令「地区」筛选只保留 originCC 覆盖到的选项,不允许选到其它地区
|
||
注:地区/语言与单一类型已由「地区/类型」筛选统一负责,导航不放任何涉及单一地区或单类型的项,
|
||
只保留筛选选不出来的精选合集(高分/冷门/老片/华语)与冷门类型(战争/西部/音乐/纪录)及剧集入口 */
|
||
var CATS = [
|
||
{ key:"new", name:"最新热门", recent:true },
|
||
{ key:"huayu", name:"华语佳片", originCC:["CN","HK","TW"], airSort:true },
|
||
{ key:"huayu_tv", name:"华语热播", tv:true, originCC:["CN","HK","TW"], recent:true, airSort:true },
|
||
{ key:"top250", name:"高分经典", sort:"vote", minVotes:3000 },
|
||
{ key:"jingdian", name:"殿堂老片", sort:"vote", minVotes:1500, yearLte:"2010-12-31" },
|
||
{ key:"lengmen", name:"冷门佳片", sort:"vote", minVotes:80, maxVotes:1200 },
|
||
{ key:"zhanzheng", name:"战争史诗", genres:[10752,36] },
|
||
{ key:"xibu", name:"西部片", genres:[37] },
|
||
{ key:"yinyue", name:"音乐歌舞", genres:[10402] },
|
||
{ key:"jilu", name:"纪录片", genres:[99] },
|
||
{ key:"ju", name:"热门剧集", tv:true }
|
||
];
|
||
function catDef(key) { for (var i = 0; i < CATS.length; i++) if (CATS[i].key === key) return CATS[i]; return {}; }
|
||
|
||
var IS_NATIVE = !!(window.fongmiBridge || window.fm || window.fongmi);
|
||
var CLIENT = window.fongmiClient || {};
|
||
var IS_TV = !!CLIENT.isLeanback || document.documentElement.classList.contains("tv-mode");
|
||
|
||
function fmReady() {
|
||
return new Promise(function (resolve) {
|
||
if (window.fm) return resolve(window.fm);
|
||
if (!IS_NATIVE) return resolve(null);
|
||
var done = false;
|
||
function ok() { if (done) return; done = true; resolve(window.fm || null); }
|
||
window.addEventListener("fmsdk", ok);
|
||
var t = 0, iv = setInterval(function () { t += 60; if (window.fm || t > 4000) { clearInterval(iv); ok(); } }, 60);
|
||
});
|
||
}
|
||
|
||
var Store = {
|
||
get: function (k) { return fmReady().then(function (fm) {
|
||
if (fm && fm.cache) return fm.cache.get(k, "片库");
|
||
try { return localStorage.getItem("片库_" + k) || ""; } catch (e) { return ""; }
|
||
}).catch(function () { return ""; }); },
|
||
set: function (k, v) { return fmReady().then(function (fm) {
|
||
if (fm && fm.cache) return fm.cache.set(k, v, "片库");
|
||
try { localStorage.setItem("片库_" + k, v); } catch (e) {} return {};
|
||
}).catch(function () { return {}; }); }
|
||
};
|
||
|
||
function parseJSONLoose(txt) {
|
||
if (txt == null) return null;
|
||
if (typeof txt === "object") return txt;
|
||
var s = String(txt).replace(/^\uFEFF/, "").trim();
|
||
if (!s) return null;
|
||
try { return JSON.parse(s); } catch (e) {
|
||
var a = s.indexOf("{"), b = s.indexOf("[");
|
||
var start = (a === -1) ? b : (b === -1 ? a : Math.min(a, b));
|
||
var end = Math.max(s.lastIndexOf("}"), s.lastIndexOf("]"));
|
||
if (start !== -1 && end > start) { try { return JSON.parse(s.slice(start, end + 1)); } catch (e2) {} }
|
||
throw e;
|
||
}
|
||
}
|
||
function request(url, options) {
|
||
options = options || {};
|
||
return fmReady().then(function (fm) {
|
||
if (fm && fm.req) return fm.req(url, {
|
||
method: options.method || "GET", headers: options.headers || {}, body: options.body || "",
|
||
responseType: "text", timeout: options.timeout || 20
|
||
}).then(function (r) { if (!r || !r.ok) throw new Error((r && r.error) || ("HTTP " + (r ? r.status : "?"))); return parseJSONLoose(r.body); });
|
||
return fetch(url, { method: options.method || "GET", headers: options.headers || {}, body: options.body || undefined })
|
||
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.text(); })
|
||
.then(parseJSONLoose);
|
||
});
|
||
}
|
||
/* 直连加载(参考 nostr-emby):图片不走 fm.res/代理,直接 img.src;
|
||
失败时在 images.tmdb.org ↔ image.tmdb.org 之间换一次域名再重试 */
|
||
function altHost(url) {
|
||
if (!url) return url;
|
||
if (url.indexOf("://images.tmdb.org") !== -1) return url.replace("://images.tmdb.org", "://image.tmdb.org");
|
||
if (url.indexOf("://image.tmdb.org") !== -1) return url.replace("://image.tmdb.org", "://images.tmdb.org");
|
||
return url;
|
||
}
|
||
/* 兼容旧调用:直连返回原图地址 */
|
||
function resUrl(url) { return url || ""; }
|
||
function makeImg(url, altText) {
|
||
var img = el("img"); img.decoding = "async"; img.alt = altText || "";
|
||
img.loading = IS_TV ? "eager" : "lazy"; /* TV WebView 不支持 lazy,用 eager */
|
||
var tried = false;
|
||
img.onerror = function () { if (!tried && url) { tried = true; this.src = altHost(url); return; } this.style.display = "none"; };
|
||
img.src = url;
|
||
return img;
|
||
}
|
||
/* ── 海报轮播(参考 nostr-emby:token + 预加载 + crossfade) ── */
|
||
var _carousel = { urls: [], idx: 0, timer: null, token: 0, interval: 4500 };
|
||
|
||
function _carouselSetBg(url) {
|
||
var main = $("#heroBgMain"), blur = $("#heroBgBlur");
|
||
if (!main) return;
|
||
var val = url ? "url('" + url + "')" : "none";
|
||
main.style.backgroundImage = val;
|
||
if (blur) blur.style.backgroundImage = val;
|
||
}
|
||
|
||
function _carouselCrossfade(url) {
|
||
var main = $("#heroBgMain"), next = $("#heroBgNext"), blur = $("#heroBgBlur");
|
||
if (!main || !next) return;
|
||
/* next 层预先设好图,opacity 0 在下方 */
|
||
next.style.backgroundImage = "url('" + url + "')";
|
||
next.style.zIndex = "0";
|
||
next.style.opacity = "0";
|
||
next.style.transition = "none";
|
||
/* 下一帧把 next 推到上方并淡入,同时 main 淡出 */
|
||
requestAnimationFrame(function () {
|
||
requestAnimationFrame(function () {
|
||
next.style.zIndex = "2";
|
||
next.style.transition = "opacity .7s ease";
|
||
next.style.opacity = "1";
|
||
main.style.transition = "opacity .7s ease";
|
||
main.style.opacity = "0";
|
||
/* blur 层在动画结束后再换图,避免透出旧图时 blur 已经是新图 */
|
||
setTimeout(function () {
|
||
main.style.backgroundImage = "url('" + url + "')";
|
||
main.style.transition = "none";
|
||
main.style.opacity = "1";
|
||
next.style.transition = "none";
|
||
next.style.opacity = "0";
|
||
next.style.zIndex = "0";
|
||
if (blur) blur.style.backgroundImage = "url('" + url + "')";
|
||
}, 750);
|
||
});
|
||
});
|
||
}
|
||
|
||
function _carouselGo(token) {
|
||
if (token !== _carousel.token) return;
|
||
var images = _carousel.urls, len = images.length;
|
||
if (!len) return;
|
||
var start = _carousel.idx;
|
||
var next = (start + 1) % len;
|
||
_carouselTryLoad(next, 0, token);
|
||
}
|
||
|
||
function _carouselTryLoad(idx, attempts, token) {
|
||
if (token !== _carousel.token) return;
|
||
var images = _carousel.urls, len = images.length;
|
||
if (!images.length || attempts >= len) return;
|
||
var url = images[idx];
|
||
if (!url) { _carouselTryLoad((idx + 1) % len, attempts + 1, token); return; }
|
||
if (IS_TV) {
|
||
/* TV 端跳过预加载直接切换 */
|
||
_carousel.idx = idx;
|
||
_carouselCrossfade(url);
|
||
return;
|
||
}
|
||
var probe = new Image();
|
||
probe.onload = function () {
|
||
if (token !== _carousel.token) return;
|
||
_carousel.idx = idx;
|
||
_carouselCrossfade(url);
|
||
};
|
||
probe.onerror = function () {
|
||
_carouselTryLoad((idx + 1) % len, attempts + 1, token);
|
||
};
|
||
probe.src = url;
|
||
}
|
||
|
||
function _carouselStart() {
|
||
_carouselStop();
|
||
if (_carousel.urls.length < 2) return;
|
||
var token = _carousel.token;
|
||
_carousel.timer = setInterval(function () {
|
||
if (document.visibilityState === "hidden") return;
|
||
var sheet = $("#sheet"); if (!sheet || !sheet.classList.contains("active")) return;
|
||
_carouselGo(token);
|
||
}, _carousel.interval);
|
||
}
|
||
|
||
function _carouselStop() {
|
||
if (_carousel.timer) { clearInterval(_carousel.timer); _carousel.timer = null; }
|
||
_carousel.token += 1;
|
||
}
|
||
|
||
function setCarouselUrls(urls) {
|
||
_carouselStop();
|
||
_carousel.urls = urls || []; _carousel.idx = 0;
|
||
_carouselSetBg(_carousel.urls[0] || "");
|
||
_carouselStart();
|
||
}
|
||
|
||
/* TMDB 图片换尺寸(如 w342 -> w780),非 TMDB URL 原样返回 */
|
||
function tmdbResize(url, size) { if (!url) return url; return url.replace(/\/w\d+\//, "/" + size + "/").replace(/\/original\//, "/" + size + "/"); }
|
||
|
||
/* 把任意图片 URL 经 Canvas 模糊后返回 base64 data URI
|
||
radius: CSS blur px(默认 8);走 fm.res() 解决跨域 */
|
||
function blurImageToDataURL(url, radius) {
|
||
radius = radius || 8;
|
||
return new Promise(function (resolve) {
|
||
if (!url) { resolve(""); return; }
|
||
/* 尝试用 fm.res() 代理绕过跨域 */
|
||
var src = url;
|
||
try {
|
||
if (window.fm && window.fm.res) src = window.fm.res(url);
|
||
} catch (e) {}
|
||
var img = new Image();
|
||
img.crossOrigin = "anonymous";
|
||
img.onload = function () {
|
||
try {
|
||
var w = img.naturalWidth || img.width || 960;
|
||
var h = img.naturalHeight || img.height || 540;
|
||
/* 缩小绘制降低内存,blur 视觉上看不出差别 */
|
||
var scale = Math.min(1, 480 / Math.max(w, h));
|
||
var cw = Math.round(w * scale), ch = Math.round(h * scale);
|
||
var canvas = document.createElement("canvas");
|
||
canvas.width = cw; canvas.height = ch;
|
||
var ctx = canvas.getContext("2d");
|
||
ctx.filter = "blur(" + radius + "px)";
|
||
/* 多绘一圈防止边缘透明晕 */
|
||
var pad = radius * 2;
|
||
ctx.drawImage(img, -pad, -pad, cw + pad * 2, ch + pad * 2);
|
||
resolve(canvas.toDataURL("image/jpeg", 0.82));
|
||
} catch (e) { resolve(url); /* canvas 污染等异常:降级用原图 */ }
|
||
};
|
||
img.onerror = function () { resolve(url); };
|
||
img.src = src;
|
||
});
|
||
}
|
||
/* 竖屏?优先 matchMedia,回退宽高比较 */
|
||
function isPortrait() {
|
||
if (window.matchMedia) { try { return window.matchMedia("(orientation: portrait)").matches; } catch (e) {} }
|
||
return (window.innerHeight || 0) >= (window.innerWidth || 0);
|
||
}
|
||
/* 沉浸大图:竖屏用竖版海报(升清 original),横屏用横版剧照;缺图互相回退 */
|
||
function heroImageUrl(item) {
|
||
var poster = pick(item, "poster", ""), backdrop = pick(item, "backdrop", "");
|
||
var bigP = poster ? tmdbResize(poster, "original") : "";
|
||
var bigB = backdrop ? tmdbResize(backdrop, "original") : "";
|
||
return isPortrait() ? (bigP || bigB || "") : (bigB || bigP || "");
|
||
}
|
||
function applyHeroImage(item) {
|
||
if (!item) return;
|
||
/* 等待 stills 加载,不提前显示海报 */
|
||
setCarouselUrls([]);
|
||
}
|
||
function applyHeroCarousel(item, stills) {
|
||
if (!item) return;
|
||
/* 轮播只展示后续加入的 stills,不包含 poster/backdrop 海报图 */
|
||
var urls = [];
|
||
(stills || []).forEach(function (u) { if (urls.indexOf(u) < 0) urls.push(u); });
|
||
setCarouselUrls(urls);
|
||
}
|
||
|
||
function $(sel, root) { return (root || document).querySelector(sel); }
|
||
function el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }
|
||
function pick(o, k, d) { return (o && o[k] != null) ? o[k] : d; }
|
||
function num(v) { var n = parseFloat(v); return isNaN(n) ? 0 : n; }
|
||
function cssEsc(s) { return String(s).replace(/["\\]/g, "\\$&"); }
|
||
|
||
var toastTimer = null;
|
||
function toast(msg) { var t = $("#toast"); t.textContent = msg; t.classList.add("show"); if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(function () { t.classList.remove("show"); }, 2200); }
|
||
|
||
function copyText(text) {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(function () { toast("已复制链接"); }, function () { fallbackCopy(text); });
|
||
} else fallbackCopy(text);
|
||
}
|
||
function fallbackCopy(text) {
|
||
try {
|
||
var ta = el("textarea"); ta.value = text; ta.style.cssText = "position:fixed;opacity:0;"; document.body.appendChild(ta);
|
||
ta.select(); document.execCommand("copy"); document.body.removeChild(ta); toast("已复制链接");
|
||
} catch (e) { toast("复制失败,请手动选择"); }
|
||
}
|
||
|
||
function hueOf(s) { var h = 0, i; s = s || ""; for (i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360; return h; }
|
||
function posterInner(item) {
|
||
var poster = pick(item, "poster", "");
|
||
if (poster) return makeImg(poster, pick(item, "title", ""));
|
||
var h = hueOf(pick(item, "title", "")); var ph = el("div", "ph"); ph.setAttribute("data-ph", "1");
|
||
ph.style.background = "linear-gradient(150deg, hsl(" + h + ",18%,28%), hsl(" + ((h + 30) % 360) + ",16%,14%))";
|
||
ph.textContent = pick(item, "title", "影片"); return ph;
|
||
}
|
||
function qualityClass(q) { q = (q || "").toUpperCase();
|
||
if (q.indexOf("4K") !== -1 || q.indexOf("2160") !== -1) return "q4k";
|
||
if (q.indexOf("原盘") !== -1 || q.indexOf("BLURAY") !== -1 || q.indexOf("REMUX") !== -1 || q.indexOf("蓝光") !== -1) return "qbd";
|
||
if (q.indexOf("WEB") !== -1 || q.indexOf("HD") !== -1) return "qweb";
|
||
return "";
|
||
}
|
||
|
||
/* ---------- TMDB 自动海报/剧照(填 apiKey 才启用) ---------- */
|
||
function tmdbEnabled() { return !!(CONFIG.tmdb && CONFIG.tmdb.apiKey); }
|
||
function getArtwork(item) {
|
||
if (!tmdbEnabled()) {
|
||
/* 没有 TMDB,直接用数据源原图(可能低清) */
|
||
return Promise.resolve(pick(item, "poster", "") ? { poster: item.poster, backdrop: pick(item, "backdrop", ""), id: pick(item, "tmdbId", 0), type: pick(item, "tmdbType", "") } : null);
|
||
}
|
||
var ck = "art_" + pick(item, "title", "") + "_" + pick(item, "year", "");
|
||
return Store.get(ck).then(function (cached) {
|
||
if (cached) { try { var o = JSON.parse(cached); if (o && o.poster) return o; } catch (e) {} }
|
||
var t = CONFIG.tmdb;
|
||
var q = pick(item, "en", "") || pick(item, "title", "");
|
||
var url = (t.apiBase || "https://api.tmdb.org/3") + "/search/multi?api_key=" + encodeURIComponent(t.apiKey) +
|
||
"&language=" + encodeURIComponent(t.lang || "zh-CN") + "&query=" + encodeURIComponent(q);
|
||
return request(url, { timeout: 15 }).then(function (data) {
|
||
var arr = (data && data.results) ? data.results : [];
|
||
var hit = null, i; for (i = 0; i < arr.length; i++) { if (arr[i] && arr[i].poster_path) { hit = arr[i]; break; } }
|
||
if (!hit) return null;
|
||
var out = {
|
||
poster: t.imageBase + (t.listSize || "w780") + hit.poster_path,
|
||
backdrop: hit.backdrop_path ? (t.imageBase + (t.heroSize || "original") + hit.backdrop_path) : "",
|
||
id: hit.id || 0, type: hit.media_type || "movie"
|
||
};
|
||
Store.set(ck, JSON.stringify(out)); return out;
|
||
});
|
||
}).catch(function () { return null; });
|
||
}
|
||
function getStills(art) {
|
||
if (!tmdbEnabled() || !art || !art.id) return Promise.resolve([]);
|
||
var t = CONFIG.tmdb, type = art.type === "tv" ? "tv" : "movie";
|
||
var url = (t.apiBase || "https://api.tmdb.org/3") + "/" + type + "/" + art.id + "/images?api_key=" + encodeURIComponent(t.apiKey);
|
||
return request(url, { timeout: 15 }).then(function (data) {
|
||
var arr = (data && data.backdrops) ? data.backdrops : [];
|
||
/* 参考 Eclipse 选图策略:无文字水印的剧照优先(更干净),同档再按评分降序,最多取 6 张 */
|
||
arr = arr.filter(function (b) { return b && b.file_path; }).sort(function (a, b) {
|
||
var an = a.iso_639_1 ? 1 : 0, bn = b.iso_639_1 ? 1 : 0;
|
||
if (an !== bn) return an - bn;
|
||
return (b.vote_average || 0) - (a.vote_average || 0);
|
||
});
|
||
return arr.slice(0, 6).map(function (b) { return t.imageBase + (t.stillSize || "w500") + b.file_path; });
|
||
}).catch(function () { return []; });
|
||
}
|
||
/* TMDB 详情:类型 / 别名 / 国家 / 导演 / 时间 / 演员 / 简介 */
|
||
function getDetailMeta(art) {
|
||
if (!tmdbEnabled() || !art || !art.id) return Promise.resolve(null);
|
||
var t = CONFIG.tmdb, type = art.type === "tv" ? "tv" : "movie";
|
||
var ck = "meta_" + type + "_" + art.id;
|
||
return Store.get(ck).then(function (cached) {
|
||
if (cached) { try { var o = JSON.parse(cached); if (o) return o; } catch (e) {} }
|
||
var url = (t.apiBase || "https://api.tmdb.org/3") + "/" + type + "/" + art.id + "?api_key=" + encodeURIComponent(t.apiKey) +
|
||
"&language=" + encodeURIComponent(t.lang || "zh-CN") + "&append_to_response=credits,alternative_titles";
|
||
return request(url, { timeout: 15 }).then(function (d) {
|
||
if (!d) return null;
|
||
var genre = (d.genres || []).map(function (g) { return g.name; }).join(", ");
|
||
var date = type === "tv" ? (d.first_air_date || "") : (d.release_date || "");
|
||
var countries = [];
|
||
(d.origin_country || []).forEach(function (c) { countries.push(COUNTRY[c] || c); });
|
||
if (!countries.length && d.production_countries) d.production_countries.forEach(function (c) { countries.push(COUNTRY[c.iso_3166_1] || c.name); });
|
||
var credits = d.credits || {};
|
||
var director = "";
|
||
if (type === "tv") director = (d.created_by || []).map(function (p) { return p.name; }).join(" / ");
|
||
if (!director && credits.crew) director = credits.crew.filter(function (p) { return p.job === "Director"; }).slice(0, 2).map(function (p) { return p.name; }).join(" / ");
|
||
var cast = (credits.cast || []).slice(0, 8).map(function (p) { return p.name; }).join(" / ");
|
||
/* 头像获取方式:在参考文件 renderCast 的基础上加入导演(导演排在最前面),同样按 profile_path 过滤,没有真实头像的人直接不显示,不做任何兜底 */
|
||
var dirList = (credits.crew || []).filter(function (p) { return p.job === "Director" && p.profile_path; }).slice(0, 4).map(function (p) {
|
||
return { name: p.name, role: "导演", avatar: t.imageBase + (t.castSize || "w185") + p.profile_path, tmdbPersonId: p.id };
|
||
});
|
||
var castList = (credits.cast || []).filter(function (p) { return p.profile_path; }).slice(0, 20).map(function (p) {
|
||
return { name: p.name, role: p.character || "", avatar: t.imageBase + (t.castSize || "w185") + p.profile_path, tmdbPersonId: p.id };
|
||
});
|
||
castList = dirList.concat(castList);
|
||
var at = d.alternative_titles || {}; var titles = at.titles || at.results || []; var alts = [];
|
||
titles.forEach(function (x) { if (["CN", "TW", "HK"].indexOf(x.iso_3166_1) !== -1 && x.title) alts.push(x.title); });
|
||
var meta = { genre: genre, date: date, country: countries.join(" / "), director: director, cast: cast, castList: castList,
|
||
alias: alts.slice(0, 4).join(" / "), overview: d.overview || "", runtime: d.runtime || 0 };
|
||
Store.set(ck, JSON.stringify(meta));
|
||
return meta;
|
||
});
|
||
}).catch(function () { return null; });
|
||
}
|
||
function runPool(items, worker, concurrency) {
|
||
concurrency = concurrency || 4;
|
||
return new Promise(function (resolve) {
|
||
var idx = 0, active = 0, done = 0, n = items.length; if (!n) return resolve();
|
||
function next() { while (active < concurrency && idx < n) { var cur = items[idx++]; active++;
|
||
Promise.resolve(worker(cur))["finally"](function () { active--; done++; if (done >= n) resolve(); else next(); }); } }
|
||
next();
|
||
});
|
||
}
|
||
function patchCardPoster(item, art) {
|
||
if (!art || !art.poster) return;
|
||
item.poster = art.poster; if (art.backdrop) item.backdrop = art.backdrop;
|
||
var card = $("#grid .card[data-id=\"" + cssEsc(pick(item, "id", "")) + "\"]"); if (!card) return;
|
||
var poster = card.querySelector(".poster"); var ph = poster ? poster.querySelector("[data-ph]") : null;
|
||
if (poster && ph) { poster.replaceChild(makeImg(art.poster, pick(item, "title", "")), ph); }
|
||
}
|
||
function enrichPosters(items) {
|
||
if (!tmdbEnabled()) return;
|
||
var pending = (items || []).filter(function (it) { return !pick(it, "poster", ""); });
|
||
var myToken = STATE.reqToken;
|
||
runPool(pending, function (it) { return getArtwork(it).then(function (art) { if (STATE.reqToken !== myToken) return; patchCardPoster(it, art); }); }, 4);
|
||
}
|
||
|
||
/* ============================================================
|
||
内置演示数据(catalogApi 为空时用;片名取自 片库 公开收录,作占位演示)
|
||
============================================================ */
|
||
var DEMO = [
|
||
{ id:"1715", title:"造梦之家", en:"The Fabelmans", year:"2022", genre:"剧情, 家庭", country:"美国", director:"史蒂文·斯皮尔伯格", cast:"米歇尔·威廉姆斯 / 保罗·达诺", imdb:"7.5", quality:"4K原盘", remark:"原盘", cats:["jiating","blu"],
|
||
desc:"斯皮尔伯格的初心之作。少年萨姆爱上电影并尝试创作,多年后却透过镜头发现关于母亲的心碎真相。" },
|
||
{ id:"1503", title:"我们一起摇太阳", en:"Viva La Vida", year:"2024", genre:"剧情, 爱情", country:"中国大陆", director:"韩延", cast:"彭昱畅 / 李庚希", imdb:"6.8", quality:"4K", remark:"4K", cats:["huayu"],
|
||
desc:"两个身患重病的年轻人在至暗时刻相遇,彼此照亮,决定一起认真地活下去。" },
|
||
{ id:"1792", title:"邻里美好的一天", en:"A Beautiful Day in the Neighborhood", year:"2019", genre:"剧情, 传记", country:"美国", director:"玛丽埃尔·海勒", cast:"汤姆·汉克斯 / 马修·里斯", imdb:"7.2", quality:"1080P", remark:"蓝光", cats:["jiating","lengmen"],
|
||
desc:"愤世嫉俗的记者采访儿童节目主持人罗杰斯先生,在相处中被其善意慢慢改变。" },
|
||
{ id:"1163", title:"美少女特攻队", en:"Sucker Punch", year:"2011", genre:"动作, 奇幻", country:"美国", director:"扎克·施奈德", cast:"艾米莉·布朗宁 / 阿比·考尼什", imdb:"6.0", quality:"4K原盘", remark:"原盘", cats:["lengmen","blu"],
|
||
desc:"被送进精神病院的少女在想象世界里召集同伴,策划一场惊心动魄的越狱与反抗。" },
|
||
{ id:"2287", title:"玩尽杀绝", en:"Wild Things", year:"1998", genre:"惊悚, 悬疑", country:"美国", director:"约翰·麦克诺顿", cast:"凯文·贝肯 / 马特·狄龙", imdb:"6.5", quality:"1080P原盘", remark:"原盘", cats:["lengmen","blu"],
|
||
desc:"佛罗里达小镇一桩指控引出层层反转,谁是猎人谁是猎物始终扑朔迷离。" },
|
||
{ id:"3001", title:"侏罗纪公园", en:"Jurassic Park", year:"1993", genre:"科幻, 冒险", country:"美国", director:"史蒂文·斯皮尔伯格", cast:"萨姆·尼尔 / 劳拉·邓恩", imdb:"8.2", quality:"4K原盘", remark:"原盘", cats:["top250","blu"],
|
||
desc:"科学家用远古 DNA 复活恐龙建成主题公园,系统失控后游客陷入生死逃亡。" },
|
||
{ id:"3002", title:"千与千寻", en:"Spirited Away", year:"2001", genre:"动画, 奇幻", country:"日本", director:"宫崎骏", cast:"柊瑠美 / 入野自由", imdb:"8.6", quality:"4K", remark:"4K", cats:["donghua","top250"],
|
||
desc:"少女千寻误入神灵世界,父母被变成猪,她在汤屋打工求生,找回名字与归途。" },
|
||
{ id:"4101", title:"周处除三害", en:"The Pig the Snake and the Pigeon", year:"2023", genre:"动作, 犯罪", country:"中国台湾", director:"黄精甫", cast:"阮经天 / 袁富华", imdb:"7.4", quality:"4K原盘", remark:"原盘", cats:["huayu","blu"],
|
||
desc:"绝症通缉犯决意在死前除掉两名比自己更恶的通缉要犯,以求被人记住。" },
|
||
{ id:"4102", title:"满江红", en:"Full River Red", year:"2023", genre:"剧情, 悬疑", country:"中国大陆", director:"张艺谋", cast:"沈腾 / 易烊千玺", imdb:"6.5", quality:"4K", remark:"4K", cats:["huayu"],
|
||
desc:"南宋年间,一桩命案牵出宰相府的惊天密谋,一夜之间众人性命交织。" },
|
||
{ id:"4103", title:"流浪地球2", en:"The Wandering Earth II", year:"2023", genre:"科幻, 灾难", country:"中国大陆", director:"郭帆", cast:"吴京 / 刘德华", imdb:"8.0", quality:"4K原盘", remark:"原盘", cats:["huayu","blu"],
|
||
desc:"太阳危机迫近,人类启动“移山计划”推动地球远航,危机与牺牲接踵而至。" },
|
||
{ id:"4104", title:"封神第一部", en:"Creation of the Gods I", year:"2023", genre:"奇幻, 古装", country:"中国大陆", director:"乌尔善", cast:"费翔 / 李雪健", imdb:"7.0", quality:"4K", remark:"4K", cats:["huayu"],
|
||
desc:"商王殷寿与妖狐妲己祸乱朝纲,姬发逐渐看清真相,踏上反抗暴政之路。" },
|
||
{ id:"4105", title:"热辣滚烫", en:"YOLO", year:"2024", genre:"剧情, 喜剧", country:"中国大陆", director:"贾玲", cast:"贾玲 / 雷佳音", imdb:"6.6", quality:"4K", remark:"4K", cats:["huayu"],
|
||
desc:"宅家多年的女孩在一次次跌落后接触拳击,慢慢学会为自己活一次。" },
|
||
{ id:"4106", title:"长安三万里", en:"Chang An", year:"2023", genre:"动画, 历史", country:"中国大陆", director:"谢君伟 / 邹靖", cast:"配音 / 杨天翔", imdb:"7.6", quality:"4K", remark:"4K", cats:["huayu","donghua"],
|
||
desc:"暮年高适回望与李白的一生交集,盛唐群星与诗篇在烽烟中徐徐展开。" },
|
||
{ id:"4201", title:"寻梦环游记", en:"Coco", year:"2017", genre:"动画, 家庭", country:"美国", director:"李·昂克里奇", cast:"安东尼·冈萨雷斯", imdb:"8.4", quality:"4K原盘", remark:"原盘", cats:["donghua","jiating","blu"],
|
||
desc:"热爱音乐的男孩误入亡灵世界,追寻家族秘密,也读懂了记忆与爱的意义。" },
|
||
{ id:"4202", title:"你的名字", en:"Your Name", year:"2016", genre:"动画, 爱情", country:"日本", director:"新海诚", cast:"神木隆之介 / 上白石萌音", imdb:"8.4", quality:"4K", remark:"4K", cats:["donghua"],
|
||
desc:"素未谋面的少年少女在梦中互换身体,跨越时间与距离追寻彼此。" },
|
||
{ id:"4203", title:"蜘蛛侠:纵横宇宙", en:"Spider-Man: Across the Spider-Verse", year:"2023", genre:"动画, 动作", country:"美国", director:"杰昆·多斯桑托斯", cast:"沙梅克·摩尔", imdb:"8.5", quality:"4K原盘", remark:"原盘", cats:["donghua","blu"],
|
||
desc:"迈尔斯穿梭多元宇宙,与无数蜘蛛侠相遇,也面对关于命运的抉择。" },
|
||
{ id:"4204", title:"疯狂动物城", en:"Zootopia", year:"2016", genre:"动画, 喜剧", country:"美国", director:"拜伦·霍华德", cast:"金妮弗·古德温", imdb:"8.0", quality:"4K", remark:"4K", cats:["donghua"],
|
||
desc:"兔子警官与狐狸骗子联手破案,揭开动物乌托邦背后的偏见与阴谋。" },
|
||
{ id:"4205", title:"心灵奇旅", en:"Soul", year:"2020", genre:"动画, 奇幻", country:"美国", director:"彼特·道格特", cast:"杰米·福克斯", imdb:"8.0", quality:"4K", remark:"4K", cats:["donghua"],
|
||
desc:"中学音乐老师意外坠入灵魂世界,重新思考活着的意义与生活的火花。" },
|
||
{ id:"4301", title:"当幸福来敲门", en:"The Pursuit of Happyness", year:"2006", genre:"剧情, 传记", country:"美国", director:"加布里埃尔·穆奇诺", cast:"威尔·史密斯", imdb:"8.0", quality:"1080P", remark:"蓝光", cats:["jiating"],
|
||
desc:"濒临破产的父亲带着儿子在困顿中坚持,最终凭努力赢得人生转机。" },
|
||
{ id:"4302", title:"怦然心动", en:"Flipped", year:"2010", genre:"爱情, 喜剧", country:"美国", director:"罗伯·莱纳", cast:"玛德琳·卡罗尔", imdb:"7.7", quality:"1080P", remark:"蓝光", cats:["jiating","lengmen"],
|
||
desc:"少男少女从童年到青春的双视角初恋,关于成长与看见彼此的故事。" },
|
||
{ id:"4303", title:"阿甘正传", en:"Forrest Gump", year:"1994", genre:"剧情", country:"美国", director:"罗伯特·泽米吉斯", cast:"汤姆·汉克斯", imdb:"8.8", quality:"4K原盘", remark:"原盘", cats:["top250","jiating","blu"],
|
||
desc:"智商不高却心地纯善的阿甘,用奔跑的一生见证了美国几十年的变迁。" },
|
||
{ id:"4401", title:"三块广告牌", en:"Three Billboards Outside Ebbing, Missouri", year:"2017", genre:"剧情, 犯罪", country:"美国", director:"马丁·麦克唐纳", cast:"弗兰西斯·麦克多蒙德", imdb:"8.1", quality:"1080P", remark:"蓝光", cats:["lengmen"],
|
||
desc:"母亲为女儿命案立起三块广告牌质问警方,引爆小镇的愤怒与悲悯。" },
|
||
{ id:"4402", title:"海边的曼彻斯特", en:"Manchester by the Sea", year:"2016", genre:"剧情", country:"美国", director:"肯尼斯·罗纳根", cast:"卡西·阿弗莱克", imdb:"7.8", quality:"1080P", remark:"蓝光", cats:["lengmen"],
|
||
desc:"沉默寡言的修理工被迫回乡照顾侄子,旧日伤痛在海风中无声翻涌。" },
|
||
{ id:"4403", title:"时空恋旅人", en:"About Time", year:"2013", genre:"爱情, 奇幻", country:"英国", director:"理查德·柯蒂斯", cast:"多姆纳尔·格里森", imdb:"7.8", quality:"1080P", remark:"蓝光", cats:["lengmen"],
|
||
desc:"能穿越时间的青年反复回到过去,最终懂得珍惜的是平凡的每一天。" },
|
||
{ id:"4501", title:"肖申克的救赎", en:"The Shawshank Redemption", year:"1994", genre:"剧情, 犯罪", country:"美国", director:"弗兰克·德拉邦特", cast:"蒂姆·罗宾斯", imdb:"9.3", quality:"4K原盘", remark:"原盘", cats:["top250","blu"],
|
||
desc:"蒙冤入狱的银行家以惊人的耐心与希望,在高墙之内谋划自己的自由。" },
|
||
{ id:"4502", title:"教父", en:"The Godfather", year:"1972", genre:"剧情, 犯罪", country:"美国", director:"弗朗西斯·科波拉", cast:"马龙·白兰度 / 阿尔·帕西诺", imdb:"9.2", quality:"4K原盘", remark:"原盘", cats:["top250","blu"],
|
||
desc:"柯里昂家族的权力更迭,一个不愿入局的儿子最终成为冷酷的新教父。" },
|
||
{ id:"4503", title:"盗梦空间", en:"Inception", year:"2010", genre:"科幻, 动作", country:"美国", director:"克里斯托弗·诺兰", cast:"莱昂纳多·迪卡普里奥", imdb:"8.8", quality:"4K原盘", remark:"原盘", cats:["top250","blu"],
|
||
desc:"盗梦高手潜入他人梦境植入意念,层层梦境中真实与虚幻逐渐模糊。" },
|
||
{ id:"4504", title:"星际穿越", en:"Interstellar", year:"2014", genre:"科幻, 冒险", country:"美国", director:"克里斯托弗·诺兰", cast:"马修·麦康纳", imdb:"8.7", quality:"4K原盘", remark:"原盘", cats:["top250","blu"],
|
||
desc:"地球濒危,宇航员穿越虫洞为人类寻找新家园,跨越时空守护亲情。" },
|
||
{ id:"4505", title:"黑客帝国", en:"The Matrix", year:"1999", genre:"科幻, 动作", country:"美国", director:"沃卓斯基姐妹", cast:"基努·里维斯", imdb:"8.7", quality:"4K原盘", remark:"原盘", cats:["top250","blu"],
|
||
desc:"程序员发现世界是机器制造的幻象,觉醒后投身反抗以夺回真实。" },
|
||
{ id:"4601", title:"欢颜", en:"Joyful Reunion", year:"2023", genre:"剧情, 年代", country:"中国大陆", director:"徐兵", cast:"董子健 / 张鲁一", imdb:"7.4", quality:"4K", remark:"全11集", cats:["ju","huayu"],
|
||
desc:"乱世年代,一笔神秘款项要被护送给远方的组织,一路险象环生、人心难测。" },
|
||
{ id:"4602", title:"漫长的季节", en:"The Long Season", year:"2023", genre:"剧情, 悬疑", country:"中国大陆", director:"辛爽", cast:"范伟 / 秦昊", imdb:"9.0", quality:"4K", remark:"全12集", cats:["ju"],
|
||
desc:"东北小城一桩旧案缠绕三个家庭二十年,秋叶飘落处真相终被翻起。" },
|
||
{ id:"4603", title:"繁花", en:"Blossoms Shanghai", year:"2023", genre:"剧情, 年代", country:"中国大陆", director:"王家卫", cast:"胡歌 / 马伊琍", imdb:"7.8", quality:"4K原盘", remark:"全30集", cats:["ju","blu"],
|
||
desc:"九十年代的上海滩,宝总在商海沉浮,霓虹之下是机遇、情义与时代。" },
|
||
{ id:"4604", title:"三体", en:"Three-Body", year:"2023", genre:"科幻, 悬疑", country:"中国大陆", director:"杨磊", cast:"张鲁一 / 于和伟", imdb:"8.7", quality:"4K", remark:"全30集", cats:["ju"],
|
||
desc:"纳米科学家卷入一连串科学家离奇死亡,逐步揭开三体文明的降临。" },
|
||
{ id:"4605", title:"狂飙", en:"The Knockout", year:"2023", genre:"剧情, 犯罪", country:"中国大陆", director:"徐纪周", cast:"张译 / 张颂文", imdb:"8.3", quality:"1080P", remark:"全39集", cats:["ju"],
|
||
desc:"扫黑除恶跨越二十年,正义警察与崛起的黑恶势力之间的漫长博弈。" },
|
||
{ id:"4606", title:"黑暗荣耀", en:"The Glory", year:"2022", genre:"剧情, 复仇", country:"韩国", director:"安吉镐", cast:"宋慧乔 / 李到晛", imdb:"8.1", quality:"4K", remark:"全16集", cats:["ju"],
|
||
desc:"遭受校园暴力的女孩用半生筹谋一场精密复仇,逐一清算昔日施暴者。" },
|
||
{ id:"4607", title:"最后生还者", en:"The Last of Us", year:"2023", genre:"剧情, 末日", country:"美国", director:"克雷格·麦辛", cast:"佩德罗·帕斯卡 / 贝拉·拉姆齐", imdb:"8.7", quality:"4K原盘", remark:"全9集", cats:["ju","blu"],
|
||
desc:"真菌疫情后的废土上,老兵护送拥有免疫力的少女穿越危机四伏的美国。" },
|
||
{ id:"5101", title:"庆余年第二季", en:"Joy of Life Season 2", year:"2024", genre:"剧情, 古装", country:"中国大陆", director:"孙皓", cast:"张若昀 / 吴刚 / 李沁", imdb:"8.5", quality:"4K", remark:"全36集", cats:["huayu_tv","ju"],
|
||
desc:"范闲再度入京,权谋角力更为激烈,父子情仇与庙堂暗战交织铺开。" },
|
||
{ id:"5102", title:"与凤行", en:"My Journey with You", year:"2024", genre:"剧情, 仙侠", country:"中国大陆", director:"林玉芬", cast:"赵丽颖 / 林更新", imdb:"7.2", quality:"4K", remark:"全40集", cats:["huayu_tv"],
|
||
desc:"魔界太子与人间女子命运交缠,跨越重重阻碍共历乱世情劫。" },
|
||
{ id:"5103", title:"玫瑰的故事", en:"The Story of Rose", year:"2024", genre:"剧情, 爱情", country:"中国大陆", director:"汪俊", cast:"刘亦菲 / 万彧雨", imdb:"7.8", quality:"4K", remark:"全38集", cats:["huayu_tv"],
|
||
desc:"名为黄亦玫的女子在不同年代的爱情与人生抉择,绽放属于自己的玫瑰。" },
|
||
{ id:"5104", title:"繁花", en:"Blossoms Shanghai", year:"2023", genre:"剧情, 年代", country:"中国大陆", director:"王家卫", cast:"胡歌 / 马伊琍 / 唐嫣", imdb:"7.8", quality:"4K原盘", remark:"全30集", cats:["huayu_tv","ju","blu"],
|
||
desc:"九十年代的上海滩,宝总在商海沉浮,霓虹之下是机遇、情义与时代。" },
|
||
{ id:"5105", title:"三体", en:"Three-Body", year:"2023", genre:"科幻, 悬疑", country:"中国大陆", director:"杨磊", cast:"张鲁一 / 于和伟 / 陈瑾", imdb:"8.7", quality:"4K", remark:"全30集", cats:["huayu_tv","ju"],
|
||
desc:"纳米科学家卷入一连串科学家离奇死亡,逐步揭开三体文明的降临。" },
|
||
{ id:"5106", title:"狂飙", en:"The Knockout", year:"2023", genre:"剧情, 犯罪", country:"中国大陆", director:"徐纪周", cast:"张译 / 张颂文 / 李一桐", imdb:"8.3", quality:"1080P", remark:"全39集", cats:["huayu_tv","ju"],
|
||
desc:"扫黑除恶跨越二十年,正义警察与崛起的黑恶势力之间的漫长博弈。" },
|
||
{ id:"5107", title:"漫长的季节", en:"The Long Season", year:"2023", genre:"剧情, 悬疑", country:"中国大陆", director:"辛爽", cast:"范伟 / 秦昊 / 陈明昊", imdb:"9.0", quality:"4K", remark:"全12集", cats:["huayu_tv","ju"],
|
||
desc:"东北小城一桩旧案缠绕三个家庭二十年,秋叶飘落处真相终被翻起。" },
|
||
{ id:"5108", title:"风起陇西", en:"The Wind Blows From Longxi", year:"2022", genre:"剧情, 古装", country:"中国大陆", director:"路阳", cast:"陈坤 / 白宇", imdb:"8.5", quality:"4K", remark:"全32集", cats:["huayu_tv"],
|
||
desc:"三国时代的谍战,两个情报员在蜀魏之间夹缝求存,忠义与生死难两全。" },
|
||
{ id:"5109", title:"山海情", en:"Minning Town", year:"2021", genre:"剧情, 年代", country:"中国大陆", director:"孔笙 / 孙墨龙", cast:"黄轩 / 张嘉益 / 闫妮", imdb:"9.0", quality:"1080P", remark:"全23集", cats:["huayu_tv"],
|
||
desc:"宁夏西海固的农民在扶贫政策下背井离乡,在戈壁滩上建立新家园的艰辛历程。" },
|
||
{ id:"5110", title:"大江大河", en:"Great River", year:"2018", genre:"剧情, 年代", country:"中国大陆", director:"孔笙 / 黄伟", cast:"王凯 / 杨烁 / 童瑶", imdb:"8.9", quality:"1080P", remark:"全47集", cats:["huayu_tv"],
|
||
desc:"改革开放时代,三个出身不同的年轻人在时代浪潮中各自奋进,命运交织。" }
|
||
];
|
||
|
||
var STATE = { cat: "new", full: [], list: [], reqToken: 0, detail: null, driveCheck: true, clientFilter: true,
|
||
page: 1, totalPages: 1, hasMore: false, loadingMore: false,
|
||
filters: { genre: "全部", region: "全部", year: "全部", sort: "最新" }, filtersOpen: false,
|
||
featured: [], heroIdx: 0, heroTimer: null, playQueue: [], playIdx: 0, expand: {} };
|
||
|
||
var FILTERS = {
|
||
genre: ["全部","剧情","喜剧","动作","科幻","爱情","动画","犯罪","悬疑","奇幻","家庭","惊悚","恐怖"],
|
||
region: ["全部","华语","大陆","香港","台湾","美国","日本","韩国","英国"],
|
||
year: ["全部","2026","2025","2024","2023","2022","2021","2020","更早"],
|
||
sort: ["最新","评分"]
|
||
};
|
||
var TMDB_GENRE = { "剧情":18, "喜剧":35, "动作":28, "科幻":878, "爱情":10749, "动画":16, "犯罪":80, "悬疑":9648, "奇幻":14, "家庭":10751, "惊悚":53, "恐怖":27 };
|
||
var REGION_CC = { "大陆":"CN", "香港":"HK", "台湾":"TW", "美国":"US", "日本":"JP", "韩国":"KR", "英国":"GB" };
|
||
/* TMDB 剧集(TV)与电影的类型 ID 不同。在剧集分类里用「类型」筛选时,
|
||
把电影类型 ID 映射成剧集类型 ID;无对应的(爱情/惊悚/恐怖/音乐)则不按类型过滤,
|
||
避免筛出空列表 */
|
||
var MOVIE_TO_TV_GENRE = {
|
||
28: 10759, 12: 10759, /* 动作/冒险 → Action & Adventure */
|
||
16: 16, /* 动画 */
|
||
35: 35, /* 喜剧 */
|
||
80: 80, /* 犯罪 */
|
||
99: 99, /* 纪录 */
|
||
18: 18, /* 剧情 */
|
||
10751: 10751, /* 家庭 */
|
||
14: 10765, 878: 10765, /* 奇幻/科幻 → Sci-Fi & Fantasy */
|
||
9648: 9648, /* 悬疑 */
|
||
10752: 10768, 36: 10768,/* 战争/历史 → War & Politics */
|
||
37: 37 /* 西部 */
|
||
};
|
||
|
||
/* ============================================================
|
||
数据加载
|
||
============================================================ */
|
||
function demoFor(cat) {
|
||
var base = DEMO.slice();
|
||
if (cat !== "new") base = base.filter(function (m) { return (m.cats || []).indexOf(cat) !== -1; });
|
||
if (cat === "top250") base = base.slice().sort(function (a, b) { return num(b.imdb) - num(a.imdb); });
|
||
if (cat === "huayu_tv" || cat === "huayu") base = base.slice().sort(function (a, b) { return num(b.year) - num(a.year); });
|
||
return base;
|
||
}
|
||
function mapTmdb(r, isTV) {
|
||
if (!r) return null;
|
||
var t = CONFIG.tmdb;
|
||
var title = isTV ? (r.name || r.original_name) : (r.title || r.original_title);
|
||
var orig = isTV ? r.original_name : r.original_title;
|
||
var date = isTV ? (r.first_air_date || "") : (r.release_date || "");
|
||
return {
|
||
id: "tmdb_" + (isTV ? "tv" : "mv") + "_" + r.id, tmdbId: r.id, tmdbType: isTV ? "tv" : "movie",
|
||
title: title || "", en: (orig && orig !== title) ? orig : "", year: date ? date.slice(0, 4) : "",
|
||
rating: r.vote_average ? (Math.round(r.vote_average * 10) / 10) : 0,
|
||
poster: r.poster_path ? (t.imageBase + (t.listSize || "w780") + r.poster_path) : "",
|
||
backdrop: r.backdrop_path ? (t.imageBase + "original" + r.backdrop_path) : "",
|
||
desc: r.overview || ""
|
||
};
|
||
}
|
||
function tmdbPage(cat, f, page) {
|
||
var t = CONFIG.tmdb, def = catDef(cat), isTV = !!def.tv;
|
||
var base = (t.apiBase || "https://api.tmdb.org/3") + "/discover/" + (isTV ? "tv" : "movie");
|
||
var p = ["api_key=" + encodeURIComponent(t.apiKey), "language=" + encodeURIComponent(t.lang || "zh-CN"), "include_adult=false", "page=" + (page || 1)];
|
||
var lteField = isTV ? "first_air_date.lte" : "primary_release_date.lte";
|
||
var gteField = isTV ? "first_air_date.gte" : "primary_release_date.gte";
|
||
|
||
/* 排序:跟随用户选择(分类切换时已把默认排序设成该分类的自然排序),让排序按钮处处生效 */
|
||
var sort = (f.sort === "评分") ? "vote_average.desc" : (def.airSort ? (isTV ? "first_air_date.desc" : "primary_release_date.desc") : "popularity.desc");
|
||
|
||
/* 类型:用户手选优先(窄化为单一类型),否则用分类自带类型,多类型取并集 OR */
|
||
var genres = [];
|
||
if (f.genre !== "全部" && TMDB_GENRE[f.genre]) genres = [TMDB_GENRE[f.genre]];
|
||
else if (def.genres && def.genres.length) genres = def.genres.slice();
|
||
/* 剧集:电影类型 ID 换成剧集类型 ID,无对应则丢弃(不按类型过滤而非筛空) */
|
||
if (isTV && genres.length) {
|
||
var mapped = [];
|
||
genres.forEach(function (g) { var tv = MOVIE_TO_TV_GENRE[g]; if (tv && mapped.indexOf(tv) === -1) mapped.push(tv); });
|
||
genres = mapped;
|
||
}
|
||
if (genres.length) p.push("with_genres=" + genres.join("|"));
|
||
|
||
/* 地区/语言:选「华语」按原始语言(zh)过滤,其余按制片国家过滤;
|
||
用户未手选地区(仍为「全部」)时,分类自带的 originCC 默认值生效(如"华语佳片"默认大陆|香港|台湾) */
|
||
if (f.region === "华语") {
|
||
p.push("with_original_language=zh");
|
||
} else if (f.region !== "全部" && REGION_CC[f.region]) {
|
||
p.push("with_origin_country=" + REGION_CC[f.region]);
|
||
} else if (f.region === "全部" && def.originCC && def.originCC.length) {
|
||
p.push("with_origin_country=" + def.originCC.join("|"));
|
||
}
|
||
|
||
/* 票数门槛 / 年代上限 */
|
||
if (def.minVotes) p.push("vote_count.gte=" + def.minVotes);
|
||
if (def.maxVotes) p.push("vote_count.lte=" + def.maxVotes);
|
||
if (def.yearLte) p.push(lteField + "=" + def.yearLte);
|
||
|
||
/* 用户选择年份 */
|
||
var userYear = (f.year && f.year !== "全部");
|
||
if (userYear) {
|
||
if (f.year === "更早") p.push(lteField + "=2019-12-31");
|
||
else p.push((isTV ? "first_air_date_year" : "primary_release_year") + "=" + f.year);
|
||
}
|
||
|
||
/* 近两年偏好:仅“最新热门”启用,且未手选年份时;
|
||
其余分类(地区/类型等)按热度全量拉取,总页数多,能一直加载更多 */
|
||
var biasRecent = (def.recent && !def.airSort && sort === "popularity.desc" && !userYear);
|
||
/* 华语热播:限定近90天首播,按上线日期降序 */
|
||
if (def.airSort && !userYear) {
|
||
var now = new Date(), ago90 = new Date(now.getTime() - 90 * 86400000);
|
||
p.push(lteField + "=" + now.toISOString().slice(0, 10));
|
||
p.push(gteField + "=" + ago90.toISOString().slice(0, 10));
|
||
}
|
||
if (biasRecent) {
|
||
var today = new Date(), past = new Date(today.getTime() - 730 * 86400000);
|
||
p.push(lteField + "=" + today.toISOString().slice(0, 10));
|
||
p.push(gteField + "=" + past.toISOString().slice(0, 10));
|
||
}
|
||
if (sort === "popularity.desc" && p.join("&").indexOf("vote_count.gte") === -1) p.push("vote_count.gte=" + (biasRecent ? 20 : 10));
|
||
if (sort === "vote_average.desc" && p.join("&").indexOf("vote_count.gte") === -1) p.push("vote_count.gte=200");
|
||
p.push("sort_by=" + sort);
|
||
return request(base + "?" + p.join("&"), { timeout: 18 }).then(function (d) {
|
||
var arr = (d && d.results) ? d.results : [];
|
||
var items = arr.map(function (r) { return mapTmdb(r, isTV); }).filter(function (x) { return x && x.poster; });
|
||
return { items: items, total: (d && d.total_pages) ? d.total_pages : 1 };
|
||
});
|
||
}
|
||
function dedupeById(list) {
|
||
var seen = {}, out = [];
|
||
(list || []).forEach(function (m) { var id = pick(m, "id", ""); if (id && seen[id]) return; if (id) seen[id] = 1; out.push(m); });
|
||
return out;
|
||
}
|
||
/* 从 fromPage 起顺序拉 count 页(容错:某页失败就停在已拿到的),
|
||
过程中持续更新 STATE.page / totalPages / hasMore */
|
||
function tmdbPages(cat, f, fromPage, count) {
|
||
var acc = [];
|
||
function step(pn, left) {
|
||
if (left <= 0) return Promise.resolve();
|
||
return tmdbPage(cat, f, pn).then(function (r) {
|
||
acc = acc.concat(r.items);
|
||
STATE.totalPages = r.total || STATE.totalPages || 1;
|
||
STATE.page = pn;
|
||
STATE.hasMore = (pn < STATE.totalPages);
|
||
if (!STATE.hasMore) return;
|
||
return step(pn + 1, left - 1);
|
||
}, function () { STATE.hasMore = ((pn - 1) < (STATE.totalPages || 1)); });
|
||
}
|
||
return step(fromPage, count).then(function () { return acc; });
|
||
}
|
||
function loadCatalog(cat) {
|
||
STATE.page = 1; STATE.totalPages = 1; STATE.hasMore = false; STATE.loadingMore = false;
|
||
if (CONFIG.catalogApi) {
|
||
STATE.clientFilter = true;
|
||
var url = CONFIG.catalogApi + (CONFIG.catalogApi.indexOf("?") !== -1 ? "&" : "?") + "cat=" + encodeURIComponent(cat);
|
||
return request(url).then(function (data) { return (data && data.list) ? data.list : (Array.isArray(data) ? data : []); });
|
||
}
|
||
if (tmdbEnabled()) {
|
||
STATE.clientFilter = false;
|
||
/* 首屏直接拉 3 页 ≈ 60 部,量大一点;剩下的滚动到底自动续 */
|
||
return tmdbPages(cat, STATE.filters, 1, 3).then(function (items) {
|
||
if (!items.length) { STATE.clientFilter = true; STATE.hasMore = false; return demoFor(cat); }
|
||
return dedupeById(items);
|
||
}).catch(function () { STATE.clientFilter = true; STATE.hasMore = false; return demoFor(cat); });
|
||
}
|
||
STATE.clientFilter = true;
|
||
return Promise.resolve(demoFor(cat));
|
||
}
|
||
/* 加载更多(仅 TMDB 分页):一次续 2 页,攒得更快 */
|
||
function loadMore() {
|
||
if (STATE.clientFilter || !STATE.hasMore || STATE.loadingMore) return;
|
||
STATE.loadingMore = true;
|
||
var b = $("#listState .loadmore"); if (b) { b.textContent = "加载中…"; }
|
||
var token = STATE.reqToken, from = (STATE.page || 1) + 1;
|
||
tmdbPages(STATE.cat, STATE.filters, from, 2).then(function (items) {
|
||
if (token !== STATE.reqToken) return;
|
||
STATE.full = dedupeById((STATE.full || []).concat(items));
|
||
STATE.loadingMore = false; STATE._appendMode = true; renderList(token);
|
||
}).catch(function () { if (token !== STATE.reqToken) return; STATE.loadingMore = false; STATE.hasMore = false; renderList(token); });
|
||
}
|
||
|
||
/* ============================================================
|
||
首页
|
||
============================================================ */
|
||
function renderNav() {
|
||
var nav = $("#nav");
|
||
if (!nav.childNodes.length) {
|
||
CATS.forEach(function (c) {
|
||
var b = el("button", "navtab focusable"); b.textContent = c.name;
|
||
b.setAttribute("data-cat", c.key); b.setAttribute("data-fk", "cat-" + c.key);
|
||
b.onclick = function () { setCat(c.key); }; nav.appendChild(b);
|
||
});
|
||
}
|
||
Array.from(nav.children).forEach(function (b) { b.classList.toggle("on", b.getAttribute("data-cat") === STATE.cat); });
|
||
var c = CATS.filter(function (x) { return x.key === STATE.cat; })[0];
|
||
$("#listTitle").textContent = (STATE.cat === "new") ? "TG 4K Movies" : (c ? c.name : "TG 4K Movies");
|
||
}
|
||
function setCat(k) {
|
||
if (STATE.cat === k) return;
|
||
var from = catIndex();
|
||
STATE.cat = k;
|
||
/* 切换方向:往后(下一个)内容从右侧滑入,往前从左侧滑入 */
|
||
STATE.swapDir = (catIndex() > from) ? "fwd" : "back";
|
||
/* 切换分类后筛选自动重置;默认排序取该分类的自然排序(高分类默认按评分) */
|
||
STATE.filters = { genre: "全部", region: "全部", year: "全部", sort: (catDef(k).sort === "vote" ? "评分" : "最新") };
|
||
syncFilterPills();
|
||
syncRegionOptions();
|
||
renderNav(); renderGrid(); scrollNavToActive();
|
||
}
|
||
/* 当前分类在 CATS 中的序号 */
|
||
function catIndex() { for (var i = 0; i < CATS.length; i++) if (CATS[i].key === STATE.cat) return i; return 0; }
|
||
/* 把激活的分类标签滚到可见居中位置(滑动切换后用得上) */
|
||
function scrollNavToActive() {
|
||
var nav = $("#nav"); if (!nav) return;
|
||
var on = nav.querySelector(".navtab.on"); if (!on) return;
|
||
try { on.scrollIntoView({ block: "nearest", inline: "center" }); } catch (e) {}
|
||
}
|
||
/* 网格切换动效:按方向滑入 + 淡入;可重复触发(先移除类、强制回流再加回)
|
||
TV 端与“减少动态效果”下跳过,只换数据不做动画 */
|
||
function prefersReduceMotion() {
|
||
if (!window.matchMedia) return false;
|
||
try { return window.matchMedia("(prefers-reduced-motion: reduce)").matches; } catch (e) { return false; }
|
||
}
|
||
function playGridEnter() {
|
||
if (!STATE.swapDir) return;
|
||
if (IS_TV || prefersReduceMotion()) return;
|
||
var grid = $("#grid"); if (!grid) return;
|
||
var cls = STATE.swapDir === "back" ? "gswap-back" : "gswap-fwd";
|
||
grid.classList.remove("gswap-fwd", "gswap-back");
|
||
void grid.offsetWidth; /* 强制回流,确保动画从头播放 */
|
||
grid.classList.add(cls);
|
||
}
|
||
/* 按方向切换到相邻分类:delta=+1 下一个,-1 上一个;到边界不动 */
|
||
function switchCatBy(delta) {
|
||
var i = catIndex() + delta;
|
||
if (i < 0 || i >= CATS.length) return false;
|
||
var c = CATS[i];
|
||
setCat(c.key);
|
||
toast(c.name);
|
||
return true;
|
||
}
|
||
|
||
function deriveTag(item) {
|
||
var q = (pick(item, "quality", "") + " " + pick(item, "remark", "")).toUpperCase();
|
||
if (/HDR|DV|杜比视界/.test(q)) return { t: "HDR", c: "tHDR" };
|
||
if (/原盘|REMUX|DOLBY|蓝光|BLURAY/.test(q)) return { t: "Dolby", c: "tDolby" };
|
||
if (/4K|2160/.test(q)) return { t: "4K", c: "t4K" };
|
||
if (/WEB|1080/.test(q)) return { t: "Web", c: "tWeb" };
|
||
return { t: "HD", c: "tHD" };
|
||
}
|
||
function buildCard(item) {
|
||
var card = el("button", "card focusable");
|
||
card.setAttribute("data-fk", "card-" + pick(item, "id", Math.random())); card.setAttribute("data-id", pick(item, "id", ""));
|
||
var poster = el("div", "poster"); poster.appendChild(posterInner(item));
|
||
/* 左上角标:有画质标画质,否则标评分(只在这一处显示,避免重复) */
|
||
if (pick(item, "quality", "")) {
|
||
var tg = deriveTag(item); var qt = el("span", "qtag " + tg.c); qt.textContent = tg.t; poster.appendChild(qt);
|
||
var rmk = pick(item, "remark", ""); if (rmk && rmk !== tg.t) { var rm = el("span", "remark"); rm.textContent = rmk; poster.appendChild(rm); }
|
||
} else {
|
||
var sc = num(item.rating) || num(item.imdb) || num(item.douban);
|
||
if (sc > 0) { var st = el("span", "qtag tStar"); st.textContent = "★ " + sc; poster.appendChild(st); }
|
||
}
|
||
/* 左下角标:年份(挪到海报上,让下方只留一行片名,卡片更紧凑) */
|
||
if (pick(item, "year", "")) { var yb = el("span", "yeartag"); yb.textContent = item.year; poster.appendChild(yb); }
|
||
card.appendChild(poster);
|
||
var meta = el("div", "meta");
|
||
var t = el("div", "t"); t.textContent = pick(item, "title", ""); meta.appendChild(t);
|
||
card.appendChild(meta);
|
||
card.onclick = function () { openDetail(item); };
|
||
return card;
|
||
}
|
||
function stateNode(title, sub, retry) {
|
||
var d = el("div", "state"); var b = el("b"); b.textContent = title; d.appendChild(b);
|
||
d.appendChild(document.createTextNode(sub || ""));
|
||
if (retry) { var btn = el("button", "focusable"); btn.textContent = "重试"; btn.setAttribute("data-fk", "retry"); btn.onclick = retry; d.appendChild(document.createElement("br")); d.appendChild(btn); }
|
||
return d;
|
||
}
|
||
function renderGrid() {
|
||
var grid = $("#grid"), st = $("#listState"), token = ++STATE.reqToken;
|
||
/* 切换分类时立即清空换骨架(即时反馈),否则仅在空时铺骨架 */
|
||
if (!grid.children.length || STATE.swapDir) {
|
||
var f0 = document.createDocumentFragment();
|
||
for (var i = 0; i < 6; i++) { var c = el("div", "card"); c.appendChild(el("div", "skel poster-skel")); f0.appendChild(c); }
|
||
grid.replaceChildren(f0);
|
||
playGridEnter();
|
||
}
|
||
st.replaceChildren();
|
||
loadCatalog(STATE.cat).then(function (list) {
|
||
if (token !== STATE.reqToken) return;
|
||
STATE.full = list || [];
|
||
renderList(token);
|
||
}).catch(function (e) {
|
||
if (token !== STATE.reqToken) return;
|
||
grid.replaceChildren(); st.replaceChildren(stateNode("加载失败", String(e && e.message || e), function () { renderGrid(); }));
|
||
});
|
||
}
|
||
function renderList(token) {
|
||
if (token == null) token = STATE.reqToken;
|
||
var grid = $("#grid"), st = $("#listState");
|
||
STATE.list = STATE.clientFilter ? applyFilters(STATE.full) : (STATE.full || []).slice();
|
||
updateFilterCount();
|
||
if (!STATE.list.length) { grid.replaceChildren(); st.replaceChildren(stateNode("没有符合条件的影片", "试试放宽筛选", null)); STATE.swapDir = null; STATE._appendMode = false; STATE._renderedCount = 0; return; }
|
||
st.replaceChildren();
|
||
/* 交错淡入:续载(append)只动画新增的卡,分类切换交给容器滑入(gswap)不重复动画 */
|
||
var appendMode = !!STATE._appendMode;
|
||
var startAnim = appendMode ? (STATE._renderedCount || 0) : 0;
|
||
var doStagger = !STATE.swapDir && !IS_TV && !prefersReduceMotion();
|
||
var frag = document.createDocumentFragment();
|
||
STATE.list.forEach(function (item, i) {
|
||
var c = buildCard(item);
|
||
if (doStagger && i >= startAnim) { c.classList.add("cin"); c.style.animationDelay = Math.min((i - startAnim) * 35, 420) + "ms"; }
|
||
frag.appendChild(c);
|
||
});
|
||
grid.replaceChildren(frag);
|
||
playGridEnter(); /* 真实内容铺好后再滑入一次 */
|
||
STATE.swapDir = null; /* 仅切换分类时容器动画,筛选/加载更多不触发 */
|
||
STATE._appendMode = false;
|
||
STATE._renderedCount = STATE.list.length;
|
||
enrichPosters(STATE.list);
|
||
if (!STATE.clientFilter && STATE.hasMore) {
|
||
var more = el("button", "loadmore focusable"); more.setAttribute("data-fk", "loadmore");
|
||
more.textContent = STATE.loadingMore ? "加载中…" : "加载更多";
|
||
more.onclick = function () { loadMore(); };
|
||
st.appendChild(more);
|
||
}
|
||
}
|
||
function applyFilters(list) {
|
||
var f = STATE.filters, out = (list || []).slice();
|
||
if (f.genre !== "全部") out = out.filter(function (m) { return (pick(m, "genre", "")).indexOf(f.genre) !== -1; });
|
||
if (f.region !== "全部") out = out.filter(function (m) { return (pick(m, "country", "")).indexOf(f.region) !== -1; });
|
||
if (f.year !== "全部") out = out.filter(function (m) {
|
||
if (f.year === "更早") return num(m.year) > 0 && num(m.year) < 2020;
|
||
return String(pick(m, "year", "")) === f.year;
|
||
});
|
||
if (f.sort === "评分") out.sort(function (a, b) { return (num(b.rating) || num(b.imdb)) - (num(a.rating) || num(a.imdb)); });
|
||
else out.sort(function (a, b) { return num(b.year) - num(a.year); });
|
||
return out;
|
||
}
|
||
function updateFilterCount() { var r = $("#fres"); if (r) r.textContent = "共 " + STATE.list.length + " 部"; }
|
||
|
||
/* 地区选项收窄:分类若带 originCC(如"华语佳片"=大陆|香港|台湾),
|
||
地区筛选面板里只保留「全部」+ originCC 覆盖到的地区名,其余地区(含"华语")隐藏不可选;
|
||
普通分类(无 originCC)不受限,仍展示完整地区列表 */
|
||
function regionOptionsFor(def) {
|
||
if (!def || !def.originCC || !def.originCC.length) return null;
|
||
var rev = {}; Object.keys(REGION_CC).forEach(function (k) { rev[REGION_CC[k]] = k; });
|
||
var names = ["全部"];
|
||
def.originCC.forEach(function (cc) { var name = rev[cc]; if (name && names.indexOf(name) === -1) names.push(name); });
|
||
return names;
|
||
}
|
||
function syncRegionOptions() {
|
||
var box = $("#filters"); if (!box) return;
|
||
var allow = regionOptionsFor(catDef(STATE.cat));
|
||
Array.from(box.querySelectorAll('.fpill[data-fkey="region"]')).forEach(function (b) {
|
||
b.classList.toggle("hidden", !!allow && allow.indexOf(b.getAttribute("data-fval")) === -1);
|
||
});
|
||
}
|
||
|
||
/* 筛选器 */
|
||
function renderFilters() {
|
||
var box = $("#filters"); if (box.childNodes.length) { syncFilterPills(); syncRegionOptions(); return; }
|
||
var rows = [["类型","genre"],["地区","region"],["年份","year"],["排序","sort"]];
|
||
var frag = document.createDocumentFragment();
|
||
rows.forEach(function (r) {
|
||
var label = r[0], key = r[1];
|
||
var row = el("div", "filterrow");
|
||
var lb = el("span", "flabel"); lb.textContent = label; row.appendChild(lb);
|
||
FILTERS[key].forEach(function (opt) {
|
||
var b = el("button", "fpill focusable"); b.textContent = opt;
|
||
b.setAttribute("data-fkey", key); b.setAttribute("data-fval", opt); b.setAttribute("data-fk", "f-" + key + "-" + opt);
|
||
if (STATE.filters[key] === opt) b.classList.add("on");
|
||
b.onclick = function () { STATE.filters[key] = opt; syncFilterPills(); renderGrid(); };
|
||
row.appendChild(b);
|
||
});
|
||
frag.appendChild(row);
|
||
});
|
||
var tools = el("div", "ftools");
|
||
var res = el("span", "res"); res.id = "fres"; tools.appendChild(res);
|
||
var clr = el("button", "fclear focusable"); clr.textContent = "清除筛选"; clr.setAttribute("data-fk", "fclear");
|
||
clr.onclick = function () { STATE.filters = { genre: "全部", region: "全部", year: "全部", sort: "最新" }; syncFilterPills(); renderGrid(); };
|
||
tools.appendChild(clr);
|
||
frag.appendChild(tools);
|
||
box.replaceChildren(frag);
|
||
syncFiltersOpen();
|
||
syncRegionOptions();
|
||
}
|
||
function syncFilterPills() {
|
||
Array.from($("#filters").querySelectorAll(".fpill")).forEach(function (b) {
|
||
b.classList.toggle("on", STATE.filters[b.getAttribute("data-fkey")] === b.getAttribute("data-fval"));
|
||
});
|
||
syncFiltersOpen();
|
||
}
|
||
function syncFiltersOpen() {
|
||
var box = $("#filters"), btn = $("#ftoggle"); if (!box || !btn) return;
|
||
box.classList.toggle("collapsed", !STATE.filtersOpen);
|
||
btn.classList.toggle("open", STATE.filtersOpen);
|
||
var cnt = $("#ftoggleCount"); if (!cnt) return;
|
||
var n = 0, f = STATE.filters;
|
||
if (f.genre !== "全部") n++; if (f.region !== "全部") n++; if (f.year !== "全部") n++; if (f.sort !== "最新") n++;
|
||
cnt.textContent = n ? String(n) : "";
|
||
cnt.classList.toggle("hidden", !n);
|
||
}
|
||
|
||
/* ============================================================
|
||
详情
|
||
============================================================ */
|
||
function openDetail(item) {
|
||
STATE.detail = item;
|
||
var st = { route: "detail", id: pick(item, "id", "") };
|
||
/* 已在详情页内再打开新片(如点“猜你喜欢”)时替换当前历史记录,
|
||
避免历史层级叠加导致返回要按多次 */
|
||
if (document.body.classList.contains("detail-active")) history.replaceState(st, "", "#detail");
|
||
else history.pushState(st, "", "#detail");
|
||
showDetail(item);
|
||
}
|
||
|
||
function metaRow(k, v) { var r = el("div", "metarow"); var kk = el("div", "k"); kk.textContent = k; var vv = el("div", "v"); vv.textContent = v; r.appendChild(kk); r.appendChild(vv); return r; }
|
||
/* 多值字符串拆分(/ , 、 顿号等分隔),去空 */
|
||
function splitVals(v) { return String(v == null ? "" : v).split(/\s*[\/,、,]\s*/).map(function (s) { return s.trim(); }).filter(function (s) { return s; }); }
|
||
/* 元数据标签:可带前缀小标签(如“导演”),key=true 时高亮成金色 */
|
||
function metaTag(label, value, key) {
|
||
var s = el("span", "mtag" + (key ? " key" : ""));
|
||
if (label) { var l = el("span", "ml"); l.textContent = label; s.appendChild(l); }
|
||
s.appendChild(document.createTextNode(value));
|
||
return s;
|
||
}
|
||
|
||
/* TMDB 影片 logo(中文或英文,PNG/SVG,取第一个) */
|
||
function getTmdbLogo(art) {
|
||
if (!tmdbEnabled() || !art || !art.id) return Promise.resolve("");
|
||
var t = CONFIG.tmdb, type = art.type === "tv" ? "tv" : "movie";
|
||
var ck = "logo2_" + type + "_" + art.id;
|
||
return Store.get(ck).then(function (cached) {
|
||
if (cached !== undefined && cached !== null && cached !== "") { try { return JSON.parse(cached); } catch(e) { return cached; } }
|
||
var url = (t.apiBase || "https://api.tmdb.org/3") + "/" + type + "/" + art.id +
|
||
"/images?api_key=" + encodeURIComponent(t.apiKey) + "&include_image_language=en,zh,null";
|
||
return request(url, { timeout: 12 }).then(function (data) {
|
||
var logos = (data && data.logos) ? data.logos : [];
|
||
/* 优先中文,其次英文,再次任意 */
|
||
var pick_zh = null, pick_en = null, pick_any = null, i;
|
||
for (i = 0; i < logos.length; i++) {
|
||
var lg = logos[i];
|
||
if (!lg || !lg.file_path) continue;
|
||
if (!pick_any) pick_any = lg;
|
||
if (!pick_zh && lg.iso_639_1 === "zh") { pick_zh = lg; }
|
||
if (!pick_en && lg.iso_639_1 === "en") { pick_en = lg; }
|
||
}
|
||
var chosen = pick_zh || pick_en || pick_any;
|
||
var logoUrl = chosen ? (t.imageBase + "w500" + chosen.file_path) : "";
|
||
Store.set(ck, JSON.stringify(logoUrl));
|
||
return logoUrl;
|
||
});
|
||
}).catch(function () { return ""; });
|
||
}
|
||
function applyHeroLogo(logoUrl) {
|
||
var el2 = $("#dLogo"); if (!el2) return;
|
||
el2.replaceChildren();
|
||
if (logoUrl) {
|
||
var img = makeImg(logoUrl, "");
|
||
img.onerror = function () { el2.classList.remove("has-logo"); el2.classList.add("hidden"); };
|
||
el2.appendChild(img);
|
||
el2.classList.add("has-logo");
|
||
el2.classList.remove("hidden");
|
||
} else {
|
||
el2.classList.remove("has-logo");
|
||
el2.classList.add("hidden");
|
||
}
|
||
}
|
||
function renderDetailInfo(item) {
|
||
$("#dTitle").textContent = pick(item, "title", "—") + (pick(item, "year", "") ? (" (" + item.year + ")") : "");
|
||
|
||
var chips = $("#dChips"); chips.replaceChildren();
|
||
if (pick(item, "quality", "")) { var qc = el("span", "chip"); qc.textContent = item.quality; chips.appendChild(qc); }
|
||
|
||
/* 评分移至 body 区 dRating,做成彩色赛博卡片标签 */
|
||
var dr = $("#dRating"); dr.replaceChildren();
|
||
if (num(item.rating) > 0) { var rt = el("span", "rtag st"); rt.textContent = "★ " + item.rating; dr.appendChild(rt); }
|
||
if (num(item.imdb) > 0) { var im = el("span", "rtag im"); im.textContent = "IMDb " + item.imdb; dr.appendChild(im); }
|
||
if (num(item.douban) > 0) { var db = el("span", "rtag db"); db.textContent = "豆瓣 " + item.douban; dr.appendChild(db); }
|
||
if (!dr.children.length) dr.classList.add("hidden"); else dr.classList.remove("hidden");
|
||
|
||
/* 元数据精简为一排圆角标签:国家 / 类型 / 时间 / 时长(导演已并入下方“团队阵容”) */
|
||
var mb = $("#metablk"); mb.replaceChildren(); mb.className = "metatags";
|
||
var frag = document.createDocumentFragment();
|
||
splitVals(pick(item, "country", "")).forEach(function (c) { frag.appendChild(metaTag("", c, true)); });
|
||
splitVals(pick(item, "genre", "")).forEach(function (g) { frag.appendChild(metaTag("", g)); });
|
||
var when = pick(item, "date", "") || (pick(item, "year", "") ? String(item.year) : "");
|
||
if (when) frag.appendChild(metaTag("", when));
|
||
if (num(item.runtime) > 0) frag.appendChild(metaTag("", num(item.runtime) + " 分钟"));
|
||
mb.appendChild(frag);
|
||
if (!mb.children.length) mb.classList.add("hidden"); else mb.classList.remove("hidden");
|
||
|
||
$("#dDesc").textContent = pick(item, "desc", "") || pick(item, "overview", "") || "暂无简介。下方资源可一键复制或推送播放。";
|
||
setupDescToggle();
|
||
}
|
||
function setupDescToggle() {
|
||
var d = $("#dDesc"), btn = $("#descToggle");
|
||
if (!d || !btn) return;
|
||
d.classList.add("open"); btn.textContent = "收起";
|
||
setTimeout(function () {
|
||
if (d.scrollHeight - d.clientHeight > 4 || d.classList.contains("open")) btn.classList.remove("hidden"); else btn.classList.add("hidden");
|
||
}, 0);
|
||
}
|
||
|
||
function showDetail(item) {
|
||
closeFilmo();
|
||
STATE._detailRenderedItem = item;
|
||
document.body.classList.add("detail-active");
|
||
var sheet = $("#sheet"); sheet.classList.add("active"); sheet.setAttribute("aria-hidden", "false"); sheet.scrollTop = 0;
|
||
|
||
applyHeroImage(item);
|
||
applyHeroLogo(""); /* 先清空上一部的 logo */
|
||
renderDetailInfo(item);
|
||
renderStills(pick(item, "stills", []) || []);
|
||
renderCast(item);
|
||
|
||
/* 详情页进场动效:h1 glitch + PLAY 开机 */
|
||
(function triggerEnterAnimations() {
|
||
var h1 = sheet.querySelector(".herofg h1");
|
||
var playBtn = $("#actPlay");
|
||
if (h1) {
|
||
h1.classList.remove("glitch-enter");
|
||
void h1.offsetWidth; /* reflow 强制重新触发 */
|
||
h1.classList.add("glitch-enter");
|
||
}
|
||
if (playBtn) {
|
||
playBtn.classList.remove("power-on");
|
||
void playBtn.offsetWidth;
|
||
playBtn.classList.add("power-on");
|
||
}
|
||
})();
|
||
|
||
/* 后台预热 blur wallPic,播放时直接取缓存 */
|
||
|
||
/* 预热播放器图片(pic 预热;wallPic 运行时动态生成 blur 不预热) */
|
||
fmReady().then(function (fm) {
|
||
if (!fm || !fm.preloadArtwork) return;
|
||
var pic = pick(item, "poster", "") ? tmdbResize(pick(item, "poster", ""), "w780") : "";
|
||
if (pic) fm.preloadArtwork(pic, "");
|
||
}).catch(function () {});
|
||
|
||
STATE.playQueue = []; STATE.playIdx = 0; updatePlayBtn();
|
||
loadResources(item);
|
||
loadReco(item);
|
||
|
||
/* TMDB:补剧照大图 + 完整元数据(别名/时间/演员/简介等,仅填空缺) */
|
||
if (tmdbEnabled()) {
|
||
var artToken = STATE.reqToken;
|
||
getArtwork(item).then(function (art) {
|
||
if (!art || STATE.detail !== item || STATE.reqToken !== artToken) return;
|
||
if (art.poster && !pick(item, "poster", "")) item.poster = art.poster;
|
||
if (art.backdrop && !pick(item, "backdrop", "")) item.backdrop = art.backdrop;
|
||
applyHeroImage(item);
|
||
/* backdrop 可能刚到,重新预热 blur */
|
||
if (!(pick(item, "stills", []) || []).length) getStills(art).then(function (ss) { if (STATE.detail === item && ss.length) { item.stills = ss; renderStills(ss); applyHeroCarousel(item, ss); } }); else applyHeroCarousel(item, pick(item, "stills", []) || []);
|
||
/* 拉取影片中文徽标 */
|
||
getTmdbLogo(art).then(function (logoUrl) { if (STATE.detail === item) applyHeroLogo(logoUrl); });
|
||
/* 更新预热:pic 更新后重新预热 */
|
||
fmReady().then(function (fm) {
|
||
if (!fm || !fm.preloadArtwork || STATE.detail !== item) return;
|
||
var pic2 = pick(item, "poster", "") ? tmdbResize(pick(item, "poster", ""), "w780") : "";
|
||
if (pic2) fm.preloadArtwork(pic2, "");
|
||
}).catch(function () {});
|
||
var metaToken = STATE.reqToken;
|
||
getDetailMeta(art).then(function (m) {
|
||
if (!m || STATE.detail !== item || STATE.reqToken !== metaToken) return;
|
||
["genre","date","country","director","cast","alias","overview"].forEach(function (k) { if (!pick(item, k, "") && m[k]) item[k] = m[k]; });
|
||
if (m.castList && m.castList.length) item.castList = m.castList;
|
||
renderDetailInfo(item);
|
||
renderCast(item);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
function renderStills(stills) {
|
||
var wrap = $("#stillsWrap"), box = $("#stills");
|
||
if (!stills || !stills.length) { wrap.classList.add("hidden"); box.replaceChildren(); return; }
|
||
var frag = document.createDocumentFragment();
|
||
stills.forEach(function (u) { var s = el("div", "s"); s.appendChild(makeImg(u, "")); frag.appendChild(s); });
|
||
box.replaceChildren(frag); wrap.classList.remove("hidden");
|
||
}
|
||
|
||
/* 演员头像区:完全同步参考文件 renderCast,只用 TMDB castList(已按 profile_path 过滤),没有头像的人不会出现在列表里,不做任何占位/兜底 */
|
||
function castAvatar(name, url) {
|
||
var av = el("span", "cav");
|
||
av.appendChild(makeImg(url, name));
|
||
return av;
|
||
}
|
||
function renderCast(item) {
|
||
var wrap = $("#castWrap"), box = $("#cast");
|
||
if (!wrap || !box) return;
|
||
var list = (pick(item, "castList", []) || []).slice(0, 14);
|
||
if (!list.length) { wrap.classList.add("hidden"); box.replaceChildren(); return; }
|
||
var frag = document.createDocumentFragment();
|
||
list.forEach(function (p) {
|
||
var chip = el("div", "castchip");
|
||
chip.appendChild(castAvatar(p.name, p.avatar));
|
||
var tx = el("span", "ctx");
|
||
var nm = el("span", "cname"); nm.textContent = p.name || ""; tx.appendChild(nm);
|
||
if (p.role) { var rl = el("span", "crole" + (p.role === "导演" ? " role-dir" : "")); rl.textContent = p.role; tx.appendChild(rl); }
|
||
chip.appendChild(tx);
|
||
chip.onclick = (function (person, chipEl) { return function () { openFilmo(person, chipEl); }; })(p, chip);
|
||
frag.appendChild(chip);
|
||
});
|
||
box.replaceChildren(frag); wrap.classList.remove("hidden");
|
||
}
|
||
|
||
/* ===== 演员相关作品:内联展开(点头像 → 下方出现一排同款首页卡片,已排除综艺类) ===== */
|
||
var _filmoWrap = null; /* 当前展开的 .cast-filmo-wrap 节点 */
|
||
var _filmoChip = null; /* 当前高亮的 castchip */
|
||
var _filmoOpen = false;
|
||
var FILMO_EXCLUDE_GENRES = [10764, 10767]; /* TMDB TV:Reality(综艺/真人秀) + Talk(脱口秀) */
|
||
|
||
function openFilmo(person, chipEl) {
|
||
var castWrap = $("#castWrap"); if (!castWrap) return;
|
||
/* 若点同一个人则折叠关闭 */
|
||
if (_filmoChip === chipEl && _filmoOpen) { closeFilmo(); return; }
|
||
closeFilmo();
|
||
if (!tmdbEnabled()) return;
|
||
/* 高亮选中演员 */
|
||
if (chipEl) { chipEl.classList.add("filmo-active"); _filmoChip = chipEl; }
|
||
/* 创建内联展开容器,插在 castWrap 内部 castrow 之后 */
|
||
var wrap = el("div", "cast-filmo-wrap");
|
||
var inner = el("div", "cast-filmo-inner");
|
||
/* 小标题 */
|
||
var head = el("div", "cast-filmo-head");
|
||
var nameEl = el("span", "cast-filmo-name"); nameEl.textContent = (person.name || "") + " 的作品";
|
||
var closeBtn = el("button", "cast-filmo-close"); closeBtn.textContent = "×"; closeBtn.onclick = closeFilmo;
|
||
head.appendChild(nameEl); head.appendChild(closeBtn);
|
||
inner.appendChild(head);
|
||
/* loading */
|
||
var loadingEl = el("div", "cast-filmo-loading"); loadingEl.textContent = "LOADING ···";
|
||
inner.appendChild(loadingEl);
|
||
wrap.appendChild(inner);
|
||
castWrap.appendChild(wrap);
|
||
_filmoWrap = wrap; _filmoOpen = true;
|
||
/* 动画展开 */
|
||
requestAnimationFrame(function () { wrap.classList.add("open"); });
|
||
/* 拉 TMDB */
|
||
var t = CONFIG.tmdb;
|
||
var pid = person.tmdbPersonId;
|
||
function doLoad(id) {
|
||
var url = (t.apiBase || "https://api.tmdb.org/3") + "/person/" + id + "/combined_credits?api_key=" + encodeURIComponent(t.apiKey) + "&language=" + encodeURIComponent(t.lang || "zh-CN");
|
||
request(url, { timeout: 14 }).then(function (d) {
|
||
if (!_filmoOpen || _filmoWrap !== wrap) return;
|
||
var cast = (d && d.cast) ? d.cast : [];
|
||
/* 排除综艺/真人秀/脱口秀(仅针对剧集类,电影无此类型 id 冲突) */
|
||
cast = cast.filter(function (r) {
|
||
if (r.media_type !== "tv") return true;
|
||
var gids = r.genre_ids || [];
|
||
for (var i = 0; i < gids.length; i++) { if (FILMO_EXCLUDE_GENRES.indexOf(gids[i]) !== -1) return false; }
|
||
return true;
|
||
});
|
||
var seen = {}, items = [];
|
||
cast.sort(function (a, b) { return (b.popularity || 0) - (a.popularity || 0); });
|
||
cast.forEach(function (r) { if (r.poster_path && !seen[r.id]) { seen[r.id] = 1; items.push(r); } });
|
||
items = items.slice(0, 20);
|
||
inner.replaceChildren(head);
|
||
if (!items.length) { var emp = el("div", "cast-filmo-loading"); emp.textContent = "暂无相关作品"; inner.appendChild(emp); return; }
|
||
var grid = el("div", "filmogrid-inline");
|
||
items.forEach(function (r) {
|
||
var mapped = mapTmdb(r, r.media_type === "tv");
|
||
var cell = el("div", "fg-cell");
|
||
var card = buildCard(mapped);
|
||
cell.appendChild(card);
|
||
grid.appendChild(cell);
|
||
});
|
||
inner.appendChild(grid);
|
||
}).catch(function () { if (_filmoWrap === wrap) { inner.replaceChildren(head); var e2 = el("div", "cast-filmo-loading"); e2.textContent = "加载失败"; inner.appendChild(e2); } });
|
||
}
|
||
if (pid) { doLoad(pid); }
|
||
else if (person.name) {
|
||
var surl = (t.apiBase || "https://api.tmdb.org/3") + "/search/person?api_key=" + encodeURIComponent(t.apiKey) + "&language=" + encodeURIComponent(t.lang || "zh-CN") + "&query=" + encodeURIComponent(person.name);
|
||
request(surl, { timeout: 12 }).then(function (d) {
|
||
if (!_filmoOpen || _filmoWrap !== wrap) return;
|
||
var r = d && d.results && d.results[0];
|
||
if (!r) { inner.replaceChildren(head); var e3 = el("div", "cast-filmo-loading"); e3.textContent = "未找到演员数据"; inner.appendChild(e3); return; }
|
||
person.tmdbPersonId = r.id; doLoad(r.id);
|
||
}).catch(function () { if (_filmoWrap === wrap) { inner.replaceChildren(head); var e4 = el("div", "cast-filmo-loading"); e4.textContent = "加载失败"; inner.appendChild(e4); } });
|
||
} else {
|
||
inner.replaceChildren(head); var e5 = el("div", "cast-filmo-loading"); e5.textContent = "无演员ID"; inner.appendChild(e5);
|
||
}
|
||
}
|
||
function closeFilmo() {
|
||
if (_filmoWrap) { _filmoWrap.classList.remove("open"); var w = _filmoWrap; setTimeout(function () { if (w.parentNode) w.parentNode.removeChild(w); }, 350); _filmoWrap = null; }
|
||
if (_filmoChip) { _filmoChip.classList.remove("filmo-active"); _filmoChip = null; }
|
||
_filmoOpen = false;
|
||
}
|
||
|
||
/* ---------- 猜你喜欢(TMDB 同类推荐) ---------- */
|
||
function renderReco(items) {
|
||
var wrap = $("#recoWrap"), box = $("#reco");
|
||
if (!wrap || !box) return;
|
||
if (!items || !items.length) { wrap.classList.add("hidden"); box.replaceChildren(); return; }
|
||
var frag = document.createDocumentFragment();
|
||
items.forEach(function (it) {
|
||
var cell = el("div", "fg-cell");
|
||
cell.appendChild(buildCard(it));
|
||
frag.appendChild(cell);
|
||
});
|
||
box.replaceChildren(frag); wrap.classList.remove("hidden");
|
||
}
|
||
function pickReco(d, isTV) {
|
||
if (!d || !d.results) return [];
|
||
return d.results.map(function (r) { return mapTmdb(r, isTV); }).filter(function (x) { return x && x.poster; }).slice(0, 12);
|
||
}
|
||
function loadReco(item) {
|
||
renderReco([]); /* 先清空,避免闪上一部的推荐 */
|
||
if (!tmdbEnabled()) return;
|
||
var id = pick(item, "tmdbId", 0), type = pick(item, "tmdbType", "movie"), isTV = (type === "tv");
|
||
if (!id) return; /* 非 TMDB 来源(如聚合搜索)无 id 则不显示 */
|
||
var t = CONFIG.tmdb;
|
||
var common = "?api_key=" + encodeURIComponent(t.apiKey) + "&language=" + encodeURIComponent(t.lang || "zh-CN") + "&page=1";
|
||
var baseT = (t.apiBase || "https://api.tmdb.org/3") + "/" + type + "/" + id + "/";
|
||
function get(kind) { return request(baseT + kind + common, { timeout: 14 }); }
|
||
get("recommendations").then(function (d) {
|
||
if (STATE.detail !== item) return;
|
||
var arr = pickReco(d, isTV);
|
||
if (arr.length) { renderReco(arr); return; }
|
||
get("similar").then(function (d2) { if (STATE.detail === item) renderReco(pickReco(d2, isTV)); }, function () {});
|
||
}, function () {
|
||
get("similar").then(function (d2) { if (STATE.detail === item) renderReco(pickReco(d2, isTV)); }, function () {});
|
||
});
|
||
}
|
||
function closeDetail() {
|
||
closeFilmo();
|
||
_carouselStop();
|
||
++STATE.reqToken;
|
||
STATE.detail = null;
|
||
STATE._detailRenderedItem = null;
|
||
document.body.classList.remove("detail-active");
|
||
var sheet = $("#sheet"); sheet.classList.remove("active"); sheet.setAttribute("aria-hidden", "true");
|
||
/* sheet 以 transform 滑出(.26s),动画结束后再清理重型 DOM,
|
||
避免 display 切换 + DOM 清理同帧导致 reflow 卡顿,视觉上退出有延迟 */
|
||
setTimeout(function () {
|
||
renderReco([]);
|
||
var stills = $("#stills"); if (stills) stills.replaceChildren();
|
||
var cast = $("#cast"); if (cast) cast.replaceChildren();
|
||
var resgroups = $("#resgroups"); if (resgroups) resgroups.replaceChildren();
|
||
var resState = $("#resState"); if (resState) resState.replaceChildren();
|
||
sheet.scrollTop = 0;
|
||
if (IS_TV) restoreHomeFocus();
|
||
}, 300);
|
||
}
|
||
|
||
/* ---------- 资源:PanSou 检索 + 按画质分组 ---------- */
|
||
var TIER_ORDER = ["原盘", "4K", "中字4K", "1080P", "720P", "其它"];
|
||
function tierOf(name) {
|
||
var s = (name || "").toUpperCase();
|
||
var zh = /中字|中文字幕|简繁|国语|国配|双语/.test(name || "");
|
||
var remux = /REMUX|原盘|COMPLETE|UHD\.?BLURAY|BLURAY/.test(s);
|
||
var is4k = /2160P|\b4K\b|UHD|HDR|DOLBY|DV\b/.test(s);
|
||
if (remux) return "原盘";
|
||
if (is4k) return zh ? "中字4K" : "4K";
|
||
if (/1080P/.test(s)) return "1080P";
|
||
if (/720P/.test(s)) return "720P";
|
||
return "其它";
|
||
}
|
||
function sizeOf(name) {
|
||
var m = (name || "").match(/(\d+(?:\.\d+)?)\s?(GB|MB|TB)/i);
|
||
return m ? (m[1] + m[2].toUpperCase()) : "";
|
||
}
|
||
|
||
function loadResources(item) {
|
||
var resState = $("#resState"), groups = $("#resgroups");
|
||
groups.replaceChildren();
|
||
resState.replaceChildren(stateNode("正在检索资源…", "聚合网盘 / 磁力(PanSou)", null));
|
||
var token = ++STATE.reqToken;
|
||
|
||
/* 已有结构化资源直接用,否则 PanSou 检索 */
|
||
var pre = pick(item, "resources", []);
|
||
if (pre && pre.length) {
|
||
renderResourcesFromRows(pre.map(function (r) {
|
||
return { name: pick(r, "name", ""), url: pick(r, "url", ""), size: pick(r, "size", "") || sizeOf(pick(r, "name", "")),
|
||
type: pick(r, "type", "") || normType("", pick(r, "url", "")), tier: pick(r, "tier", "") || tierOf(pick(r, "name", "")),
|
||
password: pick(r, "password", ""), state: "" };
|
||
}));
|
||
return;
|
||
}
|
||
|
||
var kw = pick(item, "title", ""), tries = 0;
|
||
function attempt() {
|
||
tries++;
|
||
pansouSearch(kw).then(function (rows) {
|
||
if (token !== STATE.reqToken) return;
|
||
if (!rows.length) {
|
||
if (tries < 2) { setTimeout(attempt, 400); return; } /* 空结果也再试一次 */
|
||
resState.replaceChildren(stateNode("未找到网盘资源", "暂无匹配的网盘资源", null));
|
||
return;
|
||
}
|
||
renderResourcesFromRows(rows);
|
||
}).catch(function (e) {
|
||
if (token !== STATE.reqToken) return;
|
||
if (tries < 3) { setTimeout(attempt, 500); return; } /* 自动重试,免去手点重试 */
|
||
resState.replaceChildren(stateNode("检索失败", String(e && e.message || e), function () { loadResources(item); }));
|
||
});
|
||
}
|
||
attempt();
|
||
}
|
||
|
||
function parsePansou(data, kw) {
|
||
var merged = {};
|
||
if (data && data.merged_by_type) merged = data.merged_by_type;
|
||
else if (data && data.data && data.data.merged_by_type) merged = data.data.merged_by_type;
|
||
var rows = [], seen = {};
|
||
Object.keys(merged).forEach(function (type) {
|
||
(merged[type] || []).forEach(function (r) {
|
||
var u = pick(r, "url", "") || pick(r, "link", ""); if (!u) return;
|
||
var t = normType(type, u); var key = t + "|" + u; if (seen[key]) return; seen[key] = 1;
|
||
var name = pick(r, "note", "") || pick(r, "title", "") || kw;
|
||
rows.push({ name: name, url: u, size: sizeOf(name), type: t, tier: tierOf(name),
|
||
password: pick(r, "password", "") || pick(r, "pwd", ""), state: "" });
|
||
});
|
||
});
|
||
return rows;
|
||
}
|
||
function pansouSearch(kw) {
|
||
var apiBase = CONFIG.pansou.apiBase;
|
||
function viaPost() {
|
||
return request(apiBase + "/api/search", { method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ kw: kw, res: "merge", src: "all", cloud_types: CONFIG.pansou.diskTypes }), timeout: 25 });
|
||
}
|
||
function viaGet() {
|
||
return request(apiBase + "/api/search?kw=" + encodeURIComponent(kw) + "&res=merge&src=all", { method: "GET", timeout: 25 });
|
||
}
|
||
return viaPost().then(function (d) { return parsePansou(d, kw); }, function () {
|
||
return viaGet().then(function (d) { return parsePansou(d, kw); }); /* POST 400/失败时回退 GET */
|
||
});
|
||
}
|
||
function normType(type, url) {
|
||
type = (type || "").toLowerCase(); if (DISK_LABEL[type]) return type;
|
||
url = (url || "").toLowerCase();
|
||
if (url.indexOf("magnet:") === 0) return "magnet";
|
||
if (url.indexOf("ed2k:") === 0) return "ed2k";
|
||
if (url.indexOf("thunder:") === 0) return "thunder";
|
||
if (url.indexOf("quark") !== -1) return "quark";
|
||
if (url.indexOf("aliyundrive") !== -1 || url.indexOf("alipan") !== -1) return "aliyun";
|
||
if (url.indexOf("pan.baidu") !== -1) return "baidu";
|
||
if (url.indexOf("drive.uc") !== -1) return "uc";
|
||
if (url.indexOf("cloud.189") !== -1) return "tianyi";
|
||
if (url.indexOf("xunlei") !== -1) return "xunlei";
|
||
if (url.indexOf("123") !== -1 && url.indexOf("/s/") !== -1) return "123";
|
||
if (url.indexOf("115") !== -1) return "115";
|
||
if (url.indexOf("139.com") !== -1 || url.indexOf("caiyun") !== -1) return "mobile";
|
||
return "others";
|
||
}
|
||
/* 体积数值(MB)用于排序 */
|
||
function sizeVal(s) { var m = (s || "").match(/(\d+(?:\.\d+)?)\s?(TB|GB|MB)/i); if (!m) return 0; var n = parseFloat(m[1]), u = m[2].toUpperCase(); return u === "TB" ? n * 1048576 : (u === "GB" ? n * 1024 : n); }
|
||
function diskPriority(t) { var o = ["quark","baidu","aliyun","uc","tianyi","115","123","xunlei","mobile"]; var i = o.indexOf(t); if (i !== -1) return i; if (t === "magnet") return 20; if (t === "thunder") return 21; if (t === "ed2k") return 22; return 30; }
|
||
/* 同一发布名跨网盘只留一条(优先夸克/阿里等稳定盘) */
|
||
function normName(s) { return String(s || "").toLowerCase().replace(/[\s._\-\[\]()【】]+/g, ""); }
|
||
function dedupeByName(rows) {
|
||
var sorted = (rows || []).slice().sort(function (a, b) { return diskPriority(a.type) - diskPriority(b.type); });
|
||
var seen = {}, out = [];
|
||
sorted.forEach(function (r) { var k = (r.tier || "") + "|" + normName(r.name); if (seen[k]) return; seen[k] = 1; out.push(r); });
|
||
return out;
|
||
}
|
||
function groupResources(rows) {
|
||
var g = {}; rows.forEach(function (r) { var t = r.tier || "其它"; if (!g[t]) g[t] = []; g[t].push(r); });
|
||
Object.keys(g).forEach(function (t) { g[t].sort(function (a, b) { var d = sizeVal(b.size) - sizeVal(a.size); return d !== 0 ? d : (diskPriority(a.type) - diskPriority(b.type)); }); });
|
||
var ordered = [];
|
||
TIER_ORDER.forEach(function (t) { if (g[t]) { ordered.push({ tier: t, items: g[t] }); delete g[t]; } });
|
||
Object.keys(g).forEach(function (t) { ordered.push({ tier: t, items: g[t] }); });
|
||
return ordered;
|
||
}
|
||
function tierClass(t) { return ["原盘","4K","中字4K","1080P","其它"].indexOf(t) !== -1 ? t : "其它"; }
|
||
|
||
/* 只保留高画质(原盘 / 4K / 中字4K,画质关键词由 tierOf 检测);若一条都没有则回退全部,避免空列表 */
|
||
var HIGH_TIERS = ["原盘", "4K", "中字4K"];
|
||
function filterHighQuality(rows) {
|
||
var hi = (rows || []).filter(function (r) { return HIGH_TIERS.indexOf(r.tier || tierOf(r.name)) !== -1; });
|
||
return hi.length ? hi : (rows || []);
|
||
}
|
||
function renderResourcesFromRows(rows) {
|
||
rows = dedupeByName(filterHighQuality(rows));
|
||
if (!rows.length) { $("#resState").replaceChildren(stateNode("暂无可用资源", "暂无匹配的网盘资源", null)); $("#resgroups").replaceChildren(); return; }
|
||
$("#resState").replaceChildren();
|
||
STATE.expand = {};
|
||
STATE.resTier = null;
|
||
renderResGroups(groupResources(rows));
|
||
scheduleChecks();
|
||
}
|
||
|
||
var RES_LIMIT = 6;
|
||
function renderResGroups(ordered) {
|
||
var box = $("#resgroups"); box.replaceChildren();
|
||
if (!ordered.length) { STATE.allRes = []; resetPlayQueue(); return; }
|
||
STATE.allRes = [];
|
||
if (!STATE.expand) STATE.expand = {};
|
||
/* 全部资源入队列/检测(不分当前显示的是哪类) */
|
||
ordered.forEach(function (grp) { grp.items.forEach(function (r) { STATE.allRes.push(r); }); });
|
||
|
||
/* 选中的画质类别:默认第一类(原盘优先),失效则回退第一类 */
|
||
var tiers = ordered.map(function (g) { return g.tier; });
|
||
if (!STATE.resTier || tiers.indexOf(STATE.resTier) === -1) STATE.resTier = tiers[0];
|
||
|
||
/* —— 类别标签:原盘 / 4K / 中字4K… 并排一行 —— */
|
||
var tabs = el("div", "restabs");
|
||
ordered.forEach(function (grp) {
|
||
var tab = el("button", "restab focusable");
|
||
tab.setAttribute("data-tier", grp.tier);
|
||
tab.setAttribute("data-fk", "restab-" + grp.tier);
|
||
if (grp.tier === STATE.resTier) tab.classList.add("on");
|
||
var lbl = el("span", "rt-label"); lbl.textContent = grp.tier; tab.appendChild(lbl);
|
||
var cnt = el("span", "rt-cnt"); cnt.textContent = grp.items.length; tab.appendChild(cnt);
|
||
tab.onclick = function () { if (STATE.resTier === grp.tier) return; STATE.resTier = grp.tier; renderResGroups(ordered); };
|
||
tabs.appendChild(tab);
|
||
});
|
||
box.appendChild(tabs);
|
||
|
||
/* —— 下方资源框:只展示选中类别,切标签即换内容 —— */
|
||
var sel = ordered.filter(function (g) { return g.tier === STATE.resTier; })[0] || ordered[0];
|
||
var content = el("div", "resbox");
|
||
var expanded = !!STATE.expand[sel.tier];
|
||
var show = expanded ? sel.items : sel.items.slice(0, RES_LIMIT);
|
||
show.forEach(function (r) { content.appendChild(buildResItem(r)); });
|
||
if (sel.items.length > RES_LIMIT) {
|
||
var more = el("button", "resmore focusable"); more.setAttribute("data-fk", "more-" + sel.tier);
|
||
more.textContent = expanded ? "收起" : ("展开全部 " + sel.items.length + " 条 ▾");
|
||
more.onclick = function () { STATE.expand[sel.tier] = !expanded; renderResGroups(ordered); };
|
||
content.appendChild(more);
|
||
}
|
||
box.appendChild(content);
|
||
resetPlayQueue();
|
||
}
|
||
function resBadges(r) {
|
||
var s = (r.name || "").toUpperCase(), zh = (r.name || ""), out = [];
|
||
if (/2160P|\b4K\b|UHD/.test(s)) out.push("4K"); else if (/1080P/.test(s)) out.push("1080P"); else if (/720P/.test(s)) out.push("720P");
|
||
if (/REMUX/.test(s)) out.push("REMUX"); else if (/原盘|BLURAY|BDMV|COMPLETE|蓝光/.test(zh + s)) out.push("原盘");
|
||
else if (/WEB-?DL|WEBRIP|WEB\b/.test(s)) out.push("WEB");
|
||
if (/DOLBY\s?VISION|\bDV\b|杜比视界/.test(zh + s)) out.push("DV");
|
||
if (/HDR10\+|HDR/.test(s)) out.push("HDR");
|
||
if (/X265|H\.?265|HEVC/.test(s)) out.push("H265"); else if (/X264|H\.?264|AVC/.test(s)) out.push("H264");
|
||
if (/ATMOS/.test(s)) out.push("Atmos");
|
||
if (/国语|国配|双语/.test(zh)) out.push("国语");
|
||
if (/中字|中文字幕|简繁|内封|内嵌/.test(zh)) out.push("中字");
|
||
if (/高码率?|高码版/.test(zh)) out.push("高码");
|
||
return out.slice(0, 5);
|
||
}
|
||
function buildResItem(r) {
|
||
var row = el("div", "resitem");
|
||
var dot = el("span", "dot " + (r.state || "")); dot.setAttribute("data-url", r.url); row.appendChild(dot);
|
||
var ri = el("div", "ri");
|
||
var tags = el("div", "tags");
|
||
var disk = el("span", "btag disk"); disk.textContent = DISK_LABEL[r.type] || r.type; tags.appendChild(disk);
|
||
/* 各 badge 赋予独立颜色 class */
|
||
var badgeClassMap = { "4K":"btag-4k","2160P":"btag-4k","UHD":"btag-4k","1080P":"btag-1080","720P":"btag-720","REMUX":"btag-remux","原盘":"btag-remux","WEB":"btag-web","DV":"btag-dv","HDR":"btag-hdr","Atmos":"btag-atmos","国语":"btag-cn","中字":"btag-cn","H265":"btag-h265","H264":"btag-h264","高码":"btag-hibit" };
|
||
var badges = resBadges(r);
|
||
badges.forEach(function (b) { var t = el("span", "btag " + (badgeClassMap[b] || "")); t.textContent = b; tags.appendChild(t); });
|
||
/* 提取码:只有同时出现「高码」标签才显示,避免低码率资源也带提取码混淆 */
|
||
if (r.password && badges.indexOf("高码") !== -1) { var pw = el("span", "btag pw"); pw.textContent = "码 " + r.password; tags.appendChild(pw); }
|
||
ri.appendChild(tags);
|
||
row.appendChild(ri);
|
||
if (r.size) { var sz = el("span", "size"); sz.textContent = r.size; row.appendChild(sz); }
|
||
var ops = el("div", "ops");
|
||
var play = el("button", "op play focusable"); play.textContent = "▶ PLAY"; play.setAttribute("data-fk", "play"); play.onclick = function () { playResource(r); }; ops.appendChild(play);
|
||
row.appendChild(ops);
|
||
return row;
|
||
}
|
||
function scheduleChecks() {
|
||
if (!STATE.driveCheck || !STATE.allRes) { applyDots(); return; }
|
||
var items = STATE.allRes.filter(function (r) { return !r.state && checkable(r.type); }).slice(0, 24);
|
||
if (!items.length) return;
|
||
fmReady().then(function (fm) {
|
||
if (!fm || !fm.pan || !fm.pan.check) return;
|
||
return fm.pan.check(items.map(function (r) { return { type: r.type, url: r.url, password: r.password || "" }; })).then(function (res) {
|
||
var results = (res && res.results) ? res.results : [];
|
||
results.forEach(function (out, i) { if (items[i]) items[i].state = pick(out, "state", "uncertain"); });
|
||
pruneInvalid();
|
||
});
|
||
}).catch(function () {});
|
||
}/* 失效(bad / 不支持)的资源直接从列表移除 */
|
||
function pruneInvalid() {
|
||
var all = STATE.allRes || [];
|
||
var keep = all.filter(function (r) { var s = r.state || ""; return s !== "bad" && s !== "unsupported"; });
|
||
if (keep.length !== all.length) {
|
||
if (!keep.length) { $("#resgroups").replaceChildren(); $("#resState").replaceChildren(stateNode("暂无可用资源", "有效高画质资源暂时没找到", null)); STATE.allRes = []; resetPlayQueue(); return; }
|
||
renderResGroups(groupResources(keep));
|
||
} else {
|
||
applyDots();
|
||
}
|
||
}
|
||
function checkable(t) { return ["aliyun","quark","uc","baidu","tianyi","123","xunlei","115","mobile"].indexOf(t) !== -1; }
|
||
function applyDots() {
|
||
var dots = $("#resgroups").querySelectorAll(".dot");
|
||
dots.forEach(function (dot) {
|
||
var u = dot.getAttribute("data-url");
|
||
var r = (STATE.allRes || []).filter(function (x) { return x.url === u; })[0];
|
||
if (r) dot.className = "dot " + (r.state || "");
|
||
});
|
||
}
|
||
function resetPlayQueue() {
|
||
STATE.playQueue = (STATE.allRes || []).slice();
|
||
STATE.playIdx = 0;
|
||
updatePlayBtn();
|
||
}
|
||
function updatePlayBtn() {
|
||
var b = $("#actPlay"); if (!b) return;
|
||
var n = (STATE.playQueue || []).length;
|
||
b.textContent = (n > 1 && STATE.playIdx > 0) ? "换源 ▸" : "PLAY ▸";
|
||
}
|
||
function nextPlayableIndex(from) {
|
||
var q = STATE.playQueue || [], n = q.length; if (!n) return -1;
|
||
for (var c = 0; c < n; c++) {
|
||
var i = (from + c) % n, st = q[i].state || "";
|
||
if (st !== "bad" && st !== "unsupported") return i;
|
||
}
|
||
return from % n;
|
||
}
|
||
function doPlay(r) {
|
||
var detail = STATE.detail || {};
|
||
var pic = pick(detail, "poster", "") || "";
|
||
var wallPic = pick(detail, "backdrop", "") || pick(detail, "poster", "") || "";
|
||
if (wallPic) wallPic = tmdbResize(wallPic, "original");
|
||
if (pic) pic = tmdbResize(pic, "w780");
|
||
fmReady().then(function (fm) {
|
||
if (!fm) { toast("请在 App 内打开以推送播放"); return; }
|
||
if (fm.pan && fm.pan.play) {
|
||
fm.pan.play({ type: r.type, url: r.url, password: r.password || "", title: pick(detail, "title", "") || r.name, pic: pic, wallPic: wallPic });
|
||
}
|
||
}).catch(function (e) { toast("播放失败:" + (e && e.message || e)); });
|
||
}
|
||
/* 「播放」按钮:播当前源,播不了返回后再点自动换下一条 */
|
||
function playNext() {
|
||
var q = STATE.playQueue || [];
|
||
if (!q.length) { toast("暂无可播放资源"); return; }
|
||
var i = nextPlayableIndex(STATE.playIdx);
|
||
if (i < 0) { toast("暂无可播放资源"); return; }
|
||
var r = q[i];
|
||
STATE.playIdx = (i + 1) % q.length;
|
||
updatePlayBtn();
|
||
toast(q.length > 1 ? ("推送播放 " + (i + 1) + "/" + q.length + " · 播不了返回再点可换源") : "正在推送播放…");
|
||
doPlay(r);
|
||
}
|
||
/* 资源行的「下载」:播该条,并把换源游标接到它后面 */
|
||
function playResource(r) {
|
||
var q = STATE.playQueue && STATE.playQueue.length ? STATE.playQueue : (STATE.allRes || []);
|
||
var idx = q.indexOf(r);
|
||
if (idx !== -1 && q.length) { STATE.playIdx = (idx + 1) % q.length; updatePlayBtn(); }
|
||
toast("正在推送播放…");
|
||
doPlay(r);
|
||
}
|
||
|
||
/* 探索更多 */
|
||
function searchTitle(kw) { kw = (kw || "").trim(); if (!kw) return; openDetail({ id: "search", title: kw, desc: "“" + kw + "” 的网盘 / 磁力检索结果。" }); }
|
||
|
||
/* ---------- 主题色:点左上角 P 循环切换(琥珀金→蔷薇粉→湖蓝) ---------- */
|
||
var THEMES = ["", "rose", "ocean", "matrix", "reactor", "void"];
|
||
var THEME_NAMES = { "": "品红×电青", "rose": "热粉×电黄", "ocean": "电青×核橙", "matrix": "毒绿×血红", "reactor": "核橙×等离子蓝", "void": "幽灵紫×酸绿" };
|
||
function applyTheme(name) {
|
||
if (name && THEMES.indexOf(name) > 0) document.documentElement.setAttribute("data-theme", name);
|
||
else document.documentElement.removeAttribute("data-theme");
|
||
}
|
||
function cycleTheme() {
|
||
var cur = document.documentElement.getAttribute("data-theme") || "";
|
||
var i = THEMES.indexOf(cur); if (i === -1) i = 0;
|
||
var next = THEMES[(i + 1) % THEMES.length];
|
||
applyTheme(next);
|
||
Store.set("theme", next);
|
||
toast("主题色:" + THEME_NAMES[next]);
|
||
}
|
||
|
||
/* ---------- 搜索输入联想(TMDB 实时匹配) ---------- */
|
||
var sgState = { timer: null, token: 0 };
|
||
function tmdbSuggest(kw) {
|
||
if (!tmdbEnabled()) return Promise.resolve([]);
|
||
var t = CONFIG.tmdb;
|
||
var url = (t.apiBase || "https://api.tmdb.org/3") + "/search/multi?api_key=" + encodeURIComponent(t.apiKey) + "&language=" + encodeURIComponent(t.lang || "zh-CN") + "&include_adult=false&page=1&query=" + encodeURIComponent(kw);
|
||
return request(url, { timeout: 12 }).then(function (d) {
|
||
if (!d || !d.results) return [];
|
||
return d.results
|
||
.filter(function (r) { return r && (r.media_type === "movie" || r.media_type === "tv"); })
|
||
.map(function (r) { return mapTmdb(r, r.media_type === "tv"); })
|
||
.filter(function (x) { return x && x.title; })
|
||
.slice(0, 8);
|
||
});
|
||
}
|
||
function hideSuggest() { var box = $("#suggest"); if (box) { box.classList.remove("show"); box.replaceChildren(); } }
|
||
function renderSuggest(kw, items) {
|
||
var box = $("#suggest"); if (!box) return;
|
||
var frag = document.createDocumentFragment();
|
||
items.forEach(function (it) {
|
||
var row = el("div", "sg focusable"); row.setAttribute("data-fk", "sg-" + pick(it, "id", ""));
|
||
var pp = el("div", "pp"); pp.appendChild(posterInner(it)); row.appendChild(pp);
|
||
var tx = el("div", "tx");
|
||
var tt = el("div", "tt"); tt.textContent = pick(it, "title", ""); tx.appendChild(tt);
|
||
var meta = [(it.tmdbType === "tv" ? "剧集" : "电影"), pick(it, "year", "")].filter(function (s) { return s; }).join(" · ");
|
||
if (num(it.rating) > 0) meta += " ★ " + it.rating;
|
||
var mt = el("div", "mt"); mt.textContent = meta; tx.appendChild(mt);
|
||
row.appendChild(tx);
|
||
row.addEventListener("mousedown", function (e) { e.preventDefault(); }); /* 防止点击前输入框先失焦 */
|
||
row.onclick = function () { hideSuggest(); var qi = $("#q"); if (qi) qi.blur(); openDetail(it); };
|
||
frag.appendChild(row);
|
||
});
|
||
/* 末行:直接按关键词检索资源(保留原行为) */
|
||
var raw = el("div", "sg kwrow focusable"); raw.setAttribute("data-fk", "sg-raw");
|
||
var ic = el("div", "pp kwic"); ic.textContent = "🔍"; raw.appendChild(ic);
|
||
var rtx = el("div", "tx"); var rtt = el("div", "tt"); rtt.textContent = "直接搜索 “" + kw + "” 的资源"; rtx.appendChild(rtt); raw.appendChild(rtx);
|
||
raw.addEventListener("mousedown", function (e) { e.preventDefault(); });
|
||
raw.onclick = function () { hideSuggest(); var qi = $("#q"); if (qi) qi.blur(); searchTitle(kw); };
|
||
frag.appendChild(raw);
|
||
box.replaceChildren(frag); box.classList.add("show");
|
||
}
|
||
function setupSuggest() {
|
||
if (IS_TV) return; /* TV 端输入框只读、走原生搜索,不做联想 */
|
||
var q = $("#q"); if (!q) return;
|
||
function trigger() {
|
||
var kw = q.value.trim();
|
||
if (sgState.timer) clearTimeout(sgState.timer);
|
||
if (kw.length < 2) { hideSuggest(); return; }
|
||
sgState.timer = setTimeout(function () {
|
||
var token = ++sgState.token;
|
||
tmdbSuggest(kw).then(function (items) {
|
||
if (token !== sgState.token || q.value.trim() !== kw) return; /* 过期/已改动则丢弃 */
|
||
if (!items.length) { hideSuggest(); return; }
|
||
renderSuggest(kw, items);
|
||
}, function () {});
|
||
}, 260);
|
||
}
|
||
q.addEventListener("input", trigger);
|
||
q.addEventListener("focus", trigger);
|
||
q.addEventListener("blur", function () { setTimeout(hideSuggest, 160); });
|
||
q.addEventListener("keydown", function (e) { if (e.key === "Escape" || e.keyCode === 27) hideSuggest(); });
|
||
document.addEventListener("click", function (e) {
|
||
var inSb = e.target && e.target.closest ? e.target.closest(".searchbox") : null;
|
||
if (!inSb) hideSuggest();
|
||
});
|
||
}
|
||
|
||
/* ============================================================
|
||
chrome / 视口
|
||
============================================================ */
|
||
function applyChrome() {
|
||
fmReady().then(function (fm) {
|
||
if (!fm || !fm.ui || !fm.ui.setChrome) return;
|
||
fm.ui.setChrome({ mode: "edge", statusBarStyle: "light", navigationBarStyle: "light", startup: true }).catch(function () {});
|
||
});
|
||
}
|
||
|
||
/* ============================================================
|
||
TV 焦点
|
||
============================================================ */
|
||
var lastHomeFocus = null;
|
||
function focusables(scope) { return Array.from((scope || document).querySelectorAll(".focusable")).filter(isVisibleFocusable); }
|
||
function isVisibleFocusable(el) {
|
||
if (!el) return false; if (el.getAttribute("tabindex") === "-1") return false;
|
||
var a = el; while (a) { if (a.getAttribute && a.getAttribute("aria-hidden") === "true") return false; a = a.parentNode; }
|
||
var r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0;
|
||
}
|
||
function setFocusClass(el) { var p = document.querySelector(".focusable.focus"); if (p && p !== el) p.classList.remove("focus"); if (el) el.classList.add("focus"); }
|
||
function focusEl(el) {
|
||
if (!el) return;
|
||
try { el.focus({ preventScroll: true }); } catch (e) { try { el.focus(); } catch (e2) {} }
|
||
setFocusClass(el);
|
||
requestAnimationFrame(function () { var r = el.getBoundingClientRect(); if (r.top < 80 || r.bottom > (window.innerHeight - 20)) { try { el.scrollIntoView({ block: "center" }); } catch (e) { el.scrollIntoView(); } } });
|
||
}
|
||
function restoreHomeFocus() { if (lastHomeFocus && isVisibleFocusable(lastHomeFocus)) focusEl(lastHomeFocus); else { var f = focusables($("#home")); if (f.length) focusEl(f[0]); } }
|
||
/* 按实际宽度自适应网格列数:手机 3,平板 4-6,TV 6(覆盖 CSS 媒体查询) */
|
||
function gridColumns() {
|
||
var grid = $("#grid"); if (!grid) return 3;
|
||
var tpl = window.getComputedStyle(grid).getPropertyValue("grid-template-columns");
|
||
if (tpl && tpl.indexOf(" ") !== -1) return tpl.trim().split(/\s+/).length;
|
||
return IS_TV ? 6 : 3;
|
||
}
|
||
function geomNearest(cur, dir, scope) {
|
||
var list = focusables(scope), cr = cur.getBoundingClientRect();
|
||
var cx = cr.left + cr.width / 2, cy = cr.top + cr.height / 2, best = null, bestScore = Infinity;
|
||
list.forEach(function (el) {
|
||
if (el === cur) return; var r = el.getBoundingClientRect();
|
||
var x = r.left + r.width / 2, y = r.top + r.height / 2, dx = x - cx, dy = y - cy;
|
||
var ok = (dir === "right" && dx > 6) || (dir === "left" && dx < -6) || (dir === "down" && dy > 6) || (dir === "up" && dy < -6);
|
||
if (!ok) return;
|
||
var along = (dir === "left" || dir === "right") ? Math.abs(dx) : Math.abs(dy);
|
||
var cross = (dir === "left" || dir === "right") ? Math.abs(dy) : Math.abs(dx);
|
||
var score = along + cross * 2; if (score < bestScore) { bestScore = score; best = el; }
|
||
});
|
||
return best;
|
||
}
|
||
function onKey(e) {
|
||
var key = e.key || "", code = e.keyCode || 0;
|
||
var isLeft = key === "ArrowLeft" || code === 37, isUp = key === "ArrowUp" || code === 38;
|
||
var isRight = key === "ArrowRight" || code === 39, isDown = key === "ArrowDown" || code === 40;
|
||
var isOk = key === "Enter" || key === "OK" || code === 13 || code === 23 || code === 66;
|
||
var isBack = key === "Backspace" || key === "Escape" || key === "GoBack" || code === 27 || code === 8 || code === 4;
|
||
if (isBack) { if (STATE.detail || document.body.classList.contains("detail-active")) { e.preventDefault(); history.back(); return; } }
|
||
if (!IS_TV) return;
|
||
var cur = document.querySelector(".focusable.focus") || document.activeElement;
|
||
if (!cur || !cur.classList || !cur.classList.contains("focusable")) { if (isLeft || isUp || isRight || isDown) { e.preventDefault(); restoreHomeFocus(); return; } }
|
||
var scope = (STATE.detail || document.body.classList.contains("detail-active")) ? $("#sheet") : document;
|
||
if (isOk) { e.preventDefault(); if (cur) { if (cur.tagName === "INPUT") { cur.removeAttribute("readonly"); try { cur.focus(); } catch (er) {} } else cur.click(); } return; }
|
||
if (isLeft || isRight || isUp || isDown) {
|
||
e.preventDefault(); if (!cur) { restoreHomeFocus(); return; }
|
||
var dir = isLeft ? "left" : isRight ? "right" : isUp ? "up" : "down", next = null;
|
||
if (cur.classList.contains("card") && cur.parentNode && cur.parentNode.id === "grid") {
|
||
var cards = Array.from($("#grid").children).filter(function (c) { return c.classList.contains("focusable"); });
|
||
var idx = cards.indexOf(cur), cols = gridColumns();
|
||
if (dir === "right") next = cards[idx + 1]; else if (dir === "left") next = cards[idx - 1];
|
||
else if (dir === "down") next = cards[idx + cols]; else if (dir === "up") next = cards[idx - cols];
|
||
}
|
||
if (!next) next = geomNearest(cur, dir, scope);
|
||
if (next) focusEl(next);
|
||
}
|
||
}
|
||
function setupTvInput() { if (!IS_TV) return; var q = $("#q"); q.setAttribute("readonly", "readonly"); q.addEventListener("blur", function () { q.setAttribute("readonly", "readonly"); }); }
|
||
|
||
/* ============================================================
|
||
首页左右滑动切换分类
|
||
- 横向为主、位移够大、用时够短才触发,避免误伤竖向滚动
|
||
- 命中横向滚动条(分类条 / 筛选行 / 剧照 / 资源标签 / 输入框)时不接管
|
||
- 详情页与 TV 端不启用
|
||
============================================================ */
|
||
function setupSwipe() {
|
||
if (IS_TV) return;
|
||
var area = $("#home"); if (!area) return;
|
||
var sx = 0, sy = 0, st = 0, tracking = false;
|
||
area.addEventListener("touchstart", function (e) {
|
||
tracking = false;
|
||
if (document.body.classList.contains("detail-active")) return;
|
||
if (!e.touches || e.touches.length !== 1) return;
|
||
var t = e.target;
|
||
if (t && t.closest && t.closest(".nav, .filterrow, .stills, .restabs, input, textarea")) return;
|
||
var p = e.touches[0]; sx = p.clientX; sy = p.clientY; st = Date.now(); tracking = true;
|
||
}, { passive: true });
|
||
area.addEventListener("touchend", function (e) {
|
||
if (!tracking) return; tracking = false;
|
||
var p = e.changedTouches && e.changedTouches[0]; if (!p) return;
|
||
var dx = p.clientX - sx, dy = p.clientY - sy, dt = Date.now() - st;
|
||
if (dt > 700) return; /* 太慢不算滑动 */
|
||
if (Math.abs(dx) < 60) return; /* 位移不够 */
|
||
if (Math.abs(dx) < Math.abs(dy) * 1.6) return; /* 必须以横向为主 */
|
||
switchCatBy(dx < 0 ? 1 : -1); /* 左滑下一个,右滑上一个 */
|
||
});
|
||
}
|
||
|
||
/* ============================================================
|
||
滚动到底自动加载更多(无限滚动)
|
||
- rAF 节流,距底 700px 内就预加载,滚动更顺滑
|
||
- 详情页 / 客户端筛选模式(演示数据)下不触发
|
||
============================================================ */
|
||
function nearListBottom() {
|
||
var doc = document.documentElement, b = document.body;
|
||
var y = window.pageYOffset || doc.scrollTop || b.scrollTop || 0;
|
||
var vh = window.innerHeight || doc.clientHeight || 0;
|
||
var full = Math.max(b.scrollHeight, doc.scrollHeight, b.offsetHeight, doc.offsetHeight);
|
||
return (y + vh) >= (full - 700);
|
||
}
|
||
var _scrollTick = false;
|
||
function onScrollLoad() {
|
||
if (_scrollTick) return; _scrollTick = true;
|
||
requestAnimationFrame(function () {
|
||
_scrollTick = false;
|
||
var y = window.pageYOffset || document.documentElement.scrollTop || 0;
|
||
var detail = document.body.classList.contains("detail-active");
|
||
var bt = $("#backTop");
|
||
if (bt) { if (y > 800 && !detail) bt.classList.add("show"); else bt.classList.remove("show"); }
|
||
var tb = document.querySelector(".topbar");
|
||
if (tb) { if (y > 24) tb.classList.add("shrink"); else tb.classList.remove("shrink"); }
|
||
if (detail) return;
|
||
if (STATE.clientFilter || !STATE.hasMore || STATE.loadingMore) return;
|
||
if (nearListBottom()) loadMore();
|
||
});
|
||
}
|
||
function setupInfiniteScroll() {
|
||
window.addEventListener("scroll", onScrollLoad, { passive: true });
|
||
window.addEventListener("resize", onScrollLoad, { passive: true });
|
||
var bt = $("#backTop");
|
||
if (bt) bt.onclick = function () { try { window.scrollTo({ top: 0, behavior: "smooth" }); } catch (e) { window.scrollTo(0, 0); } };
|
||
/* 随机一部:从当前列表随便打开一部 */
|
||
var dice = $("#diceBtn");
|
||
if (dice) dice.onclick = function () {
|
||
var list = STATE.list || [];
|
||
if (!list.length) { toast("列表加载中…"); return; }
|
||
openDetail(list[Math.floor(Math.random() * list.length)]);
|
||
};
|
||
/* 卡片入场动画结束后摘掉 .cin,恢复按压等正常变换 */
|
||
var grid = $("#grid");
|
||
if (grid) grid.addEventListener("animationend", function (e) {
|
||
var el2 = e.target;
|
||
if (el2 && el2.classList && el2.classList.contains("cin")) { el2.classList.remove("cin"); el2.style.animationDelay = ""; }
|
||
});
|
||
}
|
||
|
||
/* ============================================================
|
||
路由
|
||
============================================================ */
|
||
function restoreDetail(item) {
|
||
/* 从播放器返回:废掉所有旧请求回调,只恢复 UI */
|
||
++STATE.reqToken;
|
||
STATE.detail = item;
|
||
closeFilmo();
|
||
document.body.classList.add("detail-active");
|
||
var sheet = $("#sheet"); sheet.classList.add("active"); sheet.setAttribute("aria-hidden", "false");
|
||
/* 还是同一部片、DOM 还在(没被 closeDetail 清空)就直接复用,不重新渲染 —
|
||
避免每次从播放器回来都整页重建造成卡顿延迟 */
|
||
if (STATE._detailRenderedItem === item && $("#resgroups") && $("#resgroups").children.length) {
|
||
return;
|
||
}
|
||
STATE._detailRenderedItem = item;
|
||
applyHeroImage(item);
|
||
renderDetailInfo(item);
|
||
renderStills(pick(item, "stills", []) || []);
|
||
renderCast(item);
|
||
applyHeroCarousel(item, pick(item, "stills", []) || []);
|
||
}
|
||
function onRoute(state) {
|
||
state = state || { route: "home" };
|
||
if (state.route === "detail" && STATE.detail) restoreDetail(STATE.detail);
|
||
else if (document.body.classList.contains("detail-active")) closeDetail();
|
||
}
|
||
window.addEventListener("popstate", function (e) { onRoute(e.state); });
|
||
|
||
/* ============================================================
|
||
启动
|
||
============================================================ */
|
||
function boot() {
|
||
renderNav();
|
||
renderFilters();
|
||
renderGrid();
|
||
|
||
/* 每次重载都重置为默认分类「最新热门」+ 默认筛选,不恢复上次选择 */
|
||
Store.get("noticeHidden").then(function (v) { if (v === "1") $("#notice").classList.add("hidden"); });
|
||
/* 清除旧的低清图片缓存(升级到 original 尺寸后一次性清理) */
|
||
try { var _ks = Object.keys(localStorage); _ks.forEach(function(k){ if(k.indexOf("片库_art_")===0||k.indexOf("片库_meta_")===0||k.indexOf("片库_logo_")===0) localStorage.removeItem(k); }); } catch(e) {}
|
||
Store.get("theme").then(function (v) { if (v) applyTheme(v); });
|
||
Store.get("filtersOpen").then(function (v) { STATE.filtersOpen = (v === "1"); syncFiltersOpen(); });
|
||
|
||
var q = $("#q");
|
||
function doSearch() { var kw = q.value.trim(); hideSuggest(); if (kw) searchTitle(kw); }
|
||
$("#goBtn").onclick = doSearch;
|
||
q.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.keyCode === 13) { e.preventDefault(); doSearch(); } });
|
||
$("#noticeX").onclick = function () { $("#notice").classList.add("hidden"); Store.set("noticeHidden", "1"); };
|
||
$("#ftoggle").onclick = function () { STATE.filtersOpen = !STATE.filtersOpen; Store.set("filtersOpen", STATE.filtersOpen ? "1" : "0"); syncFiltersOpen(); };
|
||
|
||
$("#brand").onclick = function (e) { e.preventDefault(); if (document.body.classList.contains("detail-active")) history.back(); window.scrollTo(0, 0); };
|
||
/* 左上角 P:一键循环切换主题色(拦截冒泡,不触发回首页) */
|
||
var themeBtn = $("#themeBtn");
|
||
if (themeBtn) themeBtn.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); cycleTheme(); });
|
||
|
||
|
||
$("#descToggle").onclick = function () {
|
||
var d = $("#dDesc"); var open = d.classList.toggle("open");
|
||
this.textContent = open ? "收起" : "展开";
|
||
};
|
||
/* 播放按钮:短按 → 播放;长按(500ms)→ 原生搜索 */
|
||
(function () {
|
||
var btn = $("#actPlay");
|
||
if (!btn) return;
|
||
var pressTimer = null;
|
||
var fired = false;
|
||
|
||
function startPress() {
|
||
fired = false;
|
||
pressTimer = setTimeout(function () {
|
||
fired = true;
|
||
if (navigator.vibrate) navigator.vibrate(40);
|
||
var kw = pick(STATE.detail, "title", "") || $("#dTitle").textContent;
|
||
var detail = STATE.detail || {};
|
||
var pic = pick(detail, "poster", "") ? tmdbResize(pick(detail, "poster", ""), "w780") : "";
|
||
var wallPic = pick(detail, "backdrop", "") || pick(detail, "poster", "") || "";
|
||
if (wallPic) wallPic = tmdbResize(wallPic, "original");
|
||
fmReady().then(function (fm) {
|
||
if (fm && fm.search) fm.search(kw, { direct: true, pic: pic, wallPic: wallPic });
|
||
else toast("请在 App 内使用原生搜索");
|
||
});
|
||
}, 500);
|
||
}
|
||
|
||
function cancelPress() {
|
||
clearTimeout(pressTimer);
|
||
pressTimer = null;
|
||
}
|
||
|
||
btn.addEventListener("mousedown", startPress);
|
||
btn.addEventListener("touchstart", startPress, { passive: true });
|
||
btn.addEventListener("mouseup", cancelPress);
|
||
btn.addEventListener("mouseleave", cancelPress);
|
||
btn.addEventListener("touchend", cancelPress);
|
||
btn.addEventListener("touchcancel", cancelPress);
|
||
btn.onclick = function () { if (!fired) playNext(); };
|
||
})();
|
||
|
||
document.addEventListener("keydown", onKey, true);
|
||
setupTvInput();
|
||
setupSuggest();
|
||
setupSwipe();
|
||
setupInfiniteScroll();
|
||
|
||
/* 横竖屏切换:详情页大图随朝向切换竖版/横版 */
|
||
function onOrient() { if (STATE.detail && document.body.classList.contains("detail-active")) applyHeroImage(STATE.detail); }
|
||
window.addEventListener("resize", onOrient);
|
||
window.addEventListener("orientationchange", onOrient);
|
||
window.addEventListener("fmviewport", onOrient);
|
||
|
||
fmReady().then(function (fm) {
|
||
if (!fm) return;
|
||
if (fm.config) fm.config().then(function (c) { STATE.driveCheck = (c && c.driveCheck !== false); }).catch(function () {});
|
||
applyChrome();
|
||
});
|
||
|
||
if (IS_TV) setTimeout(function () { var f = $("#nav").querySelector(".focusable"); if (f) { lastHomeFocus = f; focusEl(f); } }, 120);
|
||
}
|
||
|
||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
||
else boot();
|
||
|
||
/* ============================================================
|
||
区块标题随机 glitch 帧
|
||
每隔随机 3–9 秒,挑一个可见的 .blk-title 触发单次错位闪烁
|
||
============================================================ */
|
||
(function () {
|
||
"use strict";
|
||
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||
|
||
function fireOne() {
|
||
var titles = document.querySelectorAll(".blk-title:not(.glitch-fire)");
|
||
/* 只选视口内可见的 */
|
||
var visible = [];
|
||
for (var i = 0; i < titles.length; i++) {
|
||
var r = titles[i].getBoundingClientRect();
|
||
if (r.top >= 0 && r.bottom <= window.innerHeight) visible.push(titles[i]);
|
||
}
|
||
if (!visible.length) { schedule(); return; }
|
||
var target = visible[Math.random() * visible.length | 0];
|
||
target.classList.add("glitch-fire");
|
||
target.addEventListener("animationend", function onEnd() {
|
||
target.classList.remove("glitch-fire");
|
||
target.removeEventListener("animationend", onEnd);
|
||
});
|
||
schedule();
|
||
}
|
||
|
||
function schedule() {
|
||
/* 详情页开着时跳过,回首页再启动 */
|
||
if (document.body.classList.contains("detail-active")) {
|
||
setTimeout(schedule, 1200);
|
||
return;
|
||
}
|
||
setTimeout(fireOne, 3000 + Math.random() * 6000);
|
||
}
|
||
|
||
schedule();
|
||
})();
|
||
|
||
/* ============================================================
|
||
搜索框打字机光标
|
||
输入框空闲时逐字打印提示文字 + _ 光标闪烁
|
||
聚焦 / 有输入值时暂停并隐藏
|
||
============================================================ */
|
||
(function () {
|
||
"use strict";
|
||
var HINTS = [
|
||
"搜片名·网盘资源_",
|
||
"4K 原盘·直链下载_",
|
||
"输入标题·一键复制_"
|
||
];
|
||
var TYPE_SPD = 68; /* 每字符间隔 ms */
|
||
var PAUSE = 2200; /* 打完后停顿 ms */
|
||
var ERASE_SPD = 32; /* 退格间隔 ms */
|
||
|
||
var input = document.getElementById("q");
|
||
if (!input) return;
|
||
|
||
var hintIdx = 0, charIdx = 0, timer = null, erasing = false, paused = false;
|
||
|
||
function setPlaceholder(txt) { input.placeholder = txt; }
|
||
|
||
function tick() {
|
||
if (paused || input.value.length > 0) { timer = setTimeout(tick, 200); return; }
|
||
|
||
var full = HINTS[hintIdx];
|
||
|
||
if (!erasing) {
|
||
charIdx++;
|
||
setPlaceholder(full.slice(0, charIdx));
|
||
if (charIdx >= full.length) {
|
||
erasing = true;
|
||
timer = setTimeout(tick, PAUSE);
|
||
} else {
|
||
timer = setTimeout(tick, TYPE_SPD + (Math.random() * 30 | 0));
|
||
}
|
||
} else {
|
||
charIdx--;
|
||
/* 光标 _ 始终跟着退格 */
|
||
var cur = full.slice(0, charIdx);
|
||
setPlaceholder(charIdx > 0 ? cur + (cur.slice(-1) === "_" ? "" : "_") : "");
|
||
if (charIdx <= 0) {
|
||
erasing = false;
|
||
charIdx = 0;
|
||
hintIdx = (hintIdx + 1) % HINTS.length;
|
||
timer = setTimeout(tick, 380);
|
||
} else {
|
||
timer = setTimeout(tick, ERASE_SPD);
|
||
}
|
||
}
|
||
}
|
||
|
||
input.addEventListener("focus", function () {
|
||
paused = true;
|
||
setPlaceholder("");
|
||
clearTimeout(timer);
|
||
});
|
||
|
||
input.addEventListener("blur", function () {
|
||
if (input.value.length > 0) return;
|
||
paused = false;
|
||
/* 重新从头打 */
|
||
erasing = false; charIdx = 0;
|
||
setPlaceholder("");
|
||
timer = setTimeout(tick, 600);
|
||
});
|
||
|
||
input.addEventListener("input", function () {
|
||
if (input.value.length > 0) { clearTimeout(timer); setPlaceholder(""); }
|
||
});
|
||
|
||
/* 启动 */
|
||
timer = setTimeout(tick, 800);
|
||
})();
|
||
|
||
/* ============================================================
|
||
topbar 实时状态文字
|
||
格式:HH:MM:SS FPS CAT_CODE NET_LINK::ACTIVE
|
||
============================================================ */
|
||
(function () {
|
||
"use strict";
|
||
var el = document.getElementById("topbarStatus");
|
||
if (!el) return;
|
||
|
||
var fpsCount = 0, fps = 0, lastFpsSec = 0;
|
||
|
||
/* FPS 计数 */
|
||
(function fpsTick(ts) {
|
||
fpsCount++;
|
||
var sec = (ts / 1000) | 0;
|
||
if (sec !== lastFpsSec) { fps = fpsCount; fpsCount = 0; lastFpsSec = sec; }
|
||
requestAnimationFrame(fpsTick);
|
||
})(0);
|
||
|
||
function pad2(n) { return n < 10 ? "0" + n : "" + n; }
|
||
|
||
function tick() {
|
||
var d = new Date();
|
||
var timeStr = pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
|
||
|
||
/* detail 模式显示片名代码 */
|
||
var detailActive = document.body.classList.contains("detail-active");
|
||
var statusCode = detailActive ? "MODE::DETAIL" : "NET_LINK::ACTIVE";
|
||
|
||
var fpsNum = fps > 99 ? 99 : fps;
|
||
var fpsStr = fps > 0 ? ("FPS:" + pad2(fpsNum)) : "FPS:--";
|
||
|
||
el.textContent = timeStr + " " + fpsStr + " " + statusCode;
|
||
}
|
||
|
||
setInterval(tick, 1000);
|
||
tick();
|
||
})();
|
||
|
||
/* ============================================================
|
||
赛博进度条 CyBar
|
||
============================================================ */
|
||
var CyBar = (function () {
|
||
"use strict";
|
||
var bar = document.getElementById("cybar");
|
||
var track = document.getElementById("cybar-track");
|
||
var cursor = document.getElementById("cybar-cursor");
|
||
var label = document.getElementById("cybar-label");
|
||
var segs = document.getElementById("cybar-segs");
|
||
var _pct = 0, _timer = null, _doneTimer = null, _tasks = 0, _done = 0;
|
||
var SEG_COUNT = 12;
|
||
|
||
/* 生成分段节点 */
|
||
(function buildSegs() {
|
||
for (var i = 0; i < SEG_COUNT; i++) {
|
||
var s = document.createElement("div");
|
||
s.className = "cybar-seg";
|
||
segs.appendChild(s);
|
||
}
|
||
})();
|
||
|
||
function _setWidth(pct) {
|
||
_pct = Math.min(100, Math.max(0, pct));
|
||
track.style.width = _pct + "%";
|
||
cursor.style.left = _pct + "%";
|
||
/* 点亮经过的分段 */
|
||
var kids = segs.children;
|
||
for (var i = 0; i < kids.length; i++) {
|
||
var threshold = (i + 1) / SEG_COUNT * 100;
|
||
if (_pct >= threshold) {
|
||
kids[i].style.background = "color-mix(in srgb, var(--gold) " + (20 + i * 4) + "%, transparent)";
|
||
kids[i].style.boxShadow = "0 0 6px rgba(var(--gold-rgb),.3)";
|
||
} else {
|
||
kids[i].style.background = "";
|
||
kids[i].style.boxShadow = "";
|
||
}
|
||
}
|
||
}
|
||
|
||
function _animateTo(target, duration, cb) {
|
||
var start = _pct, startTime = null;
|
||
duration = duration || 400;
|
||
function step(ts) {
|
||
if (!startTime) startTime = ts;
|
||
var p = Math.min((ts - startTime) / duration, 1);
|
||
var ease = 1 - Math.pow(1 - p, 3); /* ease-out-cubic */
|
||
_setWidth(start + (target - start) * ease);
|
||
if (p < 1) requestAnimationFrame(step);
|
||
else if (cb) cb();
|
||
}
|
||
requestAnimationFrame(step);
|
||
}
|
||
|
||
/* 分段闪烁填充:每段依次点亮 */
|
||
function _segFlash(fromPct, toPct, stepCount, interval, cb) {
|
||
stepCount = stepCount || 4;
|
||
interval = interval || 80;
|
||
var step = (toPct - fromPct) / stepCount;
|
||
var i = 0;
|
||
function next() {
|
||
i++;
|
||
_setWidth(fromPct + step * i);
|
||
if (i < stepCount) setTimeout(next, interval);
|
||
else if (cb) setTimeout(cb, interval);
|
||
}
|
||
next();
|
||
}
|
||
|
||
/* 公开 API */
|
||
function start(labelText, totalTasks) {
|
||
_tasks = totalTasks || 1; _done = 0;
|
||
if (_doneTimer) { clearTimeout(_doneTimer); _doneTimer = null; }
|
||
bar.classList.remove("done");
|
||
bar.classList.add("active");
|
||
label.textContent = labelText || "LOADING";
|
||
_setWidth(0);
|
||
/* 快速填到 15% 给用户即时反馈 */
|
||
_segFlash(0, 15, 3, 60);
|
||
}
|
||
|
||
function progress(pct, labelText) {
|
||
if (labelText) label.textContent = labelText;
|
||
_animateTo(pct);
|
||
}
|
||
|
||
/* 单任务推进:n/total */
|
||
function tick(labelText) {
|
||
_done++;
|
||
var target = Math.min(90, (_done / _tasks) * 90 + 10);
|
||
if (labelText) label.textContent = labelText;
|
||
_segFlash(_pct, target, 3, 55);
|
||
}
|
||
|
||
function finish(labelText) {
|
||
if (labelText) label.textContent = labelText || "DONE";
|
||
/* 闪烁填满再消失 */
|
||
_segFlash(_pct, 100, 6, 45, function () {
|
||
bar.classList.remove("active");
|
||
bar.classList.add("done");
|
||
_doneTimer = setTimeout(function () {
|
||
bar.classList.remove("done");
|
||
_setWidth(0);
|
||
var kids = segs.children;
|
||
for (var i = 0; i < kids.length; i++) { kids[i].style.background = ""; kids[i].style.boxShadow = ""; }
|
||
}, 600);
|
||
});
|
||
}
|
||
|
||
function error(labelText) {
|
||
label.textContent = labelText || "ERR";
|
||
label.style.color = "var(--bad)";
|
||
setTimeout(function () { finish(); label.style.color = ""; }, 800);
|
||
}
|
||
|
||
return { start: start, progress: progress, tick: tick, finish: finish, error: error };
|
||
})();
|
||
|
||
/* ============================================================
|
||
Typewriter — 逐字打印,带光标
|
||
============================================================ */
|
||
var Typewriter = (function () {
|
||
"use strict";
|
||
var _queue = [], _running = false;
|
||
|
||
function _type(el, text, speed, cb) {
|
||
/* 清空,追加光标 */
|
||
el.textContent = "";
|
||
var cur = document.createElement("span");
|
||
cur.className = "tw-cursor";
|
||
cur.textContent = "_";
|
||
el.appendChild(cur);
|
||
|
||
var i = 0;
|
||
var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:_.<>[]|/-";
|
||
function step() {
|
||
if (i >= text.length) {
|
||
/* 完成后光标保留 */
|
||
if (cb) cb();
|
||
return;
|
||
}
|
||
/* 插入乱码帧再还原 */
|
||
var fake = CHARS[Math.floor(Math.random() * CHARS.length)];
|
||
el.insertBefore(document.createTextNode(fake), cur);
|
||
setTimeout(function () {
|
||
/* 替换为真实字符 */
|
||
el.replaceChild(document.createTextNode(text[i]), el.childNodes[i]);
|
||
i++;
|
||
setTimeout(step, speed || 28);
|
||
}, 30);
|
||
}
|
||
setTimeout(step, 0);
|
||
}
|
||
|
||
/* 公开:直接打印到元素 */
|
||
function to(el, text, speed) {
|
||
if (!el) return;
|
||
_type(el, text, speed || 28);
|
||
}
|
||
|
||
return { to: to };
|
||
})();
|
||
|
||
/* ============================================================
|
||
数字翻牌动画
|
||
============================================================ */
|
||
function numRollIn(el) {
|
||
if (!el) return;
|
||
var nodes = el.querySelectorAll ? el.querySelectorAll(".num-roll") : [];
|
||
/* 如果没有子节点则包裹整个文本 */
|
||
if (!nodes.length) {
|
||
var orig = el.textContent;
|
||
var span = document.createElement("span");
|
||
span.className = "num-roll";
|
||
span.textContent = orig;
|
||
el.textContent = "";
|
||
el.appendChild(span);
|
||
/* 触发 reflow */
|
||
void span.offsetWidth;
|
||
span.style.animationDuration = ".38s";
|
||
} else {
|
||
nodes.forEach(function (n, i) {
|
||
void n.offsetWidth;
|
||
n.style.animationDelay = (i * 60) + "ms";
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
文字乱码还原
|
||
scramble(el, finalText, duration_ms)
|
||
============================================================ */
|
||
function scrambleText(el, finalText, duration) {
|
||
if (!el) return;
|
||
duration = duration || 320;
|
||
var CHARS = "アイウエオカキクケコ01サシスセソ10タチツテト#$%&ABXYZ";
|
||
var len = finalText.length;
|
||
var revealed = 0;
|
||
var interval = duration / (len * 2.5);
|
||
var timer = setInterval(function () {
|
||
var out = "";
|
||
for (var i = 0; i < len; i++) {
|
||
if (i < revealed) {
|
||
out += finalText[i];
|
||
} else {
|
||
out += CHARS[Math.floor(Math.random() * CHARS.length)];
|
||
}
|
||
}
|
||
el.textContent = out;
|
||
revealed += 0.8;
|
||
if (revealed >= len) {
|
||
el.textContent = finalText;
|
||
clearInterval(timer);
|
||
}
|
||
}, interval);
|
||
}
|
||
|
||
/* ============================================================
|
||
接入:给 renderGrid / loadResources / loadReco / openDetail 挂进度条
|
||
============================================================ */
|
||
(function patchWithCyBar() {
|
||
"use strict";
|
||
|
||
/* — renderGrid — */
|
||
var _origRenderGrid = window.renderGrid || (typeof renderGrid !== "undefined" ? renderGrid : null);
|
||
if (_origRenderGrid) {
|
||
window.renderGrid = function () {
|
||
CyBar.start("CATALOG::FETCH", 1);
|
||
_origRenderGrid.apply(this, arguments);
|
||
};
|
||
}
|
||
|
||
/* — loadCatalog 完成时推进进度条(monkey-patch renderList) —
|
||
renderList 在 loadCatalog resolve 后调用,是个好的钩子点 */
|
||
var _origRenderList = window.renderList || (typeof renderList !== "undefined" ? renderList : null);
|
||
if (_origRenderList) {
|
||
window.renderList = function () {
|
||
CyBar.finish("CATALOG::OK");
|
||
_origRenderList.apply(this, arguments);
|
||
};
|
||
}
|
||
|
||
/* — loadResources — */
|
||
var _origLoadResources = window.loadResources || (typeof loadResources !== "undefined" ? loadResources : null);
|
||
if (_origLoadResources) {
|
||
window.loadResources = function (item) {
|
||
CyBar.start("RESOURCE::FETCH", 1);
|
||
_origLoadResources.apply(this, arguments);
|
||
};
|
||
}
|
||
|
||
/* — renderResourcesFromRows — 资源渲染完毕 */
|
||
var _origRenderRows = window.renderResourcesFromRows || (typeof renderResourcesFromRows !== "undefined" ? renderResourcesFromRows : null);
|
||
if (_origRenderRows) {
|
||
window.renderResourcesFromRows = function () {
|
||
_origRenderRows.apply(this, arguments);
|
||
CyBar.finish("RESOURCE::OK");
|
||
};
|
||
}
|
||
|
||
/* — openDetail — */
|
||
var _origOpenDetail = window.openDetail || (typeof openDetail !== "undefined" ? openDetail : null);
|
||
if (_origOpenDetail) {
|
||
window.openDetail = function (item) {
|
||
CyBar.start("DETAIL::LOAD", 2);
|
||
_origOpenDetail.apply(this, arguments);
|
||
};
|
||
}
|
||
|
||
/* — loadReco 完成后 tick — */
|
||
var _origLoadReco = window.loadReco || (typeof loadReco !== "undefined" ? loadReco : null);
|
||
if (_origLoadReco) {
|
||
window.loadReco = function () {
|
||
_origLoadReco.apply(this, arguments);
|
||
setTimeout(function () { CyBar.tick("RECO::OK"); }, 600);
|
||
};
|
||
}
|
||
})();
|
||
|
||
/* ============================================================
|
||
Status bar:已禁用打字机效果,常显示
|
||
============================================================ */
|
||
|
||
/* ============================================================
|
||
评分翻牌:showDetail 后触发
|
||
============================================================ */
|
||
(function patchNumRoll() {
|
||
"use strict";
|
||
var _origRenderDetailInfo = window.renderDetailInfo || (typeof renderDetailInfo !== "undefined" ? renderDetailInfo : null);
|
||
if (!_origRenderDetailInfo) return;
|
||
window.renderDetailInfo = function (item) {
|
||
_origRenderDetailInfo.apply(this, arguments);
|
||
/* 评分元素在 #dRating 下 */
|
||
setTimeout(function () {
|
||
var dr = document.getElementById("dRating");
|
||
if (!dr) return;
|
||
var tags = dr.querySelectorAll(".rtag");
|
||
tags.forEach(function (tag, i) {
|
||
var orig = tag.textContent;
|
||
tag.textContent = "";
|
||
var span = document.createElement("span");
|
||
span.className = "num-roll";
|
||
span.style.animationDelay = (i * 80) + "ms";
|
||
span.textContent = orig;
|
||
tag.appendChild(span);
|
||
});
|
||
}, 120);
|
||
};
|
||
})();
|
||
|
||
/* ============================================================
|
||
标题乱码还原:openDetail 打开详情时触发
|
||
============================================================ */
|
||
(function patchTitleScramble() {
|
||
"use strict";
|
||
var _origShowDetail = window.showDetail || (typeof showDetail !== "undefined" ? showDetail : null);
|
||
if (!_origShowDetail) return;
|
||
window.showDetail = function (item) {
|
||
_origShowDetail.apply(this, arguments);
|
||
setTimeout(function () {
|
||
var h1 = document.querySelector("#sheet .herofg h1");
|
||
if (!h1) return;
|
||
var title = h1.textContent;
|
||
if (title) scrambleText(h1, title, 380);
|
||
}, 80); /* glitch-enter 动画开始后触发 */
|
||
};
|
||
})();
|
||
</script></body></html> |