feat: v2.0 — 插件改造,集成 PPTX 导出
改造内容: - 改名 Web to Pixso → Web to PPT - 删除 pixso-plugin/、element-picker.js、popup-panel.js - 新增 convert-browser.js(浏览器版转化逻辑) - 新增 lib-pptxgen.js(pptxgenjs 浏览器 bundle) - 重写 popup.js(触发捕获→转化→下载) - 重写 background.js(缓存采集数据,支持 WEB_TO_PPT_GET_DATA) - manifest.json 改名 + 添加 default_popup 流程:popup 点击导出 → background 抓取 DOM → popup 运行转化 → chrome.downloads 下载 PPTX
This commit is contained in:
+49
-194
@@ -1,7 +1,5 @@
|
||||
const CAPTURE_FILE = "capture.js";
|
||||
const RUNNER_FILE = "runner.js";
|
||||
const POPUP_PANEL_FILE = "popup-panel.js";
|
||||
const ELEMENT_PICKER_FILE = "element-picker.js";
|
||||
const SETTINGS_KEY = "webToPixsoSettings";
|
||||
const DEFAULT_SETTINGS = {
|
||||
useProxy: false,
|
||||
@@ -12,6 +10,9 @@ const DEFAULT_SETTINGS = {
|
||||
const MIN_CAPTURE_WIDTH = 320;
|
||||
const MAX_CAPTURE_WIDTH = 3840;
|
||||
|
||||
// 缓存最近一次采集数据,供 popup 获取
|
||||
let lastCaptureData = null;
|
||||
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
function normalizeSettings(value = {}) {
|
||||
@@ -43,7 +44,6 @@ function assertCaptureableTab(tab) {
|
||||
if (!tab?.id || !tab.url) {
|
||||
throw new Error("没有可采集的当前标签页");
|
||||
}
|
||||
|
||||
if (/^(chrome|edge|about|devtools|chrome-extension):/i.test(tab.url)) {
|
||||
throw new Error("浏览器内置页面不支持采集,请切换到普通网页");
|
||||
}
|
||||
@@ -60,22 +60,18 @@ async function runCapture(tabId, options) {
|
||||
target: { tabId },
|
||||
files: [CAPTURE_FILE]
|
||||
});
|
||||
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: [RUNNER_FILE]
|
||||
});
|
||||
|
||||
const [{ result }] = await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
func: captureOptions => window.__webToPixsoRunCapture(captureOptions),
|
||||
args: [options]
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new Error("页面没有返回采集结果");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -133,9 +129,7 @@ async function prepareCaptureViewport(tab, requestedWidth) {
|
||||
await chrome.windows.update(tab.windowId, { state: originalState });
|
||||
}
|
||||
await delay(250);
|
||||
} catch {
|
||||
// Restoring the user's window is best effort only.
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -157,7 +151,7 @@ async function prepareCaptureViewport(tab, requestedWidth) {
|
||||
}
|
||||
|
||||
if (Math.abs(currentViewportWidth - targetWidth) > 2) {
|
||||
throw new Error(`采集视口未生效:目标 ${targetWidth}px,实际 ${currentViewportWidth}px。请退出全屏或手动放宽浏览器窗口后重试。`);
|
||||
throw new Error(`采集视口未生效:目标 ${targetWidth}px,实际 ${currentViewportWidth}px`);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -203,7 +197,7 @@ async function downloadCapture(data) {
|
||||
.replace(/[\\/:*?"<>|]+/g, "-")
|
||||
.replace(/\s+/g, "-")
|
||||
.slice(0, 64) || "webpage";
|
||||
const filename = `web-to-pixso/${safeTitle}-${Date.now()}.json`;
|
||||
const filename = `web-to-ppt/${safeTitle}-${Date.now()}.json`;
|
||||
|
||||
await chrome.downloads.download({
|
||||
url,
|
||||
@@ -214,206 +208,67 @@ async function downloadCapture(data) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
async function startCapture(options) {
|
||||
const settings = normalizeSettings(options);
|
||||
await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
|
||||
const tab = await getActiveTab();
|
||||
const data = await captureCurrentTab(tab, settings);
|
||||
const filename = await downloadCapture(data);
|
||||
return {
|
||||
ok: true,
|
||||
filename,
|
||||
actualViewportWidth: data.source?.actualViewportWidth,
|
||||
requestedViewportWidth: data.source?.requestedViewportWidth,
|
||||
usedTemporaryWindow: Boolean(data.capture?.usedTemporaryWindow)
|
||||
};
|
||||
}
|
||||
|
||||
async function startCaptureFromSender(options, sender) {
|
||||
const settings = normalizeSettings(options);
|
||||
await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
|
||||
const tab = sender?.tab || await getActiveTab();
|
||||
assertCaptureableTab(tab);
|
||||
const data = await captureCurrentTab(tab, settings);
|
||||
const filename = await downloadCapture(data);
|
||||
return {
|
||||
ok: true,
|
||||
filename,
|
||||
actualViewportWidth: data.source?.actualViewportWidth,
|
||||
requestedViewportWidth: data.source?.requestedViewportWidth,
|
||||
usedTemporaryWindow: Boolean(data.capture?.usedTemporaryWindow)
|
||||
};
|
||||
}
|
||||
|
||||
async function captureClipboardFromSender(options, sender) {
|
||||
const settings = normalizeSettings(options);
|
||||
await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
|
||||
const tab = sender?.tab || await getActiveTab();
|
||||
assertCaptureableTab(tab);
|
||||
const data = await captureCurrentTab(tab, settings);
|
||||
return {
|
||||
ok: true,
|
||||
json: JSON.stringify(data, null, 2),
|
||||
actualViewportWidth: data.source?.actualViewportWidth,
|
||||
requestedViewportWidth: data.source?.requestedViewportWidth,
|
||||
usedTemporaryWindow: Boolean(data.capture?.usedTemporaryWindow)
|
||||
};
|
||||
}
|
||||
|
||||
async function startElementCapture(options, sender) {
|
||||
const settings = {
|
||||
...normalizeSettings(options),
|
||||
captureWidth: null,
|
||||
selectionId: options?.selectionId,
|
||||
selectionWidth: Math.max(1, Math.round(Number(options?.selectionWidth || 0))),
|
||||
captureMode: "mixed"
|
||||
};
|
||||
const tab = sender?.tab || await getActiveTab();
|
||||
assertCaptureableTab(tab);
|
||||
const data = await runCapture(tab.id, settings);
|
||||
const filename = await downloadCapture(data);
|
||||
return {
|
||||
ok: true,
|
||||
filename,
|
||||
actualViewportWidth: data.source?.actualViewportWidth,
|
||||
requestedViewportWidth: data.source?.requestedViewportWidth,
|
||||
selectionWidth: data.import?.defaultWidth
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, timeout = 10000) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
return await fetch(url, {
|
||||
signal: controller.signal,
|
||||
credentials: "include",
|
||||
cache: "force-cache"
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const chunkSize = 0x8000;
|
||||
let binary = "";
|
||||
|
||||
for (let index = 0; index < bytes.length; index += chunkSize) {
|
||||
binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
|
||||
}
|
||||
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
async function proxyFetchAsset(url) {
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
return { ok: false, error: "仅支持 http/https 图片代理" };
|
||||
// 消息处理
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
// 采集并下载 JSON(兼容旧流程)
|
||||
if (message?.type === "PIXSO_CAPTURE_START") {
|
||||
(async () => {
|
||||
const settings = normalizeSettings(message.options);
|
||||
await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
|
||||
const tab = sender?.tab || await getActiveTab();
|
||||
assertCaptureableTab(tab);
|
||||
const data = await captureCurrentTab(tab, settings);
|
||||
lastCaptureData = data; // 缓存数据
|
||||
const filename = await downloadCapture(data);
|
||||
return {
|
||||
ok: true,
|
||||
filename,
|
||||
actualViewportWidth: data.source?.actualViewportWidth,
|
||||
requestedViewportWidth: data.source?.requestedViewportWidth
|
||||
};
|
||||
})()
|
||||
.then(sendResponse)
|
||||
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
const response = await fetchWithTimeout(url, 12000);
|
||||
if (!response.ok) {
|
||||
return { ok: false, status: response.status, error: `HTTP ${response.status}` };
|
||||
// 获取最近一次采集数据(供 popup 转化用)
|
||||
if (message?.type === "WEB_TO_PPT_GET_DATA") {
|
||||
if (lastCaptureData) {
|
||||
sendResponse({ ok: true, data: lastCaptureData });
|
||||
} else {
|
||||
// 如果没有缓存,重新采集
|
||||
(async () => {
|
||||
const settings = normalizeSettings({});
|
||||
const tab = await getActiveTab();
|
||||
const data = await captureCurrentTab(tab, settings);
|
||||
lastCaptureData = data;
|
||||
return { ok: true, data };
|
||||
})()
|
||||
.then(sendResponse)
|
||||
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
contentType,
|
||||
base64: arrayBufferToBase64(buffer)
|
||||
};
|
||||
}
|
||||
|
||||
async function captureVisibleTab(sender) {
|
||||
if (!sender?.tab?.windowId || !chrome.tabs?.captureVisibleTab) {
|
||||
return { ok: false, error: "当前标签页截图不可用" };
|
||||
}
|
||||
|
||||
try {
|
||||
const dataUrl = await chrome.tabs.captureVisibleTab(sender.tab.windowId, {
|
||||
format: "png"
|
||||
});
|
||||
return { ok: true, dataUrl };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: error.message || String(error),
|
||||
nonFatal: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function showInPagePanel(tab) {
|
||||
assertCaptureableTab(tab);
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: [ELEMENT_PICKER_FILE, POPUP_PANEL_FILE]
|
||||
});
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => window.__webToPixsoShowPanel?.()
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 安装时初始化设置
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.storage.local.get({ [SETTINGS_KEY]: DEFAULT_SETTINGS }).then(result => {
|
||||
chrome.storage.local.set({ [SETTINGS_KEY]: normalizeSettings(result[SETTINGS_KEY]) });
|
||||
});
|
||||
});
|
||||
|
||||
chrome.action.onClicked.addListener(tab => {
|
||||
showInPagePanel(tab).catch(error => {
|
||||
console.warn("[Web to Pixso] Failed to open panel", error);
|
||||
});
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message?.type === "PIXSO_CAPTURE_START") {
|
||||
startCapture(message.options)
|
||||
.then(sendResponse)
|
||||
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message?.type === "PIXSO_CAPTURE_ELEMENT_START") {
|
||||
startElementCapture(message.options, sender)
|
||||
.then(sendResponse)
|
||||
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message?.type === "PIXSO_CAPTURE_CURRENT_TAB") {
|
||||
startCaptureFromSender(message.options, sender)
|
||||
.then(sendResponse)
|
||||
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message?.type === "PIXSO_CAPTURE_CLIPBOARD") {
|
||||
captureClipboardFromSender(message.options, sender)
|
||||
.then(sendResponse)
|
||||
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message?.type === "PIXSO_CAPTURE_FETCH_ASSET") {
|
||||
proxyFetchAsset(message.url)
|
||||
.then(sendResponse)
|
||||
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message?.type === "PIXSO_CAPTURE_VISIBLE_TAB") {
|
||||
captureVisibleTab(sender)
|
||||
.then(sendResponse)
|
||||
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
// 点击图标打开 popup(MV3 默认行为)
|
||||
|
||||
Reference in New Issue
Block a user