Skip to content

Commit bf51e42

Browse files
committed
feat: 10 - handle open internal links in new tab
1 parent 432260e commit bf51e42

File tree

1 file changed

+113
-31
lines changed

1 file changed

+113
-31
lines changed

src/main.ts

+113-31
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { app, BrowserWindow } from 'electron';
1+
import type { BrowserWindowConstructorOptions } from 'electron';
2+
import { app, BrowserWindow, shell } from 'electron';
23
import path from 'path';
34
import { migrateToLatest } from '@server/db/migrations';
45
import router from '@server/router';
@@ -11,12 +12,35 @@ if (require('electron-squirrel-startup')) {
1112
app.quit();
1213
}
1314

14-
const createWindow = async () => {
15+
// This method will be called when Electron has finished
16+
// initialization and is ready to create browser windows.
17+
// Some APIs can only be used after this event occurs.
18+
app.on('ready', onReady);
19+
20+
// Quit when all windows are closed, except on macOS. There, it's common
21+
// for applications and their menu bar to stay active until the user quits
22+
// explicitly with Cmd + Q.
23+
app.on('window-all-closed', () => {
24+
if (process.platform !== 'darwin') {
25+
app.quit();
26+
}
27+
});
28+
29+
app.on('activate', async () => {
30+
// On OS X it's common to re-create a window in the app when the
31+
// dock icon is clicked and there are no other windows open.
32+
if (BrowserWindow.getAllWindows().length === 0) {
33+
await createWindow();
34+
}
35+
});
36+
37+
function createWindow(options?: BrowserWindowConstructorOptions) {
1538
// Create the browser window.
16-
const mainWindow = new BrowserWindow({
39+
const window = new BrowserWindow({
1740
width: 1280,
1841
height: 720,
1942
webPreferences: {
43+
...(options?.webPreferences || {}),
2044
preload: path.join(__dirname, 'preload.js'),
2145
nodeIntegration: true,
2246
},
@@ -25,48 +49,106 @@ const createWindow = async () => {
2549

2650
// and load the index.html of the app.
2751
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
28-
await mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
52+
void window.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
2953
} else {
30-
await mainWindow.loadFile(
54+
void window.loadFile(
3155
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
3256
);
3357
}
3458

3559
// Open the DevTools.
3660
if (!app.isPackaged) {
37-
mainWindow.webContents.openDevTools();
61+
window.webContents.openDevTools();
3862
}
39-
return mainWindow;
40-
};
4163

42-
const onReady = async () => {
43-
await migrateToLatest(getUserSettings().dbPath);
44-
const window = await createWindow();
45-
createIPCHandler({ router, windows: [window] });
46-
setAppMenu();
47-
};
64+
window.webContents.setWindowOpenHandler(({ url }) => {
65+
const newUrl = maybeParseUrl(url);
66+
const currentUrl = maybeParseUrl(window.webContents.getURL());
4867

49-
// This method will be called when Electron has finished
50-
// initialization and is ready to create browser windows.
51-
// Some APIs can only be used after this event occurs.
52-
app.on('ready', onReady);
68+
if (!newUrl || !currentUrl) {
69+
return { action: 'deny' };
70+
}
5371

54-
// Quit when all windows are closed, except on macOS. There, it's common
55-
// for applications and their menu bar to stay active until the user quits
56-
// explicitly with Cmd + Q.
57-
app.on('window-all-closed', () => {
58-
if (process.platform !== 'darwin') {
59-
app.quit();
72+
if (!isOpeningApp(currentUrl, newUrl)) {
73+
void openExternalUrl(url);
74+
return { action: 'deny' };
75+
}
76+
77+
return {
78+
action: 'allow',
79+
createWindow: (options) => {
80+
const newWindow = createWindow(options);
81+
void newWindow.webContents.loadURL(url);
82+
window.addTabbedWindow(newWindow);
83+
return newWindow.webContents;
84+
},
85+
outlivesOpener: true,
86+
overrideBrowserWindowOptions: {
87+
width: 1280,
88+
height: 720,
89+
webPreferences: {
90+
preload: path.join(__dirname, 'preload.js'),
91+
nodeIntegration: true,
92+
},
93+
},
94+
};
95+
});
96+
97+
return window;
98+
}
99+
100+
function isOpeningApp(currentUrl: URL, newUrl: URL) {
101+
if (app.isPackaged) {
102+
return (
103+
currentUrl.protocol === 'file:' &&
104+
currentUrl.protocol === newUrl.protocol &&
105+
currentUrl.pathname === newUrl.pathname
106+
);
60107
}
61-
});
62108

63-
app.on('activate', async () => {
64-
// On OS X it's common to re-create a window in the app when the
65-
// dock icon is clicked and there are no other windows open.
66-
if (BrowserWindow.getAllWindows().length === 0) {
67-
await createWindow();
109+
return (
110+
currentUrl.hostname === 'localhost' &&
111+
currentUrl.host === newUrl.host &&
112+
currentUrl.pathname === newUrl.pathname
113+
);
114+
}
115+
116+
async function openExternalUrl(url: string) {
117+
const parsedUrl = maybeParseUrl(url);
118+
if (!parsedUrl) {
119+
return;
68120
}
69-
});
121+
122+
const { protocol } = parsedUrl;
123+
// We could handle all possible link cases here, not only http/https
124+
if (protocol === 'http:' || protocol === 'https:') {
125+
try {
126+
await shell.openExternal(url);
127+
} catch (error: unknown) {
128+
console.error(`Failed to open url: ${error}`);
129+
}
130+
}
131+
}
132+
133+
function maybeParseUrl(value: string): URL | undefined {
134+
if (typeof value === 'string') {
135+
try {
136+
return new URL(value);
137+
} catch (err) {
138+
// Errors are ignored, as we only want to check if the value is a valid url
139+
console.error(`Failed to parse url: ${value}`);
140+
}
141+
}
142+
143+
return undefined;
144+
}
145+
146+
async function onReady() {
147+
await migrateToLatest(getUserSettings().dbPath);
148+
const window = await createWindow();
149+
createIPCHandler({ router, windows: [window] });
150+
setAppMenu();
151+
}
70152

71153
// In this file you can include the rest of your app's specific main process
72154
// code. You can also put them in separate files and import them here.

0 commit comments

Comments
 (0)