feat: support multiple desktop windows
This commit is contained in:
parent
28e8a77fa3
commit
642fa39bb8
|
|
@ -385,30 +385,6 @@ dependencies = [
|
|||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.42"
|
||||
|
|
@ -421,96 +397,6 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix 1.1.4",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix 1.1.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix 1.1.4",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
|
|
@ -533,12 +419,6 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
|
|
@ -823,19 +703,6 @@ dependencies = [
|
|||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
|
|
@ -1299,15 +1166,6 @@ version = "0.4.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "connection-string"
|
||||
version = "0.2.0"
|
||||
|
|
@ -1872,7 +1730,6 @@ dependencies = [
|
|||
"tauri-plugin-log",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tiberius",
|
||||
|
|
@ -2422,12 +2279,6 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enum_dispatch"
|
||||
version = "0.3.13"
|
||||
|
|
@ -2447,7 +2298,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2504,27 +2354,6 @@ version = "3.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.2.0"
|
||||
|
|
@ -2811,19 +2640,6 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
|
|
@ -5233,16 +5049,6 @@ dependencies = [
|
|||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
|
|
@ -5353,12 +5159,6 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
|
|
@ -5542,17 +5342,6 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.8.0-rc.4"
|
||||
|
|
@ -5637,20 +5426,6 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
|
|
@ -8059,21 +7834,6 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
|
|
@ -8864,17 +8624,6 @@ version = "1.20.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
|
|
@ -10383,67 +10132,6 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"libc",
|
||||
"ordered-stream",
|
||||
"rustix 1.1.4",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow 1.0.3",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"winnow 1.0.3",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
|
|
@ -10645,43 +10333,3 @@ checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
|||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow 1.0.3",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "3.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.117",
|
||||
"winnow 1.0.3",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { useNavigationTargets } from "@/composables/useNavigationTargets";
|
|||
import { useDataGridActions } from "@/composables/useDataGridActions";
|
||||
import { useTauriEvents } from "@/composables/useTauriEvents";
|
||||
import { useVisibilityChange } from "@/composables/useVisibilityChange";
|
||||
import { installWindowDragRegions } from "@/composables/useWindowDragRegion";
|
||||
import "@/i18n";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import * as api from "@/lib/api";
|
||||
|
|
@ -46,6 +47,7 @@ import {
|
|||
isFocusSearchShortcut,
|
||||
isModRShortcut,
|
||||
isNewQueryShortcut,
|
||||
isNewWindowShortcut,
|
||||
isObjectSourceSaveShortcutTarget,
|
||||
isResetZoomShortcut,
|
||||
isRefreshDataShortcut,
|
||||
|
|
@ -59,6 +61,8 @@ import { classifyAiSqlExecution } from "@/lib/aiSqlExecutionPolicy";
|
|||
import { buildHistoryAiAnalysisPrompt } from "@/lib/historyAiAnalysis";
|
||||
import { countAvailableAgentDriverUpdates, type AgentDriverUpdateBadgeState } from "@/lib/agentDriverUpdateBadge";
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/safeStorage";
|
||||
import { openDesktopWindow } from "@/lib/desktopWindows";
|
||||
import { subscribeTableMutations } from "@/lib/tableMutationBroadcast";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -103,6 +107,8 @@ const { setupFileDrop } = useFileDrop();
|
|||
const isDesktop = isTauriRuntime();
|
||||
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000;
|
||||
let updateCheckTimer: ReturnType<typeof setInterval> | undefined;
|
||||
let unsubscribeTableMutations: (() => void) | undefined;
|
||||
let uninstallWindowDragRegions: (() => void) | undefined;
|
||||
const needsAuth = ref(!isDesktop);
|
||||
const authenticated = ref(isDesktop);
|
||||
const setupRequired = ref(false);
|
||||
|
|
@ -760,6 +766,12 @@ function handleKeydown(e: KeyboardEvent) {
|
|||
contentAreaRef.value?.refreshData();
|
||||
return;
|
||||
}
|
||||
if (isNewWindowShortcut(e)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void openDesktopWindow();
|
||||
return;
|
||||
}
|
||||
if (isNewQueryShortcut(e, shortcuts)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -888,7 +900,11 @@ onMounted(async () => {
|
|||
});
|
||||
applyTheme();
|
||||
void applyUiScale(settingsStore.editorSettings.uiScale);
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
window.addEventListener("keydown", handleKeydown, true);
|
||||
uninstallWindowDragRegions = installWindowDragRegions();
|
||||
unsubscribeTableMutations = subscribeTableMutations((message) => {
|
||||
queryStore.markTableTabsStale(message);
|
||||
});
|
||||
window.addEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
|
||||
if (isDesktop) {
|
||||
document.addEventListener("contextmenu", handleContextMenu);
|
||||
|
|
@ -944,7 +960,11 @@ onUnmounted(() => {
|
|||
if (updateCheckTimer) {
|
||||
clearInterval(updateCheckTimer);
|
||||
}
|
||||
window.removeEventListener("keydown", handleKeydown);
|
||||
window.removeEventListener("keydown", handleKeydown, true);
|
||||
uninstallWindowDragRegions?.();
|
||||
uninstallWindowDragRegions = undefined;
|
||||
unsubscribeTableMutations?.();
|
||||
unsubscribeTableMutations = undefined;
|
||||
window.removeEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
|
||||
document.removeEventListener("contextmenu", handleContextMenu);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -690,7 +690,7 @@ const messageRenderer = computed(() => {
|
|||
class="flex items-center gap-2 border-b px-3 shrink-0"
|
||||
:class="settings.editorSettings.appLayout === 'classic' ? 'h-9' : 'h-10'"
|
||||
>
|
||||
<span class="flex flex-1 self-stretch items-center truncate text-xs font-medium" data-tauri-drag-region>
|
||||
<span class="flex flex-1 self-stretch items-center truncate text-xs font-medium" data-window-drag-region>
|
||||
{{ chatTitle }}
|
||||
</span>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6" @click="startNewChat" :title="t('ai.newChat')">
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ const props = defineProps<{
|
|||
totalRowCount?: number;
|
||||
loading?: boolean;
|
||||
cacheKey?: string;
|
||||
dataStale?: boolean;
|
||||
onExecuteSql?: (sql: string) => Promise<void>;
|
||||
fullExportResult?: () => Promise<QueryResult | undefined>;
|
||||
customSave?: (changes: {
|
||||
|
|
@ -5816,6 +5817,12 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-5 text-xs px-1.5 shrink-0"
|
||||
:class="
|
||||
props.dataStale
|
||||
? 'bg-amber-500/15 text-amber-700 hover:bg-amber-500/25 hover:text-amber-800 dark:text-amber-300 dark:hover:text-amber-200'
|
||||
: ''
|
||||
"
|
||||
:title="props.dataStale ? t('grid.dataStaleRefresh') : undefined"
|
||||
:disabled="isSaving"
|
||||
@click="onToolbarRefresh"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -62,10 +62,10 @@ defineExpose({ focusSearch });
|
|||
class="flex items-center gap-px px-3 text-xs font-medium text-muted-foreground border-b bg-muted/20"
|
||||
:class="classicLayout ? 'h-9' : 'h-10'"
|
||||
>
|
||||
<span class="flex self-stretch items-center truncate" data-tauri-drag-region>{{
|
||||
<span class="flex self-stretch items-center truncate" data-window-drag-region>{{
|
||||
t("sidebar.connections")
|
||||
}}</span>
|
||||
<span class="flex-1 self-stretch" data-tauri-drag-region />
|
||||
<span class="flex-1 self-stretch" data-window-drag-region />
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<span class="inline-flex">
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { useQueryStore } from "@/stores/queryStore";
|
|||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useTabScroll } from "@/composables/useTabScroll";
|
||||
import { useTabDrag } from "@/composables/useTabDrag";
|
||||
import { openDesktopWindow } from "@/lib/desktopWindows";
|
||||
import {
|
||||
connectionColor,
|
||||
shouldShowTabOverflowControls,
|
||||
|
|
@ -45,9 +46,14 @@ const emit = defineEmits<{
|
|||
const { t } = useI18n();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const tabDrag = useTabDrag((draggedId, targetId, position) => {
|
||||
queryStore.reorderTab(draggedId, targetId, position);
|
||||
});
|
||||
const tabDrag = useTabDrag(
|
||||
(draggedId, targetId, position) => {
|
||||
queryStore.reorderTab(draggedId, targetId, position);
|
||||
},
|
||||
(draggedId) => {
|
||||
void detachTabToNewWindow(draggedId);
|
||||
},
|
||||
);
|
||||
const editingTabId = ref<string | null>(null);
|
||||
const editingTitle = ref("");
|
||||
const compactTabTitle = computed({
|
||||
|
|
@ -87,6 +93,13 @@ function cancelRenameTab() {
|
|||
editingTabId.value = null;
|
||||
}
|
||||
|
||||
async function detachTabToNewWindow(tabId: string) {
|
||||
const tab = queryStore.tabs.find((item) => item.id === tabId);
|
||||
if (!tab) return;
|
||||
const opened = await openDesktopWindow({ detachedTab: tab });
|
||||
if (opened) queryStore.closeTab(tab.id);
|
||||
}
|
||||
|
||||
function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
|
||||
return [
|
||||
{
|
||||
|
|
@ -271,6 +284,7 @@ const tabOverflowControlClass = computed(() =>
|
|||
<template>
|
||||
<div
|
||||
v-if="queryStore.tabs.length > 0 || showDriverStore"
|
||||
data-app-tab-bar
|
||||
class="relative flex border-b shrink-0"
|
||||
:class="
|
||||
settingsStore.editorSettings.appLayout === 'classic'
|
||||
|
|
@ -415,7 +429,7 @@ const tabOverflowControlClass = computed(() =>
|
|||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div :class="tabTailDragRegionClass" data-tauri-drag-region />
|
||||
<div :class="tabTailDragRegionClass" data-window-drag-region />
|
||||
</div>
|
||||
<div v-if="showTabOverflowControls" class="relative z-30 flex shrink-0 items-center">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ function onToolbarDblClick(e: MouseEvent) {
|
|||
<div
|
||||
class="h-10 flex items-center gap-1 px-2 border-b bg-muted/30 shrink-0"
|
||||
:class="{ 'pl-17.5': shouldReserveMacTrafficLightInset(isMac, isFullscreen, isDesktop) }"
|
||||
data-tauri-drag-region
|
||||
data-window-drag-region
|
||||
@dblclick="onToolbarDblClick"
|
||||
>
|
||||
<Button variant="ghost" size="sm" class="h-8 px-2 text-xs gap-1" @click="emit('new-connection')">
|
||||
|
|
@ -172,7 +172,7 @@ function onToolbarDblClick(e: MouseEvent) {
|
|||
</span>
|
||||
</Button>
|
||||
|
||||
<div class="flex-1" data-tauri-drag-region />
|
||||
<div class="flex-1" data-window-drag-region />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
|
|
|
|||
|
|
@ -505,6 +505,12 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 shrink-0 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
:class="
|
||||
activeTab.dataStale
|
||||
? 'bg-amber-500/15 text-amber-700 hover:bg-amber-500/25 hover:text-amber-800 dark:text-amber-300 dark:hover:text-amber-200'
|
||||
: ''
|
||||
"
|
||||
:title="activeTab.dataStale ? t('grid.dataStaleRefresh') : undefined"
|
||||
:disabled="activeTab.isExecuting"
|
||||
@click="refreshData"
|
||||
>
|
||||
|
|
@ -554,6 +560,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
|||
:initial-order-by-input="activeTab.orderByInput"
|
||||
:sql="activeTab.lastExecutedSql || activeTab.sql"
|
||||
:loading="activeTab.isExecuting"
|
||||
:data-stale="activeTab.dataStale"
|
||||
:editable="!!activeTab.queryAnalysis"
|
||||
:source-columns="activeTab.querySourceColumns"
|
||||
context="results"
|
||||
|
|
@ -804,12 +811,14 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
|||
:initial-order-by-input="activeTab.orderByInput"
|
||||
:sql="activeTab.sql"
|
||||
:loading="activeTab.isExecuting"
|
||||
:data-stale="activeTab.dataStale"
|
||||
:editable="isTableDataEditable(activeEffectiveDatabaseType, activeTableMeta?.primaryKeys ?? [])"
|
||||
context="table-data"
|
||||
:initial-where-input="activeTab.whereInput"
|
||||
:database-type="activeEffectiveDatabaseType"
|
||||
:connection-id="activeTab.connectionId"
|
||||
:database="activeTab.database"
|
||||
:schema="activeTab.schema"
|
||||
:table-meta="activeDataTabTableMeta"
|
||||
:page-offset="activeTab.resultPageOffset"
|
||||
:page-limit="activeTab.resultPageLimit"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import { rowStatusFilterAfterAddingRow, type RowStatusFilter } from "@/lib/gridR
|
|||
import { supportsDataGridTransaction } from "@/lib/tableEditing";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import type { ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN } from "@/lib/tableEditing";
|
||||
import { broadcastTableMutation } from "@/lib/tableMutationBroadcast";
|
||||
|
||||
type CellValue = string | number | boolean | null;
|
||||
|
||||
|
|
@ -94,6 +96,7 @@ const pendingChangesCache = new Map<string, PendingChangesSnapshot>();
|
|||
export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
const connectionStore = useConnectionStore();
|
||||
const historyStore = useHistoryStore();
|
||||
const queryStore = useQueryStore();
|
||||
|
||||
const {
|
||||
result,
|
||||
|
|
@ -640,6 +643,19 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
);
|
||||
}
|
||||
|
||||
function notifyTableMutation() {
|
||||
if (!connectionId.value || !database.value || !tableMeta.value) return;
|
||||
const message = {
|
||||
connectionId: connectionId.value,
|
||||
database: database.value,
|
||||
schema: tableMeta.value.schema,
|
||||
tableName: tableMeta.value.tableName,
|
||||
sourceTabId: cacheKey?.value,
|
||||
};
|
||||
queryStore.markTableTabsStale(message);
|
||||
broadcastTableMutation(message);
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
saveError.value = "";
|
||||
isSaving.value = true;
|
||||
|
|
@ -662,6 +678,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
dirtyRows.value.clear();
|
||||
newRows.value = [];
|
||||
deletedRows.value.clear();
|
||||
notifyTableMutation();
|
||||
exitTransaction();
|
||||
isSaving.value = false;
|
||||
if (shouldReloadAfterSave) {
|
||||
|
|
@ -752,6 +769,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
dirtyRows.value.clear();
|
||||
newRows.value = [];
|
||||
deletedRows.value.clear();
|
||||
notifyTableMutation();
|
||||
exitTransaction();
|
||||
isSaving.value = false;
|
||||
if (shouldReloadAfterSave) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ let pending: {
|
|||
sourceEl: HTMLElement | null;
|
||||
} | null = null;
|
||||
let onDropCallback: ((draggedId: string, targetId: string, position: TabDropPosition) => void) | null = null;
|
||||
let onDetachCallback: ((draggedId: string) => void) | null = null;
|
||||
let ghostEl: HTMLElement | null = null;
|
||||
|
||||
function createGhost(sourceEl: HTMLElement, x: number, y: number) {
|
||||
|
|
@ -95,10 +96,25 @@ function onMouseMove(event: MouseEvent) {
|
|||
}
|
||||
|
||||
if (state.active) {
|
||||
if (state.draggedId && onDetachCallback && isOutsideViewport(event)) {
|
||||
const draggedId = state.draggedId;
|
||||
onDetachCallback(draggedId);
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
moveGhost(event.clientX, event.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
function isOutsideViewport(event: MouseEvent) {
|
||||
return (
|
||||
event.clientX <= 0 ||
|
||||
event.clientY <= 0 ||
|
||||
event.clientX >= window.innerWidth - 1 ||
|
||||
event.clientY >= window.innerHeight - 1
|
||||
);
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
if (state.active && state.draggedId && state.targetId && state.dropPosition && onDropCallback) {
|
||||
onDropCallback(state.draggedId, state.targetId, state.dropPosition);
|
||||
|
|
@ -128,9 +144,13 @@ function ensureListeners() {
|
|||
listenersAttached = true;
|
||||
}
|
||||
|
||||
export function useTabDrag(onDrop: (draggedId: string, targetId: string, position: TabDropPosition) => void) {
|
||||
export function useTabDrag(
|
||||
onDrop: (draggedId: string, targetId: string, position: TabDropPosition) => void,
|
||||
onDetach?: (draggedId: string) => void,
|
||||
) {
|
||||
ensureListeners();
|
||||
onDropCallback = onDrop;
|
||||
onDetachCallback = onDetach || null;
|
||||
|
||||
function startDrag(event: MouseEvent, tabId: string) {
|
||||
if (event.button !== 0) return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
|
||||
const INTERACTIVE_SELECTOR = [
|
||||
"button",
|
||||
"[role='button']",
|
||||
"a",
|
||||
"input",
|
||||
"textarea",
|
||||
"select",
|
||||
"[contenteditable='true']",
|
||||
"[data-no-window-drag]",
|
||||
].join(",");
|
||||
|
||||
function shouldStartWindowDrag(event: MouseEvent) {
|
||||
if (event.defaultPrevented || event.button !== 0) return false;
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
if (target.closest(INTERACTIVE_SELECTOR)) return false;
|
||||
return !!target.closest("[data-window-drag-region]");
|
||||
}
|
||||
|
||||
export function installWindowDragRegions() {
|
||||
if (!isTauriRuntime()) return () => {};
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
if (!shouldStartWindowDrag(event)) return;
|
||||
event.preventDefault();
|
||||
void import("@tauri-apps/api/window")
|
||||
.then(({ getCurrentWindow }) => getCurrentWindow().startDragging())
|
||||
.catch((error) => {
|
||||
console.warn("[window-drag] startDragging failed", error);
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("mousedown", onMouseDown, true);
|
||||
return () => window.removeEventListener("mousedown", onMouseDown, true);
|
||||
}
|
||||
|
|
@ -628,6 +628,7 @@ export default {
|
|||
queryError: "Query Error",
|
||||
dataUnavailable: "Table data needs to be reloaded.",
|
||||
refresh: "Refresh",
|
||||
dataStaleRefresh: "This table may have changed in another window. Refresh to update.",
|
||||
commit: "Commit",
|
||||
rollback: "Rollback",
|
||||
transactionSaveHint: "Commit {count} pending change(s) in a transaction.",
|
||||
|
|
|
|||
|
|
@ -582,6 +582,7 @@ export default {
|
|||
queryError: "Error de consulta",
|
||||
dataUnavailable: "Los datos de la tabla deben volver a cargarse.",
|
||||
refresh: "Actualizar",
|
||||
dataStaleRefresh: "Esta tabla puede haber cambiado en otra ventana. Actualiza para ver los datos.",
|
||||
commit: "Confirmar",
|
||||
rollback: "Revertir",
|
||||
transactionSaveHint: "Confirma {count} cambio(s) pendiente(s) en una transacción.",
|
||||
|
|
|
|||
|
|
@ -633,6 +633,7 @@ export default {
|
|||
queryError: "Errore Query",
|
||||
dataUnavailable: "I dati della tabella devono essere ricaricati.",
|
||||
refresh: "Aggiorna",
|
||||
dataStaleRefresh: "Questa tabella potrebbe essere cambiata in un'altra finestra. Aggiorna per vedere i dati.",
|
||||
commit: "Commit",
|
||||
rollback: "Rollback",
|
||||
transactionSaveHint: "Applica {count} modifica/he in sospeso in una transazione.",
|
||||
|
|
|
|||
|
|
@ -623,6 +623,7 @@ export default {
|
|||
queryError: "查询出错",
|
||||
dataUnavailable: "表数据需要重新加载。",
|
||||
refresh: "刷新",
|
||||
dataStaleRefresh: "这个表可能已在其他窗口变更,刷新后可更新数据。",
|
||||
commit: "提交",
|
||||
rollback: "回滚",
|
||||
transactionSaveHint: "在事务中提交 {count} 项待保存更改。",
|
||||
|
|
|
|||
|
|
@ -622,6 +622,7 @@ export default {
|
|||
tableInfoNoResults: "沒有相符結果",
|
||||
queryError: "查詢出錯",
|
||||
refresh: "重新整理",
|
||||
dataStaleRefresh: "這個資料表可能已在其他視窗變更,重新整理後可更新資料。",
|
||||
commit: "提交",
|
||||
rollback: "回溯",
|
||||
transactionSaveHint: "在事務中提交 {count} 項待儲存更改。",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { serializeOpenTabs } from "@/lib/openTabsPersistence";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import type { QueryTab } from "@/types/database";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const DETACHED_TAB_STORAGE_PREFIX = "dbx-detached-tab:";
|
||||
|
||||
function windowLabel() {
|
||||
return `main-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
function storeDetachedTab(tab: QueryTab): string {
|
||||
const key = windowLabel();
|
||||
localStorage.setItem(`${DETACHED_TAB_STORAGE_PREFIX}${key}`, JSON.stringify(serializeOpenTabs([tab])[0]));
|
||||
return key;
|
||||
}
|
||||
|
||||
export function detachedTabStorageKey(key: string) {
|
||||
return `${DETACHED_TAB_STORAGE_PREFIX}${key}`;
|
||||
}
|
||||
|
||||
export async function openDesktopWindow(options: { detachedTab?: QueryTab } = {}): Promise<boolean> {
|
||||
if (!isTauriRuntime()) return false;
|
||||
|
||||
try {
|
||||
const detachedTabKey = options.detachedTab ? storeDetachedTab(options.detachedTab) : null;
|
||||
const url = detachedTabKey ? `/?detachedTab=${encodeURIComponent(detachedTabKey)}` : "/?newWindow=1";
|
||||
await invoke("open_new_desktop_window", { url });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[DBX] Failed to open desktop window", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -80,6 +80,10 @@ export function isNewQueryShortcut(event: ShortcutLikeEvent, shortcuts?: Partial
|
|||
return matchesShortcut(event, actionShortcut("newQuery", shortcuts));
|
||||
}
|
||||
|
||||
export function isNewWindowShortcut(event: ShortcutLikeEvent): boolean {
|
||||
return matchesShortcut(event, "Mod+Shift+N");
|
||||
}
|
||||
|
||||
export function isFocusSearchShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
|
||||
return matchesShortcut(event, actionShortcut("focusSearch", shortcuts));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
export interface TableMutationMessage {
|
||||
connectionId: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
sourceTabId?: string;
|
||||
sourceWindowId: string;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
const CHANNEL_NAME = "dbx-table-mutations";
|
||||
const STORAGE_KEY = "dbx-table-mutated";
|
||||
const windowId = crypto.randomUUID();
|
||||
const channel = typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(CHANNEL_NAME);
|
||||
|
||||
export function currentTableMutationWindowId() {
|
||||
return windowId;
|
||||
}
|
||||
|
||||
export function broadcastTableMutation(message: Omit<TableMutationMessage, "sourceWindowId" | "nonce">) {
|
||||
const payload: TableMutationMessage = {
|
||||
...message,
|
||||
sourceWindowId: windowId,
|
||||
nonce: crypto.randomUUID(),
|
||||
};
|
||||
channel?.postMessage(payload);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
|
||||
}
|
||||
|
||||
export function subscribeTableMutations(callback: (message: TableMutationMessage) => void) {
|
||||
const onMessage = (event: MessageEvent<TableMutationMessage>) => {
|
||||
if (event.data?.sourceWindowId !== windowId) callback(event.data);
|
||||
};
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
if (event.key !== STORAGE_KEY || !event.newValue) return;
|
||||
try {
|
||||
const message = JSON.parse(event.newValue) as TableMutationMessage;
|
||||
if (message.sourceWindowId !== windowId) callback(message);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
channel?.addEventListener("message", onMessage);
|
||||
window.addEventListener("storage", onStorage);
|
||||
|
||||
return () => {
|
||||
channel?.removeEventListener("message", onMessage);
|
||||
window.removeEventListener("storage", onStorage);
|
||||
};
|
||||
}
|
||||
|
|
@ -32,6 +32,8 @@ import { tableMetaForDataTab } from "@/lib/tableDataTabMeta";
|
|||
import { quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
|
||||
import { queryTimeoutSecsForConnection } from "@/lib/queryTimeout";
|
||||
import { detachedTabStorageKey } from "@/lib/desktopWindows";
|
||||
import type { TableMutationMessage } from "@/lib/tableMutationBroadcast";
|
||||
import * as api from "@/lib/api";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
|
|
@ -40,6 +42,8 @@ import type { SavedSqlFile } from "@/types/database";
|
|||
const STORAGE_KEY = "dbx-open-tabs";
|
||||
const ACTIVE_TAB_KEY = "dbx-active-tab";
|
||||
const ORACLE_LIKE_METADATA_TYPES = new Set<string>(["oracle", "dameng", "oceanbase-oracle"]);
|
||||
const WINDOW_PARAMS = new URLSearchParams(window.location.search);
|
||||
const DISABLE_SHARED_TAB_PERSISTENCE = WINDOW_PARAMS.has("detachedTab") || WINDOW_PARAMS.get("newWindow") === "1";
|
||||
|
||||
function markQueryResultRowsRaw(result: QueryResult): QueryResult {
|
||||
markRaw(result.rows);
|
||||
|
|
@ -91,6 +95,7 @@ function normalizeOracleLikeQueryAnalysis(
|
|||
}
|
||||
|
||||
function saveTabs(tabs: QueryTab[], activeTabId: string | null) {
|
||||
if (DISABLE_SHARED_TAB_PERSISTENCE) return;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(serializeOpenTabs(tabs)));
|
||||
localStorage.setItem(ACTIVE_TAB_KEY, activeTabId || "");
|
||||
|
|
@ -99,6 +104,15 @@ function saveTabs(tabs: QueryTab[], activeTabId: string | null) {
|
|||
|
||||
function loadSavedTabs(): { tabs: QueryTab[]; activeTabId: string | null } {
|
||||
try {
|
||||
const detachedTabKey = WINDOW_PARAMS.get("detachedTab");
|
||||
if (detachedTabKey) {
|
||||
const key = detachedTabStorageKey(detachedTabKey);
|
||||
const raw = localStorage.getItem(key);
|
||||
localStorage.removeItem(key);
|
||||
return restoreOpenTabsState(raw ? `[${raw}]` : null, null);
|
||||
}
|
||||
if (WINDOW_PARAMS.get("newWindow") === "1") return { tabs: [], activeTabId: null };
|
||||
|
||||
return restoreOpenTabsState(localStorage.getItem(STORAGE_KEY), localStorage.getItem(ACTIVE_TAB_KEY));
|
||||
} catch {
|
||||
return { tabs: [], activeTabId: null };
|
||||
|
|
@ -570,6 +584,45 @@ export const useQueryStore = defineStore("query", () => {
|
|||
if (tab) tab.tableMeta = meta;
|
||||
}
|
||||
|
||||
function tableIdentity(tab: QueryTab) {
|
||||
const meta = tableMetaForDataTab(tab);
|
||||
if (!meta?.tableName) return null;
|
||||
return {
|
||||
connectionId: tab.connectionId,
|
||||
database: tab.database,
|
||||
schema: meta.schema || "",
|
||||
tableName: meta.tableName,
|
||||
};
|
||||
}
|
||||
|
||||
function isSameTable(
|
||||
tab: QueryTab,
|
||||
message: Pick<TableMutationMessage, "connectionId" | "database" | "schema" | "tableName">,
|
||||
) {
|
||||
const identity = tableIdentity(tab);
|
||||
return (
|
||||
!!identity &&
|
||||
identity.connectionId === message.connectionId &&
|
||||
identity.database === message.database &&
|
||||
identity.schema === (message.schema || "") &&
|
||||
identity.tableName === message.tableName
|
||||
);
|
||||
}
|
||||
|
||||
function markTableTabsStale(
|
||||
message: Pick<TableMutationMessage, "connectionId" | "database" | "schema" | "tableName" | "sourceTabId">,
|
||||
) {
|
||||
for (const tab of tabs.value) {
|
||||
if (tab.id === message.sourceTabId) continue;
|
||||
if (isSameTable(tab, message)) tab.dataStale = true;
|
||||
}
|
||||
}
|
||||
|
||||
function clearDataStale(id: string) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (tab) tab.dataStale = undefined;
|
||||
}
|
||||
|
||||
function setObjectSource(id: string, objectSource: NonNullable<QueryTab["objectSource"]>) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (tab) tab.objectSource = objectSource;
|
||||
|
|
@ -820,6 +873,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
tab.executionId = executionId;
|
||||
tab.lastExecutedSql = sql;
|
||||
tab.resultTotalRowCount = undefined;
|
||||
tab.dataStale = undefined;
|
||||
const previousResultSessionClose = closeResultSession(tab, options?.pagination?.sessionId);
|
||||
if (!options?.preserveResultDuringExecution || !tab.result) {
|
||||
clearResultPayload(tab);
|
||||
|
|
@ -1493,6 +1547,8 @@ export const useQueryStore = defineStore("query", () => {
|
|||
updateSchema,
|
||||
updateConnection,
|
||||
setTableMeta,
|
||||
markTableTabsStale,
|
||||
clearDataStale,
|
||||
invalidateTableStructure,
|
||||
tableStructureRefreshVersion,
|
||||
setObjectSource,
|
||||
|
|
|
|||
|
|
@ -362,6 +362,7 @@ export interface QueryTab {
|
|||
resultTotalRowCount?: number;
|
||||
resultSessionId?: string;
|
||||
resultAccessedAt?: number;
|
||||
dataStale?: boolean;
|
||||
pinned?: boolean;
|
||||
result?: QueryResult;
|
||||
results?: QueryResult[];
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ impl Storage {
|
|||
|
||||
async fn init_schema(&self) -> Result<(), String> {
|
||||
self.db.with_connection(|conn| {
|
||||
configure_app_storage_connection(conn)?;
|
||||
for statement in SCHEMA_STATEMENTS {
|
||||
conn.execute(statement, []).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
|
@ -148,6 +149,14 @@ impl Storage {
|
|||
}
|
||||
}
|
||||
|
||||
fn configure_app_storage_connection(conn: &Connection) -> Result<(), String> {
|
||||
conn.execute_batch(
|
||||
"PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;",
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn ensure_history_columns_sync(conn: &Connection) -> Result<(), String> {
|
||||
const COLUMNS: &[(&str, &str)] = &[
|
||||
("activity_kind", "TEXT NOT NULL DEFAULT 'query'"),
|
||||
|
|
@ -1139,6 +1148,7 @@ fn map_from_sql_err(err: serde_json::Error) -> rusqlite::Error {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DesktopIconTheme, DesktopSettings, Storage};
|
||||
use crate::history::HistoryEntry;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn temp_db_path(name: &str) -> std::path::PathBuf {
|
||||
|
|
@ -1154,6 +1164,68 @@ mod tests {
|
|||
assert_eq!(storage.load_desktop_settings().await.unwrap(), DesktopSettings::default());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_storage_supports_two_handles_opening_and_writing_same_db() {
|
||||
let path = temp_db_path("multi-instance-shared-store");
|
||||
let first = Storage::open(&path).await.unwrap();
|
||||
let second = Storage::open(&path).await.unwrap();
|
||||
|
||||
first
|
||||
.save_desktop_settings(&DesktopSettings { show_tray_icon: false, icon_theme: DesktopIconTheme::Black })
|
||||
.await
|
||||
.unwrap();
|
||||
second.save_sidebar_layout(&serde_json::json!({"conn-1": ["db", "schema"]})).await.unwrap();
|
||||
|
||||
let first_history = HistoryEntry {
|
||||
id: "history-1".to_string(),
|
||||
connection_id: "conn-1".to_string(),
|
||||
connection_name: "Local".to_string(),
|
||||
database: "main".to_string(),
|
||||
sql: "select 1".to_string(),
|
||||
executed_at: "2026-06-05T00:00:00Z".to_string(),
|
||||
execution_time_ms: 1,
|
||||
success: true,
|
||||
error: None,
|
||||
activity_kind: "query".to_string(),
|
||||
operation: String::new(),
|
||||
target: String::new(),
|
||||
affected_rows: None,
|
||||
rollback_sql: None,
|
||||
details_json: None,
|
||||
};
|
||||
let second_history = HistoryEntry {
|
||||
id: "history-2".to_string(),
|
||||
connection_id: "conn-2".to_string(),
|
||||
connection_name: "Local 2".to_string(),
|
||||
database: "main".to_string(),
|
||||
sql: "select 2".to_string(),
|
||||
executed_at: "2026-06-05T00:00:01Z".to_string(),
|
||||
execution_time_ms: 2,
|
||||
success: true,
|
||||
error: None,
|
||||
activity_kind: "query".to_string(),
|
||||
operation: String::new(),
|
||||
target: String::new(),
|
||||
affected_rows: None,
|
||||
rollback_sql: None,
|
||||
details_json: None,
|
||||
};
|
||||
let write_first = first.save_history_entry(&first_history);
|
||||
let write_second = second.save_history_entry(&second_history);
|
||||
|
||||
let (first_result, second_result) = tokio::join!(write_first, write_second);
|
||||
first_result.unwrap();
|
||||
second_result.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
second.load_desktop_settings().await.unwrap(),
|
||||
DesktopSettings { show_tray_icon: false, icon_theme: DesktopIconTheme::Black }
|
||||
);
|
||||
assert_eq!(first.load_sidebar_layout().await.unwrap(), Some(serde_json::json!({"conn-1": ["db", "schema"]})));
|
||||
let history = first.load_history_entries(10, 0).await.unwrap();
|
||||
assert_eq!(history.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn desktop_settings_fall_back_to_legacy_background_preference() {
|
||||
let path = temp_db_path("desktop-settings-legacy-background");
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-dialog = "2.7.0"
|
||||
tauri-plugin-fs = "2.5.0"
|
||||
tauri-plugin-single-instance = "2.4.2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-process = "2"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
"main",
|
||||
"main-*"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ mod window_state_guard;
|
|||
|
||||
use commands::connection::AppState;
|
||||
use dbx_core::storage::{DesktopIconTheme, DesktopSettings, Storage};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -14,7 +15,7 @@ use tauri::{
|
|||
menu::MenuBuilder,
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
};
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
|
|
@ -23,6 +24,33 @@ const DESKTOP_TRAY_ID: &str = "main-tray";
|
|||
const MACOS_TRAY_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/tray-macos-template.png");
|
||||
const BLACK_APP_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/icon-black.png");
|
||||
|
||||
#[tauri::command]
|
||||
fn open_new_desktop_window(app: tauri::AppHandle, url: Option<String>) -> Result<(), String> {
|
||||
let label = format!("main-{}", uuid::Uuid::new_v4());
|
||||
let url = WebviewUrl::App(url.unwrap_or_else(|| "/?newWindow=1".to_string()).into());
|
||||
let mut builder = WebviewWindowBuilder::new(&app, label, url)
|
||||
.title("DBX")
|
||||
.inner_size(1280.0, 800.0)
|
||||
.min_inner_size(900.0, 600.0)
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.visible(true);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.accept_first_mouse(true)
|
||||
.traffic_light_position(tauri::LogicalPosition::new(16.0, 18.0))
|
||||
.hidden_title(true);
|
||||
}
|
||||
|
||||
let window = builder.build().map_err(|err| err.to_string())?;
|
||||
window.show().map_err(|err| err.to_string())?;
|
||||
window.set_focus().map_err(|err| err.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_hide_window_on_close(target_os: &str) -> bool {
|
||||
matches!(target_os, "macos" | "windows")
|
||||
}
|
||||
|
|
@ -35,6 +63,10 @@ fn should_show_main_window_after_setup() -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
fn should_register_single_instance(_target_os: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn show_main_window<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
|
|
@ -43,14 +75,81 @@ fn show_main_window<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
|||
}
|
||||
}
|
||||
|
||||
fn open_connection_deep_links(app: &tauri::AppHandle, links: Vec<String>) {
|
||||
if links.is_empty() {
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
struct StartupPayload {
|
||||
connection_links: Vec<String>,
|
||||
sql_file_paths: Vec<String>,
|
||||
db_file_paths: Vec<String>,
|
||||
}
|
||||
|
||||
impl StartupPayload {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.connection_links.is_empty() && self.sql_file_paths.is_empty() && self.db_file_paths.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn startup_payload_from_args<I, S>(args: I, cwd: &Path) -> StartupPayload
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let args: Vec<String> = args.into_iter().map(|arg| arg.as_ref().to_string()).collect();
|
||||
StartupPayload {
|
||||
connection_links: commands::deep_link::connection_deep_links_from_args(args.iter()),
|
||||
sql_file_paths: commands::external_sql::sql_file_paths_from_args(args.iter(), cwd),
|
||||
db_file_paths: commands::external_db::db_file_paths_from_args(args.iter(), cwd),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn startup_payload_from_opened_urls(urls: &[tauri::Url]) -> StartupPayload {
|
||||
let connection_links = urls
|
||||
.iter()
|
||||
.map(|url| url.to_string())
|
||||
.filter_map(|url| commands::deep_link::connection_deep_link_from_arg(&url))
|
||||
.collect();
|
||||
let sql_file_paths = urls
|
||||
.iter()
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
.filter(|path| commands::external_sql::is_sql_file_path(path))
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.collect();
|
||||
let db_file_paths = urls
|
||||
.iter()
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
.filter(|path| commands::external_db::is_db_file_path(path))
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
StartupPayload { connection_links, sql_file_paths, db_file_paths }
|
||||
}
|
||||
|
||||
fn open_startup_payload(app: &tauri::AppHandle, payload: StartupPayload) {
|
||||
if payload.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(state) = app.try_state::<commands::deep_link::DeepLinkOpenState>() {
|
||||
state.push(links.clone());
|
||||
|
||||
if !payload.connection_links.is_empty() {
|
||||
if let Some(state) = app.try_state::<commands::deep_link::DeepLinkOpenState>() {
|
||||
state.push(payload.connection_links.clone());
|
||||
}
|
||||
let _ = app.emit("dbx-open-connection-links", payload.connection_links);
|
||||
}
|
||||
let _ = app.emit("dbx-open-connection-links", links);
|
||||
|
||||
if !payload.sql_file_paths.is_empty() {
|
||||
if let Some(state) = app.try_state::<commands::external_sql::ExternalSqlOpenState>() {
|
||||
state.push(payload.sql_file_paths.clone());
|
||||
}
|
||||
let _ = app.emit("dbx-open-sql-files", payload.sql_file_paths);
|
||||
}
|
||||
|
||||
if !payload.db_file_paths.is_empty() {
|
||||
if let Some(state) = app.try_state::<commands::external_db::ExternalDbOpenState>() {
|
||||
state.push(payload.db_file_paths.clone());
|
||||
}
|
||||
let _ = app.emit("dbx-open-db-files", payload.db_file_paths);
|
||||
}
|
||||
|
||||
show_main_window(app);
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +266,42 @@ pub(crate) fn apply_desktop_settings(app: &tauri::AppHandle, desktop_settings: &
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{should_hide_window_on_close, should_setup_desktop_tray, should_show_main_window_after_setup};
|
||||
use super::{
|
||||
should_hide_window_on_close, should_register_single_instance, should_setup_desktop_tray,
|
||||
should_show_main_window_after_setup, startup_payload_from_args, StartupPayload,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn single_instance_is_disabled_for_all_desktop_platforms() {
|
||||
assert!(!should_register_single_instance("windows"));
|
||||
assert!(!should_register_single_instance("macos"));
|
||||
assert!(!should_register_single_instance("linux"));
|
||||
assert!(!should_register_single_instance("freebsd"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_all_startup_payload_types_from_args() {
|
||||
let payload = startup_payload_from_args(
|
||||
[
|
||||
"dbx://connection/new?type=mysql&host=127.0.0.1",
|
||||
"queries/report.sql",
|
||||
"/tmp/local.sqlite",
|
||||
"--ignored",
|
||||
"/tmp/readme.txt",
|
||||
],
|
||||
Path::new("/work"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
StartupPayload {
|
||||
connection_links: vec!["dbx://connection/new?type=mysql&host=127.0.0.1".to_string()],
|
||||
sql_file_paths: vec!["/work/queries/report.sql".to_string()],
|
||||
db_file_paths: vec!["/tmp/local.sqlite".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hides_window_on_close_for_windows_and_macos() {
|
||||
|
|
@ -223,6 +357,20 @@ mod tests {
|
|||
assert!(source
|
||||
.contains("if should_show_main_window_after_setup() {\n show_main_window(app.handle());"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regular_startup_uses_shared_payload_dispatch() {
|
||||
let source = include_str!("lib.rs");
|
||||
assert!(source.contains("let startup_payload = startup_payload_from_args(std::env::args().skip(1), &cwd);"));
|
||||
assert!(source.contains("open_startup_payload(app.handle(), startup_payload);"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_instance_plugin_is_not_registered() {
|
||||
let source = include_str!("lib.rs");
|
||||
let plugin_init = concat!("tauri_plugin_", "single_instance", "::init");
|
||||
assert!(!source.contains(plugin_init));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
|
|
@ -230,33 +378,13 @@ pub fn run() {
|
|||
rustls::crypto::aws_lc_rs::default_provider().install_default().expect("Failed to install rustls crypto provider");
|
||||
|
||||
let startup_begin = Instant::now();
|
||||
debug_assert!(!should_register_single_instance(std::env::consts::OS));
|
||||
|
||||
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| {
|
||||
let links = commands::deep_link::connection_deep_links_from_args(args.clone());
|
||||
open_connection_deep_links(app, links);
|
||||
|
||||
let paths = commands::external_sql::sql_file_paths_from_args(args.clone(), std::path::Path::new(&cwd));
|
||||
if !paths.is_empty() {
|
||||
if let Some(state) = app.try_state::<commands::external_sql::ExternalSqlOpenState>() {
|
||||
state.push(paths.clone());
|
||||
}
|
||||
let _ = app.emit("dbx-open-sql-files", paths);
|
||||
}
|
||||
|
||||
let db_paths = commands::external_db::db_file_paths_from_args(args, std::path::Path::new(&cwd));
|
||||
if !db_paths.is_empty() {
|
||||
if let Some(state) = app.try_state::<commands::external_db::ExternalDbOpenState>() {
|
||||
state.push(db_paths.clone());
|
||||
}
|
||||
let _ = app.emit("dbx-open-db-files", db_paths);
|
||||
}
|
||||
show_main_window(app);
|
||||
}))
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
|
|
@ -305,8 +433,9 @@ pub fn run() {
|
|||
app.manage(commands::external_sql::ExternalSqlOpenState::default());
|
||||
app.manage(commands::external_db::ExternalDbOpenState::default());
|
||||
app.manage(commands::deep_link::DeepLinkOpenState::default());
|
||||
let startup_links = commands::deep_link::connection_deep_links_from_args(std::env::args().skip(1));
|
||||
open_connection_deep_links(app.handle(), startup_links);
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
let startup_payload = startup_payload_from_args(std::env::args().skip(1), &cwd);
|
||||
open_startup_payload(app.handle(), startup_payload);
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
commands::mcp_bridge::start(app_handle, state);
|
||||
|
|
@ -526,6 +655,7 @@ pub fn run() {
|
|||
commands::agents::import_agents_from_zip,
|
||||
commands::agents::import_agent_jar_cmd,
|
||||
commands::system_fonts::list_system_fonts,
|
||||
open_new_desktop_window,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
|
|
@ -535,46 +665,7 @@ pub fn run() {
|
|||
|
||||
#[cfg(target_os = "macos")]
|
||||
if let RunEvent::Opened { urls } = &event {
|
||||
let links: Vec<String> = urls
|
||||
.iter()
|
||||
.map(|url| url.to_string())
|
||||
.filter_map(|url| commands::deep_link::connection_deep_link_from_arg(&url))
|
||||
.collect();
|
||||
open_connection_deep_links(app_handle, links);
|
||||
|
||||
let paths: Vec<String> = urls
|
||||
.iter()
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
.filter(|path| commands::external_sql::is_sql_file_path(path))
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.collect();
|
||||
if !paths.is_empty() {
|
||||
if let Some(state) = app_handle.try_state::<commands::external_sql::ExternalSqlOpenState>() {
|
||||
state.push(paths.clone());
|
||||
}
|
||||
let _ = app_handle.emit("dbx-open-sql-files", paths);
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
let db_paths: Vec<String> = urls
|
||||
.iter()
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
.filter(|path| commands::external_db::is_db_file_path(path))
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.collect();
|
||||
if !db_paths.is_empty() {
|
||||
if let Some(state) = app_handle.try_state::<commands::external_db::ExternalDbOpenState>() {
|
||||
state.push(db_paths.clone());
|
||||
}
|
||||
let _ = app_handle.emit("dbx-open-db-files", db_paths);
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
open_startup_payload(app_handle, startup_payload_from_opened_urls(urls));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"fullscreen": false,
|
||||
"visible": false,
|
||||
"titleBarStyle": "Overlay",
|
||||
"acceptFirstMouse": true,
|
||||
"trafficLightPosition": {
|
||||
"x": 16,
|
||||
"y": 18
|
||||
|
|
|
|||
Loading…
Reference in New Issue