288 lines
7.9 KiB
TypeScript
288 lines
7.9 KiB
TypeScript
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();
|
||
}
|