Skip to content

Commit 250b3d4

Browse files
authored
Merge pull request #66 from legiz-ru/pandora-deeplink
feat: deeplink import
2 parents ad3ae9f + d66e027 commit 250b3d4

File tree

12 files changed

+466
-7
lines changed

12 files changed

+466
-7
lines changed

doc/README.en.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@
3030
- Go to `Settings``Enable Authorization` → Restart the app → When the authorization prompt appears, grant
3131
permission → TUN mode can then be enabled in the app
3232

33+
## Deeplink Profile Import
34+
35+
Pandora-Box supports importing profiles via deeplink URLs, allowing users to easily add subscriptions from external sources.
36+
37+
### URL Scheme
38+
39+
The deeplink uses the custom protocol `pandora-box://` with the following format:
40+
41+
```
42+
pandora-box://install-config?url=SUBSCRIPTION_URL
43+
```
44+
3345
## Note: Px Requires Network Access
3446

3547
- When prompted, click "Allow" to grant network access

doc/README.ru.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@
3030
- Настройки → Включить авторизацию → Перезапустить приложение → Подтвердить авторизацию в всплывающем окне → Готово
3131
- После входа в приложение можно активировать режим TUN
3232

33+
## Импорт профилей через Deeplink
34+
35+
Pandora-Box поддерживает импорт профилей через deeplink URL, что позволяет пользователям легко добавлять подписки из внешних источников.
36+
37+
### Схема URL
38+
39+
Deeplink использует пользовательский протокол `pandora-box://` в следующем формате:
40+
41+
```
42+
pandora-box://install-config?url=SUBSCRIPTION_URL
43+
```
44+
3345
## Запрос Px на сетевой доступ
3446

3547
- Просто нажмите "Разрешить"

doc/README.zh-CN.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@
3030
- 设置 → 开启授权 → 重启软件 → 弹出授权框 → 完成授权
3131
- 进入软件后即可开启 TUN 模式
3232

33+
## 深度链接配置导入
34+
35+
Pandora-Box 支持通过深度链接 URL 导入配置,让用户可以轻松地从外部来源添加订阅。
36+
37+
### URL 格式
38+
39+
深度链接使用自定义协议 `pandora-box://`,格式如下:
40+
41+
```
42+
pandora-box://install-config?url=SUBSCRIPTION_URL
43+
```
44+
3345
## 提示 Px 需要网络接入
3446

3547
- 点击 “允许” 即可

forge.config.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ const config: ForgeConfig = {
2020
LSMinimumSystemVersion: "10.13.0"
2121
},
2222
appBundleId: 'com.snakem982.pandora-box',
23+
protocols: [
24+
{
25+
name: 'Pandora-Box Protocol',
26+
schemes: ['pandora-box']
27+
}
28+
],
2329
},
2430
rebuildConfig: {},
2531
makers: [
@@ -31,6 +37,43 @@ const config: ForgeConfig = {
3137
chooseDirectory: true,
3238
},
3339
upgradeCode: 'c1d377b2-2c61-4c5e-8773-8e3c703b8b41',
40+
registry: [
41+
{
42+
key: 'HKEY_CLASSES_ROOT\\pandora-box',
43+
values: [
44+
{
45+
name: '',
46+
type: 'REG_SZ',
47+
value: 'URL:Pandora-Box Protocol'
48+
},
49+
{
50+
name: 'URL Protocol',
51+
type: 'REG_SZ',
52+
value: ''
53+
}
54+
]
55+
},
56+
{
57+
key: 'HKEY_CLASSES_ROOT\\pandora-box\\DefaultIcon',
58+
values: [
59+
{
60+
name: '',
61+
type: 'REG_SZ',
62+
value: '[APPLICATIONROOTDIRECTORY]Pandora-Box.exe,0'
63+
}
64+
]
65+
},
66+
{
67+
key: 'HKEY_CLASSES_ROOT\\pandora-box\\shell\\open\\command',
68+
values: [
69+
{
70+
name: '',
71+
type: 'REG_SZ',
72+
value: '"[APPLICATIONROOTDIRECTORY]Pandora-Box.exe" "%1"'
73+
}
74+
]
75+
}
76+
]
3477
}),
3578
new MakerDMG({
3679
icon: 'build/appicon.icns',
@@ -47,6 +90,7 @@ const config: ForgeConfig = {
4790
icon: 'build/appicon.png',
4891
maintainer: 'snakem982',
4992
homepage: 'https://github.com/snakem982/Pandora-Box',
93+
mimeType: ['x-scheme-handler/pandora-box']
5094
}
5195
})
5296
],

src-electron/main.ts

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {app, BrowserWindow, BrowserWindowConstructorOptions, session} from 'electron';
1+
import {app, BrowserWindow, BrowserWindowConstructorOptions, ipcMain, session} from 'electron';
22
import path from 'node:path';
33
import {startServer, storeInfo} from "./server";
44
import {doQuit, initTray, showWindow} from "./tray";
@@ -11,7 +11,15 @@ import {isBootAutoLaunch, updateAutoLaunchRegistration, waitForNetworkReady} fro
1111
const isDev = !app.isPackaged;
1212

1313
// 主窗口
14-
let mainWindow: BrowserWindow;
14+
let mainWindow: BrowserWindow | null = null;
15+
16+
// 深度链接相关
17+
const DEEP_LINK_SCHEME = 'pandora-box';
18+
const DEEP_LINK_HOST_INSTALL = 'install-config';
19+
const DEEP_LINK_EVENT = 'import-profile-from-deeplink';
20+
const DEEP_LINK_READY_EVENT = 'deeplink-handler-ready';
21+
const pendingDeepLinks: string[] = [];
22+
let deepLinkHandlerReady = false;
1523
// 屏蔽安全警告
1624
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
1725
const createWindow = (isBoot: boolean) => {
@@ -44,6 +52,7 @@ const createWindow = (isBoot: boolean) => {
4452
};
4553
}
4654

55+
deepLinkHandlerReady = false;
4756
mainWindow = new BrowserWindow(windowOptions);
4857

4958
// 隐藏菜单栏
@@ -62,6 +71,14 @@ const createWindow = (isBoot: boolean) => {
6271
log.error('页面加载失败:', err);
6372
});
6473

74+
mainWindow.webContents.on('did-start-loading', () => {
75+
deepLinkHandlerReady = false;
76+
});
77+
78+
mainWindow.webContents.on('did-finish-load', () => {
79+
processPendingDeepLinks();
80+
});
81+
6582
// 页面加载完成再显示,避免白屏
6683
mainWindow.webContents.once('did-finish-load', () => {
6784
if (isBoot) {
@@ -72,8 +89,77 @@ const createWindow = (isBoot: boolean) => {
7289
log.info('页面加载成功');
7390
}
7491
});
92+
93+
mainWindow.on('closed', () => {
94+
deepLinkHandlerReady = false;
95+
mainWindow = null;
96+
});
97+
};
98+
99+
const isDeepLinkUrl = (arg: string | undefined): arg is string => {
100+
return typeof arg === 'string' && arg.startsWith(`${DEEP_LINK_SCHEME}://`);
101+
};
102+
103+
const processPendingDeepLinks = () => {
104+
if (!mainWindow || mainWindow.isDestroyed() || !deepLinkHandlerReady) {
105+
return;
106+
}
107+
108+
if (pendingDeepLinks.length === 0) {
109+
return;
110+
}
111+
112+
const queue = pendingDeepLinks.splice(0, pendingDeepLinks.length);
113+
showWindow();
114+
115+
for (const url of queue) {
116+
if (!url) {
117+
continue;
118+
}
119+
120+
log.info('处理深度链接队列:', url);
121+
mainWindow.webContents.send(DEEP_LINK_EVENT, {rawUrl: url});
122+
}
75123
};
76124

125+
const enqueueDeepLink = (url: string) => {
126+
pendingDeepLinks.push(url);
127+
processPendingDeepLinks();
128+
};
129+
130+
function handleDeepLink(url: string) {
131+
const trimmed = url?.trim();
132+
if (!trimmed) {
133+
return;
134+
}
135+
136+
try {
137+
const parsedUrl = new URL(trimmed);
138+
if (parsedUrl.protocol !== `${DEEP_LINK_SCHEME}:`) {
139+
return;
140+
}
141+
142+
const host = parsedUrl.hostname || parsedUrl.host;
143+
if (host && host.toLowerCase() === DEEP_LINK_HOST_INSTALL) {
144+
log.info('收到深度链接:', trimmed);
145+
enqueueDeepLink(trimmed);
146+
} else {
147+
log.warn('未知深度链接:', trimmed);
148+
}
149+
} catch (error) {
150+
log.error('解析深度链接失败:', error);
151+
}
152+
}
153+
154+
ipcMain.on(DEEP_LINK_READY_EVENT, (event) => {
155+
if (!mainWindow || event.sender !== mainWindow.webContents) {
156+
return;
157+
}
158+
159+
deepLinkHandlerReady = true;
160+
processPendingDeepLinks();
161+
});
162+
77163
// 等待 backend 传来的 port 和 secret
78164
let resolveReady: () => void;
79165
const waitForReady = new Promise<void>((resolve) => {
@@ -100,17 +186,50 @@ const agents = [
100186
}
101187
];
102188

189+
const registerDeepLinkProtocol = () => {
190+
try {
191+
if (process.defaultApp && process.argv.length >= 2) {
192+
const exePath = process.execPath;
193+
const resolvedPath = path.resolve(process.argv[1]);
194+
app.setAsDefaultProtocolClient(DEEP_LINK_SCHEME, exePath, [resolvedPath]);
195+
} else if (!app.isDefaultProtocolClient(DEEP_LINK_SCHEME)) {
196+
app.setAsDefaultProtocolClient(DEEP_LINK_SCHEME);
197+
}
198+
} catch (error) {
199+
log.error('注册深度链接协议失败:', error);
200+
}
201+
};
202+
203+
for (const arg of process.argv) {
204+
if (isDeepLinkUrl(arg)) {
205+
pendingDeepLinks.push(arg);
206+
}
207+
}
208+
103209
// 单例模式
104210
const gotTheLock = app.requestSingleInstanceLock();
105211
if (!gotTheLock) {
106212
doQuit()
107213
} else {
108214
// 试图启动第二个应用实例
109-
app.on('second-instance', showWindow);
215+
app.on('second-instance', (_event, commandLine) => {
216+
const urls = commandLine.filter(isDeepLinkUrl);
217+
if (urls.length > 0) {
218+
urls.forEach(handleDeepLink);
219+
}
220+
showWindow();
221+
});
110222

111223
// 监听应用被激活
112224
app.on('activate', showWindow);
113225

226+
if (process.platform === 'darwin') {
227+
app.on('open-url', (event, url) => {
228+
event.preventDefault();
229+
handleDeepLink(url);
230+
});
231+
}
232+
114233
app.whenReady().then(async () => {
115234
// 判断是否开机启动
116235
const isBoot = await isBootAutoLaunch();
@@ -139,6 +258,8 @@ if (!gotTheLock) {
139258
// 等待后端启动
140259
await waitForReady;
141260

261+
registerDeepLinkProtocol();
262+
142263
// 设置请求头 Referer
143264
const agent = agents[Math.floor(Math.random() * agents.length)];
144265
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
@@ -156,4 +277,4 @@ if (!gotTheLock) {
156277
// 更新开机自启路径
157278
await updateAutoLaunchRegistration()
158279
});
159-
}
280+
}

src-electron/preload.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ contextBridge.exposeInMainWorld('pxTray', {
1414
emit: (name, ...value) => ipcRenderer.send('px_' + name, ...value)
1515
});
1616

17+
// 深度链接相关
18+
contextBridge.exposeInMainWorld('pxDeepLink', {
19+
onImportProfile: (callback) => {
20+
// 移除旧监听器,确保只注册一次
21+
ipcRenderer.removeAllListeners('import-profile-from-deeplink');
22+
ipcRenderer.on('import-profile-from-deeplink', (_event, data) => callback(data));
23+
},
24+
notifyReady: () => {
25+
ipcRenderer.send('deeplink-handler-ready');
26+
}
27+
});
28+
1729

1830
// 缓存接口
1931
contextBridge.exposeInMainWorld('pxStore', {

src/locales/en.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ profiles:
158158
switch:
159159
ing: Switching...
160160
success: Switch success
161+
deeplink:
162+
importing: Importing profile...
163+
import-success: Profile imported successfully
164+
import-failed: Failed to import profile
165+
invalid-url: Invalid profile URL
166+
invalid-url-format: Invalid URL format, must be HTTP or HTTPS
161167

162168
rule:
163169
title: Rule

src/locales/ru.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ profiles:
158158
switch:
159159
ing: Переключение...
160160
success: Переключено успешно
161+
deeplink:
162+
importing: Импорт профиля...
163+
import-success: Профиль успешно импортирован
164+
import-failed: Не удалось импортировать профиль
165+
invalid-url: Неверный URL профиля
166+
invalid-url-format: Неверный формат URL, должен быть HTTP или HTTPS
161167

162168
rule:
163169
title: Правило

src/locales/zh.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ profiles:
158158
switch:
159159
ing: 切换中。。。
160160
success: 切换成功
161+
deeplink:
162+
importing: 正在导入配置。。。
163+
import-success: 配置导入成功
164+
import-failed: 配置导入失败
165+
invalid-url: 无效的配置链接
166+
invalid-url-format: 配置链接格式错误,必须是 HTTP 或 HTTPS
161167

162168
rule:
163169
title: 规则

0 commit comments

Comments
 (0)