初始提交:短剧库源码(含转码优化 ultrafast)
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import { $, element, button, sourceLabel, normalizeSource, readPreference, savePreference, populateIcons } from './ui-core.js';
|
||||
|
||||
export function createShell(app) {
|
||||
const main = $('workspaceMain');
|
||||
const positions = {library: 0, following: 0, downloads: 0, users: 0};
|
||||
let current = '', sourceStates = {}, sourceLoading = false, sourceRequest = '';
|
||||
let sourceNotice;
|
||||
const sourceRows = new Map(), sourceErrors = new Map();
|
||||
|
||||
function setPage(page) {
|
||||
if (!app.viewer && page === 'users') return;
|
||||
if (app.viewer && (page === 'users' && !app.viewer.account?.admin || page === 'downloads' && app.viewer.onlineOnly)) {
|
||||
window.JukuDialogs.navigate('library');
|
||||
page = 'library';
|
||||
}
|
||||
if (page === current) return;
|
||||
if (current) positions[current] = main.scrollTop;
|
||||
current = page;
|
||||
document.body.dataset.page = page;
|
||||
document.querySelectorAll('[data-view]').forEach(view => view.hidden = view.dataset.view !== page);
|
||||
document.querySelectorAll('button[data-page]').forEach(control => {
|
||||
if (control.dataset.page === page) control.setAttribute('aria-current', 'page');
|
||||
else control.removeAttribute('aria-current');
|
||||
});
|
||||
if (page === 'users') app.users?.load();
|
||||
if (page === 'following') app.following?.render();
|
||||
if (page === 'downloads') app.downloads?.render();
|
||||
if (page === 'library') app.library?.layout();
|
||||
requestAnimationFrame(() => {main.scrollTop = positions[page] || 0;});
|
||||
}
|
||||
|
||||
function activity(visible) {
|
||||
visible = visible && !app.viewer?.onlineOnly;
|
||||
$('activityPanel').hidden = !visible;
|
||||
$('catalogLayout').classList.toggle('with-activity', visible);
|
||||
$('activityToggle').setAttribute('aria-pressed', String(visible));
|
||||
$('activityToggleLabel').textContent = visible ? '收起任务' : '任务侧栏';
|
||||
if (!app.viewer?.onlineOnly) savePreference('activitySidebar', visible);
|
||||
if (visible) app.downloads?.render();
|
||||
}
|
||||
|
||||
function info(title, build) {
|
||||
sourceRows.clear();
|
||||
sourceNotice = null;
|
||||
$('infoTitle').textContent = title;
|
||||
$('infoContent').replaceChildren();
|
||||
if (typeof build === 'function') build($('infoContent'));
|
||||
else $('infoContent').appendChild(element('p', '', build));
|
||||
window.JukuDialogs.open('infoPanel');
|
||||
}
|
||||
|
||||
function updateSourceRows() {
|
||||
const statuses = {loading: '正在获取', ready: '已就绪', success: '已更新', done: '已更新', failed: '暂不可用', error: '暂不可用', partial: '部分更新', cached: '使用缓存', pending: '等待更新'};
|
||||
for (const [id, controls] of sourceRows) {
|
||||
const state = sourceStates[id];
|
||||
const allowed = !app.viewer?.sources || app.viewer.sources.includes(normalizeSource(id));
|
||||
const busy = sourceRequest === id || sourceLoading && state?.status === 'loading';
|
||||
controls.row.hidden = !allowed;
|
||||
controls.status.textContent = sourceRequest === id ? '正在提交' : state ? statuses[state.status] || (state.error ? '暂不可用' : '已有缓存') : '未检查';
|
||||
const date = state?.updatedAt && !state.updatedAt.startsWith('0001') ? new Date(state.updatedAt).toLocaleString() : '';
|
||||
controls.detail.textContent = [Number.isFinite(state?.count) ? state.count + ' 部' : '', date].filter(Boolean).join(' · ');
|
||||
controls.error.textContent = sourceErrors.get(id) || state?.error || '';
|
||||
controls.action.disabled = !allowed || sourceLoading || Boolean(sourceRequest);
|
||||
controls.action.textContent = busy ? '更新中…' : '更新';
|
||||
controls.action.setAttribute('aria-busy', String(busy));
|
||||
controls.action.title = sourceLoading || sourceRequest ? '正在更新剧库,完成后可再次更新' : '只更新' + controls.label + '的目录和资料';
|
||||
}
|
||||
if (sourceNotice) sourceNotice.textContent = sourceLoading || sourceRequest ? '正在更新剧库,已有内容可继续使用;完成后可单独更新站源。' : '点击对应站源的“更新”可查新、续载和补齐资料,失败时保留已有内容。';
|
||||
}
|
||||
|
||||
async function updateSource(id) {
|
||||
if (sourceLoading || sourceRequest || app.viewer?.sources && !app.viewer.sources.includes(normalizeSource(id))) return;
|
||||
sourceRequest = id;
|
||||
sourceErrors.delete(id);
|
||||
updateSourceRows();
|
||||
try {
|
||||
const result = await app.api('/api/ui/dramas?update=1&source=' + encodeURIComponent(id));
|
||||
sourceStates = result.sources || {};
|
||||
sourceLoading = Boolean(result.loading);
|
||||
if (result.updateAccepted === false) sourceErrors.set(id, '本次未提交:已有更新任务,可在完成后重试。');
|
||||
} catch (error) {
|
||||
sourceErrors.set(id, '更新请求失败:' + error.message);
|
||||
} finally {
|
||||
sourceRequest = '';
|
||||
updateSourceRows();
|
||||
void app.library.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function showSources() {
|
||||
const names = {cloudfront: '黄果旧 API', huangguoai: '黄果网页', 'huangguo-video': '黄果备用网页', huangdou: '黄豆', hongguo: '红果'};
|
||||
info('站源状态', content => {
|
||||
for (const [id, label] of Object.entries(names)) {
|
||||
if (app.viewer?.sources && !app.viewer.sources.includes(normalizeSource(id))) continue;
|
||||
const row = element('section', 'source-status-row');
|
||||
row.dataset.source = id;
|
||||
const heading = element('div', 'heading source-status-heading');
|
||||
const actions = element('div', 'source-status-actions');
|
||||
const status = element('span', 'tag');
|
||||
status.setAttribute('aria-live', 'polite');
|
||||
const action = button('更新', () => updateSource(id), false, 'secondary source-update');
|
||||
action.setAttribute('aria-label', '更新' + label);
|
||||
actions.append(status, action);
|
||||
heading.append(element('h3', '', label), actions);
|
||||
const detail = element('p', 'small'), error = element('p', 'error');
|
||||
error.setAttribute('role', 'status');
|
||||
row.append(heading, detail, error);
|
||||
sourceRows.set(id, {row, label, status, action, detail, error});
|
||||
content.appendChild(row);
|
||||
}
|
||||
sourceNotice = element('p', 'small notice');
|
||||
content.appendChild(sourceNotice);
|
||||
updateSourceRows();
|
||||
});
|
||||
void app.library.refresh();
|
||||
}
|
||||
|
||||
function positionMenu(menu) {
|
||||
const popover = menu.querySelector('.action-popover');
|
||||
if (!menu.open || !popover) return;
|
||||
popover.style.maxHeight = '';
|
||||
const anchor = menu.getBoundingClientRect();
|
||||
const viewport = main.getBoundingClientRect();
|
||||
const width = popover.getBoundingClientRect().width;
|
||||
const left = Math.max(12, Math.min(anchor.left, window.innerWidth - width - 12));
|
||||
const below = Math.max(0, Math.min(window.innerHeight, viewport.bottom) - anchor.bottom - 12);
|
||||
const above = Math.max(0, anchor.top - Math.max(0, viewport.top) - 12);
|
||||
const height = popover.scrollHeight + 2;
|
||||
const upward = height > below && above > below;
|
||||
popover.style.left = left - anchor.left + 'px';
|
||||
popover.style.right = 'auto';
|
||||
popover.style.maxHeight = Math.max(80, upward ? above : below) + 'px';
|
||||
popover.style.overflowY = 'auto';
|
||||
popover.style.top = upward ? -(Math.min(height, above) + 6) + 'px' : anchor.height + 6 + 'px';
|
||||
}
|
||||
|
||||
function access() {
|
||||
updateSourceRows();
|
||||
document.body.classList.toggle('online-only', Boolean(app.viewer?.onlineOnly));
|
||||
document.querySelectorAll('[data-admin-only]').forEach(control => {control.hidden = !app.viewer?.account?.admin;});
|
||||
document.querySelectorAll('button[data-page=downloads], [data-go=downloads]').forEach(control => {control.hidden = Boolean(app.viewer?.onlineOnly);});
|
||||
activity(readPreference('activitySidebar', false) === true);
|
||||
setPage(window.JukuDialogs.page());
|
||||
}
|
||||
|
||||
function init() {
|
||||
populateIcons();
|
||||
window.JukuDialogs.listen(setPage);
|
||||
document.querySelectorAll('button[data-page], [data-go]').forEach(control => control.addEventListener('click', () => {
|
||||
if (control.dataset.go === 'following') app.following.showTab('watching');
|
||||
window.JukuDialogs.navigate(control.dataset.page || control.dataset.go);
|
||||
}));
|
||||
document.querySelector('.brand').addEventListener('click', event => {event.preventDefault(); window.JukuDialogs.navigate('library');});
|
||||
document.querySelector('.skip-link').addEventListener('click', event => {event.preventDefault(); main.focus();});
|
||||
$('headerMenu').addEventListener('click', event => {
|
||||
if (event.target.closest('button')) {
|
||||
$('headerMenu').open = false;
|
||||
$('moreButton').focus({preventScroll: true});
|
||||
}
|
||||
}, true);
|
||||
$('headerMenu').addEventListener('toggle', () => $('moreButton').setAttribute('aria-expanded', String($('headerMenu').open)));
|
||||
$('menuHistoryBtn').addEventListener('click', () => $('openHistoryBtn').click());
|
||||
$('sourceStatusBtn').addEventListener('click', showSources);
|
||||
$('aboutBtn').addEventListener('click', () => info('使用提示', content => {
|
||||
for (const text of ['输入关键词先筛选本地剧库,点击“联网搜索”补充红果结果。', '更新剧库会查新、继续加载目录,并在后台补齐历史资料。', '登录账号后,观看进度和追剧清单可跨设备接续;允许匿名访问时,匿名记录仍按浏览器独立保存。', '下载保存到运行短剧库的电脑;手机浏览器不会把下载任务保存到手机相册。']) content.appendChild(element('p', 'notice', text));
|
||||
}));
|
||||
$('activityToggle').addEventListener('click', () => activity($('activityPanel').hidden));
|
||||
$('closeActivityBtn').addEventListener('click', () => activity(false));
|
||||
$('downloadLocationBtn').addEventListener('click', () => {
|
||||
window.JukuDialogs.open('settingsPanel');
|
||||
$('downloadDirectory').focus();
|
||||
});
|
||||
document.addEventListener('click', event => {
|
||||
for (const menu of document.querySelectorAll('.action-menu[open]')) {
|
||||
if (!menu.contains(event.target) || event.target.closest('button')) menu.open = false;
|
||||
}
|
||||
});
|
||||
document.addEventListener('toggle', event => {
|
||||
if (event.target.matches?.('.action-menu')) positionMenu(event.target);
|
||||
}, true);
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key !== 'Escape' || document.querySelector('dialog[open]')) return;
|
||||
const menu = document.querySelector('.action-menu[open]');
|
||||
if (!menu) return;
|
||||
event.preventDefault();
|
||||
menu.open = false;
|
||||
menu.querySelector('summary').focus({preventScroll: true});
|
||||
});
|
||||
const positionMenus = () => document.querySelectorAll('.action-menu[open]').forEach(positionMenu);
|
||||
window.addEventListener('resize', positionMenus);
|
||||
main.addEventListener('scroll', positionMenus, {passive: true});
|
||||
activity(readPreference('activitySidebar', false) === true);
|
||||
setPage(window.JukuDialogs.page());
|
||||
}
|
||||
|
||||
return {init, access, info, page: () => current, resetScroll: () => {main.scrollTop = 0;}, sourceStates: (value, loading) => {sourceStates = value || {}; sourceLoading = Boolean(loading); updateSourceRows();}};
|
||||
}
|
||||
Reference in New Issue
Block a user