Merge c372ba03ddd169622c8be17ccbe78643db51bc8f into 25667e84b7f54b6cc6cb1a0a790413d73348b71d

This commit is contained in:
Zexin Yuan 2026-02-06 22:40:15 +02:00 committed by GitHub
commit 8d83b4e084
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 6 deletions

View File

@ -111,6 +111,9 @@
"opml_version_1": "OPML v1.0 - plain text only", "opml_version_1": "OPML v1.0 - plain text only",
"opml_version_2": "OPML v2.0 - allows also HTML", "opml_version_2": "OPML v2.0 - allows also HTML",
"export_type_single": "Only this note without its descendants", "export_type_single": "Only this note without its descendants",
"export_to_clipboard": "Export to clipboard",
"export_to_clipboard_on_tooltip": "Export the note content to clipboard.",
"export_to_clipboard_off_tooltip": "Download the note as a file.",
"export": "Export", "export": "Export",
"choose_export_type": "Choose export type first please", "choose_export_type": "Choose export type first please",
"export_status": "Export status", "export_status": "Export status",

View File

@ -13,4 +13,8 @@
.export-dialog form .form-check-label { .export-dialog form .form-check-label {
padding: 2px; padding: 2px;
} }
.export-dialog form .export-single-formats .switch-widget {
margin-top: 10px;
}

View File

@ -1,8 +1,10 @@
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import tree from "../../services/tree"; import tree from "../../services/tree";
import { copyTextWithToast } from "../../services/clipboard_ext.js";
import Button from "../react/Button"; import Button from "../react/Button";
import FormRadioGroup from "../react/FormRadioGroup"; import FormRadioGroup from "../react/FormRadioGroup";
import FormToggle from "../react/FormToggle";
import Modal from "../react/Modal"; import Modal from "../react/Modal";
import "./export.css"; import "./export.css";
import ws from "../../services/ws"; import ws from "../../services/ws";
@ -21,10 +23,12 @@ interface ExportDialogProps {
export default function ExportDialog() { export default function ExportDialog() {
const [ opts, setOpts ] = useState<ExportDialogProps>(); const [ opts, setOpts ] = useState<ExportDialogProps>();
const [ exportType, setExportType ] = useState<string>(opts?.defaultType ?? "subtree"); const [ exportType, setExportType ] = useState<string>(opts?.defaultType ?? "subtree");
const [ exportToClipboard, setExportToClipboard ] = useState(false);
const [ subtreeFormat, setSubtreeFormat ] = useState("html"); const [ subtreeFormat, setSubtreeFormat ] = useState("html");
const [ singleFormat, setSingleFormat ] = useState("html"); const [ singleFormat, setSingleFormat ] = useState("html");
const [ opmlVersion, setOpmlVersion ] = useState("2.0"); const [ opmlVersion, setOpmlVersion ] = useState("2.0");
const [ shown, setShown ] = useState(false); const [ shown, setShown ] = useState(false);
const [ exporting, setExporting ] = useState(false);
useTriliumEvent("showExportDialog", async ({ notePath, defaultType }) => { useTriliumEvent("showExportDialog", async ({ notePath, defaultType }) => {
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath); const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
@ -47,18 +51,20 @@ export default function ExportDialog() {
className="export-dialog" className="export-dialog"
title={`${t("export.export_note_title")} ${opts?.noteTitle ?? ""}`} title={`${t("export.export_note_title")} ${opts?.noteTitle ?? ""}`}
size="lg" size="lg"
onSubmit={() => { onSubmit={async () => {
if (!opts || !opts.branchId) { if (!opts || !opts.branchId) {
return; return;
} }
const format = (exportType === "subtree" ? subtreeFormat : singleFormat); const format = (exportType === "subtree" ? subtreeFormat : singleFormat);
const version = (format === "opml" ? opmlVersion : "1.0"); const version = (format === "opml" ? opmlVersion : "1.0");
exportBranch(opts.branchId, exportType, format, version); setExporting(true);
await exportBranch(opts.branchId, exportType, format, version, exportToClipboard);
setExporting(false);
setShown(false); setShown(false);
}} }}
onHidden={() => setShown(false)} onHidden={() => setShown(false)}
footer={<Button className="export-button" text={t("export.export")} primary />} footer={<Button className="export-button" text={t("export.export")} primary disabled={exporting} />}
show={shown} show={shown}
> >
@ -118,6 +124,12 @@ export default function ExportDialog() {
{ value: "markdown", label: t("export.format_markdown") } { value: "markdown", label: t("export.format_markdown") }
]} ]}
/> />
<FormToggle
switchOnName={t("export.export_to_clipboard")} switchOnTooltip={t("export.export_to_clipboard_on_tooltip")}
switchOffName={t("export.export_to_clipboard")} switchOffTooltip={t("export.export_to_clipboard_off_tooltip")}
currentValue={exportToClipboard} onChange={setExportToClipboard}
/>
</div> </div>
} }
@ -125,10 +137,37 @@ export default function ExportDialog() {
); );
} }
function exportBranch(branchId: string, type: string, format: string, version: string) { async function exportBranch(branchId: string, type: string, format: string, version: string, exportToClipboard: boolean) {
const taskId = utils.randomString(10); const taskId = utils.randomString(10);
const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`); const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`);
open.download(url); if (type === "single" && exportToClipboard) {
await exportSingleToClipboard(url);
} else {
open.download(url);
}
}
async function exportSingleToClipboard(url: string) {
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`);
}
const blob = await res.blob();
// Try reading as text (HTML/Markdown are text); if that fails, fall back to ArrayBuffer->UTF-8
let text: string;
try {
text = await blob.text();
} catch {
const ab = await blob.arrayBuffer();
text = new TextDecoder("utf-8").decode(new Uint8Array(ab));
}
await copyTextWithToast(text);
} catch (error) {
console.error("Failed to copy exported note to clipboard:", error);
}
} }
ws.subscribeToMessages(async (message) => { ws.subscribeToMessages(async (message) => {