Files
djk/internal/webui/details.js
T

167 lines
8.2 KiB
JavaScript

import { $, element, button, icon, coverURL, withFocus, dramaTitle, sourceKey, sourceLabel, categoryName, episodeCount, firstNonEmpty, tagsText, releaseText, setMessage } from './ui-core.js';
export function createDetails(app) {
let currentID = '';
const metadataMessages = new Map();
let poster;
const exporting = new Set();
const panel = $('detailPanel');
const content = $('detailContent');
function renderCover(drama) {
const address = coverURL(drama), title = dramaTitle(drama);
if (poster?.id === drama.id && poster.address === address) {
if (poster.image) poster.image.alt = title + ' 海报';
return poster.node;
}
const node = element('div', 'detail-cover');
const status = element('span', 'detail-cover-status', address ? '加载海报…' : '暂无海报');
status.setAttribute('role', 'status');
node.appendChild(status);
const current = {id: drama.id, address, node, failed: false};
poster = current;
if (!address) return node;
const image = element('img');
current.image = image;
image.alt = title + ' 海报';
image.decoding = 'async';
image.hidden = true;
image.addEventListener('load', () => {
if (poster !== current) return;
current.failed = false;
image.hidden = false;
status.hidden = true;
app.library.coverLoaded(drama.id, address);
});
image.addEventListener('error', () => {
if (poster !== current) return;
current.failed = true;
image.hidden = true;
status.hidden = false;
status.textContent = '海报加载失败';
app.library.coverFailed(drama.id, address);
});
current.retry = () => {
if (!current.failed) return;
current.failed = false;
status.textContent = '加载海报…';
image.src = address;
};
node.appendChild(image);
image.src = address;
return node;
}
function refreshCover(id, retry = false) {
if (!panel.open || currentID !== id) return;
const drama = app.library.get(id);
if (!drama) return;
const previous = poster?.node;
const next = renderCover(drama);
if (previous !== next) previous?.replaceWith(next);
if (retry) poster?.retry?.();
}
function render() {
if (!currentID) return;
const saved = app.following.get(currentID);
const history = window.JukuHistory.get(currentID);
const known = app.library.get(currentID);
const drama = known || {id: currentID, title: saved?.title || history?.title || '短剧', source: saved?.source || history?.source, totalEpisode: saved?.totalEpisode || history?.total, categoryName: saved?.category};
const title = dramaTitle(drama);
withFocus(content, () => {
content.replaceChildren();
const top = element('div', 'detail-top');
const heading = element('div', 'spacer');
const name = element('h1', '', title);
name.id = 'detailTitle';
heading.append(name, element('p', 'detail-meta', sourceLabel(sourceKey(drama)) + ' · ' + categoryName(drama)));
const tags = element('div', 'tags detail-tags');
tagsText(drama).slice(0, 10).forEach(tag => tags.appendChild(element('span', 'tag', tag)));
if (drama.vip === true) tags.prepend(element('span', 'tag vip-tag', 'VIP · 仅试看'));
heading.appendChild(tags);
top.append(renderCover(drama), heading);
content.appendChild(top);
const metadataStatus = element('p', 'small', metadataMessages.get(currentID) || '');
metadataStatus.id = 'detailMetadataStatus';
metadataStatus.setAttribute('role', 'status');
metadataStatus.hidden = !metadataStatus.textContent;
content.appendChild(metadataStatus);
if (drama.vip === true) content.appendChild(element('p', 'notice vip-notice', '此剧含 VIP 分集,站源仅提供试看;试看内容不会作为完整分集下载。'));
if (history) content.appendChild(element('p', 'detail-progress', window.JukuHistory.progressText(history)));
if (saved?.newEpisodes > 0) content.appendChild(element('p', 'following-update notice', '剧库新增 ' + saved.newEpisodes + ' 集'));
const actions = element('div', 'detail-actions');
const play = button(history ? '继续观看' : '开始观看', () => app.play(currentID, title), false, 'primary-action button-content');
play.prepend(icon('play'));
play.dataset.focusKey = 'detail-play';
const bookmark = button(saved?.saved ? '已收藏' : '加入想看', () => app.following.toggleSaved(currentID), app.following.busy(currentID), 'secondary');
bookmark.id = 'detailSaveBtn';
bookmark.dataset.focusKey = 'detail-save';
bookmark.setAttribute('aria-pressed', String(Boolean(saved?.saved)));
actions.append(play, bookmark);
content.appendChild(actions);
content.appendChild(element('p', 'detail-description', firstNonEmpty(drama.desc, drama.intro) || '站源暂未提供简介。'));
const facts = element('dl', 'detail-facts');
const values = [['集数', episodeCount(drama) ? episodeCount(drama) + ' 集' : '暂未提供'], ['状态', releaseText(drama.releaseStatus)], ['上线时间', drama.onlineDate], ['站点热度', drama.heat], ['播放量', drama.views], ['站点评分', drama.score]];
for (const [label, value] of values) {
if (!value) continue;
const item = element('div');
item.append(element('dt', '', label), element('dd', '', value));
facts.appendChild(item);
}
content.appendChild(facts);
const secondary = element('div', 'detail-secondary');
const download = button('加入下载', () => app.downloads.enqueue([currentID]), !known, 'secondary');
download.id = 'detailDownloadBtn';
download.dataset.focusKey = 'detail-download';
if (!known) download.title = '更新剧库找到此剧后可加入下载';
const watched = button(saved?.completed ? '取消已看标记' : '标为已看', () => app.following.setCompleted(currentID, !saved?.completed), app.following.busy(currentID), 'secondary');
watched.id = 'detailCompletedBtn';
watched.dataset.focusKey = 'detail-completed';
const emby = button(exporting.has(currentID) ? '导出中…' : '导出 Emby', () => exportEmby(currentID, title), !known || exporting.has(currentID), 'secondary');
emby.id = 'detailEmbyBtn';
emby.dataset.focusKey = 'detail-emby';
emby.title = '导出 STRM 分集文件,用于现有 Emby 电视剧媒体库';
if (app.viewer?.onlineOnly) secondary.appendChild(watched);
else secondary.append(app.downloads.qualityControl(), download, watched, emby);
actions.after(secondary);
content.appendChild(element('p', 'small notice', '手动标记用于整理清单,实际播放进度仍自动保存。'));
});
}
async function exportEmby(id, title) {
if (exporting.has(id)) return;
exporting.add(id);
render();
try {
const response = await fetch('/api/emby/export', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({dramaId: id, baseUrl: location.origin})});
if (!response.ok) {const result = await response.json(); throw new Error(result.error || '导出失败');}
const address = URL.createObjectURL(await response.blob());
const link = document.createElement('a');
link.href = address;
link.download = title + '-Emby.zip';
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(address), 60000);
setMessage('已导出 Emby 分集;解压到 Emby 电视剧媒体库后扫描即可。');
} catch (error) {setMessage('Emby 导出失败:' + error.message, true);}
finally {exporting.delete(id); if (panel.open && currentID === id) render();}
}
function open(id) {
currentID = id;
render();
window.JukuDialogs.open(panel);
app.library.refreshDrama(id);
}
function metadataStatus(id, text) {
metadataMessages.set(id, text);
const status = $('detailMetadataStatus');
if (panel.open && currentID === id && status) {status.textContent = text; status.hidden = !text;}
}
return {open, metadataStatus, refreshCover, retryCover: () => refreshCover(currentID, true), refresh: () => {if (panel.open) render();}};
}