288 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<AuthConfig>;
currentVersion?: string;
document?: Document;
fetchProtectedApi?: () => Promise<unknown>;
fetchUpdateManifest?: () => Promise<UpdateManifest>;
sendMessage?: (message: unknown) => Promise<unknown>;
updateManifestUrl?: string;
}
export async function bootPopup(options: BootPopupOptions = {}): Promise<void> {
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<unknown>;
};
};
}
).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<unknown>,
fetchProtectedApi: () => Promise<unknown>,
updateOptions: {
currentVersion: string;
fetchUpdateManifest: () => Promise<UpdateManifest>;
}
): Promise<void> {
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<unknown>,
options: {
actionMessage: { type: "auth:sign-in" } | { type: "auth:sign-out" };
fetchProtectedApi: () => Promise<unknown>;
updateOptions: {
currentVersion: string;
fetchUpdateManifest: () => Promise<UpdateManifest>;
};
}
): Promise<void> {
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<AuthResponseMessage, { ok: false }> {
return (
isAuthResponseMessage(response) &&
!response.ok &&
response.type === "auth:error"
);
}
async function runUpdateCheck(
root: HTMLElement,
sendMessage: (message: unknown) => Promise<unknown>,
options: {
currentVersion: string;
fetchUpdateManifest: () => Promise<UpdateManifest>;
}
): Promise<void> {
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<unknown>,
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<unknown>,
options: {
filename: string;
url: string;
}
): Promise<void> {
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<unknown>
): Promise<void> {
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();
}