const HOTNESS_AUTH_TOKEN_KEY = "video-hotness-auth-token-v1";
const authGate = document.querySelector("#auth-gate");
const authPassword = document.querySelector("#auth-password");
const authMessage = document.querySelector("#auth-message");
const form = document.querySelector("#collect-form");
const input = document.querySelector("#program-name");
const button = document.querySelector("#collect-button");
const exportLink = document.querySelector("#export-link");
const statusDot = document.querySelector("#status-dot");
const statusText = document.querySelector("#status-text");
const tableTitle = document.querySelector("#table-title");
const runCount = document.querySelector("#run-count");
const cards = document.querySelector("#cards");
const programList = document.querySelector("#program-list");
const networkLinks = document.querySelector("#network-links");
const collectPlatformBox = document.querySelector(".collect-platforms");
const mobileNote = document.querySelector("#mobile-note");
const mobileDeviceNameInput = document.querySelector("#mobile-device-name");
const saveDeviceNameButton = document.querySelector("#save-device-name-button");
const saveOfflineButton = document.querySelector("#save-offline-button");
const offlineCount = document.querySelector("#offline-count");
const offlineList = document.querySelector("#offline-list");
const clearOfflineButton = document.querySelector("#clear-offline-button");
const syncOfflineButton = document.querySelector("#sync-offline-button");
const mobileBatchText = document.querySelector("#mobile-batch-text");
const saveBatchOfflineButton = document.querySelector("#save-batch-offline-button");
const offlineStatus = document.querySelector("#offline-status");
const installHint = document.querySelector("#install-hint");
const installStatus = document.querySelector("#install-status");
const installAppButton = document.querySelector("#install-app-button");
const mobileServerUrlInput = document.querySelector("#mobile-server-url");
const saveMobileServerButton = document.querySelector("#save-mobile-server-button");
const testMobileServerButton = document.querySelector("#test-mobile-server-button");
const mobileBindingSummary = document.querySelector("#mobile-binding-summary");
const mobileAppState = document.querySelector("#mobile-app-state");
const MOBILE_DRAFTS_KEY = "video-hotness-mobile-drafts-v1";
const MOBILE_DEVICE_KEY = "video-hotness-mobile-device-v1";
const MOBILE_SERVER_KEY = "video-hotness-mobile-server-v1";
const platformOrder = ["tencent", "youku", "iqiyi", "mgtv"];
const platformLabels = {
tencent: "腾讯视频",
youku: "优酷",
iqiyi: "爱奇艺",
mgtv: "芒果TV",
};
const metricLabels = {
tencent: "热度值",
youku: "热度值",
iqiyi: "内容热度",
mgtv: "播放次数",
};
const urlInputs = {
tencent: document.querySelector("#url-tencent"),
youku: document.querySelector("#url-youku"),
iqiyi: document.querySelector("#url-iqiyi"),
mgtv: document.querySelector("#url-mgtv"),
};
let activeName = "";
let dirtyUrlInputs = new Set();
let deferredInstallPrompt = null;
let appStarted = false;
for (const [platform, element] of Object.entries(urlInputs)) {
element.addEventListener("input", () => {
dirtyUrlInputs.add(platform);
});
}
input.addEventListener("input", () => {
const name = input.value.trim();
if (activeName && name !== activeName) {
clearUrlInputs();
}
});
form.addEventListener("submit", async (event) => {
event.preventDefault();
const name = input.value.trim();
if (!name) return;
activeName = name;
const platforms = readCollectPlatforms();
if (platforms.length === 0) {
setStatus("error", "请至少选择一个采集平台");
return;
}
setBusy(true, `正在采集《${name}》`);
try {
const payload = await postJson("/api/collect", { name, urls: readUrlInputs(), platforms });
renderHistory(payload.history);
await refreshPrograms();
setStatus("ok", `已新增 ${formatTime(payload.collection.captured_at)} 这一列`);
} catch (error) {
setStatus("error", error.message);
} finally {
setBusy(false);
}
});
collectPlatformBox.addEventListener("change", (event) => {
if (!event.target.matches("input[type='checkbox']")) return;
updateCollectPlatformState();
});
saveOfflineButton.addEventListener("click", () => {
saveOfflineDraft();
});
saveDeviceNameButton.addEventListener("click", () => {
saveMobileDeviceName();
});
saveMobileServerButton.addEventListener("click", () => {
saveMobileServerUrl();
});
testMobileServerButton.addEventListener("click", () => {
testMobileServerConnection();
});
installAppButton.addEventListener("click", () => {
installMobileApp();
});
saveBatchOfflineButton.addEventListener("click", () => {
saveBatchOfflineDrafts();
});
syncOfflineButton.addEventListener("click", () => {
syncOfflineDrafts();
});
clearOfflineButton.addEventListener("click", () => {
const drafts = readOfflineDrafts();
if (drafts.length === 0) return;
if (!window.confirm(`确定清空 ${drafts.length} 条手机待同步记录吗?`)) return;
localStorage.setItem(MOBILE_DRAFTS_KEY, "[]");
renderOfflineDrafts();
setStatus("ok", "已清空手机待同步列表");
});
offlineList.addEventListener("click", (event) => {
const editButton = event.target.closest("[data-edit-draft]");
if (editButton) {
editOfflineDraft(editButton.dataset.editDraft);
return;
}
const deleteButton = event.target.closest("[data-delete-draft]");
if (deleteButton) {
deleteOfflineDraft(deleteButton.dataset.deleteDraft);
}
});
programList.addEventListener("click", async (event) => {
const item = event.target.closest("[data-name]");
if (!item) return;
activeName = item.dataset.name;
input.value = activeName;
await loadHistory(activeName);
});
window.addEventListener("online", updateOfflineStatus);
window.addEventListener("offline", updateOfflineStatus);
window.addEventListener("beforeinstallprompt", (event) => {
event.preventDefault();
deferredInstallPrompt = event;
updateInstallPrompt("ready");
});
window.addEventListener("appinstalled", () => {
deferredInstallPrompt = null;
updateInstallPrompt("installed");
});
initializeApp();
async function initializeApp() {
if (!(await ensureAccessAuth())) return;
startApp();
}
async function startApp() {
if (appStarted) return;
appStarted = true;
updateCollectPlatformState();
mobileDeviceNameInput.value = mobileDeviceName();
mobileServerUrlInput.value = mobileServerBaseUrl();
registerMobileServiceWorker();
updateInstallPrompt(isStandaloneDisplay() ? "installed" : "manual");
updateOfflineStatus();
updateMobileBindingSummary();
renderOfflineDrafts();
await Promise.all([refreshPrograms(), loadNetworkLinks()]);
}
async function registerMobileServiceWorker() {
if (!("serviceWorker" in navigator)) {
if (installStatus) installStatus.textContent = "当前浏览器不支持离线缓存,可继续使用手机待同步列表。";
return;
}
try {
await navigator.serviceWorker.register("/mobile-sw.js");
} catch {
if (installStatus) installStatus.textContent = "离线缓存注册失败,可刷新后重试。";
}
}
async function installMobileApp() {
if (!deferredInstallPrompt) {
updateInstallPrompt("manual");
return;
}
installAppButton.disabled = true;
deferredInstallPrompt.prompt();
const choice = await deferredInstallPrompt.userChoice.catch(() => ({ outcome: "dismissed" }));
deferredInstallPrompt = null;
installAppButton.disabled = false;
updateInstallPrompt(choice.outcome === "accepted" ? "installed" : "manual");
}
function updateInstallPrompt(state) {
if (!installStatus || !installAppButton) return;
installHint.classList.toggle("install-ready", state === "ready");
if (state === "ready") {
installStatus.textContent = "当前浏览器支持直接安装,点击按钮后会添加到手机桌面。";
installAppButton.hidden = false;
return;
}
installAppButton.hidden = true;
installStatus.textContent = state === "installed"
? "已用 App 模式打开;离线录入和待同步列表可继续使用。"
: "如果没有安装按钮,请在手机浏览器菜单选择“添加到主屏幕”。";
}
function isStandaloneDisplay() {
return window.matchMedia?.("(display-mode: standalone)").matches || window.navigator.standalone === true;
}
function updateOfflineStatus() {
if (!offlineStatus) return;
const online = navigator.onLine;
const pendingDrafts = readOfflineDrafts().filter((draft) => draft.sync_status !== "synced");
if (mobileAppState) {
mobileAppState.textContent = online ? "在线" : "离线";
mobileAppState.classList.toggle("offline", !online);
}
offlineStatus.classList.toggle("offline", !online);
offlineStatus.innerHTML = online && pendingDrafts.length
? `有 ${pendingDrafts.length} 条可同步电脑可访问时点击“同步到电脑”,同步后会显示电脑已收到。`
: online
? `离线录入已准备首次打开后会缓存手机版;离开局域网时仍可保存待同步。`
: `当前离线可以继续录入并保存待同步;回到局域网后再同步到电脑。`;
}
async function loadHistory(name) {
setStatus("busy", `正在读取《${name}》历史`);
try {
const payload = await getJson(`/api/history?name=${encodeURIComponent(name)}`);
renderHistory(payload.history);
setStatus("ok", `已载入《${name}》`);
} catch (error) {
setStatus("error", error.message);
}
}
async function refreshPrograms() {
try {
const payload = await getJson("/api/programs");
renderPrograms(payload.programs || []);
} catch {
renderPrograms([]);
}
}
async function loadNetworkLinks() {
try {
const payload = await getJson("/api/network");
const urls = payload.urls || [];
networkLinks.innerHTML = urls.length
? urls.map((url) => `${escapeHtml(url)}`).join("
")
: "没有读取到局域网地址,可先用本机浏览器访问。";
} catch {
networkLinks.textContent = "局域网地址读取失败。";
}
}
function mobileServerBaseUrl() {
const saved = (localStorage.getItem(MOBILE_SERVER_KEY) || "").trim();
return saved || window.location.origin;
}
function normalizeServerUrl(value) {
const text = String(value || "").trim().replace(/\/+$/, "");
if (!text) return "";
try {
const parsed = new URL(text);
return parsed.origin;
} catch {
return "";
}
}
function apiUrl(path) {
return `${mobileServerBaseUrl()}${path}`;
}
function saveMobileServerUrl() {
const normalized = normalizeServerUrl(mobileServerUrlInput.value);
if (!normalized) {
setStatus("error", "请输入正确的电脑或 NAS 地址,例如 http://192.168.18.120:3001");
return;
}
localStorage.setItem(MOBILE_SERVER_KEY, normalized);
mobileServerUrlInput.value = normalized;
updateMobileBindingSummary();
setStatus("ok", `已绑定地址:${normalized}`);
}
async function testMobileServerConnection() {
const normalized = normalizeServerUrl(mobileServerUrlInput.value) || mobileServerBaseUrl();
if (normalized !== mobileServerBaseUrl()) {
localStorage.setItem(MOBILE_SERVER_KEY, normalized);
mobileServerUrlInput.value = normalized;
}
testMobileServerButton.disabled = true;
setStatus("busy", "正在测试电脑端连接");
try {
const payload = await getJson("/api/network");
updateMobileBindingSummary({ ok: true });
setStatus("ok", `连接正常,读取到 ${(payload.urls || []).length} 个手机访问地址`);
} catch (error) {
updateMobileBindingSummary({ ok: false, error: error.message });
setStatus("error", `连接失败:${error.message}`);
} finally {
testMobileServerButton.disabled = false;
}
}
function updateMobileBindingSummary(result = null) {
const base = mobileServerBaseUrl();
const pendingDrafts = readOfflineDrafts().filter((draft) => draft.sync_status !== "synced").length;
const resultText = result?.ok ? " · 连接正常" : result?.error ? ` · 连接失败:${result.error}` : "";
mobileBindingSummary.textContent = `当前绑定:${base} · 待同步 ${pendingDrafts} 条${resultText}`;
}
function renderPrograms(programs) {
if (programs.length === 0) {
programList.innerHTML = `