From 212cd6afb7cce0b24123238ee929058607fce156 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Sat, 23 Aug 2025 14:11:05 +0800 Subject: [PATCH] :sparkles: Support right click to download pictures and open links --- README.md | 5 +- README_CN.md | 5 +- README_JP.md | 5 +- dist/cli.js | 33 ++- src-tauri/src/app/invoke.rs | 30 +- src-tauri/src/inject/event.js | 466 ++++++++++++++++++++------------ src-tauri/src/util.rs | 20 +- src-tauri/tauri.macos.conf.json | 8 - 8 files changed, 367 insertions(+), 205 deletions(-) diff --git a/README.md b/README.md index 14e7020..18fbe75 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ - 🎐 Nearly 20 times smaller than an Electron package (around 5M!) - 🚀 With Rust Tauri, Pake is much more lightweight and faster than JS-based frameworks. - 📦 Battery-included package — shortcut pass-through, immersive windows, and minimalist customization. +- 🖱️ Smart right-click context menus with download support for images, videos, and files. - 👻 Pake is just a simple tool — replaces the old bundle approach with Tauri (though PWA is also a good alternative). ## Popular Packages @@ -526,10 +527,6 @@ Pake's development can not be without these Hackers. They contributed a lot of c -## Frequently Asked Questions - -1. Right-clicking on an image element in the page to open the menu and select download image or other events does not work (common in MacOS systems). This issue is due to the MacOS built-in webview not supporting this feature. - ## Support 1. I have two cats, TangYuan and Coke. If you think Pake delights your life, you can feed them some canned food 🥩. diff --git a/README_CN.md b/README_CN.md index cffcc21..a173fbe 100644 --- a/README_CN.md +++ b/README_CN.md @@ -25,6 +25,7 @@ - 🎐 相比传统的 Electron 套壳打包,要小将近 20 倍,5M 上下。 - 🚀 Pake 的底层使用的 Rust Tauri 框架,性能体验较 JS 框架要轻快不少,内存小很多。 - 📦 不是单纯打包,实现了快捷键透传、沉浸式窗口、拖动、样式改写、去广告、产品极简风格定制。 +- 🖱️ 智能右键菜单,支持图片、视频、文件的下载和操作功能。 - 👻 只是一个很简单的小玩具,用 Tauri 替代之前套壳网页打包的老思路,其实 PWA 也很好。 ## 常用包下载 @@ -525,10 +526,6 @@ Pake 的发展离不开这些 Hacker 们,一起贡献了大量能力,也欢 -## 常见问题 - -1. 页面中对图片元素鼠标右键打开菜单中选择下载图片或者其他事件不生效(常见于 MacOS 系统)。该问题是因为 MacOS 内置的 webview 无法支持该功能。 - ## 支持 1. 我有两只猫,一只叫汤圆,一只叫可乐,假如 Pake 让你生活更美好,可以给汤圆可乐 喂罐头 🥩。 diff --git a/README_JP.md b/README_JP.md index f533da9..be9947f 100644 --- a/README_JP.md +++ b/README_JP.md @@ -26,6 +26,7 @@ - 🎐 Electron パッケージと比較して約 20 倍小さい(約 5M!) - 🚀 Rust Tauri を使用しているため、Pake は JS ベースのフレームワークよりもはるかに軽量で高速です。 - 📦 パッケージにはショートカットの透過、没入型ウィンドウ、ミニマリストのカスタマイズが含まれています。 +- 🖱️ 画像、動画、ファイルのダウンロードをサポートするスマートな右クリックコンテキストメニュー。 - 👻 Pake は単なるシンプルなツールです — Tauri を使用して古いバンドルアプローチを置き換えます(PWA も十分に良い代替手段です)。 ## 人気のパッケージ @@ -526,10 +527,6 @@ Pake の開発はこれらのハッカーたちなしにはあり得ませんで -## よくある質問 - -1. ページ内の画像要素を右クリックしてメニューを開き、「画像をダウンロード」または他のイベントを選択しても機能しない(MacOS システムで一般的)。この問題は、MacOS の組み込み webview がこの機能をサポートしていないためです。 - ## サポート 1. 私には汤圆と可乐という 2 匹の猫がいます。Pake があなたの生活をより良くしてくれると思ったら、缶詰をあげてください 🥩。 diff --git a/dist/cli.js b/dist/cli.js index 5b0a46d..79b15b5 100755 --- a/dist/cli.js +++ b/dist/cli.js @@ -435,9 +435,19 @@ StartupNotify=true tauriConf.bundle.linux.deb.files = { [`/usr/share/applications/${desktopFileName}`]: `assets/${desktopFileName}`, }; - const validTargets = ['deb', 'appimage', 'rpm']; + const validTargets = [ + 'deb', + 'appimage', + 'rpm', + 'deb-arm64', + 'appimage-arm64', + 'rpm-arm64', + ]; + const baseTarget = options.targets.includes('-arm64') + ? options.targets.replace('-arm64', '') + : options.targets; if (validTargets.includes(options.targets)) { - tauriConf.bundle.targets = [options.targets]; + tauriConf.bundle.targets = [baseTarget]; } else { logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`); @@ -792,7 +802,7 @@ class MacBuilder extends BaseBuilder { : 'npm run tauri build --'; const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json'); let fullCommand = `${baseCommand} --target x86_64-apple-darwin -c "${configPath}"`; - // Add features + // Add features const features = ['cli-build']; const macOSVersion = this.getMacOSMajorVersion(); if (macOSVersion >= 23) { @@ -844,8 +854,8 @@ class WinBuilder extends BaseBuilder { else { // Auto-detect based on current architecture if no explicit target const archMap = { - 'x64': 'x64', - 'arm64': 'aarch64', + x64: 'x64', + arm64: 'aarch64', }; targetArch = archMap[process.arch] || process.arch; } @@ -868,7 +878,10 @@ class WinBuilder extends BaseBuilder { } else { // Auto-detect based on current architecture if no explicit target - buildTarget = process.arch === 'arm64' ? 'aarch64-pc-windows-msvc' : 'x86_64-pc-windows-msvc'; + buildTarget = + process.arch === 'arm64' + ? 'aarch64-pc-windows-msvc' + : 'x86_64-pc-windows-msvc'; } fullCommand += ` --target ${buildTarget}`; // Add features @@ -890,7 +903,10 @@ class WinBuilder extends BaseBuilder { } else { // Auto-detect based on current architecture if no explicit target - target = process.arch === 'arm64' ? 'aarch64-pc-windows-msvc' : 'x86_64-pc-windows-msvc'; + target = + process.arch === 'arm64' + ? 'aarch64-pc-windows-msvc' + : 'x86_64-pc-windows-msvc'; } return `src-tauri/target/${target}/${basePath}/bundle/`; } @@ -1436,8 +1452,7 @@ program .addOption(new Option('--user-agent ', 'Custom user agent') .default(DEFAULT_PAKE_OPTIONS.userAgent) .hideHelp()) - .addOption(new Option('--targets ', 'Build target: Linux: "deb", "appimage", "deb-arm64", "appimage-arm64"; Windows: "x64", "arm64"; macOS: "intel", "apple", "universal"') - .default(DEFAULT_PAKE_OPTIONS.targets)) + .addOption(new Option('--targets ', 'Build target: Linux: "deb", "rpm", "appimage", "deb-arm64", "rpm-arm64", "appimage-arm64"; Windows: "x64", "arm64"; macOS: "intel", "apple", "universal"').default(DEFAULT_PAKE_OPTIONS.targets)) .addOption(new Option('--app-version ', 'App version, the same as package.json version') .default(DEFAULT_PAKE_OPTIONS.appVersion) .hideHelp()) diff --git a/src-tauri/src/app/invoke.rs b/src-tauri/src/app/invoke.rs index 646e071..b0815c0 100644 --- a/src-tauri/src/app/invoke.rs +++ b/src-tauri/src/app/invoke.rs @@ -30,7 +30,10 @@ pub struct NotificationParams { #[command] pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> { let window: WebviewWindow = app.get_webview_window("pake").unwrap(); - show_toast(&window, &get_download_message_with_lang(MessageType::Start, params.language.clone())); + show_toast( + &window, + &get_download_message_with_lang(MessageType::Start, params.language.clone()), + ); let output_path = app.path().download_dir().unwrap().join(params.filename); let file_path = check_file_or_append(output_path.to_str().unwrap()); @@ -49,11 +52,17 @@ pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result let mut file = File::create(file_path).unwrap(); file.write_all(&bytes).unwrap(); - show_toast(&window, &get_download_message_with_lang(MessageType::Success, params.language.clone())); + show_toast( + &window, + &get_download_message_with_lang(MessageType::Success, params.language.clone()), + ); Ok(()) } Err(e) => { - show_toast(&window, &get_download_message_with_lang(MessageType::Failure, params.language)); + show_toast( + &window, + &get_download_message_with_lang(MessageType::Failure, params.language), + ); Err(e.to_string()) } } @@ -65,17 +74,26 @@ pub async fn download_file_by_binary( params: BinaryDownloadParams, ) -> Result<(), String> { let window: WebviewWindow = app.get_webview_window("pake").unwrap(); - show_toast(&window, &get_download_message_with_lang(MessageType::Start, params.language.clone())); + show_toast( + &window, + &get_download_message_with_lang(MessageType::Start, params.language.clone()), + ); let output_path = app.path().download_dir().unwrap().join(params.filename); let file_path = check_file_or_append(output_path.to_str().unwrap()); let download_file_result = fs::write(file_path, ¶ms.binary); match download_file_result { Ok(_) => { - show_toast(&window, &get_download_message_with_lang(MessageType::Success, params.language.clone())); + show_toast( + &window, + &get_download_message_with_lang(MessageType::Success, params.language.clone()), + ); Ok(()) } Err(e) => { - show_toast(&window, &get_download_message_with_lang(MessageType::Failure, params.language)); + show_toast( + &window, + &get_download_message_with_lang(MessageType::Failure, params.language), + ); Err(e.to_string()) } } diff --git a/src-tauri/src/inject/event.js b/src-tauri/src/inject/event.js index 0ced10c..03864b4 100644 --- a/src-tauri/src/inject/event.js +++ b/src-tauri/src/inject/event.js @@ -38,18 +38,115 @@ function handleShortcut(event) { // Configuration constants const DOWNLOADABLE_FILE_EXTENSIONS = { - documents: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'odt', 'ods', 'odp', 'pages', 'numbers', 'key', 'epub', 'mobi'], - archives: ['zip', 'rar', '7z', 'tar', 'gz', 'gzip', 'bz2', 'xz', 'lzma', 'deb', 'rpm', 'pkg', 'msi', 'exe', 'dmg', 'apk', 'ipa'], - data: ['json', 'xml', 'csv', 'sql', 'db', 'sqlite', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf', 'log'], - code: ['js', 'ts', 'jsx', 'tsx', 'css', 'scss', 'sass', 'less', 'html', 'htm', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'swift', 'kt', 'scala', 'sh', 'bat', 'ps1'], - fonts: ['ttf', 'otf', 'woff', 'woff2', 'eot'], - design: ['ai', 'psd', 'sketch', 'fig', 'xd'], - system: ['iso', 'img', 'bin', 'torrent', 'jar', 'war', 'indd', 'fla', 'swf', 'raw'] + documents: [ + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + "txt", + "rtf", + "odt", + "ods", + "odp", + "pages", + "numbers", + "key", + "epub", + "mobi", + ], + archives: [ + "zip", + "rar", + "7z", + "tar", + "gz", + "gzip", + "bz2", + "xz", + "lzma", + "deb", + "rpm", + "pkg", + "msi", + "exe", + "dmg", + "apk", + "ipa", + ], + data: [ + "json", + "xml", + "csv", + "sql", + "db", + "sqlite", + "yaml", + "yml", + "toml", + "ini", + "cfg", + "conf", + "log", + ], + code: [ + "js", + "ts", + "jsx", + "tsx", + "css", + "scss", + "sass", + "less", + "html", + "htm", + "php", + "py", + "java", + "cpp", + "c", + "h", + "cs", + "rb", + "go", + "rs", + "swift", + "kt", + "scala", + "sh", + "bat", + "ps1", + ], + fonts: ["ttf", "otf", "woff", "woff2", "eot"], + design: ["ai", "psd", "sketch", "fig", "xd"], + system: [ + "iso", + "img", + "bin", + "torrent", + "jar", + "war", + "indd", + "fla", + "swf", + "raw", + ], }; -const ALL_DOWNLOADABLE_EXTENSIONS = Object.values(DOWNLOADABLE_FILE_EXTENSIONS).flat(); +const ALL_DOWNLOADABLE_EXTENSIONS = Object.values( + DOWNLOADABLE_FILE_EXTENSIONS, +).flat(); -const DOWNLOAD_PATH_PATTERNS = ['/download/', '/files/', '/attachments/', '/assets/', '/releases/', '/dist/']; +const DOWNLOAD_PATH_PATTERNS = [ + "/download/", + "/files/", + "/attachments/", + "/assets/", + "/releases/", + "/dist/", +]; // Language detection utilities function getUserLanguage() { @@ -57,7 +154,13 @@ function getUserLanguage() { } function isChineseLanguage(language = getUserLanguage()) { - return language && (language.startsWith('zh') || language.includes('CN') || language.includes('TW') || language.includes('HK')); + return ( + language && + (language.startsWith("zh") || + language.includes("CN") || + language.includes("TW") || + language.includes("HK")) + ); } // Unified file detection - replaces both isDownloadLink and isFileLink @@ -65,18 +168,20 @@ function isDownloadableFile(url) { try { const urlObj = new URL(url); const pathname = urlObj.pathname.toLowerCase(); - + // Get file extension - const extension = pathname.substring(pathname.lastIndexOf('.') + 1); - + const extension = pathname.substring(pathname.lastIndexOf(".") + 1); + const fileExtensions = ALL_DOWNLOADABLE_EXTENSIONS; - - return fileExtensions.includes(extension) || - // Check for download hints - urlObj.searchParams.has('download') || - urlObj.searchParams.has('attachment') || - // Check for common download paths - DOWNLOAD_PATH_PATTERNS.some(pattern => pathname.includes(pattern)); + + return ( + fileExtensions.includes(extension) || + // Check for download hints + urlObj.searchParams.has("download") || + urlObj.searchParams.has("attachment") || + // Check for common download paths + DOWNLOAD_PATH_PATTERNS.some((pattern) => pathname.includes(pattern)) + ); } catch (e) { return false; } @@ -287,7 +392,9 @@ document.addEventListener("DOMContentLoaded", () => { e.preventDefault(); e.stopImmediatePropagation(); const userLanguage = getUserLanguage(); - invoke("download_file", { params: { url: absoluteUrl, filename, language: userLanguage } }); + invoke("download_file", { + params: { url: absoluteUrl, filename, language: userLanguage }, + }); return; } @@ -363,55 +470,57 @@ document.addEventListener("DOMContentLoaded", () => { document.addEventListener( "keydown", (e) => { - if (e.key === 'Process') e.stopPropagation(); + if (e.key === "Process") e.stopPropagation(); }, true, ); // Language detection and texts const isChinese = isChineseLanguage(); - + const menuTexts = { // Media operations - downloadImage: isChinese ? '下载图片' : 'Download Image', - downloadVideo: isChinese ? '下载视频' : 'Download Video', - downloadFile: isChinese ? '下载文件' : 'Download File', - copyAddress: isChinese ? '复制地址' : 'Copy Address', - openInBrowser: isChinese ? '浏览器打开' : 'Open in Browser' + downloadImage: isChinese ? "下载图片" : "Download Image", + downloadVideo: isChinese ? "下载视频" : "Download Video", + downloadFile: isChinese ? "下载文件" : "Download File", + copyAddress: isChinese ? "复制地址" : "Copy Address", + openInBrowser: isChinese ? "浏览器打开" : "Open in Browser", }; // Menu theme configuration const MENU_THEMES = { dark: { menu: { - background: '#2d2d2d', - border: '1px solid #404040', - color: '#ffffff', - shadow: '0 4px 16px rgba(0, 0, 0, 0.4)' + background: "#2d2d2d", + border: "1px solid #404040", + color: "#ffffff", + shadow: "0 4px 16px rgba(0, 0, 0, 0.4)", }, item: { - divider: '#404040', - hoverBg: '#404040' - } + divider: "#404040", + hoverBg: "#404040", + }, }, light: { menu: { - background: '#ffffff', - border: '1px solid #e0e0e0', - color: '#333333', - shadow: '0 4px 16px rgba(0, 0, 0, 0.15)' + background: "#ffffff", + border: "1px solid #e0e0e0", + color: "#333333", + shadow: "0 4px 16px rgba(0, 0, 0, 0.15)", }, item: { - divider: '#f0f0f0', - hoverBg: '#d0d0d0' - } - } + divider: "#f0f0f0", + hoverBg: "#d0d0d0", + }, + }, }; // Theme detection and menu styles function getTheme() { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - return prefersDark ? 'dark' : 'light'; + const prefersDark = window.matchMedia( + "(prefers-color-scheme: dark)", + ).matches; + return prefersDark ? "dark" : "light"; } function getMenuStyles(theme = getTheme()) { @@ -420,25 +529,26 @@ document.addEventListener("DOMContentLoaded", () => { // Menu configuration constants const MENU_CONFIG = { - id: 'pake-context-menu', - minWidth: '120px', // Compact width for better UX - borderRadius: '6px', // Slightly more rounded for modern look - fontSize: '13px', - zIndex: '999999', - fontFamily: '-apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif', + id: "pake-context-menu", + minWidth: "120px", // Compact width for better UX + borderRadius: "6px", // Slightly more rounded for modern look + fontSize: "13px", + zIndex: "999999", + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", // Menu item dimensions - itemPadding: '8px 16px', // Increased vertical padding for better comfort - itemLineHeight: '1.2', - itemBorderRadius: '3px', // Subtle rounded corners for menu items - itemTransition: 'background-color 0.1s ease' + itemPadding: "8px 16px", // Increased vertical padding for better comfort + itemLineHeight: "1.2", + itemBorderRadius: "3px", // Subtle rounded corners for menu items + itemTransition: "background-color 0.1s ease", }; // Create custom context menu function createContextMenu() { - const contextMenu = document.createElement('div'); + const contextMenu = document.createElement("div"); contextMenu.id = MENU_CONFIG.id; const styles = getMenuStyles(); - + contextMenu.style.cssText = ` position: fixed; background: ${styles.menu.background}; @@ -459,9 +569,9 @@ document.addEventListener("DOMContentLoaded", () => { } function createMenuItem(text, onClick, divider = false) { - const item = document.createElement('div'); + const item = document.createElement("div"); const styles = getMenuStyles(); - + item.style.cssText = ` padding: ${MENU_CONFIG.itemPadding}; cursor: pointer; @@ -472,201 +582,225 @@ document.addEventListener("DOMContentLoaded", () => { white-space: nowrap; border-radius: ${MENU_CONFIG.itemBorderRadius}; margin: 2px 4px; - border-bottom: ${divider ? `1px solid ${styles.item.divider}` : 'none'}; + border-bottom: ${divider ? `1px solid ${styles.item.divider}` : "none"}; `; item.textContent = text; - - item.addEventListener('mouseenter', () => { + + item.addEventListener("mouseenter", () => { item.style.backgroundColor = styles.item.hoverBg; }); - - item.addEventListener('mouseleave', () => { - item.style.backgroundColor = 'transparent'; + + item.addEventListener("mouseleave", () => { + item.style.backgroundColor = "transparent"; }); - - item.addEventListener('click', (e) => { + + item.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); onClick(); hideContextMenu(); }); - + return item; } function showContextMenu(x, y, items) { let contextMenu = document.getElementById(MENU_CONFIG.id); - + // Always recreate menu to ensure theme is up-to-date if (contextMenu) { contextMenu.remove(); } contextMenu = createContextMenu(); - - items.forEach(item => { + + items.forEach((item) => { contextMenu.appendChild(item); }); - - contextMenu.style.left = x + 'px'; - contextMenu.style.top = y + 'px'; - contextMenu.style.display = 'block'; - + + contextMenu.style.left = x + "px"; + contextMenu.style.top = y + "px"; + contextMenu.style.display = "block"; + // Adjust position if menu goes off screen const rect = contextMenu.getBoundingClientRect(); if (rect.right > window.innerWidth) { - contextMenu.style.left = (x - rect.width) + 'px'; + contextMenu.style.left = x - rect.width + "px"; } if (rect.bottom > window.innerHeight) { - contextMenu.style.top = (y - rect.height) + 'px'; + contextMenu.style.top = y - rect.height + "px"; } } function hideContextMenu() { const contextMenu = document.getElementById(MENU_CONFIG.id); if (contextMenu) { - contextMenu.style.display = 'none'; + contextMenu.style.display = "none"; } } function downloadImage(imageUrl) { // Convert relative URLs to absolute - if (imageUrl.startsWith('/')) { + if (imageUrl.startsWith("/")) { imageUrl = window.location.origin + imageUrl; - } else if (imageUrl.startsWith('./')) { + } else if (imageUrl.startsWith("./")) { imageUrl = new URL(imageUrl, window.location.href).href; - } else if (!imageUrl.startsWith('http') && !imageUrl.startsWith('data:') && !imageUrl.startsWith('blob:')) { + } else if ( + !imageUrl.startsWith("http") && + !imageUrl.startsWith("data:") && + !imageUrl.startsWith("blob:") + ) { imageUrl = new URL(imageUrl, window.location.href).href; } - + // Generate filename from URL - const filename = getFilenameFromUrl(imageUrl) || 'image'; - + const filename = getFilenameFromUrl(imageUrl) || "image"; + // Handle different URL types - if (imageUrl.startsWith('data:')) { + if (imageUrl.startsWith("data:")) { downloadFromDataUri(imageUrl, filename); - } else if (imageUrl.startsWith('blob:')) { + } else if (imageUrl.startsWith("blob:")) { if (window.blobToUrlCaches && window.blobToUrlCaches.has(imageUrl)) { downloadFromBlobUrl(imageUrl, filename); } } else { // Regular HTTP(S) image const userLanguage = navigator.language || navigator.userLanguage; - invoke("download_file", { - params: { - url: imageUrl, + invoke("download_file", { + params: { + url: imageUrl, filename: filename, - language: userLanguage - } + language: userLanguage, + }, }); } } - // Check if element is media (image or video) function getMediaInfo(target) { // Check for img tags - if (target.tagName.toLowerCase() === 'img') { - return { isMedia: true, url: target.src, type: 'image' }; + if (target.tagName.toLowerCase() === "img") { + return { isMedia: true, url: target.src, type: "image" }; } - + // Check for video tags - if (target.tagName.toLowerCase() === 'video') { - return { isMedia: true, url: target.src || target.currentSrc, type: 'video' }; + if (target.tagName.toLowerCase() === "video") { + return { + isMedia: true, + url: target.src || target.currentSrc, + type: "video", + }; } - + // Check for elements with background images if (target.style && target.style.backgroundImage) { const bgImage = target.style.backgroundImage; const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/); if (urlMatch) { - return { isMedia: true, url: urlMatch[1], type: 'image' }; + return { isMedia: true, url: urlMatch[1], type: "image" }; } } - + // Check for parent elements with background images const parentWithBg = target.closest('[style*="background-image"]'); if (parentWithBg) { const bgImage = parentWithBg.style.backgroundImage; const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/); if (urlMatch) { - return { isMedia: true, url: urlMatch[1], type: 'image' }; + return { isMedia: true, url: urlMatch[1], type: "image" }; } } - - return { isMedia: false, url: '', type: '' }; + + return { isMedia: false, url: "", type: "" }; } // Simplified menu builder function buildMenuItems(type, data) { const userLanguage = navigator.language || navigator.userLanguage; const items = []; - + switch (type) { - case 'media': - const downloadText = data.type === 'image' ? menuTexts.downloadImage : menuTexts.downloadVideo; + case "media": + const downloadText = + data.type === "image" + ? menuTexts.downloadImage + : menuTexts.downloadVideo; items.push( createMenuItem(downloadText, () => downloadImage(data.url)), - createMenuItem(menuTexts.copyAddress, () => navigator.clipboard.writeText(data.url)), - createMenuItem(menuTexts.openInBrowser, () => invoke("plugin:shell|open", { path: data.url })) + createMenuItem(menuTexts.copyAddress, () => + navigator.clipboard.writeText(data.url), + ), + createMenuItem(menuTexts.openInBrowser, () => + invoke("plugin:shell|open", { path: data.url }), + ), ); break; - - case 'link': + + case "link": if (data.isFile) { - items.push(createMenuItem(menuTexts.downloadFile, () => { - const filename = getFilenameFromUrl(data.url); - invoke("download_file", { - params: { url: data.url, filename, language: userLanguage } - }); - })); + items.push( + createMenuItem(menuTexts.downloadFile, () => { + const filename = getFilenameFromUrl(data.url); + invoke("download_file", { + params: { url: data.url, filename, language: userLanguage }, + }); + }), + ); } items.push( - createMenuItem(menuTexts.copyAddress, () => navigator.clipboard.writeText(data.url)), - createMenuItem(menuTexts.openInBrowser, () => invoke("plugin:shell|open", { path: data.url })) + createMenuItem(menuTexts.copyAddress, () => + navigator.clipboard.writeText(data.url), + ), + createMenuItem(menuTexts.openInBrowser, () => + invoke("plugin:shell|open", { path: data.url }), + ), ); break; } - + return items; } // Handle right-click context menu - document.addEventListener('contextmenu', function(event) { - const target = event.target; - - // Check for media elements (images/videos) - const mediaInfo = getMediaInfo(target); - - // Check for links (but not if it's media) - const linkElement = target.closest('a'); - const isLink = linkElement && linkElement.href && !mediaInfo.isMedia; - - // Only show custom menu for media or links - if (mediaInfo.isMedia || isLink) { - event.preventDefault(); - event.stopPropagation(); - - let menuItems = []; - - if (mediaInfo.isMedia) { - menuItems = buildMenuItems('media', mediaInfo); - } else if (isLink) { - const linkUrl = linkElement.href; - menuItems = buildMenuItems('link', { - url: linkUrl, - isFile: isDownloadableFile(linkUrl) - }); + document.addEventListener( + "contextmenu", + function (event) { + const target = event.target; + + // Check for media elements (images/videos) + const mediaInfo = getMediaInfo(target); + + // Check for links (but not if it's media) + const linkElement = target.closest("a"); + const isLink = linkElement && linkElement.href && !mediaInfo.isMedia; + + // Only show custom menu for media or links + if (mediaInfo.isMedia || isLink) { + event.preventDefault(); + event.stopPropagation(); + + let menuItems = []; + + if (mediaInfo.isMedia) { + menuItems = buildMenuItems("media", mediaInfo); + } else if (isLink) { + const linkUrl = linkElement.href; + menuItems = buildMenuItems("link", { + url: linkUrl, + isFile: isDownloadableFile(linkUrl), + }); + } + + showContextMenu(event.clientX, event.clientY, menuItems); } - - showContextMenu(event.clientX, event.clientY, menuItems); - } - // For all other elements, let browser's default context menu handle it - }, true); + // For all other elements, let browser's default context menu handle it + }, + true, + ); // Hide context menu when clicking elsewhere - document.addEventListener('click', hideContextMenu); - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + document.addEventListener("click", hideContextMenu); + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { hideContextMenu(); } }); @@ -715,37 +849,37 @@ function getFilenameFromUrl(url) { try { const urlPath = new URL(url).pathname; let filename = urlPath.substring(urlPath.lastIndexOf("/") + 1); - + // If no filename or no extension, generate one - if (!filename || !filename.includes('.')) { - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - + if (!filename || !filename.includes(".")) { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + // Detect image type from URL or data URI - if (url.startsWith('data:image/')) { - const mimeType = url.substring(11, url.indexOf(';')); + if (url.startsWith("data:image/")) { + const mimeType = url.substring(11, url.indexOf(";")); filename = `image-${timestamp}.${mimeType}`; } else { // Default to common image extensions based on common patterns - if (url.includes('jpg') || url.includes('jpeg')) { + if (url.includes("jpg") || url.includes("jpeg")) { filename = `image-${timestamp}.jpg`; - } else if (url.includes('png')) { + } else if (url.includes("png")) { filename = `image-${timestamp}.png`; - } else if (url.includes('gif')) { + } else if (url.includes("gif")) { filename = `image-${timestamp}.gif`; - } else if (url.includes('webp')) { + } else if (url.includes("webp")) { filename = `image-${timestamp}.webp`; - } else if (url.includes('svg')) { + } else if (url.includes("svg")) { filename = `image-${timestamp}.svg`; } else { filename = `image-${timestamp}.png`; // default } } } - + return filename; } catch (e) { // Fallback for invalid URLs - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); return `image-${timestamp}.png`; } } diff --git a/src-tauri/src/util.rs b/src-tauri/src/util.rs index 552156b..c506aed 100644 --- a/src-tauri/src/util.rs +++ b/src-tauri/src/util.rs @@ -50,8 +50,10 @@ pub enum MessageType { Failure, } - -pub fn get_download_message_with_lang(message_type: MessageType, language: Option) -> String { +pub fn get_download_message_with_lang( + message_type: MessageType, + language: Option, +) -> String { let default_start_message = "Start downloading~"; let chinese_start_message = "开始下载中~"; @@ -63,13 +65,23 @@ pub fn get_download_message_with_lang(message_type: MessageType, language: Optio let is_chinese = language .as_ref() - .map(|lang| lang.starts_with("zh") || lang.contains("CN") || lang.contains("TW") || lang.contains("HK")) + .map(|lang| { + lang.starts_with("zh") + || lang.contains("CN") + || lang.contains("TW") + || lang.contains("HK") + }) .unwrap_or_else(|| { // Try multiple environment variables for better system detection ["LANG", "LC_ALL", "LC_MESSAGES", "LANGUAGE"] .iter() .find_map(|var| env::var(var).ok()) - .map(|lang| lang.starts_with("zh") || lang.contains("CN") || lang.contains("TW") || lang.contains("HK")) + .map(|lang| { + lang.starts_with("zh") + || lang.contains("CN") + || lang.contains("TW") + || lang.contains("HK") + }) .unwrap_or(false) }); diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index 3f6b75d..d7fe8bd 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -2,14 +2,6 @@ "bundle": { "icon": ["icons/weekly.icns"], "active": true, - "macOS": { - "frameworks": [], - "minimumSystemVersion": "10.13", - "exceptionDomain": "", - "signingIdentity": null, - "hardenedRuntime": true, - "entitlements": null - }, "targets": ["dmg"] } }