Skip to content

Commit c372ba0

Browse files
committed
feat(dialog/export): export to clipboard
1 parent f8b292d commit c372ba0

File tree

3 files changed

+52
-6
lines changed

3 files changed

+52
-6
lines changed

apps/client/src/translations/en/translation.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@
102102
"opml_version_1": "OPML v1.0 - plain text only",
103103
"opml_version_2": "OPML v2.0 - allows also HTML",
104104
"export_type_single": "Only this note without its descendants",
105+
"export_to_clipboard": "Export to clipboard",
106+
"export_to_clipboard_on_tooltip": "Export the note content to clipboard.",
107+
"export_to_clipboard_off_tooltip": "Download the note as a file.",
105108
"export": "Export",
106109
"choose_export_type": "Choose export type first please",
107110
"export_status": "Export status",

apps/client/src/widgets/dialogs/export.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313

1414
.export-dialog form .form-check-label {
1515
padding: 2px;
16-
}
16+
}
17+
18+
.export-dialog form .export-single-formats .switch-widget {
19+
margin-top: 10px;
20+
}

apps/client/src/widgets/dialogs/export.tsx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useState } from "preact/hooks";
22
import { t } from "../../services/i18n";
33
import tree from "../../services/tree";
4+
import { copyTextWithToast } from "../../services/clipboard_ext.js";
45
import Button from "../react/Button";
56
import FormRadioGroup from "../react/FormRadioGroup";
7+
import FormToggle from "../react/FormToggle";
68
import Modal from "../react/Modal";
79
import "./export.css";
810
import ws from "../../services/ws";
@@ -21,10 +23,12 @@ interface ExportDialogProps {
2123
export default function ExportDialog() {
2224
const [ opts, setOpts ] = useState<ExportDialogProps>();
2325
const [ exportType, setExportType ] = useState<string>(opts?.defaultType ?? "subtree");
26+
const [ exportToClipboard, setExportToClipboard ] = useState(false);
2427
const [ subtreeFormat, setSubtreeFormat ] = useState("html");
2528
const [ singleFormat, setSingleFormat ] = useState("html");
2629
const [ opmlVersion, setOpmlVersion ] = useState("2.0");
2730
const [ shown, setShown ] = useState(false);
31+
const [ exporting, setExporting ] = useState(false);
2832

2933
useTriliumEvent("showExportDialog", async ({ notePath, defaultType }) => {
3034
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
@@ -47,18 +51,20 @@ export default function ExportDialog() {
4751
className="export-dialog"
4852
title={`${t("export.export_note_title")} ${opts?.noteTitle ?? ""}`}
4953
size="lg"
50-
onSubmit={() => {
54+
onSubmit={async () => {
5155
if (!opts || !opts.branchId) {
5256
return;
5357
}
5458

5559
const format = (exportType === "subtree" ? subtreeFormat : singleFormat);
5660
const version = (format === "opml" ? opmlVersion : "1.0");
57-
exportBranch(opts.branchId, exportType, format, version);
61+
setExporting(true);
62+
await exportBranch(opts.branchId, exportType, format, version, exportToClipboard);
63+
setExporting(false);
5864
setShown(false);
5965
}}
6066
onHidden={() => setShown(false)}
61-
footer={<Button className="export-button" text={t("export.export")} primary />}
67+
footer={<Button className="export-button" text={t("export.export")} primary disabled={exporting} />}
6268
show={shown}
6369
>
6470

@@ -118,17 +124,50 @@ export default function ExportDialog() {
118124
{ value: "markdown", label: t("export.format_markdown") }
119125
]}
120126
/>
127+
128+
<FormToggle
129+
switchOnName={t("export.export_to_clipboard")} switchOnTooltip={t("export.export_to_clipboard_on_tooltip")}
130+
switchOffName={t("export.export_to_clipboard")} switchOffTooltip={t("export.export_to_clipboard_off_tooltip")}
131+
currentValue={exportToClipboard} onChange={setExportToClipboard}
132+
/>
121133
</div>
122134
}
123135

124136
</Modal>
125137
);
126138
}
127139

128-
function exportBranch(branchId: string, type: string, format: string, version: string) {
140+
async function exportBranch(branchId: string, type: string, format: string, version: string, exportToClipboard: boolean) {
129141
const taskId = utils.randomString(10);
130142
const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`);
131-
open.download(url);
143+
if (type === "single" && exportToClipboard) {
144+
await exportSingleToClipboard(url);
145+
} else {
146+
open.download(url);
147+
}
148+
}
149+
150+
async function exportSingleToClipboard(url: string) {
151+
try {
152+
const res = await fetch(url);
153+
if (!res.ok) {
154+
throw new Error(`${res.status} ${res.statusText}`);
155+
}
156+
const blob = await res.blob();
157+
158+
// Try reading as text (HTML/Markdown are text); if that fails, fall back to ArrayBuffer->UTF-8
159+
let text: string;
160+
try {
161+
text = await blob.text();
162+
} catch {
163+
const ab = await blob.arrayBuffer();
164+
text = new TextDecoder("utf-8").decode(new Uint8Array(ab));
165+
}
166+
167+
await copyTextWithToast(text);
168+
} catch (error) {
169+
console.error("Failed to copy exported note to clipboard:", error);
170+
}
132171
}
133172

134173
ws.subscribeToMessages(async (message) => {

0 commit comments

Comments
 (0)