pake-cli 3.2.15 → 3.2.16

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.
@@ -36,22 +36,155 @@ function handleShortcut(event) {
36
36
  }
37
37
  }
38
38
 
39
- // Judgment of file download.
40
- function isDownloadLink(url) {
41
- // prettier-ignore
42
- const fileExtensions = [
43
- '3gp', '7z', 'ai', 'apk', 'avi', 'bmp', 'csv', 'dmg', 'doc', 'docx',
44
- 'fla', 'flv', 'gif', 'gz', 'gzip', 'ico', 'iso', 'indd', 'jar', 'jpeg',
45
- 'jpg', 'm3u8', 'mov', 'mp3', 'mp4', 'mpa', 'mpg', 'mpeg', 'msi', 'odt',
46
- 'ogg', 'ogv', 'pdf', 'png', 'ppt', 'pptx', 'psd', 'rar', 'raw',
47
- 'svg', 'swf', 'tar', 'tif', 'tiff', 'ts', 'txt', 'wav', 'webm', 'webp',
48
- 'wma', 'wmv', 'xls', 'xlsx', 'xml', 'zip', 'json', 'yaml', '7zip', 'mkv',
49
- ];
50
- const downloadLinkPattern = new RegExp(
51
- `\\.(${fileExtensions.join("|")})$`,
52
- "i",
39
+ // Configuration constants
40
+ const DOWNLOADABLE_FILE_EXTENSIONS = {
41
+ documents: [
42
+ "pdf",
43
+ "doc",
44
+ "docx",
45
+ "xls",
46
+ "xlsx",
47
+ "ppt",
48
+ "pptx",
49
+ "txt",
50
+ "rtf",
51
+ "odt",
52
+ "ods",
53
+ "odp",
54
+ "pages",
55
+ "numbers",
56
+ "key",
57
+ "epub",
58
+ "mobi",
59
+ ],
60
+ archives: [
61
+ "zip",
62
+ "rar",
63
+ "7z",
64
+ "tar",
65
+ "gz",
66
+ "gzip",
67
+ "bz2",
68
+ "xz",
69
+ "lzma",
70
+ "deb",
71
+ "rpm",
72
+ "pkg",
73
+ "msi",
74
+ "exe",
75
+ "dmg",
76
+ "apk",
77
+ "ipa",
78
+ ],
79
+ data: [
80
+ "json",
81
+ "xml",
82
+ "csv",
83
+ "sql",
84
+ "db",
85
+ "sqlite",
86
+ "yaml",
87
+ "yml",
88
+ "toml",
89
+ "ini",
90
+ "cfg",
91
+ "conf",
92
+ "log",
93
+ ],
94
+ code: [
95
+ "js",
96
+ "ts",
97
+ "jsx",
98
+ "tsx",
99
+ "css",
100
+ "scss",
101
+ "sass",
102
+ "less",
103
+ "html",
104
+ "htm",
105
+ "php",
106
+ "py",
107
+ "java",
108
+ "cpp",
109
+ "c",
110
+ "h",
111
+ "cs",
112
+ "rb",
113
+ "go",
114
+ "rs",
115
+ "swift",
116
+ "kt",
117
+ "scala",
118
+ "sh",
119
+ "bat",
120
+ "ps1",
121
+ ],
122
+ fonts: ["ttf", "otf", "woff", "woff2", "eot"],
123
+ design: ["ai", "psd", "sketch", "fig", "xd"],
124
+ system: [
125
+ "iso",
126
+ "img",
127
+ "bin",
128
+ "torrent",
129
+ "jar",
130
+ "war",
131
+ "indd",
132
+ "fla",
133
+ "swf",
134
+ "raw",
135
+ ],
136
+ };
137
+
138
+ const ALL_DOWNLOADABLE_EXTENSIONS = Object.values(
139
+ DOWNLOADABLE_FILE_EXTENSIONS,
140
+ ).flat();
141
+
142
+ const DOWNLOAD_PATH_PATTERNS = [
143
+ "/download/",
144
+ "/files/",
145
+ "/attachments/",
146
+ "/assets/",
147
+ "/releases/",
148
+ "/dist/",
149
+ ];
150
+
151
+ // Language detection utilities
152
+ function getUserLanguage() {
153
+ return navigator.language || navigator.userLanguage;
154
+ }
155
+
156
+ function isChineseLanguage(language = getUserLanguage()) {
157
+ return (
158
+ language &&
159
+ (language.startsWith("zh") ||
160
+ language.includes("CN") ||
161
+ language.includes("TW") ||
162
+ language.includes("HK"))
53
163
  );
54
- return downloadLinkPattern.test(url);
164
+ }
165
+
166
+ // Unified file detection - replaces both isDownloadLink and isFileLink
167
+ function isDownloadableFile(url) {
168
+ try {
169
+ const urlObj = new URL(url);
170
+ const pathname = urlObj.pathname.toLowerCase();
171
+
172
+ // Get file extension
173
+ const extension = pathname.substring(pathname.lastIndexOf(".") + 1);
174
+
175
+ const fileExtensions = ALL_DOWNLOADABLE_EXTENSIONS;
176
+
177
+ return (
178
+ fileExtensions.includes(extension) ||
179
+ // Check for download hints
180
+ urlObj.searchParams.has("download") ||
181
+ urlObj.searchParams.has("attachment") ||
182
+ // Check for common download paths
183
+ DOWNLOAD_PATH_PATTERNS.some((pattern) => pathname.includes(pattern))
184
+ );
185
+ } catch (e) {
186
+ return false;
187
+ }
55
188
  }
56
189
 
57
190
  document.addEventListener("DOMContentLoaded", () => {
@@ -132,20 +265,24 @@ document.addEventListener("DOMContentLoaded", () => {
132
265
  }
133
266
 
134
267
  // write the ArrayBuffer to a binary, and you're done
268
+ const userLanguage = navigator.language || navigator.userLanguage;
135
269
  invoke("download_file_by_binary", {
136
270
  params: {
137
271
  filename,
138
272
  binary: Array.from(binary),
273
+ language: userLanguage,
139
274
  },
140
275
  });
141
276
  }
142
277
 
143
278
  function downloadFromBlobUrl(blobUrl, filename) {
144
279
  convertBlobUrlToBinary(blobUrl).then((binary) => {
280
+ const userLanguage = navigator.language || navigator.userLanguage;
145
281
  invoke("download_file_by_binary", {
146
282
  params: {
147
283
  filename,
148
284
  binary,
285
+ language: userLanguage,
149
286
  },
150
287
  });
151
288
  });
@@ -183,7 +320,7 @@ document.addEventListener("DOMContentLoaded", () => {
183
320
  ["blob", "data"].some((protocol) => url.startsWith(protocol));
184
321
 
185
322
  const isDownloadRequired = (url, anchorElement, e) =>
186
- anchorElement.download || e.metaKey || e.ctrlKey || isDownloadLink(url);
323
+ anchorElement.download || e.metaKey || e.ctrlKey || isDownloadableFile(url);
187
324
 
188
325
  const handleExternalLink = (url) => {
189
326
  invoke("plugin:shell|open", {
@@ -254,7 +391,10 @@ document.addEventListener("DOMContentLoaded", () => {
254
391
  ) {
255
392
  e.preventDefault();
256
393
  e.stopImmediatePropagation();
257
- invoke("download_file", { params: { url: absoluteUrl, filename } });
394
+ const userLanguage = getUserLanguage();
395
+ invoke("download_file", {
396
+ params: { url: absoluteUrl, filename, language: userLanguage },
397
+ });
258
398
  return;
259
399
  }
260
400
 
@@ -330,10 +470,340 @@ document.addEventListener("DOMContentLoaded", () => {
330
470
  document.addEventListener(
331
471
  "keydown",
332
472
  (e) => {
333
- if (e.keyCode === 229) e.stopPropagation();
473
+ if (e.key === "Process") e.stopPropagation();
334
474
  },
335
475
  true,
336
476
  );
477
+
478
+ // Language detection and texts
479
+ const isChinese = isChineseLanguage();
480
+
481
+ const menuTexts = {
482
+ // Media operations
483
+ downloadImage: isChinese ? "下载图片" : "Download Image",
484
+ downloadVideo: isChinese ? "下载视频" : "Download Video",
485
+ downloadFile: isChinese ? "下载文件" : "Download File",
486
+ copyAddress: isChinese ? "复制地址" : "Copy Address",
487
+ openInBrowser: isChinese ? "浏览器打开" : "Open in Browser",
488
+ };
489
+
490
+ // Menu theme configuration
491
+ const MENU_THEMES = {
492
+ dark: {
493
+ menu: {
494
+ background: "#2d2d2d",
495
+ border: "1px solid #404040",
496
+ color: "#ffffff",
497
+ shadow: "0 4px 16px rgba(0, 0, 0, 0.4)",
498
+ },
499
+ item: {
500
+ divider: "#404040",
501
+ hoverBg: "#404040",
502
+ },
503
+ },
504
+ light: {
505
+ menu: {
506
+ background: "#ffffff",
507
+ border: "1px solid #e0e0e0",
508
+ color: "#333333",
509
+ shadow: "0 4px 16px rgba(0, 0, 0, 0.15)",
510
+ },
511
+ item: {
512
+ divider: "#f0f0f0",
513
+ hoverBg: "#d0d0d0",
514
+ },
515
+ },
516
+ };
517
+
518
+ // Theme detection and menu styles
519
+ function getTheme() {
520
+ const prefersDark = window.matchMedia(
521
+ "(prefers-color-scheme: dark)",
522
+ ).matches;
523
+ return prefersDark ? "dark" : "light";
524
+ }
525
+
526
+ function getMenuStyles(theme = getTheme()) {
527
+ return MENU_THEMES[theme] || MENU_THEMES.light;
528
+ }
529
+
530
+ // Menu configuration constants
531
+ const MENU_CONFIG = {
532
+ id: "pake-context-menu",
533
+ minWidth: "120px", // Compact width for better UX
534
+ borderRadius: "6px", // Slightly more rounded for modern look
535
+ fontSize: "13px",
536
+ zIndex: "999999",
537
+ fontFamily:
538
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
539
+ // Menu item dimensions
540
+ itemPadding: "8px 16px", // Increased vertical padding for better comfort
541
+ itemLineHeight: "1.2",
542
+ itemBorderRadius: "3px", // Subtle rounded corners for menu items
543
+ itemTransition: "background-color 0.1s ease",
544
+ };
545
+
546
+ // Create custom context menu
547
+ function createContextMenu() {
548
+ const contextMenu = document.createElement("div");
549
+ contextMenu.id = MENU_CONFIG.id;
550
+ const styles = getMenuStyles();
551
+
552
+ contextMenu.style.cssText = `
553
+ position: fixed;
554
+ background: ${styles.menu.background};
555
+ border: ${styles.menu.border};
556
+ border-radius: ${MENU_CONFIG.borderRadius};
557
+ box-shadow: ${styles.menu.shadow};
558
+ padding: 4px 0;
559
+ min-width: ${MENU_CONFIG.minWidth};
560
+ font-family: ${MENU_CONFIG.fontFamily};
561
+ font-size: ${MENU_CONFIG.fontSize};
562
+ color: ${styles.menu.color};
563
+ z-index: ${MENU_CONFIG.zIndex};
564
+ display: none;
565
+ user-select: none;
566
+ `;
567
+ document.body.appendChild(contextMenu);
568
+ return contextMenu;
569
+ }
570
+
571
+ function createMenuItem(text, onClick, divider = false) {
572
+ const item = document.createElement("div");
573
+ const styles = getMenuStyles();
574
+
575
+ item.style.cssText = `
576
+ padding: ${MENU_CONFIG.itemPadding};
577
+ cursor: pointer;
578
+ user-select: none;
579
+ font-weight: 400;
580
+ line-height: ${MENU_CONFIG.itemLineHeight};
581
+ transition: ${MENU_CONFIG.itemTransition};
582
+ white-space: nowrap;
583
+ border-radius: ${MENU_CONFIG.itemBorderRadius};
584
+ margin: 2px 4px;
585
+ border-bottom: ${divider ? `1px solid ${styles.item.divider}` : "none"};
586
+ `;
587
+ item.textContent = text;
588
+
589
+ item.addEventListener("mouseenter", () => {
590
+ item.style.backgroundColor = styles.item.hoverBg;
591
+ });
592
+
593
+ item.addEventListener("mouseleave", () => {
594
+ item.style.backgroundColor = "transparent";
595
+ });
596
+
597
+ item.addEventListener("click", (e) => {
598
+ e.preventDefault();
599
+ e.stopPropagation();
600
+ onClick();
601
+ hideContextMenu();
602
+ });
603
+
604
+ return item;
605
+ }
606
+
607
+ function showContextMenu(x, y, items) {
608
+ let contextMenu = document.getElementById(MENU_CONFIG.id);
609
+
610
+ // Always recreate menu to ensure theme is up-to-date
611
+ if (contextMenu) {
612
+ contextMenu.remove();
613
+ }
614
+ contextMenu = createContextMenu();
615
+
616
+ items.forEach((item) => {
617
+ contextMenu.appendChild(item);
618
+ });
619
+
620
+ contextMenu.style.left = x + "px";
621
+ contextMenu.style.top = y + "px";
622
+ contextMenu.style.display = "block";
623
+
624
+ // Adjust position if menu goes off screen
625
+ const rect = contextMenu.getBoundingClientRect();
626
+ if (rect.right > window.innerWidth) {
627
+ contextMenu.style.left = x - rect.width + "px";
628
+ }
629
+ if (rect.bottom > window.innerHeight) {
630
+ contextMenu.style.top = y - rect.height + "px";
631
+ }
632
+ }
633
+
634
+ function hideContextMenu() {
635
+ const contextMenu = document.getElementById(MENU_CONFIG.id);
636
+ if (contextMenu) {
637
+ contextMenu.style.display = "none";
638
+ }
639
+ }
640
+
641
+ function downloadImage(imageUrl) {
642
+ // Convert relative URLs to absolute
643
+ if (imageUrl.startsWith("/")) {
644
+ imageUrl = window.location.origin + imageUrl;
645
+ } else if (imageUrl.startsWith("./")) {
646
+ imageUrl = new URL(imageUrl, window.location.href).href;
647
+ } else if (
648
+ !imageUrl.startsWith("http") &&
649
+ !imageUrl.startsWith("data:") &&
650
+ !imageUrl.startsWith("blob:")
651
+ ) {
652
+ imageUrl = new URL(imageUrl, window.location.href).href;
653
+ }
654
+
655
+ // Generate filename from URL
656
+ const filename = getFilenameFromUrl(imageUrl) || "image";
657
+
658
+ // Handle different URL types
659
+ if (imageUrl.startsWith("data:")) {
660
+ downloadFromDataUri(imageUrl, filename);
661
+ } else if (imageUrl.startsWith("blob:")) {
662
+ if (window.blobToUrlCaches && window.blobToUrlCaches.has(imageUrl)) {
663
+ downloadFromBlobUrl(imageUrl, filename);
664
+ }
665
+ } else {
666
+ // Regular HTTP(S) image
667
+ const userLanguage = navigator.language || navigator.userLanguage;
668
+ invoke("download_file", {
669
+ params: {
670
+ url: imageUrl,
671
+ filename: filename,
672
+ language: userLanguage,
673
+ },
674
+ });
675
+ }
676
+ }
677
+
678
+ // Check if element is media (image or video)
679
+ function getMediaInfo(target) {
680
+ // Check for img tags
681
+ if (target.tagName.toLowerCase() === "img") {
682
+ return { isMedia: true, url: target.src, type: "image" };
683
+ }
684
+
685
+ // Check for video tags
686
+ if (target.tagName.toLowerCase() === "video") {
687
+ return {
688
+ isMedia: true,
689
+ url: target.src || target.currentSrc,
690
+ type: "video",
691
+ };
692
+ }
693
+
694
+ // Check for elements with background images
695
+ if (target.style && target.style.backgroundImage) {
696
+ const bgImage = target.style.backgroundImage;
697
+ const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
698
+ if (urlMatch) {
699
+ return { isMedia: true, url: urlMatch[1], type: "image" };
700
+ }
701
+ }
702
+
703
+ // Check for parent elements with background images
704
+ const parentWithBg = target.closest('[style*="background-image"]');
705
+ if (parentWithBg) {
706
+ const bgImage = parentWithBg.style.backgroundImage;
707
+ const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
708
+ if (urlMatch) {
709
+ return { isMedia: true, url: urlMatch[1], type: "image" };
710
+ }
711
+ }
712
+
713
+ return { isMedia: false, url: "", type: "" };
714
+ }
715
+
716
+ // Simplified menu builder
717
+ function buildMenuItems(type, data) {
718
+ const userLanguage = navigator.language || navigator.userLanguage;
719
+ const items = [];
720
+
721
+ switch (type) {
722
+ case "media":
723
+ const downloadText =
724
+ data.type === "image"
725
+ ? menuTexts.downloadImage
726
+ : menuTexts.downloadVideo;
727
+ items.push(
728
+ createMenuItem(downloadText, () => downloadImage(data.url)),
729
+ createMenuItem(menuTexts.copyAddress, () =>
730
+ navigator.clipboard.writeText(data.url),
731
+ ),
732
+ createMenuItem(menuTexts.openInBrowser, () =>
733
+ invoke("plugin:shell|open", { path: data.url }),
734
+ ),
735
+ );
736
+ break;
737
+
738
+ case "link":
739
+ if (data.isFile) {
740
+ items.push(
741
+ createMenuItem(menuTexts.downloadFile, () => {
742
+ const filename = getFilenameFromUrl(data.url);
743
+ invoke("download_file", {
744
+ params: { url: data.url, filename, language: userLanguage },
745
+ });
746
+ }),
747
+ );
748
+ }
749
+ items.push(
750
+ createMenuItem(menuTexts.copyAddress, () =>
751
+ navigator.clipboard.writeText(data.url),
752
+ ),
753
+ createMenuItem(menuTexts.openInBrowser, () =>
754
+ invoke("plugin:shell|open", { path: data.url }),
755
+ ),
756
+ );
757
+ break;
758
+ }
759
+
760
+ return items;
761
+ }
762
+
763
+ // Handle right-click context menu
764
+ document.addEventListener(
765
+ "contextmenu",
766
+ function (event) {
767
+ const target = event.target;
768
+
769
+ // Check for media elements (images/videos)
770
+ const mediaInfo = getMediaInfo(target);
771
+
772
+ // Check for links (but not if it's media)
773
+ const linkElement = target.closest("a");
774
+ const isLink = linkElement && linkElement.href && !mediaInfo.isMedia;
775
+
776
+ // Only show custom menu for media or links
777
+ if (mediaInfo.isMedia || isLink) {
778
+ event.preventDefault();
779
+ event.stopPropagation();
780
+
781
+ let menuItems = [];
782
+
783
+ if (mediaInfo.isMedia) {
784
+ menuItems = buildMenuItems("media", mediaInfo);
785
+ } else if (isLink) {
786
+ const linkUrl = linkElement.href;
787
+ menuItems = buildMenuItems("link", {
788
+ url: linkUrl,
789
+ isFile: isDownloadableFile(linkUrl),
790
+ });
791
+ }
792
+
793
+ showContextMenu(event.clientX, event.clientY, menuItems);
794
+ }
795
+ // For all other elements, let browser's default context menu handle it
796
+ },
797
+ true,
798
+ );
799
+
800
+ // Hide context menu when clicking elsewhere
801
+ document.addEventListener("click", hideContextMenu);
802
+ document.addEventListener("keydown", (e) => {
803
+ if (e.key === "Escape") {
804
+ hideContextMenu();
805
+ }
806
+ });
337
807
  });
338
808
 
339
809
  document.addEventListener("DOMContentLoaded", function () {
@@ -376,6 +846,40 @@ function setDefaultZoom() {
376
846
  }
377
847
 
378
848
  function getFilenameFromUrl(url) {
379
- const urlPath = new URL(url).pathname;
380
- return urlPath.substring(urlPath.lastIndexOf("/") + 1);
849
+ try {
850
+ const urlPath = new URL(url).pathname;
851
+ let filename = urlPath.substring(urlPath.lastIndexOf("/") + 1);
852
+
853
+ // If no filename or no extension, generate one
854
+ if (!filename || !filename.includes(".")) {
855
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
856
+
857
+ // Detect image type from URL or data URI
858
+ if (url.startsWith("data:image/")) {
859
+ const mimeType = url.substring(11, url.indexOf(";"));
860
+ filename = `image-${timestamp}.${mimeType}`;
861
+ } else {
862
+ // Default to common image extensions based on common patterns
863
+ if (url.includes("jpg") || url.includes("jpeg")) {
864
+ filename = `image-${timestamp}.jpg`;
865
+ } else if (url.includes("png")) {
866
+ filename = `image-${timestamp}.png`;
867
+ } else if (url.includes("gif")) {
868
+ filename = `image-${timestamp}.gif`;
869
+ } else if (url.includes("webp")) {
870
+ filename = `image-${timestamp}.webp`;
871
+ } else if (url.includes("svg")) {
872
+ filename = `image-${timestamp}.svg`;
873
+ } else {
874
+ filename = `image-${timestamp}.png`; // default
875
+ }
876
+ }
877
+ }
878
+
879
+ return filename;
880
+ } catch (e) {
881
+ // Fallback for invalid URLs
882
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
883
+ return `image-${timestamp}.png`;
884
+ }
381
885
  }
@@ -50,7 +50,10 @@ pub enum MessageType {
50
50
  Failure,
51
51
  }
52
52
 
53
- pub fn get_download_message(message_type: MessageType) -> String {
53
+ pub fn get_download_message_with_lang(
54
+ message_type: MessageType,
55
+ language: Option<String>,
56
+ ) -> String {
54
57
  let default_start_message = "Start downloading~";
55
58
  let chinese_start_message = "开始下载中~";
56
59
 
@@ -60,28 +63,42 @@ pub fn get_download_message(message_type: MessageType) -> String {
60
63
  let default_failure_message = "Download failed, please check your network connection~";
61
64
  let chinese_failure_message = "下载失败,请检查你的网络连接~";
62
65
 
63
- env::var("LANG")
66
+ let is_chinese = language
67
+ .as_ref()
64
68
  .map(|lang| {
65
- if lang.starts_with("zh") {
66
- match message_type {
67
- MessageType::Start => chinese_start_message,
68
- MessageType::Success => chinese_success_message,
69
- MessageType::Failure => chinese_failure_message,
70
- }
71
- } else {
72
- match message_type {
73
- MessageType::Start => default_start_message,
74
- MessageType::Success => default_success_message,
75
- MessageType::Failure => default_failure_message,
76
- }
77
- }
69
+ lang.starts_with("zh")
70
+ || lang.contains("CN")
71
+ || lang.contains("TW")
72
+ || lang.contains("HK")
78
73
  })
79
- .unwrap_or_else(|_| match message_type {
74
+ .unwrap_or_else(|| {
75
+ // Try multiple environment variables for better system detection
76
+ ["LANG", "LC_ALL", "LC_MESSAGES", "LANGUAGE"]
77
+ .iter()
78
+ .find_map(|var| env::var(var).ok())
79
+ .map(|lang| {
80
+ lang.starts_with("zh")
81
+ || lang.contains("CN")
82
+ || lang.contains("TW")
83
+ || lang.contains("HK")
84
+ })
85
+ .unwrap_or(false)
86
+ });
87
+
88
+ if is_chinese {
89
+ match message_type {
90
+ MessageType::Start => chinese_start_message,
91
+ MessageType::Success => chinese_success_message,
92
+ MessageType::Failure => chinese_failure_message,
93
+ }
94
+ } else {
95
+ match message_type {
80
96
  MessageType::Start => default_start_message,
81
97
  MessageType::Success => default_success_message,
82
98
  MessageType::Failure => default_failure_message,
83
- })
84
- .to_string()
99
+ }
100
+ }
101
+ .to_string()
85
102
  }
86
103
 
87
104
  // Check if the file exists, if it exists, add a number to file name
@@ -2,7 +2,6 @@
2
2
  "bundle": {
3
3
  "icon": ["icons/weekly.icns"],
4
4
  "active": true,
5
- "macOS": {},
6
5
  "targets": ["dmg"]
7
6
  }
8
7
  }