@bghitcode/bghitapp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/cli.js +2995 -0
- package/package.json +104 -0
- package/src-tauri/Cargo.lock +5966 -0
- package/src-tauri/Cargo.toml +59 -0
- package/src-tauri/Info.plist +14 -0
- package/src-tauri/assets/macos/dmg/background.png +0 -0
- package/src-tauri/assets/main.wxs +350 -0
- package/src-tauri/bghitapp.json +42 -0
- package/src-tauri/build.rs +5 -0
- package/src-tauri/capabilities/default.json +29 -0
- package/src-tauri/entitlements.plist +7 -0
- package/src-tauri/icons/chatgpt.icns +0 -0
- package/src-tauri/icons/deepseek.icns +0 -0
- package/src-tauri/icons/excalidraw.icns +0 -0
- package/src-tauri/icons/flomo.icns +0 -0
- package/src-tauri/icons/gemini.icns +0 -0
- package/src-tauri/icons/grok.icns +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/icons/lizhi.icns +0 -0
- package/src-tauri/icons/programmusic.icns +0 -0
- package/src-tauri/icons/qwerty.icns +0 -0
- package/src-tauri/icons/twitter.icns +0 -0
- package/src-tauri/icons/wechat.icns +0 -0
- package/src-tauri/icons/weekly.icns +0 -0
- package/src-tauri/icons/weread.icns +0 -0
- package/src-tauri/icons/xiaohongshu.icns +0 -0
- package/src-tauri/icons/youtube.icns +0 -0
- package/src-tauri/icons/youtubemusic.icns +0 -0
- package/src-tauri/rust_proxy.toml +10 -0
- package/src-tauri/src/app/config.rs +100 -0
- package/src-tauri/src/app/invoke.rs +242 -0
- package/src-tauri/src/app/menu.rs +324 -0
- package/src-tauri/src/app/mod.rs +6 -0
- package/src-tauri/src/app/setup.rs +172 -0
- package/src-tauri/src/app/window.rs +577 -0
- package/src-tauri/src/inject/auth.js +75 -0
- package/src-tauri/src/inject/custom.js +0 -0
- package/src-tauri/src/inject/event.js +1111 -0
- package/src-tauri/src/inject/find.js +708 -0
- package/src-tauri/src/inject/fullscreen.js +253 -0
- package/src-tauri/src/inject/offline.js +68 -0
- package/src-tauri/src/inject/splash-transition.js +13 -0
- package/src-tauri/src/inject/style.js +505 -0
- package/src-tauri/src/inject/theme_refresh.js +59 -0
- package/src-tauri/src/inject/toast.js +22 -0
- package/src-tauri/src/lib.rs +227 -0
- package/src-tauri/src/main.rs +8 -0
- package/src-tauri/src/util.rs +245 -0
- package/src-tauri/tauri.conf.json +20 -0
- package/src-tauri/tauri.linux.conf.json +12 -0
- package/src-tauri/tauri.macos.conf.json +28 -0
- package/src-tauri/tauri.windows.conf.json +15 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
use crate::app::config::BghitappConfig;
|
|
2
|
+
use crate::util::{
|
|
3
|
+
check_file_or_append, get_data_dir, get_download_message_with_lang, show_toast, MessageType,
|
|
4
|
+
};
|
|
5
|
+
use std::{
|
|
6
|
+
path::PathBuf,
|
|
7
|
+
str::FromStr,
|
|
8
|
+
sync::atomic::{AtomicU32, Ordering},
|
|
9
|
+
};
|
|
10
|
+
use tauri::{
|
|
11
|
+
webview::{DownloadEvent, NewWindowFeatures, NewWindowResponse},
|
|
12
|
+
AppHandle, Config, Manager, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
#[cfg(target_os = "macos")]
|
|
16
|
+
use tauri::{Theme, TitleBarStyle};
|
|
17
|
+
|
|
18
|
+
#[cfg(target_os = "windows")]
|
|
19
|
+
fn build_proxy_browser_arg(url: &Url) -> Option<String> {
|
|
20
|
+
let host = url.host_str()?;
|
|
21
|
+
let scheme = url.scheme();
|
|
22
|
+
let port = url.port().or_else(|| match scheme {
|
|
23
|
+
"http" => Some(80),
|
|
24
|
+
"socks5" => Some(1080),
|
|
25
|
+
_ => None,
|
|
26
|
+
})?;
|
|
27
|
+
|
|
28
|
+
match scheme {
|
|
29
|
+
"http" | "socks5" => Some(format!("--proxy-server={scheme}://{host}:{port}")),
|
|
30
|
+
_ => None,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub struct MultiWindowState {
|
|
35
|
+
pub bghitapp_config: BghitappConfig,
|
|
36
|
+
pub tauri_config: Config,
|
|
37
|
+
next_window_index: AtomicU32,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl MultiWindowState {
|
|
41
|
+
pub fn new(bghitapp_config: BghitappConfig, tauri_config: Config) -> Self {
|
|
42
|
+
Self {
|
|
43
|
+
bghitapp_config,
|
|
44
|
+
tauri_config,
|
|
45
|
+
next_window_index: AtomicU32::new(0),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn next_window_label(&self) -> String {
|
|
50
|
+
let index = self.next_window_index.fetch_add(1, Ordering::Relaxed) + 1;
|
|
51
|
+
format!("bghitapp-{index}")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub fn set_window(
|
|
56
|
+
app: &AppHandle,
|
|
57
|
+
config: &BghitappConfig,
|
|
58
|
+
|
|
59
|
+
tauri_config: &Config,
|
|
60
|
+
) -> tauri::Result<WebviewWindow> {
|
|
61
|
+
build_window_with_label(app, config, tauri_config, "bghitapp")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub fn build_splash_window(
|
|
65
|
+
app: &AppHandle,
|
|
66
|
+
_splash_asset: &str,
|
|
67
|
+
) -> tauri::Result<WebviewWindow> {
|
|
68
|
+
let url = WebviewUrl::App(PathBuf::from("splash.html"));
|
|
69
|
+
let splash_width = 800.0;
|
|
70
|
+
let splash_height = 500.0;
|
|
71
|
+
|
|
72
|
+
// Calculate centered position on primary monitor
|
|
73
|
+
let (pos_x, pos_y) = if let Some(monitor) = app.primary_monitor().ok().flatten() {
|
|
74
|
+
let monitor_size = monitor.size();
|
|
75
|
+
let scale = monitor.scale_factor();
|
|
76
|
+
let mon_w = monitor_size.width as f64 / scale;
|
|
77
|
+
let mon_h = monitor_size.height as f64 / scale;
|
|
78
|
+
let monitor_pos = monitor.position();
|
|
79
|
+
let x = monitor_pos.x as f64 / scale + (mon_w - splash_width) / 2.0;
|
|
80
|
+
let y = monitor_pos.y as f64 / scale + (mon_h - splash_height) / 2.0;
|
|
81
|
+
(x, y)
|
|
82
|
+
} else {
|
|
83
|
+
// Fallback: rough center assuming 1920x1080
|
|
84
|
+
((1920.0 - splash_width) / 2.0, (1080.0 - splash_height) / 2.0)
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
let window_builder = WebviewWindowBuilder::new(app, "splash", url)
|
|
88
|
+
.title("")
|
|
89
|
+
.visible(false)
|
|
90
|
+
.inner_size(splash_width, splash_height)
|
|
91
|
+
.position(pos_x, pos_y)
|
|
92
|
+
.resizable(false)
|
|
93
|
+
.decorations(false)
|
|
94
|
+
.skip_taskbar(true);
|
|
95
|
+
|
|
96
|
+
#[cfg(target_os = "macos")]
|
|
97
|
+
{
|
|
98
|
+
use tauri::TitleBarStyle;
|
|
99
|
+
window_builder = window_builder.title_bar_style(TitleBarStyle::Overlay);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
window_builder.build()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pub fn open_additional_window(app: &AppHandle) -> tauri::Result<WebviewWindow> {
|
|
106
|
+
let state = app.state::<MultiWindowState>();
|
|
107
|
+
let label = state.next_window_label();
|
|
108
|
+
build_window_with_label(app, &state.bghitapp_config, &state.tauri_config, &label)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
struct WindowBuildOptions<'a> {
|
|
112
|
+
label: &'a str,
|
|
113
|
+
url: WebviewUrl,
|
|
114
|
+
visible: bool,
|
|
115
|
+
new_window_features: Option<NewWindowFeatures>,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn open_requested_window(
|
|
119
|
+
app: &AppHandle,
|
|
120
|
+
config: &BghitappConfig,
|
|
121
|
+
tauri_config: &Config,
|
|
122
|
+
target_url: Url,
|
|
123
|
+
features: NewWindowFeatures,
|
|
124
|
+
) -> tauri::Result<WebviewWindow> {
|
|
125
|
+
let state = app.state::<MultiWindowState>();
|
|
126
|
+
let label = state.next_window_label();
|
|
127
|
+
let window = build_window(
|
|
128
|
+
app,
|
|
129
|
+
config,
|
|
130
|
+
tauri_config,
|
|
131
|
+
WindowBuildOptions {
|
|
132
|
+
label: &label,
|
|
133
|
+
url: WebviewUrl::External(target_url.clone()),
|
|
134
|
+
visible: true,
|
|
135
|
+
new_window_features: Some(features),
|
|
136
|
+
},
|
|
137
|
+
)?;
|
|
138
|
+
|
|
139
|
+
let title = target_url.host_str().unwrap_or(target_url.as_str());
|
|
140
|
+
let _ = window.set_title(title);
|
|
141
|
+
let _ = window.set_focus();
|
|
142
|
+
|
|
143
|
+
Ok(window)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pub fn open_additional_window_safe(app: &AppHandle) {
|
|
147
|
+
#[cfg(target_os = "windows")]
|
|
148
|
+
{
|
|
149
|
+
let app_handle = app.clone();
|
|
150
|
+
std::thread::spawn(move || {
|
|
151
|
+
if let Ok(window) = open_additional_window(&app_handle) {
|
|
152
|
+
let _ = window.show();
|
|
153
|
+
let _ = window.set_focus();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[cfg(not(target_os = "windows"))]
|
|
159
|
+
{
|
|
160
|
+
if let Ok(window) = open_additional_window(app) {
|
|
161
|
+
let _ = window.show();
|
|
162
|
+
let _ = window.set_focus();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn build_window_with_label(
|
|
168
|
+
app: &AppHandle,
|
|
169
|
+
config: &BghitappConfig,
|
|
170
|
+
tauri_config: &Config,
|
|
171
|
+
label: &str,
|
|
172
|
+
) -> tauri::Result<WebviewWindow> {
|
|
173
|
+
let window_config = config.windows.first().ok_or_else(|| {
|
|
174
|
+
tauri::Error::Io(std::io::Error::new(
|
|
175
|
+
std::io::ErrorKind::InvalidData,
|
|
176
|
+
"bghitapp.json must define at least one window configuration",
|
|
177
|
+
))
|
|
178
|
+
})?;
|
|
179
|
+
let url = match window_config.url_type.as_str() {
|
|
180
|
+
"web" => {
|
|
181
|
+
let parsed = window_config.url.parse().map_err(|err| {
|
|
182
|
+
tauri::Error::Io(std::io::Error::new(
|
|
183
|
+
std::io::ErrorKind::InvalidInput,
|
|
184
|
+
format!(
|
|
185
|
+
"Invalid 'web' url '{}' in bghitapp.json: {err}",
|
|
186
|
+
window_config.url
|
|
187
|
+
),
|
|
188
|
+
))
|
|
189
|
+
})?;
|
|
190
|
+
WebviewUrl::App(parsed)
|
|
191
|
+
}
|
|
192
|
+
"local" => WebviewUrl::App(PathBuf::from(&window_config.url)),
|
|
193
|
+
other => {
|
|
194
|
+
return Err(tauri::Error::Io(std::io::Error::new(
|
|
195
|
+
std::io::ErrorKind::InvalidInput,
|
|
196
|
+
format!("url_type must be 'web' or 'local', got '{other}'"),
|
|
197
|
+
)));
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
build_window(
|
|
202
|
+
app,
|
|
203
|
+
config,
|
|
204
|
+
tauri_config,
|
|
205
|
+
WindowBuildOptions {
|
|
206
|
+
label,
|
|
207
|
+
url,
|
|
208
|
+
visible: false,
|
|
209
|
+
new_window_features: None,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn build_window(
|
|
215
|
+
app: &AppHandle,
|
|
216
|
+
config: &BghitappConfig,
|
|
217
|
+
tauri_config: &Config,
|
|
218
|
+
opts: WindowBuildOptions,
|
|
219
|
+
) -> tauri::Result<WebviewWindow> {
|
|
220
|
+
let WindowBuildOptions {
|
|
221
|
+
label,
|
|
222
|
+
url,
|
|
223
|
+
visible,
|
|
224
|
+
new_window_features,
|
|
225
|
+
} = opts;
|
|
226
|
+
let package_name = tauri_config
|
|
227
|
+
.product_name
|
|
228
|
+
.clone()
|
|
229
|
+
.unwrap_or_else(|| "bghitapp".to_string());
|
|
230
|
+
let _data_dir = get_data_dir(app, package_name).map_err(tauri::Error::Io)?;
|
|
231
|
+
|
|
232
|
+
let window_config = config.windows.first().ok_or_else(|| {
|
|
233
|
+
tauri::Error::Io(std::io::Error::new(
|
|
234
|
+
std::io::ErrorKind::InvalidData,
|
|
235
|
+
"bghitapp.json must define at least one window configuration",
|
|
236
|
+
))
|
|
237
|
+
})?;
|
|
238
|
+
|
|
239
|
+
let user_agent = config.user_agent.get();
|
|
240
|
+
|
|
241
|
+
let config_script = format!(
|
|
242
|
+
"window.bghitappConfig = {}",
|
|
243
|
+
serde_json::to_string(&window_config).unwrap_or_else(|_| "{}".to_string())
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Platform-specific title: macOS prefers empty, others fallback to product name
|
|
247
|
+
let effective_title = window_config.title.as_deref().unwrap_or_else(|| {
|
|
248
|
+
if cfg!(target_os = "macos") {
|
|
249
|
+
""
|
|
250
|
+
} else {
|
|
251
|
+
tauri_config.product_name.as_deref().unwrap_or("")
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
let mut window_builder = WebviewWindowBuilder::new(app, label, url)
|
|
256
|
+
.title(effective_title)
|
|
257
|
+
.visible(visible)
|
|
258
|
+
.user_agent(user_agent)
|
|
259
|
+
.resizable(window_config.resizable)
|
|
260
|
+
.maximized(window_config.maximize);
|
|
261
|
+
|
|
262
|
+
#[cfg(target_os = "windows")]
|
|
263
|
+
{
|
|
264
|
+
let scale_factor = app
|
|
265
|
+
.primary_monitor()
|
|
266
|
+
.ok()
|
|
267
|
+
.flatten()
|
|
268
|
+
.map(|m| m.scale_factor())
|
|
269
|
+
.unwrap_or(1.0);
|
|
270
|
+
let logical_width = window_config.width / scale_factor;
|
|
271
|
+
let logical_height = window_config.height / scale_factor;
|
|
272
|
+
window_builder = window_builder.inner_size(logical_width, logical_height);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[cfg(not(target_os = "windows"))]
|
|
276
|
+
{
|
|
277
|
+
window_builder = window_builder.inner_size(window_config.width, window_config.height);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
window_builder = window_builder
|
|
281
|
+
.always_on_top(window_config.always_on_top)
|
|
282
|
+
.incognito(window_config.incognito);
|
|
283
|
+
|
|
284
|
+
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
|
285
|
+
{
|
|
286
|
+
window_builder = window_builder.fullscreen(window_config.fullscreen);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if window_config.min_width > 0.0 || window_config.min_height > 0.0 {
|
|
290
|
+
let min_w = if window_config.min_width > 0.0 {
|
|
291
|
+
window_config.min_width
|
|
292
|
+
} else {
|
|
293
|
+
window_config.width
|
|
294
|
+
};
|
|
295
|
+
let min_h = if window_config.min_height > 0.0 {
|
|
296
|
+
window_config.min_height
|
|
297
|
+
} else {
|
|
298
|
+
window_config.height
|
|
299
|
+
};
|
|
300
|
+
window_builder = window_builder.min_inner_size(min_w, min_h);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if !window_config.enable_drag_drop {
|
|
304
|
+
window_builder = window_builder.disable_drag_drop_handler();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if window_config.new_window {
|
|
308
|
+
let app_handle = app.clone();
|
|
309
|
+
let popup_config = config.clone();
|
|
310
|
+
let popup_tauri_config = tauri_config.clone();
|
|
311
|
+
window_builder = window_builder.on_new_window(move |target_url, features| {
|
|
312
|
+
match open_requested_window(
|
|
313
|
+
&app_handle,
|
|
314
|
+
&popup_config,
|
|
315
|
+
&popup_tauri_config,
|
|
316
|
+
target_url,
|
|
317
|
+
features,
|
|
318
|
+
) {
|
|
319
|
+
Ok(window) => NewWindowResponse::Create { window },
|
|
320
|
+
Err(error) => {
|
|
321
|
+
eprintln!("[BghitApp] Failed to open requested window: {error}");
|
|
322
|
+
NewWindowResponse::Deny
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Add initialization scripts. Order matters: bghitappConfig must land before
|
|
329
|
+
// any script that reads it (e.g. fullscreen polyfill checks for an opt-out
|
|
330
|
+
// flag), and toast must register `window.bghitappToast` before Rust code
|
|
331
|
+
// calls show_toast().
|
|
332
|
+
window_builder = window_builder
|
|
333
|
+
.initialization_script(&config_script)
|
|
334
|
+
.initialization_script(include_str!("../inject/find.js"))
|
|
335
|
+
.initialization_script(include_str!("../inject/toast.js"))
|
|
336
|
+
.initialization_script(include_str!("../inject/fullscreen.js"))
|
|
337
|
+
.initialization_script(include_str!("../inject/event.js"))
|
|
338
|
+
.initialization_script(include_str!("../inject/style.js"))
|
|
339
|
+
.initialization_script(include_str!("../inject/theme_refresh.js"))
|
|
340
|
+
.initialization_script(include_str!("../inject/auth.js"))
|
|
341
|
+
.initialization_script(include_str!("../inject/custom.js"));
|
|
342
|
+
|
|
343
|
+
// Conditionally inject offline detection script
|
|
344
|
+
if window_config.offline {
|
|
345
|
+
window_builder =
|
|
346
|
+
window_builder.initialization_script(include_str!("../inject/offline.js"));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Inject splash transition script if splash is configured
|
|
350
|
+
if !window_config.splash.is_empty() {
|
|
351
|
+
window_builder =
|
|
352
|
+
window_builder.initialization_script(include_str!("../inject/splash-transition.js"));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#[cfg(target_os = "windows")]
|
|
356
|
+
let mut windows_browser_args = String::from("--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --disable-blink-features=AutomationControlled");
|
|
357
|
+
|
|
358
|
+
#[cfg(target_os = "linux")]
|
|
359
|
+
let mut linux_browser_args = String::from("--disable-blink-features=AutomationControlled");
|
|
360
|
+
|
|
361
|
+
if window_config.ignore_certificate_errors {
|
|
362
|
+
#[cfg(target_os = "windows")]
|
|
363
|
+
{
|
|
364
|
+
windows_browser_args.push_str(" --ignore-certificate-errors");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[cfg(target_os = "linux")]
|
|
368
|
+
{
|
|
369
|
+
linux_browser_args.push_str(" --ignore-certificate-errors");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#[cfg(target_os = "macos")]
|
|
373
|
+
{
|
|
374
|
+
window_builder = window_builder.additional_browser_args("--ignore-certificate-errors");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if window_config.enable_wasm {
|
|
379
|
+
#[cfg(target_os = "windows")]
|
|
380
|
+
{
|
|
381
|
+
windows_browser_args.push_str(" --enable-features=SharedArrayBuffer");
|
|
382
|
+
windows_browser_args.push_str(" --enable-unsafe-webgpu");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[cfg(target_os = "linux")]
|
|
386
|
+
{
|
|
387
|
+
linux_browser_args.push_str(" --enable-features=SharedArrayBuffer");
|
|
388
|
+
linux_browser_args.push_str(" --enable-unsafe-webgpu");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#[cfg(target_os = "macos")]
|
|
392
|
+
{
|
|
393
|
+
window_builder = window_builder
|
|
394
|
+
.additional_browser_args("--enable-features=SharedArrayBuffer")
|
|
395
|
+
.additional_browser_args("--enable-unsafe-webgpu");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let mut parsed_proxy_url: Option<Url> = None;
|
|
400
|
+
|
|
401
|
+
// Platform-specific configuration must be set before proxy on Windows/Linux
|
|
402
|
+
#[cfg(target_os = "macos")]
|
|
403
|
+
{
|
|
404
|
+
let title_bar_style = if window_config.hide_title_bar {
|
|
405
|
+
TitleBarStyle::Overlay
|
|
406
|
+
} else {
|
|
407
|
+
TitleBarStyle::Visible
|
|
408
|
+
};
|
|
409
|
+
window_builder = window_builder.title_bar_style(title_bar_style);
|
|
410
|
+
|
|
411
|
+
// Default to following system theme (None), only force dark when explicitly set
|
|
412
|
+
let theme = if window_config.dark_mode {
|
|
413
|
+
Some(Theme::Dark)
|
|
414
|
+
} else {
|
|
415
|
+
None // Follow system theme
|
|
416
|
+
};
|
|
417
|
+
window_builder = window_builder.theme(theme);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Windows and Linux: set data_directory before proxy_url
|
|
421
|
+
#[cfg(not(target_os = "macos"))]
|
|
422
|
+
{
|
|
423
|
+
window_builder = window_builder.data_directory(_data_dir).theme(None);
|
|
424
|
+
|
|
425
|
+
if !config.proxy_url.is_empty() {
|
|
426
|
+
if let Ok(proxy_url) = Url::from_str(&config.proxy_url) {
|
|
427
|
+
parsed_proxy_url = Some(proxy_url.clone());
|
|
428
|
+
#[cfg(target_os = "windows")]
|
|
429
|
+
{
|
|
430
|
+
if let Some(arg) = build_proxy_browser_arg(&proxy_url) {
|
|
431
|
+
windows_browser_args.push(' ');
|
|
432
|
+
windows_browser_args.push_str(&arg);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[cfg(target_os = "windows")]
|
|
439
|
+
{
|
|
440
|
+
window_builder = window_builder.additional_browser_args(&windows_browser_args);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
#[cfg(target_os = "linux")]
|
|
444
|
+
{
|
|
445
|
+
window_builder = window_builder.additional_browser_args(&linux_browser_args);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Set proxy after platform-specific configs (required for Windows/Linux)
|
|
450
|
+
if parsed_proxy_url.is_none() && !config.proxy_url.is_empty() {
|
|
451
|
+
if let Ok(proxy_url) = Url::from_str(&config.proxy_url) {
|
|
452
|
+
parsed_proxy_url = Some(proxy_url);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if let Some(proxy_url) = parsed_proxy_url {
|
|
457
|
+
window_builder = window_builder.proxy_url(proxy_url);
|
|
458
|
+
#[cfg(debug_assertions)]
|
|
459
|
+
println!("Proxy configured: {}", config.proxy_url);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if let Some(features) = new_window_features {
|
|
463
|
+
// Reuse only opener-provided position/size on macOS; sharing the opener
|
|
464
|
+
// WKWebViewConfiguration triggers duplicate WKScriptMessageHandler
|
|
465
|
+
// registrations on macOS 26+ and crashes the app (issue #1194).
|
|
466
|
+
#[cfg(target_os = "macos")]
|
|
467
|
+
{
|
|
468
|
+
if let Some(position) = features.position() {
|
|
469
|
+
window_builder = window_builder.position(position.x, position.y);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if let Some(size) = features.size() {
|
|
473
|
+
window_builder = window_builder.inner_size(size.width, size.height);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
window_builder = window_builder.focused(true);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#[cfg(not(target_os = "macos"))]
|
|
480
|
+
{
|
|
481
|
+
window_builder = window_builder.window_features(features).focused(true);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Capture webview-initiated downloads (blob:, data:, Content-Disposition,
|
|
486
|
+
// etc.) and write them to the OS Downloads folder. This is essential for
|
|
487
|
+
// sites with a strict Content-Security-Policy (e.g. Gemini): their
|
|
488
|
+
// `connect-src` blocks Tauri's IPC origin, so downloads cannot be routed
|
|
489
|
+
// through the JS bridge, and downloads triggered from a sandboxed iframe
|
|
490
|
+
// can't reach the IPC either. Letting the browser download natively and
|
|
491
|
+
// catching it here is independent of the page CSP and the IPC channel.
|
|
492
|
+
{
|
|
493
|
+
let download_handle = app.clone();
|
|
494
|
+
window_builder = window_builder.on_download(move |_webview, event| match event {
|
|
495
|
+
DownloadEvent::Requested { url, destination } => {
|
|
496
|
+
match download_handle.path().download_dir() {
|
|
497
|
+
Ok(download_dir) => {
|
|
498
|
+
let filename = destination
|
|
499
|
+
.file_name()
|
|
500
|
+
.map(|name| name.to_string_lossy().to_string())
|
|
501
|
+
.filter(|name| !name.is_empty())
|
|
502
|
+
.or_else(|| {
|
|
503
|
+
url.path_segments()
|
|
504
|
+
.and_then(|mut segments| segments.next_back())
|
|
505
|
+
.map(|segment| segment.to_string())
|
|
506
|
+
.filter(|segment| !segment.is_empty())
|
|
507
|
+
})
|
|
508
|
+
.unwrap_or_else(|| "download".to_string());
|
|
509
|
+
|
|
510
|
+
let target = download_dir.join(filename);
|
|
511
|
+
if let Some(path_str) = target.to_str() {
|
|
512
|
+
*destination = PathBuf::from(check_file_or_append(path_str));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
Err(error) => {
|
|
516
|
+
eprintln!("[BghitApp] Failed to resolve download dir: {error}");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
true
|
|
520
|
+
}
|
|
521
|
+
DownloadEvent::Finished {
|
|
522
|
+
url: _,
|
|
523
|
+
path: _,
|
|
524
|
+
success,
|
|
525
|
+
} => {
|
|
526
|
+
if let Some(window) = download_handle.get_webview_window("bghitapp") {
|
|
527
|
+
let message_type = if success {
|
|
528
|
+
MessageType::Success
|
|
529
|
+
} else {
|
|
530
|
+
MessageType::Failure
|
|
531
|
+
};
|
|
532
|
+
show_toast(&window, &get_download_message_with_lang(message_type, None));
|
|
533
|
+
}
|
|
534
|
+
true
|
|
535
|
+
}
|
|
536
|
+
_ => true,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
window_builder = window_builder.on_navigation(|_| true);
|
|
541
|
+
|
|
542
|
+
window_builder.build()
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
#[cfg(all(test, target_os = "windows"))]
|
|
546
|
+
mod proxy_arg_tests {
|
|
547
|
+
use super::*;
|
|
548
|
+
|
|
549
|
+
fn parse(url: &str) -> Url {
|
|
550
|
+
Url::from_str(url).unwrap()
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#[test]
|
|
554
|
+
fn http_url_with_explicit_port() {
|
|
555
|
+
let arg = build_proxy_browser_arg(&parse("http://127.0.0.1:7890")).unwrap();
|
|
556
|
+
assert_eq!(arg, "--proxy-server=http://127.0.0.1:7890");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#[test]
|
|
560
|
+
fn http_url_uses_default_port_when_missing() {
|
|
561
|
+
let arg = build_proxy_browser_arg(&parse("http://proxy.local")).unwrap();
|
|
562
|
+
assert_eq!(arg, "--proxy-server=http://proxy.local:80");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
#[test]
|
|
566
|
+
fn socks5_url_uses_default_port_when_missing() {
|
|
567
|
+
let arg = build_proxy_browser_arg(&parse("socks5://proxy.local")).unwrap();
|
|
568
|
+
assert_eq!(arg, "--proxy-server=socks5://proxy.local:1080");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
#[test]
|
|
572
|
+
fn https_scheme_is_not_supported_yet() {
|
|
573
|
+
// https proxies fall back to platform proxy_url; we only emit a CLI arg
|
|
574
|
+
// for http/socks5 today.
|
|
575
|
+
assert!(build_proxy_browser_arg(&parse("https://proxy.local:8443")).is_none());
|
|
576
|
+
}
|
|
577
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// OAuth and Authentication Logic
|
|
2
|
+
|
|
3
|
+
// Check if URL matches OAuth/authentication patterns
|
|
4
|
+
function matchesAuthUrl(url, baseUrl = window.location.href) {
|
|
5
|
+
try {
|
|
6
|
+
const urlObj = new URL(url, baseUrl);
|
|
7
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
8
|
+
const pathname = urlObj.pathname.toLowerCase();
|
|
9
|
+
const fullUrl = urlObj.href.toLowerCase();
|
|
10
|
+
|
|
11
|
+
// Common OAuth providers and paths
|
|
12
|
+
const oauthPatterns = [
|
|
13
|
+
/accounts\.google\.com/,
|
|
14
|
+
/accounts\.google\.[a-z]+/,
|
|
15
|
+
/login\.microsoftonline\.com/,
|
|
16
|
+
/github\.com\/login/,
|
|
17
|
+
/facebook\.com\/.*\/dialog/,
|
|
18
|
+
/twitter\.com\/oauth/,
|
|
19
|
+
/appleid\.apple\.com/,
|
|
20
|
+
/\/oauth\//,
|
|
21
|
+
/\/auth\//,
|
|
22
|
+
/\/authorize/,
|
|
23
|
+
/\/login\/oauth/,
|
|
24
|
+
/\/signin/,
|
|
25
|
+
/\/login/,
|
|
26
|
+
/servicelogin/,
|
|
27
|
+
/\/o\/oauth2/,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const isMatch = oauthPatterns.some(
|
|
31
|
+
(pattern) =>
|
|
32
|
+
pattern.test(hostname) ||
|
|
33
|
+
pattern.test(pathname) ||
|
|
34
|
+
pattern.test(fullUrl),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (isMatch) {
|
|
38
|
+
console.log("[BghitApp] OAuth URL detected:", url);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return isMatch;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if URL is an OAuth/authentication link
|
|
48
|
+
function isAuthLink(url) {
|
|
49
|
+
return matchesAuthUrl(url);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if this is an OAuth/authentication popup
|
|
53
|
+
function isAuthPopup(url, name) {
|
|
54
|
+
// Check for known authentication window names
|
|
55
|
+
const authWindowNames = [
|
|
56
|
+
"AppleAuthentication",
|
|
57
|
+
"oauth2",
|
|
58
|
+
"oauth",
|
|
59
|
+
"google-auth",
|
|
60
|
+
"auth-popup",
|
|
61
|
+
"signin",
|
|
62
|
+
"login",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (authWindowNames.includes(name)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return matchesAuthUrl(url);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Export functions to global scope
|
|
73
|
+
window.matchesAuthUrl = matchesAuthUrl;
|
|
74
|
+
window.isAuthLink = isAuthLink;
|
|
75
|
+
window.isAuthPopup = isAuthPopup;
|
|
File without changes
|