From 8a8b7c6d422a6734b58c8c791fe09def4133e6f5 Mon Sep 17 00:00:00 2001 From: "@roth-dev" Date: Wed, 29 Jan 2025 22:13:43 +0700 Subject: [PATCH 1/4] add bundle url schemes --- electron-builder.json5 | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/electron-builder.json5 b/electron-builder.json5 index dd50b24..73e996b 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -9,6 +9,25 @@ }, files: ["dist", "dist-electron"], afterSign: "scripts/notarize.cjs", + + // 🔹 Enable deep linking across platforms + protocols: [ + { + name: "Outerbase Protocol", + schemes: [ + "outerbase", + "sqlite", + "mysql", + "postgres", + "turso", + "starbase", + "dolt", + "cloudflare", + ], + role: "Editor", + }, + ], + mac: { notarize: false, target: [ @@ -22,6 +41,25 @@ }, ], artifactName: "outerbase-mac-${version}.${ext}", + entitlements: "entitlements.mac.plist", + entitlementsInherit: "entitlements.mac.plist", + extendInfo: { + CFBundleURLTypes: [ + { + CFBundleURLName: "Outerbase", + CFBundleURLSchemes: [ + "outerbase", + "sqlite", + "mysql", + "postgres", + "turso", + "starbase", + "dolt", + "cloudflare", + ], + }, + ], + }, }, win: { target: [ From 32d7127765dbb3db1a83f7a35a40b99b8c643389 Mon Sep 17 00:00:00 2001 From: "@roth-dev" Date: Sun, 2 Feb 2025 21:05:37 +0700 Subject: [PATCH 2/4] add deeplink code --- electron/constants/index.ts | 11 +++++++++ electron/main.ts | 46 +++++++++++++++++++++++++++++++++++++ src/database/index.tsx | 2 ++ src/hooks/useDeeplink.ts | 32 ++++++++++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 src/hooks/useDeeplink.ts diff --git a/electron/constants/index.ts b/electron/constants/index.ts index 7f64b10..de7244b 100644 --- a/electron/constants/index.ts +++ b/electron/constants/index.ts @@ -3,3 +3,14 @@ export const STUDIO_ENDPOINT = "https://studio.outerbase.com/embed"; export const OUTERBASE_WEBSITE = "https://outerbase.com"; export const OUTERBASE_GITHUB = "https://github.com/outerbase/studio-desktop/issues"; + +export const OuterbaseProtocols = [ + "outerbase", + "sqlite", + "mysql", + "postgres", + "turso", + "starbase", + "dolt", + "cloudflare", +]; diff --git a/electron/main.ts b/electron/main.ts index fe8eff1..15cbdd3 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -16,6 +16,7 @@ import { type ConnectionStoreItem } from "@/lib/conn-manager-store"; import { createDatabaseWindow } from "./window/create-database"; import { bindMenuIpc, bindDockerIpc, bindSavedDocIpc } from "./ipc"; import { bindAnalyticIpc } from "./ipc/analytics"; +import { OuterbaseProtocols } from "./constants"; export function getAutoUpdater(): AppUpdater { // Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'. @@ -55,6 +56,10 @@ settings.load(); const mainWindow = new MainWindow(); +OuterbaseProtocols.forEach((protocol) => { + app.setAsDefaultProtocolClient(protocol); +}); + // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. @@ -138,6 +143,47 @@ ipcMain.handle("set-setting", (_, key, value) => { ipcMain.on("navigate", (event, route: string) => { event.sender.send("navigate-to", route); }); +// Handle deep links +const gotTheLock = app.requestSingleInstanceLock(); + +if (!gotTheLock) { + app.quit(); +} else { + app.on("second-instance", (_, commandLine) => { + // Process deep link when app is already running + const url = commandLine.find( + (arg) => + arg.startsWith("mysql://") || + arg.startsWith("postgres://") || + arg.startsWith("outerbase://") || + arg.startsWith("sqlite://") || + arg.startsWith("starbase://") || + arg.startsWith("turso://") || + arg.startsWith("cloudflare://"), + ); + if (url) { + handleDeepLink(url); + } + }); + + app.on("open-url", (event, url) => { + event.preventDefault(); + handleDeepLink(url); + }); +} +function handleDeepLink(url: string) { + console.log("Deep link received:", url); + const win = mainWindow.getWindow(); + // Someone tried to run a second instance, we should focus our window. + if (win) { + if (win.isMinimized()) { + win.restore(); + } else { + win.focus(); + } + win?.webContents.send("deep-link", url); + } +} bindSavedDocIpc(); bindAnalyticIpc(); diff --git a/src/database/index.tsx b/src/database/index.tsx index 26a0aab..cd27941 100644 --- a/src/database/index.tsx +++ b/src/database/index.tsx @@ -7,8 +7,10 @@ import ImportConnectionStringRoute from "./import-connection-string"; import useNavigateToRoute from "@/hooks/useNavigateToRoute"; import AddConnectionDropdown from "./add-connection-dropdown"; import ConnectionList from "@/components/database/connection-list"; +import useDeeplink from "@/hooks/useDeeplink"; function ConnectionListRoute() { + useDeeplink(); useNavigateToRoute(); const [search, setSearch] = useState(""); const [connectionList, setConnectionList] = useState(() => { diff --git a/src/hooks/useDeeplink.ts b/src/hooks/useDeeplink.ts new file mode 100644 index 0000000..e1f8b85 --- /dev/null +++ b/src/hooks/useDeeplink.ts @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; + +export default function useDeeplink() { + const navigate = useNavigate(); + + useEffect(() => { + const handleDeepLink = (_: unknown, url: URL) => { + try { + // Example: Extract database type and connection details + const urlObj = new URL(url); + const protocol = urlObj.protocol.replace(":", ""); // mysql, postgres, outerbase + const host = urlObj.hostname; + const port = urlObj.port || (protocol === "mysql" ? 3306 : 5432); + const database = urlObj.pathname.replace("/", ""); + + console.log("Protocol:", protocol); + console.log("Host:", host); + console.log("Port:", port); + console.log("Database:", database); + navigate(`/connection/create/${protocol}`); + } catch (error) { + console.error("Failed to handle deep link:", error); + } + }; + + window.outerbaseIpc.on("deep-link", handleDeepLink); + return () => { + window.outerbaseIpc.off("deep-link", handleDeepLink); + }; + }, [navigate]); +} From 9c21fcb9c7268669ef7683c41e0c12c49c9a470e Mon Sep 17 00:00:00 2001 From: roth-dev Date: Thu, 6 Feb 2025 23:25:00 +0700 Subject: [PATCH 3/4] finalize deep linking app --- electron/main.ts | 48 +++++++++++++++++++++++++++------------- src/hooks/useDeeplink.ts | 29 +++++++++++------------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 15cbdd3..02e723d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -56,10 +56,17 @@ settings.load(); const mainWindow = new MainWindow(); -OuterbaseProtocols.forEach((protocol) => { - app.setAsDefaultProtocolClient(protocol); -}); - +if (process.defaultApp) { + if (process.argv.length >= 2) { + OuterbaseProtocols.forEach((protocol) => { + app.setAsDefaultProtocolClient(protocol, process.execPath, [ + path.resolve(process.argv[1]), + ]); + }); + } +} else { + app.setAsDefaultProtocolClient("outerbase"); +} // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. @@ -151,16 +158,10 @@ if (!gotTheLock) { } else { app.on("second-instance", (_, commandLine) => { // Process deep link when app is already running - const url = commandLine.find( - (arg) => - arg.startsWith("mysql://") || - arg.startsWith("postgres://") || - arg.startsWith("outerbase://") || - arg.startsWith("sqlite://") || - arg.startsWith("starbase://") || - arg.startsWith("turso://") || - arg.startsWith("cloudflare://"), + const url = commandLine.find((arg) => + OuterbaseProtocols.some((protocol) => arg.startsWith(`${protocol}://`)), ); + if (url) { handleDeepLink(url); } @@ -173,7 +174,6 @@ if (!gotTheLock) { } function handleDeepLink(url: string) { - console.log("Deep link received:", url); const win = mainWindow.getWindow(); // Someone tried to run a second instance, we should focus our window. if (win) { @@ -182,7 +182,25 @@ function handleDeepLink(url: string) { } else { win.focus(); } - win?.webContents.send("deep-link", url); + try { + const urlObj = new URL(url); + const protocol = urlObj.protocol.replace(":", ""); + const host = urlObj.hostname; + const port = urlObj.port || (protocol === "mysql" ? 3306 : 5432); + const database = urlObj.pathname.replace("/", ""); + + // Send deep link data to the React frontend + win.webContents.send("deep-link", { + protocol, + host, + port, + database, + }); + } catch (error) { + console.error("Invalid deep link:", url); + } + } else { + mainWindow.init(); } } bindSavedDocIpc(); diff --git a/src/hooks/useDeeplink.ts b/src/hooks/useDeeplink.ts index e1f8b85..b3b5326 100644 --- a/src/hooks/useDeeplink.ts +++ b/src/hooks/useDeeplink.ts @@ -1,26 +1,23 @@ +import { OuterbaseProtocols } from "../../electron/constants"; import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; +interface Args { + protocol: string; + host: string; + port: string; + database: string; +} export default function useDeeplink() { const navigate = useNavigate(); useEffect(() => { - const handleDeepLink = (_: unknown, url: URL) => { - try { - // Example: Extract database type and connection details - const urlObj = new URL(url); - const protocol = urlObj.protocol.replace(":", ""); // mysql, postgres, outerbase - const host = urlObj.hostname; - const port = urlObj.port || (protocol === "mysql" ? 3306 : 5432); - const database = urlObj.pathname.replace("/", ""); - - console.log("Protocol:", protocol); - console.log("Host:", host); - console.log("Port:", port); - console.log("Database:", database); - navigate(`/connection/create/${protocol}`); - } catch (error) { - console.error("Failed to handle deep link:", error); + const handleDeepLink = (_event: unknown, { database }: Args) => { + const matchRoute = + OuterbaseProtocols.findIndex((protocol) => protocol === database) > -1; + // currently handle only create connection route + if (matchRoute) { + navigate(`/connection/create/${database}`); } }; From 197f1336e5b3c9390a75a9a91c1dab3756cd1921 Mon Sep 17 00:00:00 2001 From: roth-dev Date: Thu, 6 Feb 2025 23:53:06 +0700 Subject: [PATCH 4/4] support multi protocols --- electron/main.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 02e723d..16e8a1f 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -56,17 +56,17 @@ settings.load(); const mainWindow = new MainWindow(); -if (process.defaultApp) { - if (process.argv.length >= 2) { - OuterbaseProtocols.forEach((protocol) => { +OuterbaseProtocols.forEach((protocol) => { + if (process.defaultApp) { + if (process.argv.length >= 2) { app.setAsDefaultProtocolClient(protocol, process.execPath, [ path.resolve(process.argv[1]), ]); - }); + } + } else { + app.setAsDefaultProtocolClient(protocol); } -} else { - app.setAsDefaultProtocolClient("outerbase"); -} +}); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q.