import { showStatusMessage } from "./ui.js"; import { getActiveTVTab, openTVPanelTab, isTVPanelVisible, setTVPanelVisible } from "./tv.js"; // News aggregation now lives inside the shared media panel: // - outer shell: #media-panel // - this module renders into inner pane: #news-panel const EARTH_NEWS_API = "/api/v1/news/earth-feed"; const FOCUS_UPDATE_INTERVAL_MS = 4000; const DATA_REFRESH_INTERVAL_MS = 180000; const MIN_REGION_SWITCH_INTERVAL_MS = 2500; const REQUEST_TIMEOUT_MS = 15000; let initialized = false; let refreshPromise = null; let payload = null; let lastFocus = null; let lastFetchAt = 0; let lastRegionSwitchAt = 0; function getElements() { return { toggleBtn: document.getElementById("toggle-news"), refreshBtn: document.getElementById("news-refresh"), openBtn: document.getElementById("news-open-external"), status: document.getElementById("news-board-status"), focusLabel: document.getElementById("news-focus-label"), focusCoords: document.getElementById("news-focus-coords"), sourceCount: document.getElementById("news-source-count"), regionChip: document.getElementById("news-region-chip"), board: document.getElementById("news-board-list"), empty: document.getElementById("news-board-empty"), feedAnchor: document.getElementById("news-feed-anchor"), }; } function formatCoord(value, positiveLabel, negativeLabel) { const abs = Math.abs(value).toFixed(1); return `${abs}°${value >= 0 ? positiveLabel : negativeLabel}`; } function formatRelativeTime(raw) { if (!raw) return "刚刚同步"; const date = new Date(raw); if (Number.isNaN(date.getTime())) return "刚刚同步"; const diff = Date.now() - date.getTime(); const minutes = Math.max(1, Math.round(diff / 60000)); if (minutes < 60) return `${minutes} 分钟前`; const hours = Math.round(minutes / 60); if (hours < 24) return `${hours} 小时前`; const days = Math.round(hours / 24); return `${days} 天前`; } export function updateNewsToggleUI(visible) { const { toggleBtn } = getElements(); if (!toggleBtn) return; const active = visible && getActiveTVTab() === "news"; toggleBtn.classList.toggle("active", active); const tooltip = toggleBtn.querySelector(".earth-toolbar-tooltip"); if (tooltip) { tooltip.textContent = active ? "关闭态势新闻" : "打开态势新闻"; } } function renderEmptyState(message) { const { board, empty, status, openBtn } = getElements(); if (board) board.innerHTML = ""; if (empty) { empty.hidden = false; empty.textContent = message; } if (status) { status.textContent = "等待聚合新闻源"; } if (openBtn) openBtn.disabled = true; } function renderPayload(nextPayload) { payload = nextPayload; const { board, empty, status, focusLabel, focusCoords, sourceCount, regionChip, openBtn, feedAnchor, } = getElements(); if (!board || !status || !focusLabel || !focusCoords || !sourceCount || !regionChip) { return; } const items = Array.isArray(nextPayload?.items) ? nextPayload.items : []; const sources = Array.isArray(nextPayload?.sources) ? nextPayload.sources : []; const focus = nextPayload?.focus || {}; focusLabel.textContent = focus.label || "全球焦点"; regionChip.textContent = focus.region || "global"; regionChip.style.setProperty("--news-accent", focus.accent || "#d6e6ff"); if (typeof focus.lat === "number" && typeof focus.lon === "number") { focusCoords.textContent = `${formatCoord(focus.lat, "N", "S")} · ${formatCoord(focus.lon, "E", "W")}`; } else { focusCoords.textContent = "跟随当前视角自动聚焦"; } sourceCount.textContent = `${sources.length} 路聚合源`; if (nextPayload?.stale) { status.textContent = `当前显示最近一次可用新闻缓存,共 ${items.length} 条`; } else { status.textContent = nextPayload?.errors?.length ? `已聚合 ${items.length} 条,部分源不可用` : `已聚合 ${items.length} 条态势新闻`; } if (feedAnchor) { const matchedSource = sources.find((source) => source.region === focus.region) || sources[0]; feedAnchor.href = matchedSource?.homepage_url || "https://news.google.com/"; } if (openBtn) { openBtn.disabled = !feedAnchor?.href; } if (items.length === 0) { board.innerHTML = ""; if (empty) { empty.hidden = false; empty.textContent = "当前未拉到可用新闻,请稍后刷新或切换视角区域。"; } return; } if (empty) empty.hidden = true; board.innerHTML = items .map((item) => { const cardClass = item.is_focus_match ? "news-story-card news-story-card--focus" : "news-story-card"; const summary = item.summary ? `
${item.summary}
` : ""; return `
${item.source} ${formatRelativeTime(item.published_at)}
${item.title}
${summary}
`; }) .join(""); } async function fetchNews(lat, lon) { const url = new URL(EARTH_NEWS_API, window.location.origin); if (typeof lat === "number") url.searchParams.set("lat", lat.toFixed(4)); if (typeof lon === "number") url.searchParams.set("lon", lon.toFixed(4)); const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); const response = await fetch(url.toString(), { cache: "no-store", signal: controller.signal, }).finally(() => { window.clearTimeout(timeoutId); }); if (!response.ok) { throw new Error(`新闻源请求失败: ${response.status}`); } return response.json(); } async function refreshNews(lat, lon, { silent = false } = {}) { if (refreshPromise) return refreshPromise; const { status } = getElements(); if (status) { status.textContent = "正在同步全球态势新闻..."; } refreshPromise = fetchNews(lat, lon) .then((nextPayload) => { renderPayload(nextPayload); lastFetchAt = Date.now(); if (Array.isArray(nextPayload?.items) && nextPayload.items.length === 0) { const { status } = getElements(); if (status) { status.textContent = "当前区域暂无可用新闻,已完成一次聚合尝试"; } } return nextPayload; }) .catch((error) => { console.error("加载 Earth RSS 新闻失败:", error); const message = error?.name === "AbortError" ? "新闻聚合请求超时,请稍后重试" : `新闻聚合暂时不可用: ${error?.message || "未知错误"}`; if (!payload) { renderEmptyState(message); } else if (!silent) { showStatusMessage("态势新闻同步失败", "error"); } throw error; }) .finally(() => { refreshPromise = null; }); return refreshPromise; } function shouldRefreshForFocus(lat, lon, region) { const now = Date.now(); if (!lastFocus) return true; if (region !== lastFocus.region && now - lastRegionSwitchAt > MIN_REGION_SWITCH_INTERVAL_MS) { lastRegionSwitchAt = now; return true; } if (now - lastFetchAt > DATA_REFRESH_INTERVAL_MS) return true; if (now - (lastFocus.updatedAt || 0) < FOCUS_UPDATE_INTERVAL_MS) return false; const latDrift = Math.abs((lat || 0) - (lastFocus.lat || 0)); const lonDrift = Math.abs((lon || 0) - (lastFocus.lon || 0)); return latDrift >= 18 || lonDrift >= 25; } function inferRegion(lat, lon) { if (typeof lat !== "number" || typeof lon !== "number") return "global"; if (lon >= -170 && lon <= -30) return "americas"; if (lon > -30 && lon <= 45) return lat >= 30 ? "europe" : "middle-east-africa"; if (lon > 45 && lon <= 150) return lat < 10 ? "middle-east-africa" : "asia-pacific"; return "asia-pacific"; } function openCurrentSourceHomepage() { const { feedAnchor } = getElements(); if (feedAnchor?.href) { window.open(feedAnchor.href, "_blank", "noopener,noreferrer"); } } export function updateNewsViewFocus(coords) { if (!initialized) return; if (!coords || typeof coords.lat !== "number" || typeof coords.lon !== "number") return; const region = inferRegion(coords.lat, coords.lon); const nextFocus = { lat: coords.lat, lon: coords.lon, region, updatedAt: Date.now(), }; const shouldRefresh = shouldRefreshForFocus(coords.lat, coords.lon, region); lastFocus = nextFocus; if (shouldRefresh) { refreshNews(coords.lat, coords.lon, { silent: true }).catch(() => {}); } } export async function ensureNewsPanelReady() { if (!initialized) { initNewsPanel(); } if (!payload) { await refreshNews(lastFocus?.lat, lastFocus?.lon); } return payload; } export function initNewsPanel() { if (initialized) return; initialized = true; const { toggleBtn, refreshBtn, openBtn } = getElements(); updateNewsToggleUI(isTVPanelVisible()); renderEmptyState("正在准备全球态势新闻聚合源..."); const openNewsTab = async () => { openTVPanelTab("news"); updateNewsToggleUI(true); try { await ensureNewsPanelReady(); } catch { // surface already handled } }; toggleBtn?.addEventListener("click", async () => { const visible = isTVPanelVisible(); const active = visible && getActiveTVTab() === "news"; if (!visible) { await openNewsTab(); return; } if (!active) { await openNewsTab(); return; } setTVPanelVisible(false); updateNewsToggleUI(false); }); window.addEventListener("earth:tv-tab-change", () => { updateNewsToggleUI(isTVPanelVisible()); }); window.addEventListener("earth:tv-visibility-change", (event) => { updateNewsToggleUI(Boolean(event.detail?.visible)); }); refreshBtn?.addEventListener("click", async () => { try { await refreshNews(lastFocus?.lat, lastFocus?.lon); showStatusMessage("态势新闻已刷新", "info"); } catch { showStatusMessage("态势新闻刷新失败", "error"); } }); openBtn?.addEventListener("click", openCurrentSourceHomepage); refreshNews(undefined, undefined, { silent: true }).catch(() => {}); }