79 lines
8.2 KiB
TypeScript
79 lines
8.2 KiB
TypeScript
import { contentPath } from '../routing';
|
|
import type { Chapter, Config, DramaDetail } from "../api/types";
|
|
import { formatCount, recordList, safeUrl } from "../api/protocol";
|
|
import { escape, imageAttributes, title } from "./render";
|
|
|
|
// Original HTML action icons, kept as SVG paths so active colors follow the stylesheet.
|
|
const actionPaths = {
|
|
"save": "M857.6 327.68l-104.494545-34.210909a178.036364 178.036364 0 0 1-90.763637-66.56l-64-89.367273a104.96 104.96 0 0 0-23.272727-23.272727 106.123636 106.123636 0 0 0-148.48 23.272727l-64.930909 89.367273a179.665455 179.665455 0 0 1-90.763637 66.792727l-104.494545 33.978182a106.821818 106.821818 0 0 0-53.061818 162.909091l93.090909 128.930909a35.84 35.84 0 0 1 6.981818 21.643636v158.952728a109.381818 109.381818 0 0 0 5.12 33.512727 105.658182 105.658182 0 0 0 133.818182 67.956364l128-39.33091a107.985455 107.985455 0 0 1 64.232727 0l127.534546 39.33091a106.123636 106.123636 0 0 0 139.636363-101.469091v-158.254546a35.84 35.84 0 0 1 6.981818-21.643636l93.09091-128.930909a104.494545 104.494545 0 0 0 15.127272-29.556364 106.821818 106.821818 0 0 0-69.352727-134.050909z m-192 83.549091a34.909091 34.909091 0 0 1-48.174545 10.938182l-46.545455-29.090909a34.909091 34.909091 0 0 1 37.003636-59.112728l46.545455 29.090909a34.909091 34.909091 0 0 1 11.170909 48.174546z",
|
|
"like": "M855.738182 198.283636A252.043636 252.043636 0 0 0 670.254545 116.363636a225.28 225.28 0 0 0-160.814545 63.069091 248.32 248.32 0 0 0-152.901818-53.294545A251.578182 251.578182 0 0 0 171.287273 209.454545 287.883636 287.883636 0 0 0 162.909091 588.567273l256 275.083636a122.181818 122.181818 0 0 0 90.065454 39.563636 121.716364 121.716364 0 0 0 89.832728-39.563636l256-272.989091 2.792727-3.723636a290.443636 290.443636 0 0 0-1.861818-388.654546z",
|
|
"share": "M910.661818 117.992727a114.734545 114.734545 0 0 0-113.803636-16.756363L134.050909 363.752727a138.472727 138.472727 0 0 0-15.592727 250.181818l97.978182 54.225455 1.861818 162.909091a87.272727 87.272727 0 0 0 137.309091 69.818182L482.210909 814.545455l180.363636 99.374545a138.472727 138.472727 0 0 0 203.869091-101.236364L954.181818 224.814545a113.803636 113.803636 0 0 0-43.52-106.821818z",
|
|
"feedback": "M505.949091 38.4a465.454545 465.454545 0 1 0 465.454545 465.454545 465.454545 465.454545 0 0 0-465.454545-465.454545z m0 721.454545a46.545455 46.545455 0 1 1 46.545454-46.545454 46.545455 46.545455 0 0 1-46.545454 46.545454z m43.287273-188.276363a11.636364 11.636364 0 0 0-8.378182 11.170909v25.832727a34.909091 34.909091 0 0 1-69.818182 0V581.818182a81.454545 81.454545 0 0 1 58.88-78.196364 87.04 87.04 0 0 0 60.741818-102.865454A86.807273 86.807273 0 0 0 433.803636 372.363636a90.298182 90.298182 0 0 0-11.403636 24.436364 34.909091 34.909091 0 0 1-67.025455-19.549091 162.909091 162.909091 0 0 1 20.712728-43.985454 156.625455 156.625455 0 1 1 173.149091 238.312727z",
|
|
"phone": "M756.363636 69.818182h-488.727272A128.232727 128.232727 0 0 0 139.636364 197.818182v628.363636a128.232727 128.232727 0 0 0 128 128h488.727272a128 128 0 0 0 128-128v-628.363636A128 128 0 0 0 756.363636 69.818182z m-139.636363 698.181818h-209.454546a34.909091 34.909091 0 0 1 0-69.818182h209.454546a34.909091 34.909091 0 0 1 0 69.818182z"
|
|
} as const;
|
|
|
|
export function detailActionIcon(kind: keyof typeof actionPaths): string {
|
|
return `<svg class="play-action-icon" viewBox="0 0 1024 1024" aria-hidden="true"><path d="${actionPaths[kind]}"/></svg>`;
|
|
}
|
|
|
|
export function overviewMarkup(drama: DramaDetail, first: Chapter | undefined, actions: string): string {
|
|
const genres = (Array.isArray(drama.tagNames) ? drama.tagNames : Array.isArray(drama.themeNames) ? drama.themeNames : []).filter(tag => typeof tag === 'string');
|
|
const badge = drama.isFeatured ? '<span class="series-badge series-badge--hot">爆剧</span>' : drama.isNew ? '<span class="series-badge series-badge--new">新剧</span>' : '';
|
|
return `<div class="series-overview">
|
|
<img class="series-poster" ${imageAttributes(drama.cover || drama.coverImg)} width="640" height="914" alt="${escape(title(drama))}封面">
|
|
<div class="series-info">
|
|
<div class="series-title-row"><h1 id="play-detail-title">${escape(title(drama))}</h1><span class="series-heat"><img class="series-heat-icon" src="/assets/9df0afe903e8d542.svg" width="16" height="16" alt="剧集热度图标" aria-hidden="true">${formatCount(drama.heatValue)}热度</span></div>
|
|
<div class="series-tags" aria-label="剧集标签">${badge}<span class="series-stat" data-engage-count="collect" aria-live="polite">${formatCount(drama.countCollect)}收藏</span><span class="series-stat" data-engage-count="like" aria-live="polite">${formatCount(drama.countLike)}点赞</span></div>
|
|
${genres.length ? `<p class="series-genres">${escape(genres.join(' · '))}</p>` : ''}
|
|
<div class="series-description-wrap"><p class="series-description" id="series-description-text">${escape(drama.desc || drama.description)}</p><button class="series-description-toggle" type="button" aria-expanded="false" aria-controls="series-description-text" hidden>展开</button></div>
|
|
<div class="series-action-bar"><div class="series-actions">
|
|
${first ? `<a class="primary detail-play" href="${contentPath('play',drama,first.id)}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg><span>立即播放</span></a>` : '<span class="series-unavailable">暂无可用剧集</span>'}
|
|
<a class="series-download" data-app-download-entry data-app-download target="_blank" rel="sponsored nofollow noopener noreferrer" hidden><svg class="series-download-phone" viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M10 5h4M11 19h2"/></svg><span>下载 App</span></a>
|
|
</div>${actions}</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
export function officialLinks(config: Config): string {
|
|
const settings = {...config.newAddressInfo,...config.config};
|
|
const primary = safeUrl(settings.newUrlAddr) || recordList<{url?:unknown}>(config.navPageList).map(item => safeUrl(item.url)).find(Boolean) || '';
|
|
const permanent = safeUrl(settings.foreverUrlAddr);
|
|
const group = safeUrl(settings.tgGroup);
|
|
const contact = safeUrl(settings.telegram);
|
|
const external = (url: string, label = url) => `<a href="${escape(url)}" target="_blank" rel="sponsored nofollow noopener noreferrer">${escape(label)}</a>`;
|
|
return `<section class="play-official" aria-label="官方链接">
|
|
${primary ? `<div class="play-official-line play-official-inline"><span>官方入口:</span>${external(primary)}</div>` : ''}
|
|
${permanent ? `<div class="play-official-line play-official-inline"><span>永久地址:</span>${external(permanent)}</div>` : ''}
|
|
${group ? `<div class="play-official-line">${external(group,'点击加入91短剧官方 Telegram 群')}</div>` : ''}
|
|
<div class="play-official-line play-official-inline"><span>官方客服:</span>${contact ? external(contact,'立即联系') : '<a href="/info/feedback/">立即联系</a>'}</div>
|
|
</section>`;
|
|
}
|
|
|
|
export function bindSeriesDescription(root: HTMLElement, signal: AbortSignal): () => void {
|
|
const text = root.querySelector<HTMLElement>('#series-description-text');
|
|
const toggle = root.querySelector<HTMLButtonElement>('.series-description-toggle');
|
|
if (!text || !toggle) return () => {};
|
|
const update = () => {
|
|
if (signal.aborted || !text.clientWidth) return;
|
|
const lineHeight = Number.parseFloat(getComputedStyle(text).lineHeight) || 26.6;
|
|
const overflow = text.scrollHeight > lineHeight*3 + 1;
|
|
toggle.hidden = !overflow;
|
|
if (!overflow) {
|
|
text.classList.remove('is-expanded');
|
|
toggle.setAttribute('aria-expanded','false');toggle.textContent = '展开';
|
|
}
|
|
};
|
|
toggle.addEventListener('click',() => {
|
|
const expanded = toggle.getAttribute('aria-expanded') !== 'true';
|
|
text.classList.toggle('is-expanded',expanded);
|
|
toggle.setAttribute('aria-expanded',String(expanded));toggle.textContent = expanded ? '收起' : '展开';
|
|
},{signal});
|
|
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update);
|
|
observer?.observe(text);
|
|
if (!observer) window.addEventListener('resize',update,{signal});
|
|
const dispose = () => observer?.disconnect();
|
|
signal.addEventListener('abort',dispose,{once:true});
|
|
update();
|
|
return dispose;
|
|
}
|