add url snapshot

This commit is contained in:
denislov 2025-02-12 13:46:28 +08:00
parent 6401334731
commit de618f58d7
8 changed files with 291 additions and 155 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ pnpm-lock.yaml
yarn.lock
.DS_Store
.env
zotero

View File

@ -8,33 +8,33 @@
<!-- prettier-ignore -->
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css" type="text/css"?>
<!-- prettier-ignore -->
<window
id="__addonRef__-standalone"
title="Save Web Snapshot Panel"
<window id="url-snapshot-dialog"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="__addonRef__-standalone"
persist="screenX screenY width height"
onload=""
>
<xul:linkset>
<html:link rel="localization" href="browser/menubar.ftl" />
<html:link rel="localization" href="browser/browserSets.ftl" />
<html:link rel="localization" href="toolkit/global/textActions.ftl" />
<html:link rel="localization" href="zotero.ftl" />
<html:link rel="localization" href="__addonRef__-standalone.ftl" />
</xul:linkset>
<script src="chrome://zotero/content/include.js"></script>
<script src="chrome://zotero/content/customElements.js"></script>
<script src="chrome://__addonRef__/content/scripts/customElements.js"></script>
<vbox id="dialog-content" align="center">
<label value="请输入网页 URL:" />
<textbox id="url-textbox" width="200" height="300" />
<hbox>
<button id="add-source" value="添加" data-l10n-id="add-source" />
<button id="pin-window" data-l10n-id="pin-window" />
title="Import URLs with Snapshots"
style="width: 500px;">
<vbox flex="1" style="padding: 20px;">
<label value="Enter URLs (one per line):" />
<html:textarea id="urls-input" rows="10" style="margin: 10px 0;" />
<hbox style="margin-top: 10px; justify-content: flex-end;">
<button label="Cancel" oncommand="cancel();" />
<button label="Import" oncommand="accept();" default="true" style="margin-left: 10px;" />
</hbox>
</vbox>
<html:div class="separator"></html:div>
</vbox>
<script>
const params = window.arguments[0];
function accept() {
params.dataOut = document.getElementById('urls-input').value;
window.close();
}
function cancel() {
params.dataOut = null;
window.close();
}
</script>
</window>

View File

@ -10,3 +10,5 @@ prefs-table-detail = Detail
tabpanel-lib-tab-label = Lib Tab
tabpanel-reader-tab-label = Reader Tab
snapshot-dialog-title = Snapshot
scrap-webpage = Scrap Webpage
scrap-finish = Scrap Finish

View File

@ -10,3 +10,5 @@ prefs-table-detail = 详情
tabpanel-lib-tab-label = 库标签
tabpanel-reader-tab-label = 阅读器标签
snapshot-dialog-title = 快照
scrap-webpage = 获取网页中
scrap-finish = 获取完成

View File

@ -1,5 +1,5 @@
import { getLocaleID, getString } from "../utils/locale";
import { URLInputDialog } from "./urlDialog";
import { UrlDialog } from "./urlDialog";
function example(
target: any,
@ -145,7 +145,7 @@ export class UIExampleFactory {
id: "zotero-itemmenu-addontemplate-test",
label: getString("menuitem-label"),
commandListener: (ev) => {
const dialog = new URLInputDialog();
const dialog = new UrlDialog(Zotero.getMainWindow());
dialog.open();
},
icon: menuIcon,

View File

@ -1,130 +1,165 @@
import { config } from "../../package.json";
import { getString } from "../utils/locale";
export class URLInputDialog {
export class UrlDialog {
private window: Window;
private dialog: Window | null;
private document: Document;
constructor() {
this.window = Zotero.getMainWindow();
this.dialog = null;
constructor(win: Window) {
this.window = win;
this.document = win.document;
}
public open() {
// 创建对话框 HTML 内容
const dialogContent = `
<div style="padding: 20px;">
<form id="snapshot-form">
<div style="margin-bottom: 15px;">
<label for="url-input" style="display: block; margin-bottom: 5px;">Enter webpage URL:</label>
<input type="url" id="url-input" style="width: 100%; padding: 5px;"
required placeholder="https://example.com">
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" id="cancel-button">Cancel</button>
<button type="submit" id="save-button">Save Snapshot</button>
</div>
</form>
</div>
`;
// 创建对话框参数
const params: any = {
dataIn: {
url: "",
accepted: false,
},
public open(): Promise<string[]> {
return new Promise((resolve, reject) => {
const params = {
urls: "",
dataOut: null,
};
// 打开对话框
this.dialog = ztoolkit.getGlobal("openDialog")(
ztoolkit.getGlobal("openDialog")(
`chrome://${config.addonRef}/content/dialog.xhtml`,
"",
"chrome,centerscreen,resizable=false,width=500,height=300",
"chrome,centerscreen,modal,resizable=true,width=500,height=300",
params,
);
// 设置对话框内容和事件监听
this.dialog!.addEventListener("load", () => {
const doc = this.dialog!.document;
// 设置对话框标题
doc.title = getString("snapshot-dialog-title");
// 插入自定义内容
const container = doc.getElementById("dialog-content");
ztoolkit.log(container);
// 表单提交处理
// const form = doc.getElementById("snapshot-form") as HTMLFormElement;
// form.addEventListener("submit", (e) => {
// e.preventDefault();
// const urlInput = doc.getElementById("url-input") as HTMLInputElement;
// params.dataOut = {
// url: urlInput.value,
// accepted: true,
// };
// this.dialog?.close();
// });
// 取消按钮处理
// const cancelButton = doc.getElementById("cancel-button")!;
// cancelButton.addEventListener("click", () => {
// params.dataOut = {
// accepted: false,
// };
// this.dialog?.close();
// });
// 自动聚焦到 URL 输入框
// const urlInput = doc.getElementById("url-input") as HTMLInputElement;
// urlInput.focus();
if (params.dataOut === null) {
reject(new Error("Dialog cancelled"));
} else {
const urls = params.dataOut
.split(/[\n,]+/)
.map((url: string) => url.trim())
.filter((url: string) => url);
resolve(urls);
}
});
// 等待对话框关闭
if (params.dataOut?.accepted) {
this.saveSnapshot(params.dataOut.url);
}
}
async saveSnapshot(url: string) {
try {
// 创建新的 webpage 类型条目
const item = new Zotero.Item("webpage");
// 获取网页内容
const response = await fetch(url);
const html = await response.text();
// 设置基本元数据
item.setField("title", this.extractTitle(html));
item.setField("url", url);
item.setField("accessDate", Zotero.Date.dateToSQL(new Date(), true));
await item.saveTx();
// 保存网页内容作为附件
const attachment = await Zotero.Attachments.importFromURL({
url: url,
parentItemID: item.id,
contentType: "text/html",
title: item.getField("title"),
});
// 选中新创建的条目
const zp = Zotero.getActiveZoteroPane();
zp.selectItem(item.id);
Zotero.debug(`Web Snapshot: Successfully saved ${url}`);
} catch (error) {
Zotero.debug(`Web Snapshot error: ${error}`);
this.window.alert(getString("snapshot.error"));
}
}
private extractTitle(html: string): string {
const match = html.match(/<title>(.*?)<\/title>/i);
return match ? match[1] : getString("snapshot.untitled");
}
}
// export class URLInputDialog {
// private window: Window;
// private dialog: Window | null;
// constructor() {
// this.window = Zotero.getMainWindow();
// this.dialog = null;
// }
// public open() {
// // 创建对话框参数
// const params: any = {
// dataIn: {
// url: "",
// accepted: false,
// },
// dataOut: null,
// };
// // 打开对话框
// this.dialog = ztoolkit.getGlobal("openDialog")(
// `chrome://${config.addonRef}/content/dialog.xhtml`,
// "",
// "chrome,centerscreen,resizable=true,width=500,height=300",
// params,
// );
// // 设置对话框内容和事件监听
// this.dialog!.addEventListener("load", () => {
// const doc = this.dialog!.document;
// // 设置对话框标题
// doc.title = getString("snapshot-dialog-title");
// // 保存按钮处理
// const saveButton = doc.getElementById("save-button")!;
// saveButton.addEventListener("click", () => {
// ztoolkit.log("click the save button");
// const urlInput = doc.querySelector("#url-textbox") as XULTextBoxElement;
// params.dataOut = {
// url: urlInput.value,
// accepted: true,
// };
// ztoolkit.log(params);
// this.saveSnapshot(
// "https://mp.weixin.qq.com/s/ppg7eMMIFWCZ-eHzd3QtAA",
// ).then(() => this.dialog?.close());
// });
// // 取消按钮处理
// const cancelButton = doc.getElementById("cancel-button")!;
// cancelButton.addEventListener("click", () => {
// params.dataOut = {
// accepted: false,
// };
// this.dialog?.close();
// });
// // 自动聚焦到 URL 输入框
// const urlInput = doc.querySelector("#url-textbox") as XULTextBoxElement;
// urlInput.focus();
// });
// }
// async saveSnapshot(url: string) {
// try {
// // 使用解析后的第一个条目
// const items = await this.processUrl(url);
// if (items == undefined || items.length < 1) return;
// ztoolkit.log(items);
// const item = items[0];
// // 选中新创建的条目
// const attachment = await Zotero.Attachments.importFromURL({
// url: url,
// parentItemID: item.id,
// contentType: "text/html",
// title: "Snapshot",
// });
// const zp = Zotero.getActiveZoteroPane();
// zp.selectItem(item.id);
// Zotero.debug(`Web Snapshot: Successfully saved ${url}`);
// } catch (error) {
// Zotero.debug(`Web Snapshot error: ${error}`);
// this.window.alert(getString("snapshot.error"));
// }
// }
// private async processUrl(url: string): Promise<Zotero.Item[] | undefined> {
// // 获取合适的translator
// const translate = new Zotero.Translate.Web();
// let items: Zotero.Item[] | undefined = undefined;
// const { HiddenBrowser } = ChromeUtils.import(
// "chrome://zotero/content/HiddenBrowser.jsm",
// );
// try {
// const browser = new HiddenBrowser({
// docShell: { allowImages: true },
// });
// await browser.load(url, { requireSuccessfulStatus: true });
// const doc = await browser.getDocument();
// translate.setDocument(doc);
// const translators = await translate.getTranslators(false);
// if (!translators.length) {
// ztoolkit.log("No compatible translator found");
// return items;
// }
// // 使用第一个匹配的translator
// translate.setTranslator(translators[0]);
// // 执行翻译
// const { library, collection, editable } =
// Zotero.Server.Connector.getSaveTarget();
// const libraryID = library.libraryID;
// const targetID = collection ? collection.treeViewID : library.treeViewID;
// items = await translate.translate({
// libraryID,
// collections: collection ? [collection.id] : false,
// saveAttachments: false,
// });
// } catch (e) {
// ztoolkit.log("Error processing URL: " + e);
// }
// return items;
// }
// }

View File

@ -0,0 +1,78 @@
import { getString } from "../utils/locale";
export class UrlSnapshot {
public async processUrl(url: string): Promise<boolean> {
const { HiddenBrowser } = ChromeUtils.import(
"chrome://zotero/content/HiddenBrowser.jsm",
);
try {
const translate = new Zotero.Translate.Web();
const browser = new HiddenBrowser({
docShell: { allowImages: true },
});
await browser.load(url, { requireSuccessfulStatus: true });
const doc = await browser.getDocument();
translate.setDocument(doc);
const translators = await translate.getTranslators(false);
if (!translators.length) {
ztoolkit.log("No compatible translator found");
return false;
}
translate.setTranslator(translators[0]);
const { library, collection, editable } =
Zotero.Server.Connector.getSaveTarget();
const libraryID = library.libraryID;
const targetID = collection ? collection.treeViewID : library.treeViewID;
const items = await translate.translate({
libraryID,
collections: collection ? [collection.id] : false,
saveAttachments: false,
});
await Zotero.Attachments.importFromURL({
url: url,
parentItemID: items[0].id,
contentType: "text/html",
title: "Snapshot",
});
return true;
} catch (e) {
ztoolkit.log(`Error processing URL: ${e}`);
return false;
}
}
public async processBatchUrls(
urls: string[],
): Promise<Array<{ url: string; success: boolean }>> {
const results = [];
const popupWin = new ztoolkit.ProgressWindow(addon.data.config.addonName, {
closeOnClick: true,
closeTime: -1,
})
.createLine({
text: getString("scrap-webpage"),
type: "default",
progress: 0,
})
.show();
for (const url of urls) {
const success = await this.processUrl(url);
results.push({ url, success });
const pgn = Math.round((urls.indexOf(url) * 100) / urls.length);
popupWin.changeLine({
progress: pgn,
text: `[${pgn}%] ${getString("scrap-webpage")}`,
});
}
popupWin.changeLine({
progress: 100,
text: `[100%] ${getString("scrap-finish")}`,
});
popupWin.startCloseTimer(500);
return results;
}
}

View File

@ -1,6 +1,7 @@
import { config } from "../../package.json";
import { getString } from "../utils/locale";
import { URLInputDialog } from "./urlDialog";
import { UrlDialog, URLInputDialog } from "./urlDialog";
import { UrlSnapshot } from "./urlSnapshot";
export default class Views {
private id = "zotero-zetter-container";
@ -29,8 +30,25 @@ export default class Views {
const papersgptState = Zotero.Prefs.get(
`${config.addonRef}.papersgptState`,
);
const dialog = new URLInputDialog();
dialog.open();
try {
const dialog = new UrlDialog(Zotero.getMainWindow());
const urls = await dialog.open();
if (urls && urls.length > 0) {
const snapshot = new UrlSnapshot();
const results = await snapshot.processBatchUrls(urls);
// Show results
const message = results
.map((r) => `${r.url}: ${r.success ? "Success" : "Failed"}`)
.join("\n");
alert("Import Results:\n\n" + message);
}
} catch (e) {
// User cancelled or error occurred
Zotero.debug("URL Snapshot operation cancelled or failed");
}
});
const searchNode = toolbar.querySelector("#zotero-tb-search");
newNode.style.listStyleImage = `url(chrome://${config.addonRef}/content/icons/favicon.png)`;