pake-cli 3.11.5 โ 3.11.6
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/dist/cli.js +6 -1
- package/dist/pake-badge-test.html +1 -0
- package/dist/test-local.html +1 -0
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +7 -4
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/build.rs +2 -0
- package/src-tauri/pake.json +1 -0
- package/src-tauri/src/app/config.rs +2 -0
- package/src-tauri/src/app/invoke.rs +85 -0
- package/src-tauri/src/app/menu.rs +56 -5
- package/src-tauri/src/app/window.rs +1 -0
- package/src-tauri/src/inject/event.js +98 -12
- package/src-tauri/src/inject/find.js +708 -0
- package/src-tauri/src/lib.rs +8 -3
- package/src-tauri/tauri.conf.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
|
20
20
|
import fs$1 from 'fs';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.11.
|
|
23
|
+
var version = "3.11.6";
|
|
24
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
25
|
var engines = {
|
|
26
26
|
node: ">=18.0.0"
|
|
@@ -464,6 +464,7 @@ function buildWindowConfigOverrides(options, platform = asSupportedPlatform(proc
|
|
|
464
464
|
start_to_tray: options.startToTray && options.showSystemTray,
|
|
465
465
|
force_internal_navigation: options.forceInternalNavigation,
|
|
466
466
|
internal_url_regex: options.internalUrlRegex,
|
|
467
|
+
enable_find: options.enableFind,
|
|
467
468
|
zoom: options.zoom,
|
|
468
469
|
min_width: options.minWidth,
|
|
469
470
|
min_height: options.minHeight,
|
|
@@ -2402,6 +2403,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2402
2403
|
startToTray: false,
|
|
2403
2404
|
forceInternalNavigation: false,
|
|
2404
2405
|
internalUrlRegex: '',
|
|
2406
|
+
enableFind: false,
|
|
2405
2407
|
iterativeBuild: false,
|
|
2406
2408
|
zoom: 100,
|
|
2407
2409
|
minWidth: 0,
|
|
@@ -2541,6 +2543,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2541
2543
|
.addOption(new Option('--internal-url-regex <string>', 'Regex pattern to match URLs that should be considered internal')
|
|
2542
2544
|
.default(DEFAULT_PAKE_OPTIONS.internalUrlRegex)
|
|
2543
2545
|
.hideHelp())
|
|
2546
|
+
.addOption(new Option('--enable-find', 'Enable in-page Find UI with Cmd/Ctrl+F/G shortcuts')
|
|
2547
|
+
.default(DEFAULT_PAKE_OPTIONS.enableFind)
|
|
2548
|
+
.hideHelp())
|
|
2544
2549
|
.addOption(new Option('--installer-language <string>', 'Installer language')
|
|
2545
2550
|
.default(DEFAULT_PAKE_OPTIONS.installerLanguage)
|
|
2546
2551
|
.hideHelp())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"><title>Pake Badge Test</title></head><body><h1>Pake Badge Test</h1><pre id="log"></pre><script>const log=(msg)=>{document.getElementById("log").textContent+=msg+"\n"};async function run(){await new Promise(r=>setTimeout(r,1500));log("setAppBadge(3)");await navigator.setAppBadge(3);setTimeout(async()=>{log("setAppBadge() dot");await navigator.setAppBadge();},7000);setTimeout(async()=>{log("clearAppBadge()");await navigator.clearAppBadge();},14000);}run().catch(e=>log(String(e)));</script></body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<html><body><h1>Hello Pake</h1></body></html>
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
|
@@ -2194,9 +2194,9 @@ dependencies = [
|
|
|
2194
2194
|
|
|
2195
2195
|
[[package]]
|
|
2196
2196
|
name = "muda"
|
|
2197
|
-
version = "0.17.
|
|
2197
|
+
version = "0.17.2"
|
|
2198
2198
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2199
|
-
checksum = "
|
|
2199
|
+
checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177"
|
|
2200
2200
|
dependencies = [
|
|
2201
2201
|
"crossbeam-channel",
|
|
2202
2202
|
"dpi",
|
|
@@ -2564,8 +2564,11 @@ dependencies = [
|
|
|
2564
2564
|
|
|
2565
2565
|
[[package]]
|
|
2566
2566
|
name = "pake"
|
|
2567
|
-
version = "3.11.
|
|
2567
|
+
version = "3.11.6"
|
|
2568
2568
|
dependencies = [
|
|
2569
|
+
"objc2",
|
|
2570
|
+
"objc2-app-kit",
|
|
2571
|
+
"objc2-foundation",
|
|
2569
2572
|
"serde",
|
|
2570
2573
|
"serde_json",
|
|
2571
2574
|
"tauri",
|
|
@@ -3066,7 +3069,7 @@ dependencies = [
|
|
|
3066
3069
|
"once_cell",
|
|
3067
3070
|
"socket2",
|
|
3068
3071
|
"tracing",
|
|
3069
|
-
"windows-sys 0.
|
|
3072
|
+
"windows-sys 0.52.0",
|
|
3070
3073
|
]
|
|
3071
3074
|
|
|
3072
3075
|
[[package]]
|
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "pake"
|
|
3
|
-
version = "3.11.
|
|
3
|
+
version = "3.11.6"
|
|
4
4
|
description = "๐คฑ๐ป Turn any webpage into a desktop app with Rust."
|
|
5
5
|
authors = ["Tw93"]
|
|
6
6
|
license = "MIT"
|
|
@@ -36,6 +36,11 @@ tauri-plugin-opener = { version = "2.5.3" }
|
|
|
36
36
|
tauri-plugin-single-instance = "2.4.0"
|
|
37
37
|
tauri-plugin-notification = "2.3.3"
|
|
38
38
|
|
|
39
|
+
[target.'cfg(target_os = "macos")'.dependencies]
|
|
40
|
+
objc2 = "0.6"
|
|
41
|
+
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSDockTile"] }
|
|
42
|
+
objc2-foundation = { version = "0.3", features = ["NSString"] }
|
|
43
|
+
|
|
39
44
|
[features]
|
|
40
45
|
# this feature is used for development builds from development cli
|
|
41
46
|
cli-build = []
|
package/src-tauri/build.rs
CHANGED
package/src-tauri/pake.json
CHANGED
|
@@ -2,6 +2,7 @@ use crate::util::{check_file_or_append, get_download_message_with_lang, show_toa
|
|
|
2
2
|
use std::fs::{self, File};
|
|
3
3
|
use std::io::Write;
|
|
4
4
|
use std::str::FromStr;
|
|
5
|
+
use std::sync::atomic::{AtomicI64, Ordering};
|
|
5
6
|
use tauri::http::Method;
|
|
6
7
|
use tauri::{command, AppHandle, Manager, Url, WebviewWindow};
|
|
7
8
|
use tauri_plugin_http::reqwest::{ClientBuilder, Request};
|
|
@@ -9,6 +10,62 @@ use tauri_plugin_http::reqwest::{ClientBuilder, Request};
|
|
|
9
10
|
#[cfg(target_os = "macos")]
|
|
10
11
|
use tauri::Theme;
|
|
11
12
|
|
|
13
|
+
static BADGE_COUNT: AtomicI64 = AtomicI64::new(0);
|
|
14
|
+
const MAX_BADGE_COUNT: i64 = 99_999;
|
|
15
|
+
const MAX_BADGE_LABEL_CHARS: usize = 16;
|
|
16
|
+
|
|
17
|
+
fn normalize_badge_count(count: Option<i64>) -> Option<i64> {
|
|
18
|
+
count.filter(|n| (1..=MAX_BADGE_COUNT).contains(n))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn normalize_badge_label(label: Option<&str>) -> Result<Option<String>, String> {
|
|
22
|
+
let Some(label) = label.map(str::trim).filter(|label| !label.is_empty()) else {
|
|
23
|
+
return Ok(None);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if label.chars().count() > MAX_BADGE_LABEL_CHARS {
|
|
27
|
+
return Err(format!(
|
|
28
|
+
"Badge label must be {MAX_BADGE_LABEL_CHARS} characters or fewer"
|
|
29
|
+
));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Ok(Some(label.to_string()))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn apply_badge(app: &AppHandle, count: Option<i64>) -> Result<(), String> {
|
|
36
|
+
let label = normalize_badge_count(count).map(|n| n.to_string());
|
|
37
|
+
apply_badge_label(app, label.as_deref())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[cfg(target_os = "macos")]
|
|
41
|
+
fn apply_badge_label(app: &AppHandle, label: Option<&str>) -> Result<(), String> {
|
|
42
|
+
use objc2::MainThreadMarker;
|
|
43
|
+
use objc2_app_kit::NSApplication;
|
|
44
|
+
use objc2_foundation::NSString;
|
|
45
|
+
|
|
46
|
+
let label = label.map(str::to_owned);
|
|
47
|
+
app.run_on_main_thread(move || {
|
|
48
|
+
let Some(mtm) = MainThreadMarker::new() else {
|
|
49
|
+
return;
|
|
50
|
+
};
|
|
51
|
+
let dock_tile = NSApplication::sharedApplication(mtm).dockTile();
|
|
52
|
+
let ns_label = label.as_deref().map(NSString::from_str);
|
|
53
|
+
dock_tile.setBadgeLabel(ns_label.as_deref());
|
|
54
|
+
})
|
|
55
|
+
.map_err(|e| format!("Failed to dispatch dock badge update: {e}"))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[cfg(not(target_os = "macos"))]
|
|
59
|
+
fn apply_badge_label(app: &AppHandle, label: Option<&str>) -> Result<(), String> {
|
|
60
|
+
let window = app
|
|
61
|
+
.get_webview_window("pake")
|
|
62
|
+
.ok_or("Main window not found")?;
|
|
63
|
+
let count = label.and_then(|s| s.parse::<i64>().ok());
|
|
64
|
+
window
|
|
65
|
+
.set_badge_count(count)
|
|
66
|
+
.map_err(|e| format!("Failed to set badge count: {e}"))
|
|
67
|
+
}
|
|
68
|
+
|
|
12
69
|
#[derive(serde::Deserialize)]
|
|
13
70
|
pub struct DownloadFileParams {
|
|
14
71
|
url: String,
|
|
@@ -144,6 +201,34 @@ pub fn send_notification(app: AppHandle, params: NotificationParams) -> Result<(
|
|
|
144
201
|
Ok(())
|
|
145
202
|
}
|
|
146
203
|
|
|
204
|
+
#[command]
|
|
205
|
+
pub fn set_dock_badge(app: AppHandle, count: Option<i64>) -> Result<(), String> {
|
|
206
|
+
let normalized = normalize_badge_count(count);
|
|
207
|
+
BADGE_COUNT.store(normalized.unwrap_or(0), Ordering::SeqCst);
|
|
208
|
+
apply_badge(&app, normalized)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[command]
|
|
212
|
+
pub fn increment_dock_badge(app: AppHandle) -> Result<(), String> {
|
|
213
|
+
let current = BADGE_COUNT.load(Ordering::SeqCst);
|
|
214
|
+
let next = current.saturating_add(1).clamp(1, MAX_BADGE_COUNT);
|
|
215
|
+
BADGE_COUNT.store(next, Ordering::SeqCst);
|
|
216
|
+
apply_badge(&app, Some(next))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[command]
|
|
220
|
+
pub fn clear_dock_badge(app: AppHandle) -> Result<(), String> {
|
|
221
|
+
BADGE_COUNT.store(0, Ordering::SeqCst);
|
|
222
|
+
apply_badge(&app, None)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[command]
|
|
226
|
+
pub fn set_dock_badge_label(app: AppHandle, label: Option<String>) -> Result<(), String> {
|
|
227
|
+
BADGE_COUNT.store(0, Ordering::SeqCst);
|
|
228
|
+
let label = normalize_badge_label(label.as_deref())?;
|
|
229
|
+
apply_badge_label(&app, label.as_deref())
|
|
230
|
+
}
|
|
231
|
+
|
|
147
232
|
#[command]
|
|
148
233
|
pub async fn update_theme_mode(app: AppHandle, mode: String) {
|
|
149
234
|
#[cfg(target_os = "macos")]
|
|
@@ -6,24 +6,36 @@ use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu};
|
|
|
6
6
|
use tauri::{AppHandle, Manager, Wry};
|
|
7
7
|
use tauri_plugin_opener::OpenerExt;
|
|
8
8
|
|
|
9
|
-
pub fn
|
|
9
|
+
pub fn set_app_menu(
|
|
10
|
+
app: &AppHandle<Wry>,
|
|
11
|
+
allow_multi_window: bool,
|
|
12
|
+
enable_find: bool,
|
|
13
|
+
) -> tauri::Result<()> {
|
|
10
14
|
let pake_version = env!("CARGO_PKG_VERSION");
|
|
11
15
|
let pake_menu_item_title = format!("Built with Pake V{}", pake_version);
|
|
12
16
|
|
|
17
|
+
let window_submenu = window_menu(app)?;
|
|
18
|
+
|
|
13
19
|
let menu = Menu::with_items(
|
|
14
20
|
app,
|
|
15
21
|
&[
|
|
16
22
|
&app_menu(app)?,
|
|
17
23
|
&file_menu(app, allow_multi_window)?,
|
|
18
|
-
&edit_menu(app)?,
|
|
24
|
+
&edit_menu(app, enable_find)?,
|
|
19
25
|
&view_menu(app)?,
|
|
20
26
|
&navigation_menu(app)?,
|
|
21
|
-
&
|
|
27
|
+
&window_submenu,
|
|
22
28
|
&help_menu(app, &pake_menu_item_title)?,
|
|
23
29
|
],
|
|
24
30
|
)?;
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
app.set_menu(menu)?;
|
|
33
|
+
|
|
34
|
+
// AppKit injects Move & Resize, Fill, Center, Full Screen Tile, and
|
|
35
|
+
// window-cycling once the submenu is registered as the windows menu.
|
|
36
|
+
window_submenu.set_as_windows_menu_for_nsapp()?;
|
|
37
|
+
|
|
38
|
+
Ok(())
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
fn app_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {
|
|
@@ -69,7 +81,7 @@ fn file_menu(app: &AppHandle<Wry>, allow_multi_window: bool) -> tauri::Result<Su
|
|
|
69
81
|
Ok(file_menu)
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
fn edit_menu(app: &AppHandle<Wry
|
|
84
|
+
fn edit_menu(app: &AppHandle<Wry>, enable_find: bool) -> tauri::Result<Submenu<Wry>> {
|
|
73
85
|
let edit_menu = Submenu::new(app, "Edit", true)?;
|
|
74
86
|
edit_menu.append(&PredefinedMenuItem::undo(app, None)?)?;
|
|
75
87
|
edit_menu.append(&PredefinedMenuItem::redo(app, None)?)?;
|
|
@@ -86,6 +98,30 @@ fn edit_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {
|
|
|
86
98
|
)?)?;
|
|
87
99
|
edit_menu.append(&PredefinedMenuItem::select_all(app, None)?)?;
|
|
88
100
|
edit_menu.append(&PredefinedMenuItem::separator(app)?)?;
|
|
101
|
+
if enable_find {
|
|
102
|
+
edit_menu.append(&MenuItem::with_id(
|
|
103
|
+
app,
|
|
104
|
+
"find",
|
|
105
|
+
"Find",
|
|
106
|
+
true,
|
|
107
|
+
Some("CmdOrCtrl+F"),
|
|
108
|
+
)?)?;
|
|
109
|
+
edit_menu.append(&MenuItem::with_id(
|
|
110
|
+
app,
|
|
111
|
+
"find_next",
|
|
112
|
+
"Find Next",
|
|
113
|
+
true,
|
|
114
|
+
Some("CmdOrCtrl+G"),
|
|
115
|
+
)?)?;
|
|
116
|
+
edit_menu.append(&MenuItem::with_id(
|
|
117
|
+
app,
|
|
118
|
+
"find_previous",
|
|
119
|
+
"Find Previous",
|
|
120
|
+
true,
|
|
121
|
+
Some("CmdOrCtrl+Shift+G"),
|
|
122
|
+
)?)?;
|
|
123
|
+
edit_menu.append(&PredefinedMenuItem::separator(app)?)?;
|
|
124
|
+
}
|
|
89
125
|
edit_menu.append(&MenuItem::with_id(
|
|
90
126
|
app,
|
|
91
127
|
"copy_url",
|
|
@@ -255,6 +291,21 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
|
255
291
|
let _ = window.eval("triggerPasteAsPlainText()");
|
|
256
292
|
}
|
|
257
293
|
}
|
|
294
|
+
"find" => {
|
|
295
|
+
if let Some(window) = app_handle.get_webview_window("pake") {
|
|
296
|
+
let _ = window.eval("window.pakeFind?.open()");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
"find_next" => {
|
|
300
|
+
if let Some(window) = app_handle.get_webview_window("pake") {
|
|
301
|
+
let _ = window.eval("window.pakeFind?.next()");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
"find_previous" => {
|
|
305
|
+
if let Some(window) = app_handle.get_webview_window("pake") {
|
|
306
|
+
let _ = window.eval("window.pakeFind?.previous()");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
258
309
|
"clear_cache_restart" => {
|
|
259
310
|
if let Some(window) = app_handle.get_webview_window("pake") {
|
|
260
311
|
if let Ok(_) = window.clear_all_browsing_data() {
|
|
@@ -287,6 +287,7 @@ fn build_window(
|
|
|
287
287
|
// calls show_toast().
|
|
288
288
|
window_builder = window_builder
|
|
289
289
|
.initialization_script(&config_script)
|
|
290
|
+
.initialization_script(include_str!("../inject/find.js"))
|
|
290
291
|
.initialization_script(include_str!("../inject/toast.js"))
|
|
291
292
|
.initialization_script(include_str!("../inject/fullscreen.js"))
|
|
292
293
|
.initialization_script(include_str!("../inject/event.js"))
|
|
@@ -1025,10 +1025,46 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
1025
1025
|
});
|
|
1026
1026
|
});
|
|
1027
1027
|
|
|
1028
|
-
|
|
1028
|
+
// Bridge the Web Notification + Web Badging APIs to Pake's Rust commands so
|
|
1029
|
+
// pages running inside the webview can drive the macOS dock badge (and
|
|
1030
|
+
// taskbar badge on Linux/Windows). Installs synchronously instead of waiting
|
|
1031
|
+
// for DOMContentLoaded so feature-detection on Notification/setAppBadge
|
|
1032
|
+
// returns the polyfill before site scripts run.
|
|
1033
|
+
(function () {
|
|
1034
|
+
const invoke = window.__TAURI__?.core?.invoke;
|
|
1035
|
+
if (!invoke) return;
|
|
1036
|
+
|
|
1029
1037
|
let permVal = "granted";
|
|
1030
1038
|
let lastNotifTime = 0;
|
|
1031
1039
|
let lastNotif = null;
|
|
1040
|
+
// Pages that drive the badge directly via setAppBadge own its lifecycle;
|
|
1041
|
+
// notifications-driven counts auto-clear on the next user interaction.
|
|
1042
|
+
let pageManagedBadge = false;
|
|
1043
|
+
let autoBadgeActive = false;
|
|
1044
|
+
|
|
1045
|
+
const normalizeBadgeCount = (count) => {
|
|
1046
|
+
if (typeof count !== "number" || !Number.isFinite(count)) {
|
|
1047
|
+
throw new TypeError("Badge count must be a finite number.");
|
|
1048
|
+
}
|
|
1049
|
+
const normalized = Math.floor(count);
|
|
1050
|
+
return normalized > 0 ? Math.min(normalized, 99999) : null;
|
|
1051
|
+
};
|
|
1052
|
+
const setBadge = (count) => {
|
|
1053
|
+
pageManagedBadge = true;
|
|
1054
|
+
autoBadgeActive = false;
|
|
1055
|
+
return invoke("set_dock_badge", { count }).catch(() => {});
|
|
1056
|
+
};
|
|
1057
|
+
const clearBadge = () => invoke("clear_dock_badge").catch(() => {});
|
|
1058
|
+
const setLabel = (label) => {
|
|
1059
|
+
pageManagedBadge = true;
|
|
1060
|
+
autoBadgeActive = false;
|
|
1061
|
+
return invoke("set_dock_badge_label", { label }).catch(() => {});
|
|
1062
|
+
};
|
|
1063
|
+
const incrementAutoBadge = () => {
|
|
1064
|
+
if (pageManagedBadge) return Promise.resolve();
|
|
1065
|
+
autoBadgeActive = true;
|
|
1066
|
+
return invoke("increment_dock_badge").catch(() => {});
|
|
1067
|
+
};
|
|
1032
1068
|
|
|
1033
1069
|
window.addEventListener("focus", () => {
|
|
1034
1070
|
if (lastNotif?.onclick && Date.now() - lastNotifTime < 5000) {
|
|
@@ -1037,11 +1073,17 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
|
1037
1073
|
}
|
|
1038
1074
|
});
|
|
1039
1075
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1076
|
+
const clearAutoBadge = () => {
|
|
1077
|
+
if (pageManagedBadge || !autoBadgeActive) return;
|
|
1078
|
+
autoBadgeActive = false;
|
|
1079
|
+
clearBadge();
|
|
1080
|
+
};
|
|
1081
|
+
document.addEventListener("click", clearAutoBadge, true);
|
|
1082
|
+
document.addEventListener("keydown", clearAutoBadge, true);
|
|
1083
|
+
|
|
1084
|
+
const wrappedNotification = function (title, options) {
|
|
1042
1085
|
const body = options?.body || "";
|
|
1043
1086
|
let icon = options?.icon || "";
|
|
1044
|
-
|
|
1045
1087
|
if (icon.startsWith("/")) {
|
|
1046
1088
|
icon = window.location.origin + icon;
|
|
1047
1089
|
}
|
|
@@ -1056,24 +1098,68 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
|
1056
1098
|
|
|
1057
1099
|
lastNotifTime = Date.now();
|
|
1058
1100
|
lastNotif = notif;
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1101
|
+
invoke("send_notification", { params: { title, body, icon } })
|
|
1102
|
+
.then(() => incrementAutoBadge())
|
|
1103
|
+
.then(() => {
|
|
1104
|
+
if (notif.onshow) notif.onshow(new Event("show"));
|
|
1105
|
+
});
|
|
1063
1106
|
|
|
1064
1107
|
return notif;
|
|
1065
1108
|
};
|
|
1066
1109
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
Object.defineProperty(window.Notification, "permission", {
|
|
1110
|
+
wrappedNotification.requestPermission = async () => "granted";
|
|
1111
|
+
Object.defineProperty(wrappedNotification, "permission", {
|
|
1070
1112
|
enumerable: true,
|
|
1071
1113
|
get: () => permVal,
|
|
1072
1114
|
set: (v) => {
|
|
1073
1115
|
permVal = v;
|
|
1074
1116
|
},
|
|
1075
1117
|
});
|
|
1076
|
-
|
|
1118
|
+
|
|
1119
|
+
try {
|
|
1120
|
+
Object.defineProperty(window, "Notification", {
|
|
1121
|
+
configurable: true,
|
|
1122
|
+
writable: true,
|
|
1123
|
+
value: wrappedNotification,
|
|
1124
|
+
});
|
|
1125
|
+
} catch (_) {}
|
|
1126
|
+
|
|
1127
|
+
// Web Badging API: https://wicg.github.io/badging/
|
|
1128
|
+
// setAppBadge() with no argument shows an indicator dot; with a number,
|
|
1129
|
+
// shows the count (0 clears). clearAppBadge() removes the badge entirely.
|
|
1130
|
+
const setAppBadge = (count) => {
|
|
1131
|
+
if (count === undefined) return setLabel("โข");
|
|
1132
|
+
let normalized;
|
|
1133
|
+
try {
|
|
1134
|
+
normalized = normalizeBadgeCount(count);
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
return Promise.reject(error);
|
|
1137
|
+
}
|
|
1138
|
+
if (normalized === null) {
|
|
1139
|
+
pageManagedBadge = false;
|
|
1140
|
+
autoBadgeActive = false;
|
|
1141
|
+
return clearBadge();
|
|
1142
|
+
}
|
|
1143
|
+
return setBadge(normalized);
|
|
1144
|
+
};
|
|
1145
|
+
const clearAppBadge = () => {
|
|
1146
|
+
pageManagedBadge = false;
|
|
1147
|
+
autoBadgeActive = false;
|
|
1148
|
+
return clearBadge();
|
|
1149
|
+
};
|
|
1150
|
+
try {
|
|
1151
|
+
Object.defineProperty(navigator, "setAppBadge", {
|
|
1152
|
+
configurable: true,
|
|
1153
|
+
writable: true,
|
|
1154
|
+
value: setAppBadge,
|
|
1155
|
+
});
|
|
1156
|
+
Object.defineProperty(navigator, "clearAppBadge", {
|
|
1157
|
+
configurable: true,
|
|
1158
|
+
writable: true,
|
|
1159
|
+
value: clearAppBadge,
|
|
1160
|
+
});
|
|
1161
|
+
} catch (_) {}
|
|
1162
|
+
})();
|
|
1077
1163
|
|
|
1078
1164
|
function setDefaultZoom() {
|
|
1079
1165
|
const htmlZoom = window.localStorage.getItem("htmlZoom");
|
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
if (window.__PAKE_FIND_SCRIPT__) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
window.__PAKE_FIND_SCRIPT__ = true;
|
|
6
|
+
|
|
7
|
+
const PANEL_ID = "pake-find-panel";
|
|
8
|
+
const STYLE_ID = "pake-find-style";
|
|
9
|
+
const MARK_ATTR = "data-pake-find";
|
|
10
|
+
const ACTIVE_ATTR = "data-pake-find-active";
|
|
11
|
+
const MATCH_HIGHLIGHT = "pake-find-match";
|
|
12
|
+
const ACTIVE_HIGHLIGHT = "pake-find-active";
|
|
13
|
+
const MAX_MATCHES = 1000;
|
|
14
|
+
const SEARCH_DEBOUNCE_MS = 120;
|
|
15
|
+
const SKIPPED_TAGS = new Set([
|
|
16
|
+
"script",
|
|
17
|
+
"style",
|
|
18
|
+
"noscript",
|
|
19
|
+
"input",
|
|
20
|
+
"textarea",
|
|
21
|
+
"select",
|
|
22
|
+
"option",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const state = {
|
|
26
|
+
enabled: window.pakeConfig?.enable_find === true,
|
|
27
|
+
panel: null,
|
|
28
|
+
input: null,
|
|
29
|
+
counter: null,
|
|
30
|
+
status: null,
|
|
31
|
+
matches: [],
|
|
32
|
+
activeIndex: -1,
|
|
33
|
+
query: "",
|
|
34
|
+
truncated: false,
|
|
35
|
+
domMarks: [],
|
|
36
|
+
observer: null,
|
|
37
|
+
searchTimer: null,
|
|
38
|
+
isOpen: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function getState() {
|
|
42
|
+
return {
|
|
43
|
+
enabled: state.enabled,
|
|
44
|
+
isOpen: state.isOpen,
|
|
45
|
+
query: state.query,
|
|
46
|
+
matchCount: state.matches.length,
|
|
47
|
+
activeIndex: state.activeIndex,
|
|
48
|
+
truncated: state.truncated,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function noop() {
|
|
53
|
+
return getState();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!state.enabled) {
|
|
57
|
+
window.pakeFind = {
|
|
58
|
+
open: noop,
|
|
59
|
+
close: noop,
|
|
60
|
+
next: noop,
|
|
61
|
+
previous: noop,
|
|
62
|
+
search: noop,
|
|
63
|
+
getState,
|
|
64
|
+
getFindShortcutAction: () => "",
|
|
65
|
+
};
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getNodeFilter() {
|
|
70
|
+
return (
|
|
71
|
+
window.NodeFilter ||
|
|
72
|
+
globalThis.NodeFilter || {
|
|
73
|
+
SHOW_TEXT: 4,
|
|
74
|
+
FILTER_ACCEPT: 1,
|
|
75
|
+
FILTER_REJECT: 2,
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function supportsCustomHighlight() {
|
|
81
|
+
return (
|
|
82
|
+
typeof CSS !== "undefined" &&
|
|
83
|
+
CSS.highlights &&
|
|
84
|
+
typeof Highlight === "function"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isFindPanelNode(node) {
|
|
89
|
+
const element =
|
|
90
|
+
node?.nodeType === 1 ? node : node?.parentElement || node?.parentNode;
|
|
91
|
+
if (!element) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (element.id === PANEL_ID) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return element.closest?.(`#${PANEL_ID}`) != null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function shouldSkipElement(element) {
|
|
101
|
+
for (let current = element; current; current = current.parentElement) {
|
|
102
|
+
if (current.id === PANEL_ID) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const tagName = current.tagName?.toLowerCase();
|
|
107
|
+
if (tagName && SKIPPED_TAGS.has(tagName)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
current.isContentEditable ||
|
|
113
|
+
current.getAttribute?.("contenteditable") === "true"
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (current.hidden || current.getAttribute?.("aria-hidden") === "true") {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getSearchableTextNodes(root = document.body) {
|
|
127
|
+
if (!root || !document.createTreeWalker) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const nodeFilter = getNodeFilter();
|
|
132
|
+
const walker = document.createTreeWalker(root, nodeFilter.SHOW_TEXT, {
|
|
133
|
+
acceptNode(node) {
|
|
134
|
+
if (!node.nodeValue || node.nodeValue.length === 0) {
|
|
135
|
+
return nodeFilter.FILTER_REJECT;
|
|
136
|
+
}
|
|
137
|
+
if (shouldSkipElement(node.parentElement)) {
|
|
138
|
+
return nodeFilter.FILTER_REJECT;
|
|
139
|
+
}
|
|
140
|
+
return nodeFilter.FILTER_ACCEPT;
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const nodes = [];
|
|
145
|
+
let current = walker.nextNode();
|
|
146
|
+
while (current) {
|
|
147
|
+
nodes.push(current);
|
|
148
|
+
current = walker.nextNode();
|
|
149
|
+
}
|
|
150
|
+
return nodes;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function createRange(node, start, end) {
|
|
154
|
+
const range = document.createRange();
|
|
155
|
+
range.setStart(node, start);
|
|
156
|
+
range.setEnd(node, end);
|
|
157
|
+
return range;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function collectMatches(query) {
|
|
161
|
+
const matches = [];
|
|
162
|
+
const normalizedQuery = query.toLocaleLowerCase();
|
|
163
|
+
if (!normalizedQuery) {
|
|
164
|
+
return { matches, truncated: false };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const node of getSearchableTextNodes()) {
|
|
168
|
+
const text = node.nodeValue || "";
|
|
169
|
+
const normalizedText = text.toLocaleLowerCase();
|
|
170
|
+
let searchFrom = 0;
|
|
171
|
+
|
|
172
|
+
while (searchFrom <= normalizedText.length) {
|
|
173
|
+
const index = normalizedText.indexOf(normalizedQuery, searchFrom);
|
|
174
|
+
if (index === -1) {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
matches.push({
|
|
179
|
+
node,
|
|
180
|
+
start: index,
|
|
181
|
+
end: index + query.length,
|
|
182
|
+
range: createRange(node, index, index + query.length),
|
|
183
|
+
mark: null,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (matches.length >= MAX_MATCHES) {
|
|
187
|
+
return { matches, truncated: true };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
searchFrom = index + Math.max(query.length, 1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { matches, truncated: false };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function ensureStyle() {
|
|
198
|
+
if (document.getElementById(STYLE_ID)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const style = document.createElement("style");
|
|
203
|
+
style.id = STYLE_ID;
|
|
204
|
+
style.textContent = `
|
|
205
|
+
#${PANEL_ID} {
|
|
206
|
+
position: fixed;
|
|
207
|
+
top: 14px;
|
|
208
|
+
right: 14px;
|
|
209
|
+
z-index: 2147483647;
|
|
210
|
+
display: none;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 6px;
|
|
213
|
+
box-sizing: border-box;
|
|
214
|
+
min-width: 278px;
|
|
215
|
+
max-width: min(420px, calc(100vw - 28px));
|
|
216
|
+
padding: 8px;
|
|
217
|
+
border: 1px solid rgba(0, 0, 0, 0.14);
|
|
218
|
+
border-radius: 8px;
|
|
219
|
+
background: rgba(255, 255, 255, 0.96);
|
|
220
|
+
color: #1f2328;
|
|
221
|
+
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.18);
|
|
222
|
+
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
223
|
+
backdrop-filter: blur(16px);
|
|
224
|
+
}
|
|
225
|
+
#${PANEL_ID}[data-visible="true"] {
|
|
226
|
+
display: flex;
|
|
227
|
+
}
|
|
228
|
+
#${PANEL_ID} input {
|
|
229
|
+
min-width: 0;
|
|
230
|
+
flex: 1 1 auto;
|
|
231
|
+
height: 28px;
|
|
232
|
+
box-sizing: border-box;
|
|
233
|
+
border: 1px solid rgba(0, 0, 0, 0.16);
|
|
234
|
+
border-radius: 6px;
|
|
235
|
+
padding: 0 8px;
|
|
236
|
+
background: #fff;
|
|
237
|
+
color: #1f2328;
|
|
238
|
+
font: inherit;
|
|
239
|
+
outline: none;
|
|
240
|
+
}
|
|
241
|
+
#${PANEL_ID} input:focus {
|
|
242
|
+
border-color: #3b82f6;
|
|
243
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.16);
|
|
244
|
+
}
|
|
245
|
+
#${PANEL_ID} [data-pake-find-counter] {
|
|
246
|
+
flex: 0 0 auto;
|
|
247
|
+
min-width: 42px;
|
|
248
|
+
color: #5f6b7a;
|
|
249
|
+
text-align: center;
|
|
250
|
+
font-size: 12px;
|
|
251
|
+
white-space: nowrap;
|
|
252
|
+
}
|
|
253
|
+
#${PANEL_ID} button {
|
|
254
|
+
flex: 0 0 auto;
|
|
255
|
+
width: 28px;
|
|
256
|
+
height: 28px;
|
|
257
|
+
border: 0;
|
|
258
|
+
border-radius: 6px;
|
|
259
|
+
background: transparent;
|
|
260
|
+
color: #30363d;
|
|
261
|
+
font: 15px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
}
|
|
264
|
+
#${PANEL_ID} button:hover {
|
|
265
|
+
background: rgba(0, 0, 0, 0.08);
|
|
266
|
+
}
|
|
267
|
+
#${PANEL_ID} [data-pake-find-status] {
|
|
268
|
+
position: absolute;
|
|
269
|
+
left: 10px;
|
|
270
|
+
top: calc(100% + 4px);
|
|
271
|
+
color: #d1242f;
|
|
272
|
+
font-size: 12px;
|
|
273
|
+
white-space: nowrap;
|
|
274
|
+
}
|
|
275
|
+
@media (prefers-color-scheme: dark) {
|
|
276
|
+
#${PANEL_ID} {
|
|
277
|
+
border-color: rgba(255, 255, 255, 0.16);
|
|
278
|
+
background: rgba(31, 35, 40, 0.94);
|
|
279
|
+
color: #f0f3f6;
|
|
280
|
+
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.36);
|
|
281
|
+
}
|
|
282
|
+
#${PANEL_ID} input {
|
|
283
|
+
border-color: rgba(255, 255, 255, 0.18);
|
|
284
|
+
background: rgba(255, 255, 255, 0.08);
|
|
285
|
+
color: #f0f3f6;
|
|
286
|
+
}
|
|
287
|
+
#${PANEL_ID} [data-pake-find-counter] {
|
|
288
|
+
color: #b7c0cc;
|
|
289
|
+
}
|
|
290
|
+
#${PANEL_ID} button {
|
|
291
|
+
color: #f0f3f6;
|
|
292
|
+
}
|
|
293
|
+
#${PANEL_ID} button:hover {
|
|
294
|
+
background: rgba(255, 255, 255, 0.12);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
::highlight(${MATCH_HIGHLIGHT}) {
|
|
298
|
+
background: rgba(255, 214, 10, 0.58);
|
|
299
|
+
color: inherit;
|
|
300
|
+
}
|
|
301
|
+
::highlight(${ACTIVE_HIGHLIGHT}) {
|
|
302
|
+
background: rgba(255, 149, 0, 0.9);
|
|
303
|
+
color: inherit;
|
|
304
|
+
}
|
|
305
|
+
mark[${MARK_ATTR}] {
|
|
306
|
+
background: rgba(255, 214, 10, 0.58);
|
|
307
|
+
color: inherit;
|
|
308
|
+
padding: 0;
|
|
309
|
+
}
|
|
310
|
+
mark[${MARK_ATTR}][${ACTIVE_ATTR}] {
|
|
311
|
+
background: rgba(255, 149, 0, 0.9);
|
|
312
|
+
}
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
(document.head || document.body || document.documentElement)?.appendChild(
|
|
316
|
+
style,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function createButton(label, title, onClick) {
|
|
321
|
+
const button = document.createElement("button");
|
|
322
|
+
button.type = "button";
|
|
323
|
+
button.textContent = label;
|
|
324
|
+
button.title = title;
|
|
325
|
+
button.setAttribute("aria-label", title);
|
|
326
|
+
button.addEventListener("click", onClick);
|
|
327
|
+
return button;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function ensurePanel() {
|
|
331
|
+
if (state.panel) {
|
|
332
|
+
return state.panel;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
ensureStyle();
|
|
336
|
+
|
|
337
|
+
const panel = document.createElement("div");
|
|
338
|
+
panel.id = PANEL_ID;
|
|
339
|
+
panel.setAttribute("role", "search");
|
|
340
|
+
panel.setAttribute("aria-label", "Find in page");
|
|
341
|
+
|
|
342
|
+
const input = document.createElement("input");
|
|
343
|
+
input.type = "search";
|
|
344
|
+
input.autocomplete = "off";
|
|
345
|
+
input.spellcheck = false;
|
|
346
|
+
input.placeholder = "Find";
|
|
347
|
+
input.setAttribute("aria-label", "Find in page");
|
|
348
|
+
|
|
349
|
+
const counter = document.createElement("span");
|
|
350
|
+
counter.setAttribute("data-pake-find-counter", "");
|
|
351
|
+
counter.textContent = "0/0";
|
|
352
|
+
|
|
353
|
+
const previousButton = createButton("<", "Find Previous", () => previous());
|
|
354
|
+
const nextButton = createButton(">", "Find Next", () => next());
|
|
355
|
+
const closeButton = createButton("x", "Close Find", () => close());
|
|
356
|
+
|
|
357
|
+
const status = document.createElement("span");
|
|
358
|
+
status.setAttribute("data-pake-find-status", "");
|
|
359
|
+
|
|
360
|
+
input.addEventListener("input", () => {
|
|
361
|
+
debounceSearch(input.value);
|
|
362
|
+
});
|
|
363
|
+
input.addEventListener("keydown", (event) => {
|
|
364
|
+
if (event.key === "Enter") {
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
event.stopPropagation();
|
|
367
|
+
if (event.shiftKey) {
|
|
368
|
+
previous();
|
|
369
|
+
} else {
|
|
370
|
+
next();
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (event.key === "Escape") {
|
|
376
|
+
event.preventDefault();
|
|
377
|
+
event.stopPropagation();
|
|
378
|
+
close();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
panel.append(
|
|
383
|
+
input,
|
|
384
|
+
counter,
|
|
385
|
+
previousButton,
|
|
386
|
+
nextButton,
|
|
387
|
+
closeButton,
|
|
388
|
+
status,
|
|
389
|
+
);
|
|
390
|
+
(document.body || document.documentElement).appendChild(panel);
|
|
391
|
+
|
|
392
|
+
state.panel = panel;
|
|
393
|
+
state.input = input;
|
|
394
|
+
state.counter = counter;
|
|
395
|
+
state.status = status;
|
|
396
|
+
|
|
397
|
+
return panel;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function clearCustomHighlights() {
|
|
401
|
+
if (!supportsCustomHighlight()) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
CSS.highlights.delete(MATCH_HIGHLIGHT);
|
|
406
|
+
CSS.highlights.delete(ACTIVE_HIGHLIGHT);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function clearDomMarks() {
|
|
410
|
+
const marks = Array.from(
|
|
411
|
+
document.querySelectorAll?.(`mark[${MARK_ATTR}]`) || state.domMarks,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
for (const mark of marks) {
|
|
415
|
+
const parent = mark.parentNode;
|
|
416
|
+
const text = document.createTextNode(mark.textContent || "");
|
|
417
|
+
mark.replaceWith?.(text);
|
|
418
|
+
parent?.normalize?.();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
state.domMarks = [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function clearHighlights() {
|
|
425
|
+
clearCustomHighlights();
|
|
426
|
+
clearDomMarks();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function applyCustomHighlights() {
|
|
430
|
+
if (!supportsCustomHighlight()) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const ranges = state.matches.map((match) => match.range);
|
|
435
|
+
CSS.highlights.set(MATCH_HIGHLIGHT, new Highlight(...ranges));
|
|
436
|
+
updateActiveHighlight();
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function applyDomHighlights() {
|
|
441
|
+
const grouped = new Map();
|
|
442
|
+
for (const match of state.matches) {
|
|
443
|
+
const nodeMatches = grouped.get(match.node) || [];
|
|
444
|
+
nodeMatches.push(match);
|
|
445
|
+
grouped.set(match.node, nodeMatches);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
for (const nodeMatches of grouped.values()) {
|
|
449
|
+
nodeMatches.sort((a, b) => b.start - a.start);
|
|
450
|
+
for (const match of nodeMatches) {
|
|
451
|
+
try {
|
|
452
|
+
const mark = document.createElement("mark");
|
|
453
|
+
mark.setAttribute(MARK_ATTR, "");
|
|
454
|
+
match.range.surroundContents(mark);
|
|
455
|
+
match.mark = mark;
|
|
456
|
+
state.domMarks.push(mark);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
// Some browser-generated text ranges cannot be wrapped safely.
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
updateDomActiveMark();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function updateDomActiveMark() {
|
|
467
|
+
state.matches.forEach((match, index) => {
|
|
468
|
+
const mark = match.mark;
|
|
469
|
+
if (!mark) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (mark.toggleAttribute) {
|
|
474
|
+
mark.toggleAttribute(ACTIVE_ATTR, index === state.activeIndex);
|
|
475
|
+
} else if (index === state.activeIndex) {
|
|
476
|
+
mark.setAttribute(ACTIVE_ATTR, "");
|
|
477
|
+
} else {
|
|
478
|
+
mark.removeAttribute?.(ACTIVE_ATTR);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function updateActiveHighlight() {
|
|
484
|
+
if (!supportsCustomHighlight()) {
|
|
485
|
+
updateDomActiveMark();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
CSS.highlights.delete(ACTIVE_HIGHLIGHT);
|
|
490
|
+
if (state.activeIndex >= 0 && state.matches[state.activeIndex]) {
|
|
491
|
+
CSS.highlights.set(
|
|
492
|
+
ACTIVE_HIGHLIGHT,
|
|
493
|
+
new Highlight(state.matches[state.activeIndex].range),
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function scrollActiveIntoView() {
|
|
499
|
+
const active = state.matches[state.activeIndex];
|
|
500
|
+
if (!active) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const target = active.mark || active.range.startContainer?.parentElement;
|
|
505
|
+
if (target?.scrollIntoView) {
|
|
506
|
+
target.scrollIntoView({ block: "center", inline: "nearest" });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function updateCounter() {
|
|
511
|
+
if (!state.counter) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const total = state.matches.length;
|
|
516
|
+
const active = state.activeIndex >= 0 ? state.activeIndex + 1 : 0;
|
|
517
|
+
state.counter.textContent = `${active}/${total}${state.truncated ? "+" : ""}`;
|
|
518
|
+
|
|
519
|
+
if (state.status) {
|
|
520
|
+
state.status.textContent = state.query && total === 0 ? "No results" : "";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function runSearch(query = state.query) {
|
|
525
|
+
state.query = query;
|
|
526
|
+
clearHighlights();
|
|
527
|
+
|
|
528
|
+
if (!query) {
|
|
529
|
+
state.matches = [];
|
|
530
|
+
state.activeIndex = -1;
|
|
531
|
+
state.truncated = false;
|
|
532
|
+
updateCounter();
|
|
533
|
+
return getState();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const result = collectMatches(query);
|
|
537
|
+
state.matches = result.matches;
|
|
538
|
+
state.truncated = result.truncated;
|
|
539
|
+
state.activeIndex = state.matches.length > 0 ? 0 : -1;
|
|
540
|
+
|
|
541
|
+
if (!applyCustomHighlights()) {
|
|
542
|
+
applyDomHighlights();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
updateCounter();
|
|
546
|
+
scrollActiveIntoView();
|
|
547
|
+
return getState();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function debounceSearch(query) {
|
|
551
|
+
clearTimeout(state.searchTimer);
|
|
552
|
+
state.searchTimer = setTimeout(() => runSearch(query), SEARCH_DEBOUNCE_MS);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function next() {
|
|
556
|
+
if (!state.query && state.input?.value) {
|
|
557
|
+
runSearch(state.input.value);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (state.matches.length === 0) {
|
|
561
|
+
return getState();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
state.activeIndex = (state.activeIndex + 1) % state.matches.length;
|
|
565
|
+
updateActiveHighlight();
|
|
566
|
+
updateCounter();
|
|
567
|
+
scrollActiveIntoView();
|
|
568
|
+
return getState();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function previous() {
|
|
572
|
+
if (!state.query && state.input?.value) {
|
|
573
|
+
runSearch(state.input.value);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (state.matches.length === 0) {
|
|
577
|
+
return getState();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
state.activeIndex =
|
|
581
|
+
(state.activeIndex - 1 + state.matches.length) % state.matches.length;
|
|
582
|
+
updateActiveHighlight();
|
|
583
|
+
updateCounter();
|
|
584
|
+
scrollActiveIntoView();
|
|
585
|
+
return getState();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function observeDocumentChanges() {
|
|
589
|
+
if (
|
|
590
|
+
state.observer ||
|
|
591
|
+
!document.body ||
|
|
592
|
+
typeof MutationObserver !== "function"
|
|
593
|
+
) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
state.observer = new MutationObserver((mutations) => {
|
|
598
|
+
if (!state.isOpen || !state.query) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (mutations.every((mutation) => isFindPanelNode(mutation.target))) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
debounceSearch(state.query);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
state.observer.observe(document.body, {
|
|
608
|
+
childList: true,
|
|
609
|
+
characterData: true,
|
|
610
|
+
subtree: true,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function stopObservingDocumentChanges() {
|
|
615
|
+
state.observer?.disconnect();
|
|
616
|
+
state.observer = null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function open() {
|
|
620
|
+
if (!state.enabled) {
|
|
621
|
+
return getState();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const panel = ensurePanel();
|
|
625
|
+
panel.setAttribute("data-visible", "true");
|
|
626
|
+
state.isOpen = true;
|
|
627
|
+
observeDocumentChanges();
|
|
628
|
+
|
|
629
|
+
requestAnimationFrame(() => {
|
|
630
|
+
state.input?.focus();
|
|
631
|
+
state.input?.select();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
if (state.input?.value) {
|
|
635
|
+
runSearch(state.input.value);
|
|
636
|
+
} else {
|
|
637
|
+
updateCounter();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return getState();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function close() {
|
|
644
|
+
clearTimeout(state.searchTimer);
|
|
645
|
+
state.isOpen = false;
|
|
646
|
+
state.panel?.removeAttribute("data-visible");
|
|
647
|
+
clearHighlights();
|
|
648
|
+
stopObservingDocumentChanges();
|
|
649
|
+
state.matches = [];
|
|
650
|
+
state.activeIndex = -1;
|
|
651
|
+
state.truncated = false;
|
|
652
|
+
updateCounter();
|
|
653
|
+
return getState();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function search(query) {
|
|
657
|
+
if (state.input) {
|
|
658
|
+
state.input.value = query;
|
|
659
|
+
}
|
|
660
|
+
return runSearch(query);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function getFindShortcutAction(event) {
|
|
664
|
+
const userAgent = navigator.userAgent || "";
|
|
665
|
+
const isMac = /macintosh|mac os x/i.test(userAgent);
|
|
666
|
+
const hasModifier = isMac
|
|
667
|
+
? event.metaKey && !event.ctrlKey
|
|
668
|
+
: event.ctrlKey && !event.metaKey;
|
|
669
|
+
|
|
670
|
+
if (!hasModifier || event.altKey) {
|
|
671
|
+
return "";
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const key = event.key?.toLowerCase();
|
|
675
|
+
if (key === "f" && !event.shiftKey) {
|
|
676
|
+
return "open";
|
|
677
|
+
}
|
|
678
|
+
if (key === "g") {
|
|
679
|
+
return event.shiftKey ? "previous" : "next";
|
|
680
|
+
}
|
|
681
|
+
return "";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function handleFindShortcut(event) {
|
|
685
|
+
const action = getFindShortcutAction(event);
|
|
686
|
+
if (!action) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
event.preventDefault();
|
|
691
|
+
event.stopPropagation();
|
|
692
|
+
window.pakeFind[action]();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
window.pakeFind = {
|
|
696
|
+
open,
|
|
697
|
+
close,
|
|
698
|
+
next,
|
|
699
|
+
previous,
|
|
700
|
+
search,
|
|
701
|
+
getState,
|
|
702
|
+
getFindShortcutAction,
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
if (state.enabled) {
|
|
706
|
+
document.addEventListener("keydown", handleFindShortcut, true);
|
|
707
|
+
}
|
|
708
|
+
})();
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -13,7 +13,8 @@ const WINDOW_SHOW_DELAY: u64 = 50;
|
|
|
13
13
|
|
|
14
14
|
use app::{
|
|
15
15
|
invoke::{
|
|
16
|
-
clear_cache_and_restart, download_file, download_file_by_binary,
|
|
16
|
+
clear_cache_and_restart, clear_dock_badge, download_file, download_file_by_binary,
|
|
17
|
+
increment_dock_badge, send_notification, set_dock_badge, set_dock_badge_label,
|
|
17
18
|
update_theme_mode,
|
|
18
19
|
},
|
|
19
20
|
setup::{set_global_shortcut, set_system_tray},
|
|
@@ -42,6 +43,7 @@ pub fn run_app() {
|
|
|
42
43
|
let start_to_tray = pake_config.windows[0].start_to_tray && show_system_tray; // Only valid when tray is enabled
|
|
43
44
|
let multi_instance = pake_config.multi_instance;
|
|
44
45
|
let multi_window = pake_config.multi_window;
|
|
46
|
+
let enable_find = pake_config.windows[0].enable_find;
|
|
45
47
|
|
|
46
48
|
let window_state_plugin = WindowStatePlugin::default()
|
|
47
49
|
.with_state_flags(if init_fullscreen {
|
|
@@ -81,6 +83,10 @@ pub fn run_app() {
|
|
|
81
83
|
download_file,
|
|
82
84
|
download_file_by_binary,
|
|
83
85
|
send_notification,
|
|
86
|
+
increment_dock_badge,
|
|
87
|
+
set_dock_badge,
|
|
88
|
+
set_dock_badge_label,
|
|
89
|
+
clear_dock_badge,
|
|
84
90
|
update_theme_mode,
|
|
85
91
|
clear_cache_and_restart,
|
|
86
92
|
])
|
|
@@ -93,8 +99,7 @@ pub fn run_app() {
|
|
|
93
99
|
// --- Menu Construction Start ---
|
|
94
100
|
#[cfg(target_os = "macos")]
|
|
95
101
|
{
|
|
96
|
-
|
|
97
|
-
app.set_menu(menu)?;
|
|
102
|
+
app::menu::set_app_menu(app.app_handle(), multi_window, enable_find)?;
|
|
98
103
|
|
|
99
104
|
// Event Handling for Custom Menu Item
|
|
100
105
|
app.on_menu_event(move |app_handle, event| {
|