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 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.5";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pake-cli",
3
- "version": "3.11.5",
3
+ "version": "3.11.6",
4
4
  "description": "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -2194,9 +2194,9 @@ dependencies = [
2194
2194
 
2195
2195
  [[package]]
2196
2196
  name = "muda"
2197
- version = "0.17.1"
2197
+ version = "0.17.2"
2198
2198
  source = "registry+https://github.com/rust-lang/crates.io-index"
2199
- checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
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.5"
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.60.2",
3072
+ "windows-sys 0.52.0",
3070
3073
  ]
3071
3074
 
3072
3075
  [[package]]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pake"
3
- version = "3.11.5"
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 = []
@@ -1,3 +1,5 @@
1
1
  fn main() {
2
+ println!("cargo:rerun-if-changed=.pake/pake.json");
3
+ println!("cargo:rerun-if-changed=.pake/tauri.conf.json");
2
4
  tauri_build::build()
3
5
  }
@@ -20,6 +20,7 @@
20
20
  "start_to_tray": false,
21
21
  "force_internal_navigation": false,
22
22
  "internal_url_regex": "",
23
+ "enable_find": false,
23
24
  "new_window": false
24
25
  }
25
26
  ],
@@ -26,6 +26,8 @@ pub struct WindowConfig {
26
26
  pub force_internal_navigation: bool,
27
27
  #[serde(default)]
28
28
  pub internal_url_regex: String,
29
+ #[serde(default)]
30
+ pub enable_find: bool,
29
31
  #[serde(default = "default_zoom")]
30
32
  pub zoom: u32,
31
33
  #[serde(default)]
@@ -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 get_menu(app: &AppHandle<Wry>, allow_multi_window: bool) -> tauri::Result<Menu<Wry>> {
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
- &window_menu(app)?,
27
+ &window_submenu,
22
28
  &help_menu(app, &pake_menu_item_title)?,
23
29
  ],
24
30
  )?;
25
31
 
26
- Ok(menu)
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>) -> tauri::Result<Submenu<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
- document.addEventListener("DOMContentLoaded", function () {
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
- window.Notification = function (title, options) {
1041
- const { invoke } = window.__TAURI__.core;
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
- invoke("send_notification", { params: { title, body, icon } }).then(() => {
1061
- if (notif.onshow) notif.onshow(new Event("show"));
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
- window.Notification.requestPermission = async () => "granted";
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
+ })();
@@ -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, send_notification,
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
- let menu = app::menu::get_menu(app.app_handle(), multi_window)?;
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| {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "productName": "Weekly",
3
3
  "identifier": "com.pake.weekly",
4
- "version": "3.11.5",
4
+ "version": "3.11.6",
5
5
  "app": {
6
6
  "withGlobalTauri": true,
7
7
  "trayIcon": {