import { renderDevPanel, renderLoggedIn, renderLoggedOut, renderUpdateStatus, setUpdateDownloadStatus, setProtectedApiResult } from "./view"; import { readAuthConfig, type AuthConfig } from "../shared/auth-config"; import { isAuthResponseMessage, type AuthResponseMessage } from "../shared/auth-messages"; import { createProtectedApiClient } from "../shared/protected-api-client"; import { compareExtensionVersions, fetchUpdateManifest as fetchUpdateManifestFromUrl, type UpdateManifest } from "../shared/update-check"; import { UPDATE_MANIFEST_URL } from "../shared/update-config"; interface BootPopupOptions { config?: Partial; currentVersion?: string; document?: Document; fetchProtectedApi?: () => Promise; fetchUpdateManifest?: () => Promise; sendMessage?: (message: unknown) => Promise; updateManifestUrl?: string; } export async function bootPopup(options: BootPopupOptions = {}): Promise { const currentDocument = options.document ?? document; const popupConfig = readAuthConfig(options.config); const currentVersion = options.currentVersion ?? readCurrentVersion(); const root = currentDocument.querySelector("#app"); const HTMLElementCtor = currentDocument.defaultView?.HTMLElement; if (!root || (HTMLElementCtor && !(root instanceof HTMLElementCtor))) { throw new Error("popup root #app is required"); } const sendMessage = options.sendMessage ?? ((message: unknown) => Promise.resolve( ( globalThis as typeof globalThis & { chrome?: { runtime?: { sendMessage?: (payload: unknown) => Promise; }; }; } ).chrome?.runtime?.sendMessage?.(message) )); const fetchProtectedApi = options.fetchProtectedApi ?? createProtectedApiClient({ baseUrl: "http://127.0.0.1:4319", sendMessage }).loadProtectedMockData; const fetchUpdateManifest = options.fetchUpdateManifest ?? (() => fetchUpdateManifestFromUrl( options.updateManifestUrl ?? UPDATE_MANIFEST_URL )); await renderCurrentAuthState(root, popupConfig, sendMessage, fetchProtectedApi, { currentVersion, fetchUpdateManifest }); } async function renderCurrentAuthState( root: HTMLElement, popupConfig: AuthConfig, sendMessage: (message: unknown) => Promise, fetchProtectedApi: () => Promise, updateOptions: { currentVersion: string; fetchUpdateManifest: () => Promise; } ): Promise { const response = await sendMessage({ type: "auth:get-state" }); if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") { renderLoggedOut(root, "认证状态读取失败"); return; } if (!response.value.isAuthenticated) { renderLoggedOut(root, response.value.lastError); root .querySelector('[data-popup-sign-in="button"]') ?.addEventListener("click", () => { void runAuthAction(root, popupConfig, sendMessage, { actionMessage: { type: "auth:sign-in" }, fetchProtectedApi, updateOptions }); }); return; } renderLoggedIn(root, response.value); void runUpdateCheck(root, sendMessage, updateOptions); root .querySelector('[data-popup-sign-out="button"]') ?.addEventListener("click", () => { void runAuthAction(root, popupConfig, sendMessage, { actionMessage: { type: "auth:sign-out" }, fetchProtectedApi, updateOptions }); }); if (popupConfig.enableDevAuthPanel) { renderDevPanel(root, response.value); root .querySelector('[data-popup-test-protected-api="button"]') ?.addEventListener("click", () => { void runProtectedApiProbe(root, fetchProtectedApi); }); } } async function runAuthAction( root: HTMLElement, popupConfig: AuthConfig, sendMessage: (message: unknown) => Promise, options: { actionMessage: { type: "auth:sign-in" } | { type: "auth:sign-out" }; fetchProtectedApi: () => Promise; updateOptions: { currentVersion: string; fetchUpdateManifest: () => Promise; }; } ): Promise { const response = await sendMessage(options.actionMessage); if (isActionError(response)) { renderLoggedOut(root, response.error); root .querySelector('[data-popup-sign-in="button"]') ?.addEventListener("click", () => { void runAuthAction(root, popupConfig, sendMessage, options); }); return; } await renderCurrentAuthState( root, popupConfig, sendMessage, options.fetchProtectedApi, options.updateOptions ); } function isActionError(response: unknown): response is Extract { return ( isAuthResponseMessage(response) && !response.ok && response.type === "auth:error" ); } async function runUpdateCheck( root: HTMLElement, sendMessage: (message: unknown) => Promise, options: { currentVersion: string; fetchUpdateManifest: () => Promise; } ): Promise { renderUpdateStatus(root, { currentVersion: options.currentVersion, status: "checking" }); try { const manifest = await options.fetchUpdateManifest(); if (compareExtensionVersions(manifest.latestVersion, options.currentVersion) <= 0) { renderUpdateStatus(root, { currentVersion: options.currentVersion, status: "latest" }); return; } renderUpdateStatus(root, { currentVersion: options.currentVersion, manifest, status: "available" }); bindUpdateDownloadButtons(root, sendMessage, manifest); } catch { renderUpdateStatus(root, { currentVersion: options.currentVersion, status: "error" }); } } function bindUpdateDownloadButtons( root: HTMLElement, sendMessage: (message: unknown) => Promise, manifest: UpdateManifest ): void { root .querySelector('[data-popup-download-update="button"]') ?.addEventListener("click", () => { void downloadUpdateAsset(root, sendMessage, { filename: "star-chart-search-enhancer-internal.zip", url: manifest.zipUrl }); }); root .querySelector('[data-popup-download-guide="button"]') ?.addEventListener("click", () => { void downloadUpdateAsset(root, sendMessage, { filename: "星图增强插件-超简单安装使用指南.pdf", url: manifest.guideUrl }); }); } async function downloadUpdateAsset( root: HTMLElement, sendMessage: (message: unknown) => Promise, options: { filename: string; url: string; } ): Promise { setUpdateDownloadStatus(root, "正在下载..."); try { await sendMessage({ filename: options.filename, type: "update:download", url: options.url }); setUpdateDownloadStatus(root, "已触发下载。下载后请解压新版 zip,并在 chrome://extensions 里重新加载插件。"); } catch (error) { setUpdateDownloadStatus( root, error instanceof Error ? error.message : "下载失败,请稍后重试" ); } } function readCurrentVersion(): string { const runtime = ( globalThis as typeof globalThis & { chrome?: { runtime?: { getManifest?: () => { version?: string }; }; }; } ).chrome?.runtime; return runtime?.getManifest?.().version ?? "0.0.0"; } async function runProtectedApiProbe( root: HTMLElement, fetchProtectedApi: () => Promise ): Promise { setProtectedApiResult(root, "请求中..."); try { const result = await fetchProtectedApi(); setProtectedApiResult(root, JSON.stringify(result, null, 2)); } catch (error) { setProtectedApiResult( root, error instanceof Error ? error.message : String(error) ); } } if (typeof document !== "undefined") { void bootPopup(); }