diff --git a/Cargo.lock b/Cargo.lock index cfd7188ec..b763b07f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,6 +183,27 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + [[package]] name = "argon2" version = "0.5.3" @@ -963,6 +984,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -1197,6 +1224,15 @@ dependencies = [ "inout 0.2.2", ] +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "cmake" version = "0.1.58" @@ -1819,6 +1855,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-clipboard-manager", "tauri-plugin-deep-link", "tauri-plugin-dialog", "tauri-plugin-fs", @@ -2187,6 +2224,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -2443,6 +2486,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "event-listener" version = "5.4.1" @@ -2494,6 +2543,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -2550,6 +2605,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flagset" version = "0.4.7" @@ -2911,6 +2972,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -3640,6 +3711,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -4487,6 +4572,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.19.2" @@ -4667,6 +4762,15 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -4808,6 +4912,7 @@ dependencies = [ "block2", "objc2", "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation", ] @@ -5346,6 +5451,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", +] + [[package]] name = "phf" version = "0.13.1" @@ -5796,6 +5912,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -7745,6 +7873,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-deep-link" version = "2.4.9" @@ -8135,6 +8278,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -8597,6 +8754,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -9058,6 +9226,76 @@ dependencies = [ "semver", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.4", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.99" @@ -9197,6 +9435,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "whoami" version = "2.1.2" @@ -9860,6 +10104,24 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.4", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "writeable" version = "0.6.3" @@ -9940,6 +10202,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "x509-cert" version = "0.2.5" @@ -10244,6 +10523,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.11.0" diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index ba01c354e..7458dd961 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -55,7 +55,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; import { useDatabaseOptions } from "@/composables/useDatabaseOptions"; -import type { DatabaseType, TreeNode, TreeNodeType } from "@/types/database"; +import type { ColumnInfo, DatabaseType, TreeNode, TreeNodeType } from "@/types/database"; import * as api from "@/lib/api"; import { uuid } from "@/lib/utils"; import { resolveDefaultDatabase } from "@/lib/defaultDatabase"; @@ -150,6 +150,8 @@ const savedSqlStore = useSavedSqlStore(); const settingsStore = useSettingsStore(); const { toast } = useToast(); const { highlight } = useSqlHighlighter(); + +type StructureCopyFormat = "tsv" | "markdown"; const { getDatabaseOptions } = useDatabaseOptions(); const showVisibleDatabasesDialog = ref(false); @@ -795,10 +797,13 @@ const showDropTableConfirm = ref(false); const showDropTableChildObjectConfirm = ref(false); const showBatchDropConfirm = ref(false); const showStructurePreviewDialog = ref(false); +const showStructureDocCopyDialog = ref(false); const structurePreviewSql = ref(""); const structurePreviewTitle = ref(""); const structurePreviewDefaultFileName = ref("structure.sql"); const structurePreviewError = ref(""); +const structureDocCopyText = ref(""); +const structureDocCopyTitle = ref(""); const isLoadingStructurePreview = ref(false); const showEmptyTableConfirm = ref(false); const showTruncateTableConfirm = ref(false); @@ -1741,6 +1746,124 @@ function structureExportTargets(): Array node.id === props.node.id) ? selected : [props.node]; } +function structureTargetName(target: TreeNode): string { + return target.schema ? `${target.schema}.${target.label}` : target.label; +} + +function columnDocValue(value: unknown): string { + return value === null || value === undefined ? "" : String(value); +} + +function tsvCell(value: unknown): string { + return columnDocValue(value).replace(/\t/g, " ").replace(/\r?\n/g, " ").trim(); +} + +function markdownCell(value: unknown): string { + return columnDocValue(value).replace(/\|/g, "\\|").replace(/\r?\n/g, "
").trim(); +} + +function columnDocHeaders(includeTable: boolean): string[] { + const headers = [ + t("contextMenu.structureDocColumn"), + t("contextMenu.structureDocType"), + t("contextMenu.structureDocPrimaryKey"), + t("contextMenu.structureDocNullable"), + t("contextMenu.structureDocDefault"), + t("contextMenu.structureDocComment"), + ]; + return includeTable ? [t("contextMenu.structureDocTable"), ...headers] : headers; +} + +function columnDocCells(target: TreeNode, column: ColumnInfo, includeTable: boolean): unknown[] { + const cells = [ + column.name, + column.data_type, + column.is_primary_key ? t("contextMenu.structureDocYes") : t("contextMenu.structureDocNo"), + column.is_nullable ? t("contextMenu.structureDocYes") : t("contextMenu.structureDocNo"), + column.column_default, + column.comment, + ]; + return includeTable ? [structureTargetName(target), ...cells] : cells; +} + +async function tableColumnsForStructureCopy( + target: TreeNode & { connectionId: string; database: string }, +): Promise { + await connectionStore.ensureConnected(target.connectionId); + return (await api.getColumns( + target.connectionId, + target.database, + target.schema || target.database, + target.label, + )) as ColumnInfo[]; +} + +async function buildStructureCopyText(format: StructureCopyFormat): Promise { + const targets = structureExportTargets(); + if (!targets.length) return ""; + const includeTable = targets.length > 1; + const headers = columnDocHeaders(includeTable); + + if (format === "tsv") { + const lines = [headers.map(tsvCell).join("\t")]; + for (const target of targets) { + const columns = await tableColumnsForStructureCopy(target); + for (const column of columns) { + lines.push(columnDocCells(target, column, includeTable).map(tsvCell).join("\t")); + } + } + return `${lines.join("\n")}\n`; + } + + const tables: string[] = []; + const markdownHeaders = columnDocHeaders(false); + for (const target of targets) { + const columns = await tableColumnsForStructureCopy(target); + const tableLines = [ + `### ${markdownCell(structureTargetName(target))}`, + "", + `| ${markdownHeaders.map(markdownCell).join(" | ")} |`, + `| ${markdownHeaders.map(() => "---").join(" | ")} |`, + ...columns.map((column) => `| ${columnDocCells(target, column, false).map(markdownCell).join(" | ")} |`), + ]; + tables.push(tableLines.join("\n")); + } + return `${tables.join("\n\n")}\n`; +} + +async function copyStructureAs(format: StructureCopyFormat) { + let text = ""; + try { + text = await buildStructureCopyText(format); + if (!text) return; + await copyToClipboard(text); + toast(t("contextMenu.structureDocCopied"), 2000); + } catch (e: any) { + if (text) { + structureDocCopyText.value = text; + structureDocCopyTitle.value = + format === "tsv" ? t("contextMenu.copyStructureAsTsv") : t("contextMenu.copyStructureAsMarkdown"); + showStructureDocCopyDialog.value = true; + return; + } + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } +} + +async function copyStructureDocText() { + if (!structureDocCopyText.value) return; + try { + await copyToClipboard(structureDocCopyText.value); + toast(t("contextMenu.structureDocCopied"), 2000); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } +} + +function selectTextareaContent(event: FocusEvent) { + if (event.target instanceof HTMLTextAreaElement) event.target.select(); +} + async function copyStructurePreview() { if (!structurePreviewSql.value) return; try { @@ -2428,6 +2551,17 @@ function exportDataSubmenu(): ContextMenuItem { }; } +function copyStructureAsSubmenu(): ContextMenuItem { + return { + label: t("contextMenu.copyStructureAs"), + icon: Clipboard, + children: [ + { label: t("contextMenu.copyStructureAsTsv"), action: () => copyStructureAs("tsv") }, + { label: t("contextMenu.copyStructureAsMarkdown"), action: () => copyStructureAs("markdown") }, + ], + }; +} + function treeItemMenuItems(): ContextMenuItem[] { const node = props.node; const items: ContextMenuItem[] = []; @@ -2629,6 +2763,7 @@ function treeItemMenuItems(): ContextMenuItem[] { items.push(exportDataSubmenu()); items.push({ label: t("contextMenu.exportDatabase"), action: openDatabaseExport, icon: Download }); items.push({ label: t("contextMenu.exportStructure"), action: exportStructure, icon: FileCode }); + items.push(copyStructureAsSubmenu()); if (isTableNotView.value) { items.push({ label: "", separator: true }); items.push({ label: t("contextMenu.duplicateStructure"), action: duplicateStructure, icon: CopyPlus }); @@ -3037,6 +3172,30 @@ function treeItemMenuItems(): ContextMenuItem[] { + + + + {{ structureDocCopyTitle || t("contextMenu.copyStructureAs") }} + +
+

{{ t("contextMenu.structureDocCopyFallbackHint") }}

+ +
+ + + + +
+
+ diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index fa0223e35..85c5c1fe3 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -922,6 +922,20 @@ export default { exportStructurePreviewTitleMultiple: "Structure - {count} objects", exportStructureLoading: "Loading structure...", copyStructure: "Copy Structure", + copyStructureAs: "Copy Structure As", + copyStructureAsTsv: "TSV (for Excel)", + copyStructureAsMarkdown: "Markdown Table", + structureDocTable: "Table", + structureDocColumn: "Column", + structureDocType: "Type", + structureDocPrimaryKey: "Primary Key", + structureDocNullable: "Nullable", + structureDocDefault: "Default", + structureDocComment: "Comment", + structureDocYes: "Yes", + structureDocNo: "No", + structureDocCopied: "Structure copied", + structureDocCopyFallbackHint: "Automatic copy was blocked. Click Copy again, or select the content below.", saveStructure: "Save SQL", exportStructureCopied: "Structure copied", createTable: "Create Table", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 2f7d179da..acd7cb112 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -818,6 +818,21 @@ export default { exportStructurePreviewTitleMultiple: "Estructura - {count} objetos", exportStructureLoading: "Cargando estructura...", copyStructure: "Copiar estructura", + copyStructureAs: "Copiar estructura como", + copyStructureAsTsv: "TSV (para Excel)", + copyStructureAsMarkdown: "Tabla Markdown", + structureDocTable: "Tabla", + structureDocColumn: "Columna", + structureDocType: "Tipo", + structureDocPrimaryKey: "Clave primaria", + structureDocNullable: "Permite NULL", + structureDocDefault: "Valor predeterminado", + structureDocComment: "Comentario", + structureDocYes: "Sí", + structureDocNo: "No", + structureDocCopied: "Estructura copiada", + structureDocCopyFallbackHint: + "La copia automática fue bloqueada. Haz clic en Copiar de nuevo o selecciona el contenido inferior.", saveStructure: "Guardar SQL", exportStructureCopied: "Estructura copiada", createTable: "Crear tabla", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index b372d9109..0b9a516e1 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -903,6 +903,20 @@ export default { exportStructurePreviewTitleMultiple: "表结构 - {count} 个对象", exportStructureLoading: "正在读取表结构...", copyStructure: "复制表结构", + copyStructureAs: "复制表结构为", + copyStructureAsTsv: "TSV(粘贴到 Excel)", + copyStructureAsMarkdown: "Markdown 表格", + structureDocTable: "表", + structureDocColumn: "字段名称", + structureDocType: "类型", + structureDocPrimaryKey: "主键", + structureDocNullable: "允许为空", + structureDocDefault: "默认值", + structureDocComment: "注释", + structureDocYes: "是", + structureDocNo: "否", + structureDocCopied: "表结构已复制", + structureDocCopyFallbackHint: "自动复制被系统拦截。请再次点击复制,或选中下方内容手动复制。", saveStructure: "保存 SQL", exportStructureCopied: "表结构已复制", createTable: "新建表", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 427806fc4..e97ae2d6a 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -903,6 +903,20 @@ export default { exportStructurePreviewTitleMultiple: "資料表結構 - {count} 個物件", exportStructureLoading: "正在讀取資料表結構...", copyStructure: "複製資料表結構", + copyStructureAs: "複製資料表結構為", + copyStructureAsTsv: "TSV(貼到 Excel)", + copyStructureAsMarkdown: "Markdown 表格", + structureDocTable: "資料表", + structureDocColumn: "欄位名稱", + structureDocType: "類型", + structureDocPrimaryKey: "主鍵", + structureDocNullable: "允許空值", + structureDocDefault: "預設值", + structureDocComment: "註解", + structureDocYes: "是", + structureDocNo: "否", + structureDocCopied: "資料表結構已複製", + structureDocCopyFallbackHint: "自動複製被系統阻擋。請再次點擊複製,或選取下方內容手動複製。", saveStructure: "儲存 SQL", exportStructureCopied: "資料表結構已複製", createTable: "建立資料表", diff --git a/apps/desktop/src/lib/clipboard.ts b/apps/desktop/src/lib/clipboard.ts index 3eafc5fb6..fae92a5a6 100644 --- a/apps/desktop/src/lib/clipboard.ts +++ b/apps/desktop/src/lib/clipboard.ts @@ -1,3 +1,5 @@ +import { isTauriRuntime } from "./tauriRuntime"; + interface ClipboardApi { writeText?: (text: string) => Promise | void; } @@ -15,6 +17,7 @@ interface ClipboardTextarea { opacity?: string; }; setAttribute(name: string, value: string): void; + focus?(): void; select(): void; setSelectionRange?(start: number, end: number): void; } @@ -37,6 +40,16 @@ export async function copyToClipboard( text: string, env: ClipboardEnvironment = globalThis as unknown as ClipboardEnvironment, ): Promise { + if (isTauriRuntime(env as unknown as Record)) { + try { + const { writeText } = await import("@tauri-apps/plugin-clipboard-manager"); + await writeText(text); + return; + } catch { + // Fall through to Web Clipboard / legacy copy when the native plugin is unavailable. + } + } + try { if (env.navigator?.clipboard?.writeText) { await env.navigator.clipboard.writeText(text); @@ -61,6 +74,7 @@ export async function copyToClipboard( document.body.appendChild(textarea); try { + textarea.focus?.(); textarea.select(); textarea.setSelectionRange?.(0, text.length); if (!document.execCommand("copy")) { diff --git a/package.json b/package.json index 311931c46..cf77a843f 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.41.1", "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-fs": "^2.5.1", "@tauri-apps/plugin-process": "^2.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95ad3eaae..007fbb344 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@tauri-apps/api': specifier: ^2.11.0 version: 2.11.0 + '@tauri-apps/plugin-clipboard-manager': + specifier: ^2.3.2 + version: 2.3.2 '@tauri-apps/plugin-dialog': specifier: ^2.7.1 version: 2.7.1 @@ -1460,6 +1463,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-clipboard-manager@2.3.2': + resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==} + '@tauri-apps/plugin-dialog@2.7.1': resolution: {integrity: sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==} @@ -4696,6 +4702,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.11.0 '@tauri-apps/cli-win32-x64-msvc': 2.11.0 + '@tauri-apps/plugin-clipboard-manager@2.3.2': + dependencies: + '@tauri-apps/api': 2.11.0 + '@tauri-apps/plugin-dialog@2.7.1': dependencies: '@tauri-apps/api': 2.11.0 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 741b77b60..979eff657 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -52,3 +52,4 @@ calamine = "0.30.1" zip = "4.6.1" dbx-core = { path = "../crates/dbx-core" } font-kit = "0.14.3" +tauri-plugin-clipboard-manager = "2.3.2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 2787f8273..b46192451 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -18,6 +18,7 @@ "core:window:allow-close", "core:window:allow-start-dragging", "core:webview:allow-set-webview-zoom", + "clipboard-manager:allow-write-text", "dialog:default", "dialog:allow-save", "dialog:allow-open", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7a3a73336..c1889718b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -233,6 +233,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_single_instance::init(|app, args, cwd| {