235 lines
6.1 KiB
TypeScript
235 lines
6.1 KiB
TypeScript
import {
|
|
createMarketController,
|
|
type CreateMarketControllerOptions
|
|
} from "./market/index";
|
|
import { renderMarketAuthGate } from "./market/auth-gate";
|
|
import {
|
|
isAuthResponseMessage,
|
|
type AuthStateValue
|
|
} from "../shared/auth-messages";
|
|
|
|
interface ChromeRuntimeLike {
|
|
getURL?: (path: string) => string;
|
|
id?: string;
|
|
sendMessage?: (message: unknown) => void | Promise<unknown>;
|
|
}
|
|
|
|
const DOWNLOAD_MARKET_CSV_MESSAGE = "download-market-csv";
|
|
|
|
interface BootContentScriptOptions {
|
|
createMarketController?: (
|
|
options: CreateMarketControllerOptions
|
|
) => { dispose?: () => void; ready: Promise<void> };
|
|
document?: Document;
|
|
sendAuthMessage?: (message: unknown) => Promise<unknown>;
|
|
window?: Window;
|
|
}
|
|
|
|
export async function bootContentScript(
|
|
options: BootContentScriptOptions = {}
|
|
): Promise<{ ready: Promise<void> } | null> {
|
|
const currentWindow = options.window ?? window;
|
|
const currentDocument = options.document ?? document;
|
|
const controllerFactory =
|
|
options.createMarketController ?? createMarketController;
|
|
const sendAuthMessage =
|
|
options.sendAuthMessage ?? createRuntimeMessageSender();
|
|
|
|
if (!isMarketPage(currentWindow.location.href)) {
|
|
return null;
|
|
}
|
|
|
|
installMarketPageBridge(currentDocument);
|
|
|
|
const authState = await readAuthState(sendAuthMessage);
|
|
if (!authState?.isAuthenticated) {
|
|
await waitForBodyReady(currentDocument, currentWindow);
|
|
renderMarketAuthGate(
|
|
currentDocument,
|
|
currentWindow,
|
|
isExpiredAuthState(authState) ? "登录已过期,请重新登录" : undefined
|
|
);
|
|
return {
|
|
ready: Promise.resolve()
|
|
};
|
|
}
|
|
|
|
await waitForBodyReady(currentDocument, currentWindow);
|
|
|
|
return controllerFactory({
|
|
document: currentDocument,
|
|
onCsvReady: (csv: string, filename?: string) => {
|
|
if (filename) {
|
|
downloadCsv(currentDocument, currentWindow, csv, filename);
|
|
return;
|
|
}
|
|
|
|
if (requestCsvDownload(csv)) {
|
|
return;
|
|
}
|
|
|
|
downloadCsv(currentDocument, currentWindow, csv, filename);
|
|
},
|
|
window: currentWindow
|
|
});
|
|
}
|
|
|
|
async function readAuthState(
|
|
sendMessage: (message: unknown) => Promise<unknown>
|
|
): Promise<AuthStateValue | null> {
|
|
const response = await sendMessage({ type: "auth:get-state" });
|
|
if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") {
|
|
return null;
|
|
}
|
|
|
|
return response.value;
|
|
}
|
|
|
|
function isMarketPage(url: string): boolean {
|
|
const parsedUrl = new URL(url);
|
|
const isXingtuHost =
|
|
parsedUrl.hostname === "xingtu.cn" || parsedUrl.hostname.endsWith(".xingtu.cn");
|
|
|
|
return isXingtuHost && parsedUrl.pathname.startsWith("/ad/creator/market");
|
|
}
|
|
|
|
function bootstrapContentScript() {
|
|
const runtime = (
|
|
globalThis as typeof globalThis & {
|
|
chrome?: { runtime?: ChromeRuntimeLike };
|
|
}
|
|
).chrome?.runtime;
|
|
|
|
if (!runtime || typeof window === "undefined" || typeof document === "undefined") {
|
|
return;
|
|
}
|
|
|
|
const marker = "__starChartSearchEnhancerContentController";
|
|
const scopedWindow = window as Window & {
|
|
[marker]?: boolean | { dispose?: () => void; ready: Promise<void> } | null;
|
|
};
|
|
|
|
if (scopedWindow[marker]) {
|
|
return;
|
|
}
|
|
|
|
scopedWindow[marker] = true;
|
|
void bootContentScript().then((controller) => {
|
|
scopedWindow[marker] = controller;
|
|
});
|
|
}
|
|
|
|
bootstrapContentScript();
|
|
|
|
function requestCsvDownload(csv: string, filename?: string): boolean {
|
|
const runtime = (
|
|
globalThis as typeof globalThis & {
|
|
chrome?: { runtime?: ChromeRuntimeLike };
|
|
}
|
|
).chrome?.runtime;
|
|
|
|
if (!runtime?.id || typeof runtime.sendMessage !== "function") {
|
|
return false;
|
|
}
|
|
|
|
runtime.sendMessage({
|
|
csv,
|
|
filename: filename ?? `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`,
|
|
type: DOWNLOAD_MARKET_CSV_MESSAGE
|
|
});
|
|
return true;
|
|
}
|
|
|
|
function createRuntimeMessageSender(): (message: unknown) => Promise<unknown> {
|
|
return async (message: unknown) => {
|
|
const runtime = (
|
|
globalThis as typeof globalThis & {
|
|
chrome?: { runtime?: ChromeRuntimeLike };
|
|
}
|
|
).chrome?.runtime;
|
|
|
|
if (typeof runtime?.sendMessage !== "function") {
|
|
return null;
|
|
}
|
|
|
|
return runtime.sendMessage(message);
|
|
};
|
|
}
|
|
|
|
async function waitForBodyReady(document: Document, currentWindow: Window): Promise<void> {
|
|
if (document.body) {
|
|
return;
|
|
}
|
|
|
|
await new Promise<void>((resolve) => {
|
|
const handleReady = () => {
|
|
if (document.body) {
|
|
document.removeEventListener("DOMContentLoaded", handleReady);
|
|
resolve();
|
|
}
|
|
};
|
|
|
|
document.addEventListener("DOMContentLoaded", handleReady);
|
|
currentWindow.setTimeout(handleReady, 0);
|
|
});
|
|
}
|
|
|
|
function downloadCsv(
|
|
document: Document,
|
|
window: Window,
|
|
csv: string,
|
|
filename?: string
|
|
): void {
|
|
const blob = new Blob(["\uFEFF", csv], {
|
|
type: "text/csv;charset=utf-8"
|
|
});
|
|
const objectUrl = window.URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = objectUrl;
|
|
link.download = filename ?? `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
window.URL.revokeObjectURL(objectUrl);
|
|
}
|
|
|
|
function formatTimestampForFilename(): string {
|
|
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
}
|
|
|
|
function isExpiredAuthState(authState: AuthStateValue | null): boolean {
|
|
const lastError = authState?.lastError;
|
|
return (
|
|
typeof lastError === "string" &&
|
|
(/token/i.test(lastError) || lastError.includes("过期"))
|
|
);
|
|
}
|
|
|
|
function installMarketPageBridge(document: Document) {
|
|
if (
|
|
document.documentElement.querySelector(
|
|
'[data-sces-market-bridge="script"]'
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const script = document.createElement("script");
|
|
script.dataset.scesMarketBridge = "script";
|
|
|
|
const runtime = (
|
|
globalThis as typeof globalThis & {
|
|
chrome?: { runtime?: ChromeRuntimeLike };
|
|
}
|
|
).chrome?.runtime;
|
|
const bridgeUrl = runtime?.getURL?.("content/market-page-bridge.js");
|
|
|
|
if (bridgeUrl) {
|
|
script.src = bridgeUrl;
|
|
} else {
|
|
script.textContent = "";
|
|
}
|
|
|
|
(document.head ?? document.documentElement).appendChild(script);
|
|
}
|