@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,1111 @@
|
|
|
1
|
+
const shortcuts = {
|
|
2
|
+
"[": () => window.history.back(),
|
|
3
|
+
"]": () => window.history.forward(),
|
|
4
|
+
"-": () => zoomOut(),
|
|
5
|
+
"=": () => zoomIn(),
|
|
6
|
+
"+": () => zoomIn(),
|
|
7
|
+
0: () => setZoom("100%"),
|
|
8
|
+
r: () => window.location.reload(),
|
|
9
|
+
ArrowUp: () => scrollTo(0, 0),
|
|
10
|
+
ArrowDown: () => scrollTo(0, document.body.scrollHeight),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function setZoom(zoom) {
|
|
14
|
+
const html = document.getElementsByTagName("html")[0];
|
|
15
|
+
const body = document.body;
|
|
16
|
+
const zoomValue = parseFloat(zoom) / 100;
|
|
17
|
+
const isWindows = /windows/i.test(navigator.userAgent);
|
|
18
|
+
|
|
19
|
+
if (isWindows) {
|
|
20
|
+
body.style.transform = `scale(${zoomValue})`;
|
|
21
|
+
body.style.transformOrigin = "top left";
|
|
22
|
+
body.style.width = `${100 / zoomValue}%`;
|
|
23
|
+
body.style.height = `${100 / zoomValue}%`;
|
|
24
|
+
} else {
|
|
25
|
+
html.style.zoom = zoom;
|
|
26
|
+
window.dispatchEvent(new Event("resize"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
window.localStorage.setItem("htmlZoom", zoom);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function zoomCommon(zoomChange) {
|
|
33
|
+
const currentZoom = window.localStorage.getItem("htmlZoom") || "100%";
|
|
34
|
+
setZoom(zoomChange(currentZoom));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function zoomIn() {
|
|
38
|
+
zoomCommon((currentZoom) => `${Math.min(parseInt(currentZoom) + 10, 200)}%`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function zoomOut() {
|
|
42
|
+
zoomCommon((currentZoom) => `${Math.max(parseInt(currentZoom) - 10, 30)}%`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let pasteAsPlainTextPending = false;
|
|
46
|
+
|
|
47
|
+
function triggerPasteAsPlainText() {
|
|
48
|
+
pasteAsPlainTextPending = true;
|
|
49
|
+
document.execCommand("paste");
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
pasteAsPlainTextPending = false;
|
|
52
|
+
}, 100);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleShortcut(event) {
|
|
56
|
+
if (shortcuts[event.key]) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
shortcuts[event.key]();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DOWNLOADABLE_FILE_EXTENSIONS = {
|
|
63
|
+
documents: [
|
|
64
|
+
"pdf",
|
|
65
|
+
"doc",
|
|
66
|
+
"docx",
|
|
67
|
+
"xls",
|
|
68
|
+
"xlsx",
|
|
69
|
+
"ppt",
|
|
70
|
+
"pptx",
|
|
71
|
+
"txt",
|
|
72
|
+
"rtf",
|
|
73
|
+
"odt",
|
|
74
|
+
"ods",
|
|
75
|
+
"odp",
|
|
76
|
+
"pages",
|
|
77
|
+
"numbers",
|
|
78
|
+
"key",
|
|
79
|
+
"epub",
|
|
80
|
+
"mobi",
|
|
81
|
+
],
|
|
82
|
+
archives: [
|
|
83
|
+
"zip",
|
|
84
|
+
"rar",
|
|
85
|
+
"7z",
|
|
86
|
+
"tar",
|
|
87
|
+
"gz",
|
|
88
|
+
"gzip",
|
|
89
|
+
"bz2",
|
|
90
|
+
"xz",
|
|
91
|
+
"lzma",
|
|
92
|
+
"deb",
|
|
93
|
+
"rpm",
|
|
94
|
+
"pkg",
|
|
95
|
+
"msi",
|
|
96
|
+
"exe",
|
|
97
|
+
"dmg",
|
|
98
|
+
"apk",
|
|
99
|
+
"ipa",
|
|
100
|
+
],
|
|
101
|
+
data: [
|
|
102
|
+
"json",
|
|
103
|
+
"xml",
|
|
104
|
+
"csv",
|
|
105
|
+
"sql",
|
|
106
|
+
"db",
|
|
107
|
+
"sqlite",
|
|
108
|
+
"yaml",
|
|
109
|
+
"yml",
|
|
110
|
+
"toml",
|
|
111
|
+
"ini",
|
|
112
|
+
"cfg",
|
|
113
|
+
"conf",
|
|
114
|
+
"log",
|
|
115
|
+
],
|
|
116
|
+
code: [
|
|
117
|
+
"js",
|
|
118
|
+
"ts",
|
|
119
|
+
"jsx",
|
|
120
|
+
"tsx",
|
|
121
|
+
"css",
|
|
122
|
+
"scss",
|
|
123
|
+
"sass",
|
|
124
|
+
"less",
|
|
125
|
+
"sh",
|
|
126
|
+
"bat",
|
|
127
|
+
"ps1",
|
|
128
|
+
],
|
|
129
|
+
fonts: ["ttf", "otf", "woff", "woff2", "eot"],
|
|
130
|
+
design: ["ai", "psd", "sketch", "fig", "xd"],
|
|
131
|
+
system: [
|
|
132
|
+
"iso",
|
|
133
|
+
"img",
|
|
134
|
+
"bin",
|
|
135
|
+
"torrent",
|
|
136
|
+
"jar",
|
|
137
|
+
"war",
|
|
138
|
+
"indd",
|
|
139
|
+
"fla",
|
|
140
|
+
"swf",
|
|
141
|
+
"raw",
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const ALL_DOWNLOADABLE_EXTENSIONS = Object.values(
|
|
146
|
+
DOWNLOADABLE_FILE_EXTENSIONS,
|
|
147
|
+
).flat();
|
|
148
|
+
|
|
149
|
+
const PREVIEWABLE_MEDIA_EXTENSIONS = [
|
|
150
|
+
"png",
|
|
151
|
+
"jpg",
|
|
152
|
+
"jpeg",
|
|
153
|
+
"gif",
|
|
154
|
+
"webp",
|
|
155
|
+
"svg",
|
|
156
|
+
"bmp",
|
|
157
|
+
"tiff",
|
|
158
|
+
"tif",
|
|
159
|
+
"avif",
|
|
160
|
+
"heic",
|
|
161
|
+
"heif",
|
|
162
|
+
"mp4",
|
|
163
|
+
"webm",
|
|
164
|
+
"mov",
|
|
165
|
+
"m4v",
|
|
166
|
+
"mkv",
|
|
167
|
+
"avi",
|
|
168
|
+
"ogv",
|
|
169
|
+
"mp3",
|
|
170
|
+
"wav",
|
|
171
|
+
"ogg",
|
|
172
|
+
"flac",
|
|
173
|
+
"aac",
|
|
174
|
+
"m4a",
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const DOWNLOAD_PATH_PATTERNS = [
|
|
178
|
+
"/download/",
|
|
179
|
+
"/files/",
|
|
180
|
+
"/attachments/",
|
|
181
|
+
"/assets/",
|
|
182
|
+
"/releases/",
|
|
183
|
+
"/dist/",
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
// Language detection utilities
|
|
187
|
+
function getUserLanguage() {
|
|
188
|
+
return navigator.language || navigator.userLanguage;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isChineseLanguage(language = getUserLanguage()) {
|
|
192
|
+
return (
|
|
193
|
+
language &&
|
|
194
|
+
(language.startsWith("zh") ||
|
|
195
|
+
language.includes("CN") ||
|
|
196
|
+
language.includes("TW") ||
|
|
197
|
+
language.includes("HK"))
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// User notification helper
|
|
202
|
+
function showDownloadError(filename) {
|
|
203
|
+
const isChinese = isChineseLanguage();
|
|
204
|
+
const message = isChinese
|
|
205
|
+
? `下载失败: ${filename}`
|
|
206
|
+
: `Download failed: ${filename}`;
|
|
207
|
+
|
|
208
|
+
if (window.Notification && Notification.permission === "granted") {
|
|
209
|
+
new Notification(isChinese ? "下载错误" : "Download Error", {
|
|
210
|
+
body: message,
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
console.error(message);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getExtension(url) {
|
|
218
|
+
try {
|
|
219
|
+
const pathname = new URL(url).pathname.toLowerCase();
|
|
220
|
+
const extensionIndex = pathname.lastIndexOf(".");
|
|
221
|
+
return extensionIndex > -1 ? pathname.slice(extensionIndex + 1) : "";
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return "";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isPreviewableMedia(url) {
|
|
228
|
+
const extension = getExtension(url);
|
|
229
|
+
return PREVIEWABLE_MEDIA_EXTENSIONS.includes(extension);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Unified file detection - replaces both isDownloadLink and isFileLink
|
|
233
|
+
function isDownloadableFile(url) {
|
|
234
|
+
try {
|
|
235
|
+
const extension = getExtension(url);
|
|
236
|
+
if (PREVIEWABLE_MEDIA_EXTENSIONS.includes(extension)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const urlObj = new URL(url);
|
|
241
|
+
const hasDownloadHints =
|
|
242
|
+
urlObj.searchParams.has("download") ||
|
|
243
|
+
urlObj.searchParams.has("attachment");
|
|
244
|
+
|
|
245
|
+
if (hasDownloadHints) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
ALL_DOWNLOADABLE_EXTENSIONS.includes(extension) ||
|
|
251
|
+
DOWNLOAD_PATH_PATTERNS.some((pattern) =>
|
|
252
|
+
urlObj.pathname.toLowerCase().includes(pattern),
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeAnchorHref(rawHref) {
|
|
261
|
+
return typeof rawHref === "string" ? rawHref.trim() : "";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function shouldBypassBghitappLinkHandling(rawHref) {
|
|
265
|
+
const normalizedHref = normalizeAnchorHref(rawHref).toLowerCase();
|
|
266
|
+
if (!normalizedHref) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
normalizedHref.startsWith("javascript:") || normalizedHref.startsWith("#")
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
276
|
+
const tauri = window.__TAURI__;
|
|
277
|
+
const appWindow = tauri.window.getCurrentWindow();
|
|
278
|
+
const invoke = tauri.core.invoke;
|
|
279
|
+
const bghitappConfig = window["bghitappConfig"] || {};
|
|
280
|
+
const forceInternalNavigation = bghitappConfig.force_internal_navigation === true;
|
|
281
|
+
const internalUrlRegex = bghitappConfig.internal_url_regex || "";
|
|
282
|
+
let internalUrlPattern = null;
|
|
283
|
+
if (internalUrlRegex) {
|
|
284
|
+
try {
|
|
285
|
+
internalUrlPattern = new RegExp(internalUrlRegex);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.error("[BghitApp] Invalid internal_url_regex pattern:", e);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!document.getElementById("bghitapp-top-dom")) {
|
|
292
|
+
const topDom = document.createElement("div");
|
|
293
|
+
topDom.id = "bghitapp-top-dom";
|
|
294
|
+
document.body.appendChild(topDom);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const domEl = document.getElementById("bghitapp-top-dom");
|
|
298
|
+
|
|
299
|
+
domEl.addEventListener("touchstart", () => {
|
|
300
|
+
appWindow.startDragging();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
domEl.addEventListener("mousedown", (e) => {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
if (e.buttons === 1 && e.detail !== 2) {
|
|
306
|
+
appWindow.startDragging();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
domEl.addEventListener("dblclick", () => {
|
|
311
|
+
appWindow.isFullscreen().then((fullscreen) => {
|
|
312
|
+
appWindow.setFullscreen(!fullscreen);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (window["bghitappConfig"]?.disabled_web_shortcuts !== true) {
|
|
317
|
+
document.addEventListener("keyup", (event) => {
|
|
318
|
+
if (/windows|linux/i.test(navigator.userAgent) && event.ctrlKey) {
|
|
319
|
+
handleShortcut(event);
|
|
320
|
+
}
|
|
321
|
+
if (/macintosh|mac os x/i.test(navigator.userAgent) && event.metaKey) {
|
|
322
|
+
handleShortcut(event);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
document.addEventListener(
|
|
328
|
+
"paste",
|
|
329
|
+
(event) => {
|
|
330
|
+
if (pasteAsPlainTextPending) {
|
|
331
|
+
event.preventDefault();
|
|
332
|
+
event.stopImmediatePropagation();
|
|
333
|
+
|
|
334
|
+
const text = event.clipboardData?.getData("text/plain") || "";
|
|
335
|
+
if (text) {
|
|
336
|
+
document.execCommand("insertText", false, text);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
true,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Trigger a native browser download via a transient anchor click. The Rust
|
|
344
|
+
// on_download handler then writes the file to the Downloads folder. This is
|
|
345
|
+
// used for blob:/data: URLs because routing their bytes through the Tauri
|
|
346
|
+
// IPC fails on strict-CSP sites (e.g. Gemini), whose connect-src blocks the
|
|
347
|
+
// IPC origin. The native download path is independent of the page CSP.
|
|
348
|
+
function triggerNativeDownload(url, filename) {
|
|
349
|
+
const anchor = document.createElement("a");
|
|
350
|
+
anchor.href = url;
|
|
351
|
+
anchor.download = filename || "";
|
|
352
|
+
anchor.style.display = "none";
|
|
353
|
+
document.body.appendChild(anchor);
|
|
354
|
+
anchor.click();
|
|
355
|
+
document.body.removeChild(anchor);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// process special download protocol['data:','blob:']
|
|
359
|
+
const isSpecialDownload = (url) =>
|
|
360
|
+
["blob", "data"].some((protocol) => url.startsWith(protocol));
|
|
361
|
+
|
|
362
|
+
const isDownloadRequired = (url, anchorElement, e) =>
|
|
363
|
+
anchorElement.download || e.metaKey || e.ctrlKey || isDownloadableFile(url);
|
|
364
|
+
|
|
365
|
+
const handleExternalLink = (url) => {
|
|
366
|
+
// Don't try to open blob: or data: URLs with shell
|
|
367
|
+
if (isSpecialDownload(url)) {
|
|
368
|
+
console.warn("Cannot open special URL with shell:", url);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
invoke("plugin:shell|open", {
|
|
373
|
+
path: url,
|
|
374
|
+
}).catch((error) => {
|
|
375
|
+
console.error("Failed to open URL with shell:", url, error);
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Check if URL belongs to the same domain (including subdomains)
|
|
380
|
+
const isSameDomain = (url) => {
|
|
381
|
+
try {
|
|
382
|
+
const linkUrl = new URL(url);
|
|
383
|
+
const currentUrl = new URL(window.location.href);
|
|
384
|
+
|
|
385
|
+
if (linkUrl.hostname === currentUrl.hostname) return true;
|
|
386
|
+
|
|
387
|
+
// Extract root domain (e.g., bilibili.com from www.bilibili.com)
|
|
388
|
+
const getRootDomain = (hostname) => {
|
|
389
|
+
const parts = hostname.split(".");
|
|
390
|
+
return parts.length >= 2 ? parts.slice(-2).join(".") : hostname;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
getRootDomain(currentUrl.hostname) === getRootDomain(linkUrl.hostname)
|
|
395
|
+
);
|
|
396
|
+
} catch (e) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Check if URL should be treated as internal based on regex pattern or domain
|
|
402
|
+
const isInternalUrl = (url) => {
|
|
403
|
+
// If regex pattern is configured, use it as the primary check
|
|
404
|
+
if (internalUrlPattern) {
|
|
405
|
+
try {
|
|
406
|
+
return internalUrlPattern.test(url);
|
|
407
|
+
} catch (e) {
|
|
408
|
+
console.error("[BghitApp] Error testing internal_url_regex:", e);
|
|
409
|
+
// Fall back to domain check on error
|
|
410
|
+
return isSameDomain(url);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Default to domain-based check
|
|
414
|
+
return isSameDomain(url);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const detectAnchorElementClick = (e) => {
|
|
418
|
+
// Safety check: ensure e.target exists and is an Element with closest method
|
|
419
|
+
if (!e.target || typeof e.target.closest !== "function") {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const anchorElement = e.target.closest("a");
|
|
423
|
+
|
|
424
|
+
if (anchorElement && anchorElement.href) {
|
|
425
|
+
const rawHref = anchorElement.getAttribute("href") || "";
|
|
426
|
+
if (shouldBypassBghitappLinkHandling(rawHref)) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const target = anchorElement.target;
|
|
431
|
+
const hrefUrl = new URL(anchorElement.href);
|
|
432
|
+
const absoluteUrl = hrefUrl.href;
|
|
433
|
+
let filename = anchorElement.download || getFilenameFromUrl(absoluteUrl);
|
|
434
|
+
|
|
435
|
+
// Keep OAuth/authentication flows inside the app when popup support is enabled.
|
|
436
|
+
if (window.isAuthLink(absoluteUrl)) {
|
|
437
|
+
console.log("[BghitApp] Handling OAuth navigation in-app:", absoluteUrl);
|
|
438
|
+
|
|
439
|
+
if (window.bghitappConfig?.new_window) {
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
e.stopImmediatePropagation();
|
|
442
|
+
|
|
443
|
+
const authWindow = originalWindowOpen.call(
|
|
444
|
+
window,
|
|
445
|
+
absoluteUrl,
|
|
446
|
+
"_blank",
|
|
447
|
+
"width=1200,height=800,scrollbars=yes,resizable=yes",
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (!authWindow) {
|
|
451
|
+
window.location.href = absoluteUrl;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Handle _blank links: internal links stay in-app, external links open in the system browser
|
|
459
|
+
if (target === "_blank") {
|
|
460
|
+
if (forceInternalNavigation) {
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
e.stopImmediatePropagation();
|
|
463
|
+
window.location.href = absoluteUrl;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (isInternalUrl(absoluteUrl)) {
|
|
468
|
+
// For internal links (based on regex or domain), let the browser handle it naturally
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
e.stopImmediatePropagation();
|
|
474
|
+
handleExternalLink(absoluteUrl);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (target === "_new") {
|
|
479
|
+
if (forceInternalNavigation) {
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
e.stopImmediatePropagation();
|
|
482
|
+
window.location.href = absoluteUrl;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
handleExternalLink(absoluteUrl);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Process download links.
|
|
492
|
+
if (isDownloadRequired(absoluteUrl, anchorElement, e)) {
|
|
493
|
+
// Let the browser download blob:/data: URLs natively; the Rust
|
|
494
|
+
// on_download handler saves them to the Downloads folder. Routing them
|
|
495
|
+
// through the IPC fails on strict-CSP sites (e.g. Gemini), whose
|
|
496
|
+
// connect-src blocks the IPC origin, and on downloads triggered from a
|
|
497
|
+
// sandboxed iframe where the IPC can't be reached.
|
|
498
|
+
if (isSpecialDownload(absoluteUrl)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
e.preventDefault();
|
|
502
|
+
e.stopImmediatePropagation();
|
|
503
|
+
const userLanguage = getUserLanguage();
|
|
504
|
+
invoke("download_file", {
|
|
505
|
+
params: { url: absoluteUrl, filename, language: userLanguage },
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Handle regular links: internal URLs allow normal navigation, external links open in the system browser
|
|
511
|
+
if (!target || target === "_self") {
|
|
512
|
+
// Optimization: Allow previewable media to be handled by the app/browser directly
|
|
513
|
+
// This fixes issues where CDN links are treated as external
|
|
514
|
+
if (isPreviewableMedia(absoluteUrl)) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (!isInternalUrl(absoluteUrl)) {
|
|
519
|
+
if (forceInternalNavigation) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
e.preventDefault();
|
|
524
|
+
e.stopImmediatePropagation();
|
|
525
|
+
handleExternalLink(absoluteUrl);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// Prevent some special websites from executing in advance, before the click event is triggered.
|
|
532
|
+
document.addEventListener("click", detectAnchorElementClick, true);
|
|
533
|
+
|
|
534
|
+
// Rewrite the window.open function.
|
|
535
|
+
const originalWindowOpen = window.open;
|
|
536
|
+
window.open = function (url, name, specs) {
|
|
537
|
+
const normalizedUrl = normalizeAnchorHref(url);
|
|
538
|
+
if (normalizedUrl.startsWith("#")) {
|
|
539
|
+
window.location.href = new URL(normalizedUrl, window.location.href).href;
|
|
540
|
+
return window;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (shouldBypassBghitappLinkHandling(url)) {
|
|
544
|
+
return originalWindowOpen.call(window, url, name, specs);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Allow authentication popups to open normally
|
|
548
|
+
if (window.isAuthPopup(url, name)) {
|
|
549
|
+
return originalWindowOpen.call(window, url, name, specs);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const baseUrl = window.location.origin + window.location.pathname;
|
|
554
|
+
const hrefUrl = new URL(url, baseUrl);
|
|
555
|
+
const absoluteUrl = hrefUrl.href;
|
|
556
|
+
|
|
557
|
+
if (!isInternalUrl(absoluteUrl)) {
|
|
558
|
+
if (forceInternalNavigation) {
|
|
559
|
+
return originalWindowOpen.call(window, absoluteUrl, name, specs);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
handleExternalLink(absoluteUrl);
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return originalWindowOpen.call(window, absoluteUrl, name, specs);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
return originalWindowOpen.call(window, url, name, specs);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// Set the default zoom, There are problems with Loop without using try-catch.
|
|
573
|
+
try {
|
|
574
|
+
setDefaultZoom();
|
|
575
|
+
} catch (e) {
|
|
576
|
+
console.log(e);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Fix Chinese input method "Enter" on Safari
|
|
580
|
+
document.addEventListener(
|
|
581
|
+
"keydown",
|
|
582
|
+
(e) => {
|
|
583
|
+
if (e.key === "Process") e.stopPropagation();
|
|
584
|
+
},
|
|
585
|
+
true,
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
// Language detection and texts
|
|
589
|
+
const isChinese = isChineseLanguage();
|
|
590
|
+
|
|
591
|
+
const menuTexts = {
|
|
592
|
+
// Media operations
|
|
593
|
+
downloadImage: isChinese ? "下载图片" : "Download Image",
|
|
594
|
+
downloadVideo: isChinese ? "下载视频" : "Download Video",
|
|
595
|
+
downloadFile: isChinese ? "下载文件" : "Download File",
|
|
596
|
+
copyAddress: isChinese ? "复制地址" : "Copy Address",
|
|
597
|
+
openInBrowser: isChinese ? "浏览器打开" : "Open in Browser",
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Menu theme configuration
|
|
601
|
+
const MENU_THEMES = {
|
|
602
|
+
dark: {
|
|
603
|
+
menu: {
|
|
604
|
+
background: "#2d2d2d",
|
|
605
|
+
border: "1px solid #404040",
|
|
606
|
+
color: "#ffffff",
|
|
607
|
+
shadow: "0 4px 16px rgba(0, 0, 0, 0.4)",
|
|
608
|
+
},
|
|
609
|
+
item: {
|
|
610
|
+
divider: "#404040",
|
|
611
|
+
hoverBg: "#404040",
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
light: {
|
|
615
|
+
menu: {
|
|
616
|
+
background: "#ffffff",
|
|
617
|
+
border: "1px solid #e0e0e0",
|
|
618
|
+
color: "#333333",
|
|
619
|
+
shadow: "0 4px 16px rgba(0, 0, 0, 0.15)",
|
|
620
|
+
},
|
|
621
|
+
item: {
|
|
622
|
+
divider: "#f0f0f0",
|
|
623
|
+
hoverBg: "#d0d0d0",
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// Theme detection and menu styles
|
|
629
|
+
function getTheme() {
|
|
630
|
+
const prefersDark = window.matchMedia(
|
|
631
|
+
"(prefers-color-scheme: dark)",
|
|
632
|
+
).matches;
|
|
633
|
+
return prefersDark ? "dark" : "light";
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function getMenuStyles(theme = getTheme()) {
|
|
637
|
+
return MENU_THEMES[theme] || MENU_THEMES.light;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Menu configuration constants
|
|
641
|
+
const MENU_CONFIG = {
|
|
642
|
+
id: "bghitapp-context-menu",
|
|
643
|
+
minWidth: "120px", // Compact width for better UX
|
|
644
|
+
borderRadius: "6px", // Slightly more rounded for modern look
|
|
645
|
+
fontSize: "13px",
|
|
646
|
+
zIndex: "999999",
|
|
647
|
+
fontFamily:
|
|
648
|
+
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
649
|
+
// Menu item dimensions
|
|
650
|
+
itemPadding: "8px 16px", // Increased vertical padding for better comfort
|
|
651
|
+
itemLineHeight: "1.2",
|
|
652
|
+
itemBorderRadius: "3px", // Subtle rounded corners for menu items
|
|
653
|
+
itemTransition: "background-color 0.1s ease",
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// Create custom context menu
|
|
657
|
+
function createContextMenu() {
|
|
658
|
+
const contextMenu = document.createElement("div");
|
|
659
|
+
contextMenu.id = MENU_CONFIG.id;
|
|
660
|
+
const styles = getMenuStyles();
|
|
661
|
+
|
|
662
|
+
contextMenu.style.cssText = `
|
|
663
|
+
position: fixed;
|
|
664
|
+
background: ${styles.menu.background};
|
|
665
|
+
border: ${styles.menu.border};
|
|
666
|
+
border-radius: ${MENU_CONFIG.borderRadius};
|
|
667
|
+
box-shadow: ${styles.menu.shadow};
|
|
668
|
+
padding: 4px 0;
|
|
669
|
+
min-width: ${MENU_CONFIG.minWidth};
|
|
670
|
+
font-family: ${MENU_CONFIG.fontFamily};
|
|
671
|
+
font-size: ${MENU_CONFIG.fontSize};
|
|
672
|
+
color: ${styles.menu.color};
|
|
673
|
+
z-index: ${MENU_CONFIG.zIndex};
|
|
674
|
+
display: none;
|
|
675
|
+
user-select: none;
|
|
676
|
+
`;
|
|
677
|
+
document.body.appendChild(contextMenu);
|
|
678
|
+
return contextMenu;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function createMenuItem(text, onClick, divider = false) {
|
|
682
|
+
const item = document.createElement("div");
|
|
683
|
+
const styles = getMenuStyles();
|
|
684
|
+
|
|
685
|
+
item.style.cssText = `
|
|
686
|
+
padding: ${MENU_CONFIG.itemPadding};
|
|
687
|
+
cursor: pointer;
|
|
688
|
+
user-select: none;
|
|
689
|
+
font-weight: 400;
|
|
690
|
+
line-height: ${MENU_CONFIG.itemLineHeight};
|
|
691
|
+
transition: ${MENU_CONFIG.itemTransition};
|
|
692
|
+
white-space: nowrap;
|
|
693
|
+
border-radius: ${MENU_CONFIG.itemBorderRadius};
|
|
694
|
+
margin: 2px 4px;
|
|
695
|
+
border-bottom: ${divider ? `1px solid ${styles.item.divider}` : "none"};
|
|
696
|
+
`;
|
|
697
|
+
item.textContent = text;
|
|
698
|
+
|
|
699
|
+
item.addEventListener("mouseenter", () => {
|
|
700
|
+
item.style.backgroundColor = styles.item.hoverBg;
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
item.addEventListener("mouseleave", () => {
|
|
704
|
+
item.style.backgroundColor = "transparent";
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
item.addEventListener("click", (e) => {
|
|
708
|
+
e.preventDefault();
|
|
709
|
+
e.stopPropagation();
|
|
710
|
+
onClick();
|
|
711
|
+
hideContextMenu();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
return item;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function showContextMenu(x, y, items) {
|
|
718
|
+
let contextMenu = document.getElementById(MENU_CONFIG.id);
|
|
719
|
+
|
|
720
|
+
// Always recreate menu to ensure theme is up-to-date
|
|
721
|
+
if (contextMenu) {
|
|
722
|
+
contextMenu.remove();
|
|
723
|
+
}
|
|
724
|
+
contextMenu = createContextMenu();
|
|
725
|
+
|
|
726
|
+
items.forEach((item) => {
|
|
727
|
+
contextMenu.appendChild(item);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
contextMenu.style.left = x + "px";
|
|
731
|
+
contextMenu.style.top = y + "px";
|
|
732
|
+
contextMenu.style.display = "block";
|
|
733
|
+
|
|
734
|
+
// Adjust position if menu goes off screen
|
|
735
|
+
const rect = contextMenu.getBoundingClientRect();
|
|
736
|
+
if (rect.right > window.innerWidth) {
|
|
737
|
+
contextMenu.style.left = x - rect.width + "px";
|
|
738
|
+
}
|
|
739
|
+
if (rect.bottom > window.innerHeight) {
|
|
740
|
+
contextMenu.style.top = y - rect.height + "px";
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function hideContextMenu() {
|
|
745
|
+
const contextMenu = document.getElementById(MENU_CONFIG.id);
|
|
746
|
+
if (contextMenu) {
|
|
747
|
+
contextMenu.style.display = "none";
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function downloadImage(imageUrl) {
|
|
752
|
+
// Convert relative URLs to absolute
|
|
753
|
+
if (imageUrl.startsWith("/")) {
|
|
754
|
+
imageUrl = window.location.origin + imageUrl;
|
|
755
|
+
} else if (imageUrl.startsWith("./")) {
|
|
756
|
+
imageUrl = new URL(imageUrl, window.location.href).href;
|
|
757
|
+
} else if (
|
|
758
|
+
!imageUrl.startsWith("http") &&
|
|
759
|
+
!imageUrl.startsWith("data:") &&
|
|
760
|
+
!imageUrl.startsWith("blob:")
|
|
761
|
+
) {
|
|
762
|
+
imageUrl = new URL(imageUrl, window.location.href).href;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Generate filename from URL
|
|
766
|
+
const filename = getFilenameFromUrl(imageUrl) || "image";
|
|
767
|
+
|
|
768
|
+
// Handle different URL types
|
|
769
|
+
if (isSpecialDownload(imageUrl)) {
|
|
770
|
+
// Download blob:/data: natively so it works under strict CSP; the Rust
|
|
771
|
+
// on_download handler saves it to the Downloads folder.
|
|
772
|
+
triggerNativeDownload(imageUrl, filename);
|
|
773
|
+
} else {
|
|
774
|
+
// Regular HTTP(S) image
|
|
775
|
+
const userLanguage = getUserLanguage();
|
|
776
|
+
invoke("download_file", {
|
|
777
|
+
params: {
|
|
778
|
+
url: imageUrl,
|
|
779
|
+
filename: filename,
|
|
780
|
+
language: userLanguage,
|
|
781
|
+
},
|
|
782
|
+
}).catch((error) => {
|
|
783
|
+
console.error("Failed to download image:", filename, error);
|
|
784
|
+
showDownloadError(filename);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Check if element is media (image or video)
|
|
790
|
+
function getMediaInfo(target) {
|
|
791
|
+
// Check for img tags
|
|
792
|
+
if (target.tagName.toLowerCase() === "img") {
|
|
793
|
+
return { isMedia: true, url: target.src, type: "image" };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Check for video tags
|
|
797
|
+
if (target.tagName.toLowerCase() === "video") {
|
|
798
|
+
return {
|
|
799
|
+
isMedia: true,
|
|
800
|
+
url: target.src || target.currentSrc,
|
|
801
|
+
type: "video",
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Check for elements with background images
|
|
806
|
+
if (target.style && target.style.backgroundImage) {
|
|
807
|
+
const bgImage = target.style.backgroundImage;
|
|
808
|
+
const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
|
|
809
|
+
if (urlMatch) {
|
|
810
|
+
return { isMedia: true, url: urlMatch[1], type: "image" };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Check for parent elements with background images
|
|
815
|
+
const parentWithBg =
|
|
816
|
+
target && typeof target.closest === "function"
|
|
817
|
+
? target.closest('[style*="background-image"]')
|
|
818
|
+
: null;
|
|
819
|
+
if (parentWithBg) {
|
|
820
|
+
const bgImage = parentWithBg.style.backgroundImage;
|
|
821
|
+
const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
|
|
822
|
+
if (urlMatch) {
|
|
823
|
+
return { isMedia: true, url: urlMatch[1], type: "image" };
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return { isMedia: false, url: "", type: "" };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Simplified menu builder
|
|
831
|
+
function buildMenuItems(type, data) {
|
|
832
|
+
const userLanguage = getUserLanguage();
|
|
833
|
+
const items = [];
|
|
834
|
+
|
|
835
|
+
switch (type) {
|
|
836
|
+
case "media":
|
|
837
|
+
const downloadText =
|
|
838
|
+
data.type === "image"
|
|
839
|
+
? menuTexts.downloadImage
|
|
840
|
+
: menuTexts.downloadVideo;
|
|
841
|
+
items.push(
|
|
842
|
+
createMenuItem(downloadText, () => downloadImage(data.url)),
|
|
843
|
+
createMenuItem(menuTexts.copyAddress, () =>
|
|
844
|
+
navigator.clipboard.writeText(data.url),
|
|
845
|
+
),
|
|
846
|
+
createMenuItem(menuTexts.openInBrowser, () =>
|
|
847
|
+
invoke("plugin:shell|open", { path: data.url }),
|
|
848
|
+
),
|
|
849
|
+
);
|
|
850
|
+
break;
|
|
851
|
+
|
|
852
|
+
case "link":
|
|
853
|
+
if (data.isFile) {
|
|
854
|
+
items.push(
|
|
855
|
+
createMenuItem(menuTexts.downloadFile, () => {
|
|
856
|
+
const filename = getFilenameFromUrl(data.url);
|
|
857
|
+
invoke("download_file", {
|
|
858
|
+
params: { url: data.url, filename, language: userLanguage },
|
|
859
|
+
}).catch((error) => {
|
|
860
|
+
console.error("Failed to download file:", filename, error);
|
|
861
|
+
showDownloadError(filename);
|
|
862
|
+
});
|
|
863
|
+
}),
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
items.push(
|
|
867
|
+
createMenuItem(menuTexts.copyAddress, () =>
|
|
868
|
+
navigator.clipboard.writeText(data.url),
|
|
869
|
+
),
|
|
870
|
+
createMenuItem(menuTexts.openInBrowser, () =>
|
|
871
|
+
invoke("plugin:shell|open", { path: data.url }),
|
|
872
|
+
),
|
|
873
|
+
);
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return items;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Handle right-click context menu
|
|
881
|
+
document.addEventListener(
|
|
882
|
+
"contextmenu",
|
|
883
|
+
function (event) {
|
|
884
|
+
const target = event.target;
|
|
885
|
+
|
|
886
|
+
// Check for media elements (images/videos)
|
|
887
|
+
const mediaInfo = getMediaInfo(target);
|
|
888
|
+
|
|
889
|
+
// Check for links (but not if it's media)
|
|
890
|
+
const linkElement =
|
|
891
|
+
target && typeof target.closest === "function"
|
|
892
|
+
? target.closest("a")
|
|
893
|
+
: null;
|
|
894
|
+
const isLink = linkElement && linkElement.href && !mediaInfo.isMedia;
|
|
895
|
+
|
|
896
|
+
// Only show custom menu for media or links
|
|
897
|
+
if (mediaInfo.isMedia || isLink) {
|
|
898
|
+
event.preventDefault();
|
|
899
|
+
event.stopPropagation();
|
|
900
|
+
|
|
901
|
+
let menuItems = [];
|
|
902
|
+
|
|
903
|
+
if (mediaInfo.isMedia) {
|
|
904
|
+
menuItems = buildMenuItems("media", mediaInfo);
|
|
905
|
+
} else if (isLink) {
|
|
906
|
+
const linkUrl = linkElement.href;
|
|
907
|
+
menuItems = buildMenuItems("link", {
|
|
908
|
+
url: linkUrl,
|
|
909
|
+
isFile: isDownloadableFile(linkUrl),
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
showContextMenu(event.clientX, event.clientY, menuItems);
|
|
914
|
+
}
|
|
915
|
+
// For all other elements, let browser's default context menu handle it
|
|
916
|
+
},
|
|
917
|
+
true,
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
// Hide context menu when clicking elsewhere
|
|
921
|
+
document.addEventListener("click", hideContextMenu);
|
|
922
|
+
document.addEventListener("keydown", (e) => {
|
|
923
|
+
if (e.key === "Escape") {
|
|
924
|
+
hideContextMenu();
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Bridge the Web Notification + Web Badging APIs to BghitApp's Rust commands so
|
|
930
|
+
// pages running inside the webview can drive the macOS dock badge (and
|
|
931
|
+
// taskbar badge on Linux/Windows). Installs synchronously instead of waiting
|
|
932
|
+
// for DOMContentLoaded so feature-detection on Notification/setAppBadge
|
|
933
|
+
// returns the polyfill before site scripts run.
|
|
934
|
+
(function () {
|
|
935
|
+
const invoke = window.__TAURI__?.core?.invoke;
|
|
936
|
+
if (!invoke) return;
|
|
937
|
+
|
|
938
|
+
let permVal = "granted";
|
|
939
|
+
let lastNotifTime = 0;
|
|
940
|
+
let lastNotif = null;
|
|
941
|
+
// Pages that drive the badge directly via setAppBadge own its lifecycle;
|
|
942
|
+
// notifications-driven counts auto-clear on the next user interaction.
|
|
943
|
+
let pageManagedBadge = false;
|
|
944
|
+
let autoBadgeActive = false;
|
|
945
|
+
|
|
946
|
+
const normalizeBadgeCount = (count) => {
|
|
947
|
+
if (typeof count !== "number" || !Number.isFinite(count)) {
|
|
948
|
+
throw new TypeError("Badge count must be a finite number.");
|
|
949
|
+
}
|
|
950
|
+
const normalized = Math.floor(count);
|
|
951
|
+
return normalized > 0 ? Math.min(normalized, 99999) : null;
|
|
952
|
+
};
|
|
953
|
+
const setBadge = (count) => {
|
|
954
|
+
pageManagedBadge = true;
|
|
955
|
+
autoBadgeActive = false;
|
|
956
|
+
return invoke("set_dock_badge", { count }).catch(() => {});
|
|
957
|
+
};
|
|
958
|
+
const clearBadge = () => invoke("clear_dock_badge").catch(() => {});
|
|
959
|
+
const setLabel = (label) => {
|
|
960
|
+
pageManagedBadge = true;
|
|
961
|
+
autoBadgeActive = false;
|
|
962
|
+
return invoke("set_dock_badge_label", { label }).catch(() => {});
|
|
963
|
+
};
|
|
964
|
+
const incrementAutoBadge = () => {
|
|
965
|
+
if (pageManagedBadge) return Promise.resolve();
|
|
966
|
+
autoBadgeActive = true;
|
|
967
|
+
return invoke("increment_dock_badge").catch(() => {});
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
window.addEventListener("focus", () => {
|
|
971
|
+
if (lastNotif?.onclick && Date.now() - lastNotifTime < 5000) {
|
|
972
|
+
lastNotif.onclick(new Event("click"));
|
|
973
|
+
lastNotif = null;
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const clearAutoBadge = () => {
|
|
978
|
+
if (pageManagedBadge || !autoBadgeActive) return;
|
|
979
|
+
autoBadgeActive = false;
|
|
980
|
+
clearBadge();
|
|
981
|
+
};
|
|
982
|
+
document.addEventListener("click", clearAutoBadge, true);
|
|
983
|
+
document.addEventListener("keydown", clearAutoBadge, true);
|
|
984
|
+
|
|
985
|
+
const wrappedNotification = function (title, options) {
|
|
986
|
+
const body = options?.body || "";
|
|
987
|
+
let icon = options?.icon || "";
|
|
988
|
+
if (icon.startsWith("/")) {
|
|
989
|
+
icon = window.location.origin + icon;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const notif = {
|
|
993
|
+
onclick: null,
|
|
994
|
+
onclose: null,
|
|
995
|
+
onshow: null,
|
|
996
|
+
onerror: null,
|
|
997
|
+
close: () => {},
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
lastNotifTime = Date.now();
|
|
1001
|
+
lastNotif = notif;
|
|
1002
|
+
invoke("send_notification", { params: { title, body, icon } })
|
|
1003
|
+
.then(() => incrementAutoBadge())
|
|
1004
|
+
.then(() => {
|
|
1005
|
+
if (notif.onshow) notif.onshow(new Event("show"));
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
return notif;
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
wrappedNotification.requestPermission = async () => "granted";
|
|
1012
|
+
Object.defineProperty(wrappedNotification, "permission", {
|
|
1013
|
+
enumerable: true,
|
|
1014
|
+
get: () => permVal,
|
|
1015
|
+
set: (v) => {
|
|
1016
|
+
permVal = v;
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
try {
|
|
1021
|
+
Object.defineProperty(window, "Notification", {
|
|
1022
|
+
configurable: true,
|
|
1023
|
+
writable: true,
|
|
1024
|
+
value: wrappedNotification,
|
|
1025
|
+
});
|
|
1026
|
+
} catch (_) {}
|
|
1027
|
+
|
|
1028
|
+
// Web Badging API: https://wicg.github.io/badging/
|
|
1029
|
+
// setAppBadge() with no argument shows an indicator dot; with a number,
|
|
1030
|
+
// shows the count (0 clears). clearAppBadge() removes the badge entirely.
|
|
1031
|
+
const setAppBadge = (count) => {
|
|
1032
|
+
if (count === undefined) return setLabel("•");
|
|
1033
|
+
let normalized;
|
|
1034
|
+
try {
|
|
1035
|
+
normalized = normalizeBadgeCount(count);
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
return Promise.reject(error);
|
|
1038
|
+
}
|
|
1039
|
+
if (normalized === null) {
|
|
1040
|
+
pageManagedBadge = false;
|
|
1041
|
+
autoBadgeActive = false;
|
|
1042
|
+
return clearBadge();
|
|
1043
|
+
}
|
|
1044
|
+
return setBadge(normalized);
|
|
1045
|
+
};
|
|
1046
|
+
const clearAppBadge = () => {
|
|
1047
|
+
pageManagedBadge = false;
|
|
1048
|
+
autoBadgeActive = false;
|
|
1049
|
+
return clearBadge();
|
|
1050
|
+
};
|
|
1051
|
+
try {
|
|
1052
|
+
Object.defineProperty(navigator, "setAppBadge", {
|
|
1053
|
+
configurable: true,
|
|
1054
|
+
writable: true,
|
|
1055
|
+
value: setAppBadge,
|
|
1056
|
+
});
|
|
1057
|
+
Object.defineProperty(navigator, "clearAppBadge", {
|
|
1058
|
+
configurable: true,
|
|
1059
|
+
writable: true,
|
|
1060
|
+
value: clearAppBadge,
|
|
1061
|
+
});
|
|
1062
|
+
} catch (_) {}
|
|
1063
|
+
})();
|
|
1064
|
+
|
|
1065
|
+
function setDefaultZoom() {
|
|
1066
|
+
const htmlZoom = window.localStorage.getItem("htmlZoom");
|
|
1067
|
+
if (htmlZoom) {
|
|
1068
|
+
setZoom(htmlZoom);
|
|
1069
|
+
} else if (window.bghitappConfig?.zoom && window.bghitappConfig.zoom !== 100) {
|
|
1070
|
+
setZoom(`${window.bghitappConfig.zoom}%`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function getFilenameFromUrl(url) {
|
|
1075
|
+
try {
|
|
1076
|
+
const urlPath = new URL(url).pathname;
|
|
1077
|
+
let filename = urlPath.substring(urlPath.lastIndexOf("/") + 1);
|
|
1078
|
+
|
|
1079
|
+
// If no filename or no extension, generate one
|
|
1080
|
+
if (!filename || !filename.includes(".")) {
|
|
1081
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1082
|
+
|
|
1083
|
+
// Detect image type from URL or data URI
|
|
1084
|
+
if (url.startsWith("data:image/")) {
|
|
1085
|
+
const mimeType = url.substring(11, url.indexOf(";"));
|
|
1086
|
+
filename = `image-${timestamp}.${mimeType}`;
|
|
1087
|
+
} else {
|
|
1088
|
+
// Default to common image extensions based on common patterns
|
|
1089
|
+
if (url.includes("jpg") || url.includes("jpeg")) {
|
|
1090
|
+
filename = `image-${timestamp}.jpg`;
|
|
1091
|
+
} else if (url.includes("png")) {
|
|
1092
|
+
filename = `image-${timestamp}.png`;
|
|
1093
|
+
} else if (url.includes("gif")) {
|
|
1094
|
+
filename = `image-${timestamp}.gif`;
|
|
1095
|
+
} else if (url.includes("webp")) {
|
|
1096
|
+
filename = `image-${timestamp}.webp`;
|
|
1097
|
+
} else if (url.includes("svg")) {
|
|
1098
|
+
filename = `image-${timestamp}.svg`;
|
|
1099
|
+
} else {
|
|
1100
|
+
filename = `image-${timestamp}.png`; // default
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return filename;
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
// Fallback for invalid URLs
|
|
1108
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1109
|
+
return `image-${timestamp}.png`;
|
|
1110
|
+
}
|
|
1111
|
+
}
|