mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
180 lines
6.5 KiB
TypeScript
180 lines
6.5 KiB
TypeScript
import {
|
|
Copy,
|
|
Download,
|
|
FileCode,
|
|
FileText,
|
|
Share2,
|
|
} from "lucide-react";
|
|
import React, { useState, useRef, useEffect } from "react";
|
|
import { useI18n } from "../../application/i18n/I18nProvider";
|
|
import {
|
|
buildVaultNoteMarkdownExportFiles,
|
|
sanitizeNoteExportFileNamePart,
|
|
type VaultNote,
|
|
} from "../../domain/notes";
|
|
import { copyToClipboard } from "../keychain/utils";
|
|
import { toast } from "../ui/toast";
|
|
import { buildTextFilesZipBlob } from "../../lib/textZip";
|
|
|
|
export interface NoteExportMenuProps {
|
|
note: VaultNote | null;
|
|
allNotes: VaultNote[];
|
|
className?: string;
|
|
}
|
|
|
|
export const NoteExportMenu: React.FC<NoteExportMenuProps> = ({
|
|
note,
|
|
allNotes,
|
|
className = "",
|
|
}) => {
|
|
const { t } = useI18n();
|
|
const [open, setOpen] = useState(false);
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
window.addEventListener("mousedown", handleClickOutside);
|
|
return () => window.removeEventListener("mousedown", handleClickOutside);
|
|
}, [open]);
|
|
|
|
const handleExportSingleMarkdown = () => {
|
|
if (!note) return;
|
|
const fileName = `${sanitizeNoteExportFileNamePart(note.title, "note")}.md`;
|
|
const blob = new Blob([note.content], { type: "text/markdown;charset=utf-8" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = fileName;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
toast.success(t("notes.exportSuccess") || "已导出 Markdown 文件");
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleExportSingleHtml = () => {
|
|
if (!note) return;
|
|
const title = note.title || "Untitled Note";
|
|
const escapedTitle = title.replace(/</g, "<").replace(/>/g, ">");
|
|
const htmlContent = `<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>${escapedTitle}</title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 40px auto; padding: 0 20px; color: #333; }
|
|
h1, h2, h3 { color: #111; }
|
|
pre { background: #f4f4f5; padding: 16px; border-radius: 8px; overflow-x: auto; }
|
|
code { font-family: monospace; background: #f4f4f5; padding: 2px 6px; border-radius: 4px; }
|
|
pre code { background: none; padding: 0; }
|
|
blockquote { border-left: 4px solid #e4e4e7; margin: 0; padding-left: 16px; color: #71717a; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>${escapedTitle}</h1>
|
|
<pre style="white-space: pre-wrap;">${note.content.replace(/</g, "<").replace(/>/g, ">")}</pre>
|
|
</body>
|
|
</html>`;
|
|
const fileName = `${sanitizeNoteExportFileNamePart(note.title, "note")}.html`;
|
|
const blob = new Blob([htmlContent], { type: "text/html;charset=utf-8" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = fileName;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
toast.success(t("notes.exportSuccess") || "已导出 HTML 文件");
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleCopyMarkdown = async () => {
|
|
if (!note) return;
|
|
const ok = await copyToClipboard(note.content);
|
|
if (ok) {
|
|
toast.success(t("common.copied") || "已复制到剪贴板");
|
|
}
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleExportAllZip = () => {
|
|
if (!allNotes.length) return;
|
|
const files = buildVaultNoteMarkdownExportFiles(allNotes, { type: "all" });
|
|
const zipBlob = buildTextFilesZipBlob(files);
|
|
const url = URL.createObjectURL(zipBlob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `netcatty-notes-backup-${new Date().toISOString().slice(0, 10)}.zip`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
toast.success(t("notes.exportAllSuccess") || "已导出全部笔记备份 ZIP");
|
|
setOpen(false);
|
|
};
|
|
|
|
return (
|
|
<div className={`relative inline-block ${className}`} ref={menuRef}>
|
|
<button
|
|
type="button"
|
|
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
title={t("notes.export") || "导出与分享"}
|
|
onClick={() => setOpen((prev) => !prev)}
|
|
>
|
|
<Share2 size={16} />
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="absolute right-0 top-full mt-1.5 w-56 bg-popover border border-border rounded-lg shadow-lg py-1.5 z-50 text-sm animate-in fade-in-50 zoom-in-95">
|
|
{note && (
|
|
<>
|
|
<div className="px-3 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
当前笔记
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="w-full px-3 py-1.5 flex items-center gap-2.5 hover:bg-muted text-foreground transition-colors text-left"
|
|
onClick={handleExportSingleMarkdown}
|
|
>
|
|
<FileText size={14} className="text-primary" />
|
|
<span>导出为 Markdown (.md)</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="w-full px-3 py-1.5 flex items-center gap-2.5 hover:bg-muted text-foreground transition-colors text-left"
|
|
onClick={handleExportSingleHtml}
|
|
>
|
|
<FileCode size={14} className="text-primary" />
|
|
<span>导出为 HTML (.html)</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="w-full px-3 py-1.5 flex items-center gap-2.5 hover:bg-muted text-foreground transition-colors text-left"
|
|
onClick={handleCopyMarkdown}
|
|
>
|
|
<Copy size={14} className="text-muted-foreground" />
|
|
<span>复制 Markdown 源码</span>
|
|
</button>
|
|
<div className="my-1 border-t border-border" />
|
|
</>
|
|
)}
|
|
|
|
<div className="px-3 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
全部笔记
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="w-full px-3 py-1.5 flex items-center gap-2.5 hover:bg-muted text-foreground transition-colors text-left"
|
|
onClick={handleExportAllZip}
|
|
disabled={!allNotes.length}
|
|
>
|
|
<Download size={14} className="text-emerald-500" />
|
|
<span>导出全部为 ZIP 压缩包</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|