pake-cli 3.11.4 → 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.
@@ -1,28 +1,10 @@
1
- document.addEventListener("DOMContentLoaded", () => {
2
- // Toast
3
- function pakeToast(msg) {
4
- const m = document.createElement("div");
5
- m.innerHTML = msg;
6
- m.style.cssText =
7
- "max-width:60%;min-width: 80px;padding:0 12px;height: 32px;color: rgb(255, 255, 255);line-height: 32px;text-align: center;border-radius: 8px;position: fixed; bottom:24px;right: 28px;z-index: 999999;background: rgba(0, 0, 0,.8);font-size: 13px;";
8
- document.body.appendChild(m);
9
- setTimeout(function () {
10
- const d = 0.5;
11
- m.style.transition =
12
- "transform " + d + "s ease-in, opacity " + d + "s ease-in";
13
- m.style.opacity = "0";
14
- setTimeout(function () {
15
- document.body.removeChild(m);
16
- }, d * 1000);
17
- }, 3000);
18
- }
19
-
20
- window.pakeToast = pakeToast;
21
- });
22
-
23
- // Polyfill for HTML5 Fullscreen API in Tauri webview
24
- // This bridges the HTML5 Fullscreen API to Tauri's native window fullscreen
25
- // Works for all video sites (YouTube, Vimeo, Bilibili, etc.)
1
+ // Polyfill for HTML5 Fullscreen API in Tauri webview.
2
+ // Bridges the standard requestFullscreen / exitFullscreen DOM API to Tauri's
3
+ // native window fullscreen so video sites (YouTube, Vimeo, Bilibili, etc.) can
4
+ // go true fullscreen on their player buttons.
5
+ //
6
+ // Split out from component.js so a future CLI flag (or custom.js override)
7
+ // can short-circuit the polyfill for apps that don't need video fullscreen.
26
8
  (function () {
27
9
  if (window.__PAKE_FULLSCREEN_POLYFILL__) return;
28
10
  window.__PAKE_FULLSCREEN_POLYFILL__ = true;
@@ -42,7 +24,6 @@ document.addEventListener("DOMContentLoaded", () => {
42
24
  let wasInBody = false;
43
25
  let monitorId = null;
44
26
 
45
- // Inject fullscreen styles
46
27
  if (!document.getElementById("pake-fullscreen-style")) {
47
28
  const styleEl = document.createElement("style");
48
29
  styleEl.id = "pake-fullscreen-style";
@@ -93,7 +74,6 @@ document.addEventListener("DOMContentLoaded", () => {
93
74
  monitorId = null;
94
75
  }
95
76
 
96
- // Find the actual video element
97
77
  function findMediaElement() {
98
78
  const videos = document.querySelectorAll("video");
99
79
  if (videos.length > 0) {
@@ -112,11 +92,9 @@ document.addEventListener("DOMContentLoaded", () => {
112
92
  return null;
113
93
  }
114
94
 
115
- // Enter fullscreen
116
95
  function enterFullscreen(element) {
117
96
  fullscreenElement = element;
118
97
 
119
- // If html/body element, find the video instead
120
98
  let targetElement = element;
121
99
  if (element === document.documentElement || element === document.body) {
122
100
  const mediaElement = findMediaElement();
@@ -130,7 +108,6 @@ document.addEventListener("DOMContentLoaded", () => {
130
108
  actualFullscreenElement = element;
131
109
  }
132
110
 
133
- // Save original state
134
111
  originalStyles = {
135
112
  position: targetElement.style.position,
136
113
  top: targetElement.style.top,
@@ -152,7 +129,6 @@ document.addEventListener("DOMContentLoaded", () => {
152
129
  originalNextSibling = targetElement.nextSibling;
153
130
  }
154
131
 
155
- // Apply fullscreen
156
132
  targetElement.classList.add("pake-fullscreen-element");
157
133
  document.body.classList.add("pake-fullscreen-active");
158
134
 
@@ -160,7 +136,6 @@ document.addEventListener("DOMContentLoaded", () => {
160
136
  document.body.appendChild(targetElement);
161
137
  }
162
138
 
163
- // Fullscreen window
164
139
  appWindow.setFullscreen(true).then(() => {
165
140
  startFullscreenMonitor();
166
141
  const event = new Event("fullscreenchange", { bubbles: true });
@@ -177,7 +152,6 @@ document.addEventListener("DOMContentLoaded", () => {
177
152
  return Promise.resolve();
178
153
  }
179
154
 
180
- // Exit fullscreen
181
155
  function exitFullscreen() {
182
156
  if (!fullscreenElement) {
183
157
  return Promise.resolve();
@@ -188,7 +162,6 @@ document.addEventListener("DOMContentLoaded", () => {
188
162
  const exitingElement = fullscreenElement;
189
163
  const targetElement = actualFullscreenElement;
190
164
 
191
- // Restore styles and position
192
165
  targetElement.classList.remove("pake-fullscreen-element");
193
166
  document.body.classList.remove("pake-fullscreen-active");
194
167
 
@@ -209,7 +182,6 @@ document.addEventListener("DOMContentLoaded", () => {
209
182
  }
210
183
  }
211
184
 
212
- // Reset state
213
185
  fullscreenElement = null;
214
186
  actualFullscreenElement = null;
215
187
  originalStyles = null;
@@ -217,7 +189,6 @@ document.addEventListener("DOMContentLoaded", () => {
217
189
  originalNextSibling = null;
218
190
  wasInBody = false;
219
191
 
220
- // Exit window fullscreen
221
192
  return appWindow.setFullscreen(false).then(() => {
222
193
  const event = new Event("fullscreenchange", { bubbles: true });
223
194
  document.dispatchEvent(event);
@@ -231,7 +202,6 @@ document.addEventListener("DOMContentLoaded", () => {
231
202
  });
232
203
  }
233
204
 
234
- // Override fullscreenEnabled
235
205
  Object.defineProperty(document, "fullscreenEnabled", {
236
206
  get: () => true,
237
207
  configurable: true,
@@ -241,7 +211,6 @@ document.addEventListener("DOMContentLoaded", () => {
241
211
  configurable: true,
242
212
  });
243
213
 
244
- // Override fullscreenElement
245
214
  Object.defineProperty(document, "fullscreenElement", {
246
215
  get: () => fullscreenElement,
247
216
  configurable: true,
@@ -255,7 +224,6 @@ document.addEventListener("DOMContentLoaded", () => {
255
224
  configurable: true,
256
225
  });
257
226
 
258
- // Override requestFullscreen
259
227
  Element.prototype.requestFullscreen = function () {
260
228
  return enterFullscreen(this);
261
229
  };
@@ -266,12 +234,10 @@ document.addEventListener("DOMContentLoaded", () => {
266
234
  return enterFullscreen(this);
267
235
  };
268
236
 
269
- // Override exitFullscreen
270
237
  document.exitFullscreen = exitFullscreen;
271
238
  document.webkitExitFullscreen = exitFullscreen;
272
239
  document.webkitCancelFullScreen = exitFullscreen;
273
240
 
274
- // Handle Escape key
275
241
  document.addEventListener(
276
242
  "keydown",
277
243
  (e) => {
@@ -0,0 +1,22 @@
1
+ // Lightweight in-page toast used by Rust `show_toast` (download status, etc).
2
+ // Kept tiny and always loaded so the Rust side can rely on `window.pakeToast`.
3
+ document.addEventListener("DOMContentLoaded", () => {
4
+ function pakeToast(msg) {
5
+ const m = document.createElement("div");
6
+ m.innerHTML = msg;
7
+ m.style.cssText =
8
+ "max-width:60%;min-width: 80px;padding:0 12px;height: 32px;color: rgb(255, 255, 255);line-height: 32px;text-align: center;border-radius: 8px;position: fixed; bottom:24px;right: 28px;z-index: 999999;background: rgba(0, 0, 0,.8);font-size: 13px;";
9
+ document.body.appendChild(m);
10
+ setTimeout(function () {
11
+ const d = 0.5;
12
+ m.style.transition =
13
+ "transform " + d + "s ease-in, opacity " + d + "s ease-in";
14
+ m.style.opacity = "0";
15
+ setTimeout(function () {
16
+ document.body.removeChild(m);
17
+ }, d * 1000);
18
+ }, 3000);
19
+ }
20
+
21
+ window.pakeToast = pakeToast;
22
+ });
@@ -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| {
@@ -103,7 +108,7 @@ pub fn run_app() {
103
108
  }
104
109
  // --- Menu Construction End ---
105
110
 
106
- let window = set_window(app.app_handle(), &pake_config, &tauri_config);
111
+ let window = set_window(app.app_handle(), &pake_config, &tauri_config)?;
107
112
  set_system_tray(
108
113
  app.app_handle(),
109
114
  show_system_tray,
@@ -174,7 +179,10 @@ pub fn run_app() {
174
179
  }
175
180
  })
176
181
  .build(tauri::generate_context!())
177
- .expect("error while building tauri application")
182
+ .unwrap_or_else(|error| {
183
+ eprintln!("[Pake] Fatal error while building Tauri application: {error}");
184
+ std::process::exit(1);
185
+ })
178
186
  .run(|_app, _event| {
179
187
  // Handle macOS dock icon click to reopen hidden window
180
188
  #[cfg(target_os = "macos")]
@@ -23,25 +23,35 @@ pub fn get_pake_config() -> (PakeConfig, Config) {
23
23
  (pake_config, tauri_config)
24
24
  }
25
25
 
26
- pub fn get_data_dir(app: &AppHandle, package_name: String) -> PathBuf {
27
- {
28
- let data_dir = app
29
- .path()
30
- .config_dir()
31
- .expect("Failed to get data dirname")
32
- .join(package_name);
33
-
34
- if !data_dir.exists() {
35
- std::fs::create_dir(&data_dir)
36
- .unwrap_or_else(|_| panic!("Can't create dir {}", data_dir.display()));
37
- }
38
- data_dir
26
+ pub fn get_data_dir(app: &AppHandle, package_name: String) -> std::io::Result<PathBuf> {
27
+ let data_dir = app
28
+ .path()
29
+ .config_dir()
30
+ .map_err(|err| {
31
+ std::io::Error::new(
32
+ std::io::ErrorKind::NotFound,
33
+ format!("Failed to resolve config dir: {err}"),
34
+ )
35
+ })?
36
+ .join(package_name);
37
+
38
+ if !data_dir.exists() {
39
+ std::fs::create_dir_all(&data_dir).map_err(|err| {
40
+ std::io::Error::new(
41
+ err.kind(),
42
+ format!("Can't create dir {}: {err}", data_dir.display()),
43
+ )
44
+ })?;
39
45
  }
46
+
47
+ Ok(data_dir)
40
48
  }
41
49
 
42
50
  pub fn show_toast(window: &WebviewWindow, message: &str) {
43
51
  let script = format!(r#"pakeToast("{message}");"#);
44
- window.eval(&script).unwrap();
52
+ if let Err(error) = window.eval(&script) {
53
+ eprintln!("[Pake] Failed to show toast: {error}");
54
+ }
45
55
  }
46
56
 
47
57
  pub enum MessageType {
@@ -101,10 +111,16 @@ pub fn get_download_message_with_lang(
101
111
  .to_string()
102
112
  }
103
113
 
104
- // Check if the file exists, if it exists, add a number to file name
114
+ /// Check if the file exists. If it does, append `-N` to the stem until a free
115
+ /// path is found.
116
+ ///
117
+ /// Robustness notes:
118
+ /// - Files without an extension are handled (we keep them extensionless).
119
+ /// - If the numeric suffix would overflow `u32::MAX` we fall back to the
120
+ /// original file_path so the caller never enters an infinite loop on
121
+ /// pathologically large filenames (regression guard for #1183).
105
122
  pub fn check_file_or_append(file_path: &str) -> String {
106
123
  let mut new_path = PathBuf::from(file_path);
107
- let mut counter = 0;
108
124
 
109
125
  while new_path.exists() {
110
126
  let file_stem = new_path
@@ -116,16 +132,24 @@ pub fn check_file_or_append(file_path: &str) -> String {
116
132
  .map(|e| e.to_string_lossy().to_string());
117
133
  let parent_dir = new_path.parent().unwrap_or(Path::new(""));
118
134
 
119
- let new_file_stem = match file_stem.rfind('-') {
120
- Some(index) if file_stem[index + 1..].parse::<u32>().is_ok() => {
135
+ let parsed_suffix = file_stem.rfind('-').and_then(|index| {
136
+ file_stem[index + 1..]
137
+ .parse::<u32>()
138
+ .ok()
139
+ .map(|n| (index, n))
140
+ });
141
+
142
+ let new_file_stem = match parsed_suffix {
143
+ Some((index, current)) => {
144
+ let Some(next) = current.checked_add(1) else {
145
+ // u32::MAX collisions are a sign of something pathological;
146
+ // bail with the original path instead of looping forever.
147
+ return file_path.to_string();
148
+ };
121
149
  let base_name = &file_stem[..index];
122
- counter = file_stem[index + 1..].parse::<u32>().unwrap() + 1;
123
- format!("{base_name}-{counter}")
124
- }
125
- _ => {
126
- counter += 1;
127
- format!("{file_stem}-{counter}")
150
+ format!("{base_name}-{next}")
128
151
  }
152
+ None => format!("{file_stem}-1"),
129
153
  };
130
154
 
131
155
  new_path = match &extension {
@@ -136,3 +160,86 @@ pub fn check_file_or_append(file_path: &str) -> String {
136
160
 
137
161
  new_path.to_string_lossy().into_owned()
138
162
  }
163
+
164
+ #[cfg(test)]
165
+ mod tests {
166
+ use super::*;
167
+ use std::env;
168
+ use std::fs;
169
+ use std::path::PathBuf;
170
+
171
+ fn temp_path(name: &str) -> PathBuf {
172
+ let mut dir = env::temp_dir();
173
+ dir.push(format!(
174
+ "pake-util-test-{}-{}",
175
+ std::process::id(),
176
+ std::time::SystemTime::now()
177
+ .duration_since(std::time::UNIX_EPOCH)
178
+ .map(|d| d.as_nanos())
179
+ .unwrap_or(0)
180
+ ));
181
+ fs::create_dir_all(&dir).unwrap();
182
+ dir.push(name);
183
+ dir
184
+ }
185
+
186
+ #[test]
187
+ fn check_file_or_append_returns_input_when_missing() {
188
+ let path = temp_path("ghost.txt");
189
+ let resolved = check_file_or_append(path.to_str().unwrap());
190
+ assert_eq!(resolved, path.to_string_lossy());
191
+ let _ = fs::remove_dir_all(path.parent().unwrap());
192
+ }
193
+
194
+ #[test]
195
+ fn check_file_or_append_increments_suffix() {
196
+ let path = temp_path("dup.txt");
197
+ fs::write(&path, b"existing").unwrap();
198
+ let resolved = check_file_or_append(path.to_str().unwrap());
199
+ assert!(resolved.ends_with("dup-1.txt"), "got {resolved}");
200
+ let _ = fs::remove_dir_all(path.parent().unwrap());
201
+ }
202
+
203
+ #[test]
204
+ fn check_file_or_append_handles_files_without_extension() {
205
+ let path = temp_path("README");
206
+ fs::write(&path, b"existing").unwrap();
207
+ let resolved = check_file_or_append(path.to_str().unwrap());
208
+ assert!(resolved.ends_with("README-1"), "got {resolved}");
209
+ let _ = fs::remove_dir_all(path.parent().unwrap());
210
+ }
211
+
212
+ #[test]
213
+ fn check_file_or_append_does_not_panic_on_huge_suffix() {
214
+ let path = temp_path(&format!("huge-{}.txt", u32::MAX));
215
+ fs::write(&path, b"existing").unwrap();
216
+ let resolved = check_file_or_append(path.to_str().unwrap());
217
+ assert!(resolved.contains("huge-"));
218
+ let _ = fs::remove_dir_all(path.parent().unwrap());
219
+ }
220
+
221
+ #[test]
222
+ fn download_message_falls_back_to_english_for_unknown_locale() {
223
+ let msg = get_download_message_with_lang(MessageType::Start, Some("fr-FR".to_string()));
224
+ assert_eq!(msg, "Start downloading~");
225
+ }
226
+
227
+ #[test]
228
+ fn download_message_picks_chinese_for_zh_locales() {
229
+ for tag in ["zh", "zh-CN", "zh-TW", "en-CN", "en-HK"] {
230
+ let msg = get_download_message_with_lang(MessageType::Success, Some(tag.to_string()));
231
+ assert_eq!(
232
+ msg, "下载成功,已保存到下载目录~",
233
+ "expected Chinese for {tag}"
234
+ );
235
+ }
236
+ }
237
+
238
+ #[test]
239
+ fn download_message_failure_localized() {
240
+ let en = get_download_message_with_lang(MessageType::Failure, Some("en".into()));
241
+ let zh = get_download_message_with_lang(MessageType::Failure, Some("zh".into()));
242
+ assert!(en.contains("Download failed"));
243
+ assert!(zh.contains("下载失败"));
244
+ }
245
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "productName": "Weekly",
3
3
  "identifier": "com.pake.weekly",
4
- "version": "3.11.4",
4
+ "version": "3.11.6",
5
5
  "app": {
6
6
  "withGlobalTauri": true,
7
7
  "trayIcon": {