✨ Support image download and right-click menu
This commit is contained in:
@@ -47,7 +47,7 @@ grep -r "window_config" src-tauri/src/
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm i
|
npm i
|
||||||
|
|
||||||
# Development with hot reload
|
# Development with hot reload (for testing app functionality)
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# CLI development
|
# CLI development
|
||||||
@@ -88,7 +88,9 @@ npm run build:mac # macOS universal build
|
|||||||
**Testing Notes:**
|
**Testing Notes:**
|
||||||
|
|
||||||
- Do NOT use `PAKE_NO_CONFIG_OVERWRITE=1` - this environment variable is not implemented
|
- Do NOT use `PAKE_NO_CONFIG_OVERWRITE=1` - this environment variable is not implemented
|
||||||
- For testing, simply use: `node dist/cli.js https://example.com --name TestApp --debug`
|
- For CLI testing: `node dist/cli.js https://example.com --name TestApp --debug`
|
||||||
|
- **For app functionality testing**: Use `npm run dev` to start development server with hot reload. This allows real-time testing of injected JavaScript changes without rebuilding the entire app.
|
||||||
|
- The dev server automatically reloads when you modify files in `src-tauri/src/inject/` directory
|
||||||
|
|
||||||
## Core Components
|
## Core Components
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"notification:allow-get-active",
|
"notification:allow-get-active",
|
||||||
"notification:allow-register-listener",
|
"notification:allow-register-listener",
|
||||||
"notification:allow-register-action-types",
|
"notification:allow-register-action-types",
|
||||||
"notification:default"
|
"notification:default",
|
||||||
|
"core:path:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::util::{check_file_or_append, get_download_message, show_toast, MessageType};
|
use crate::util::{check_file_or_append, get_download_message_with_lang, show_toast, MessageType};
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -10,12 +10,14 @@ use tauri_plugin_http::reqwest::{ClientBuilder, Request};
|
|||||||
pub struct DownloadFileParams {
|
pub struct DownloadFileParams {
|
||||||
url: String,
|
url: String,
|
||||||
filename: String,
|
filename: String,
|
||||||
|
language: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct BinaryDownloadParams {
|
pub struct BinaryDownloadParams {
|
||||||
filename: String,
|
filename: String,
|
||||||
binary: Vec<u8>,
|
binary: Vec<u8>,
|
||||||
|
language: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
@@ -28,7 +30,7 @@ pub struct NotificationParams {
|
|||||||
#[command]
|
#[command]
|
||||||
pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> {
|
pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> {
|
||||||
let window: WebviewWindow = app.get_webview_window("pake").unwrap();
|
let window: WebviewWindow = app.get_webview_window("pake").unwrap();
|
||||||
show_toast(&window, &get_download_message(MessageType::Start));
|
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 output_path = app.path().download_dir().unwrap().join(params.filename);
|
||||||
let file_path = check_file_or_append(output_path.to_str().unwrap());
|
let file_path = check_file_or_append(output_path.to_str().unwrap());
|
||||||
@@ -47,11 +49,11 @@ pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result
|
|||||||
|
|
||||||
let mut file = File::create(file_path).unwrap();
|
let mut file = File::create(file_path).unwrap();
|
||||||
file.write_all(&bytes).unwrap();
|
file.write_all(&bytes).unwrap();
|
||||||
show_toast(&window, &get_download_message(MessageType::Success));
|
show_toast(&window, &get_download_message_with_lang(MessageType::Success, params.language.clone()));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
show_toast(&window, &get_download_message(MessageType::Failure));
|
show_toast(&window, &get_download_message_with_lang(MessageType::Failure, params.language));
|
||||||
Err(e.to_string())
|
Err(e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,17 +65,17 @@ pub async fn download_file_by_binary(
|
|||||||
params: BinaryDownloadParams,
|
params: BinaryDownloadParams,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let window: WebviewWindow = app.get_webview_window("pake").unwrap();
|
let window: WebviewWindow = app.get_webview_window("pake").unwrap();
|
||||||
show_toast(&window, &get_download_message(MessageType::Start));
|
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 output_path = app.path().download_dir().unwrap().join(params.filename);
|
||||||
let file_path = check_file_or_append(output_path.to_str().unwrap());
|
let file_path = check_file_or_append(output_path.to_str().unwrap());
|
||||||
let download_file_result = fs::write(file_path, ¶ms.binary);
|
let download_file_result = fs::write(file_path, ¶ms.binary);
|
||||||
match download_file_result {
|
match download_file_result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
show_toast(&window, &get_download_message(MessageType::Success));
|
show_toast(&window, &get_download_message_with_lang(MessageType::Success, params.language.clone()));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
show_toast(&window, &get_download_message(MessageType::Failure));
|
show_toast(&window, &get_download_message_with_lang(MessageType::Failure, params.language));
|
||||||
Err(e.to_string())
|
Err(e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
410
src-tauri/src/inject/event.js
vendored
410
src-tauri/src/inject/event.js
vendored
@@ -36,22 +36,50 @@ function handleShortcut(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Judgment of file download.
|
// Configuration constants
|
||||||
function isDownloadLink(url) {
|
const DOWNLOADABLE_FILE_EXTENSIONS = {
|
||||||
// prettier-ignore
|
documents: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'odt', 'ods', 'odp', 'pages', 'numbers', 'key', 'epub', 'mobi'],
|
||||||
const fileExtensions = [
|
archives: ['zip', 'rar', '7z', 'tar', 'gz', 'gzip', 'bz2', 'xz', 'lzma', 'deb', 'rpm', 'pkg', 'msi', 'exe', 'dmg', 'apk', 'ipa'],
|
||||||
'3gp', '7z', 'ai', 'apk', 'avi', 'bmp', 'csv', 'dmg', 'doc', 'docx',
|
data: ['json', 'xml', 'csv', 'sql', 'db', 'sqlite', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf', 'log'],
|
||||||
'fla', 'flv', 'gif', 'gz', 'gzip', 'ico', 'iso', 'indd', 'jar', 'jpeg',
|
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'],
|
||||||
'jpg', 'm3u8', 'mov', 'mp3', 'mp4', 'mpa', 'mpg', 'mpeg', 'msi', 'odt',
|
fonts: ['ttf', 'otf', 'woff', 'woff2', 'eot'],
|
||||||
'ogg', 'ogv', 'pdf', 'png', 'ppt', 'pptx', 'psd', 'rar', 'raw',
|
design: ['ai', 'psd', 'sketch', 'fig', 'xd'],
|
||||||
'svg', 'swf', 'tar', 'tif', 'tiff', 'ts', 'txt', 'wav', 'webm', 'webp',
|
system: ['iso', 'img', 'bin', 'torrent', 'jar', 'war', 'indd', 'fla', 'swf', 'raw']
|
||||||
'wma', 'wmv', 'xls', 'xlsx', 'xml', 'zip', 'json', 'yaml', '7zip', 'mkv',
|
};
|
||||||
];
|
|
||||||
const downloadLinkPattern = new RegExp(
|
const ALL_DOWNLOADABLE_EXTENSIONS = Object.values(DOWNLOADABLE_FILE_EXTENSIONS).flat();
|
||||||
`\\.(${fileExtensions.join("|")})$`,
|
|
||||||
"i",
|
const DOWNLOAD_PATH_PATTERNS = ['/download/', '/files/', '/attachments/', '/assets/', '/releases/', '/dist/'];
|
||||||
);
|
|
||||||
return downloadLinkPattern.test(url);
|
// Language detection utilities
|
||||||
|
function getUserLanguage() {
|
||||||
|
return navigator.language || navigator.userLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChineseLanguage(language = getUserLanguage()) {
|
||||||
|
return language && (language.startsWith('zh') || language.includes('CN') || language.includes('TW') || language.includes('HK'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unified file detection - replaces both isDownloadLink and isFileLink
|
||||||
|
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 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));
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
@@ -132,20 +160,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// write the ArrayBuffer to a binary, and you're done
|
// write the ArrayBuffer to a binary, and you're done
|
||||||
|
const userLanguage = navigator.language || navigator.userLanguage;
|
||||||
invoke("download_file_by_binary", {
|
invoke("download_file_by_binary", {
|
||||||
params: {
|
params: {
|
||||||
filename,
|
filename,
|
||||||
binary: Array.from(binary),
|
binary: Array.from(binary),
|
||||||
|
language: userLanguage,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFromBlobUrl(blobUrl, filename) {
|
function downloadFromBlobUrl(blobUrl, filename) {
|
||||||
convertBlobUrlToBinary(blobUrl).then((binary) => {
|
convertBlobUrlToBinary(blobUrl).then((binary) => {
|
||||||
|
const userLanguage = navigator.language || navigator.userLanguage;
|
||||||
invoke("download_file_by_binary", {
|
invoke("download_file_by_binary", {
|
||||||
params: {
|
params: {
|
||||||
filename,
|
filename,
|
||||||
binary,
|
binary,
|
||||||
|
language: userLanguage,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -183,7 +215,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
["blob", "data"].some((protocol) => url.startsWith(protocol));
|
["blob", "data"].some((protocol) => url.startsWith(protocol));
|
||||||
|
|
||||||
const isDownloadRequired = (url, anchorElement, e) =>
|
const isDownloadRequired = (url, anchorElement, e) =>
|
||||||
anchorElement.download || e.metaKey || e.ctrlKey || isDownloadLink(url);
|
anchorElement.download || e.metaKey || e.ctrlKey || isDownloadableFile(url);
|
||||||
|
|
||||||
const handleExternalLink = (url) => {
|
const handleExternalLink = (url) => {
|
||||||
invoke("plugin:shell|open", {
|
invoke("plugin:shell|open", {
|
||||||
@@ -254,7 +286,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
invoke("download_file", { params: { url: absoluteUrl, filename } });
|
const userLanguage = getUserLanguage();
|
||||||
|
invoke("download_file", { params: { url: absoluteUrl, filename, language: userLanguage } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,10 +363,313 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"keydown",
|
"keydown",
|
||||||
(e) => {
|
(e) => {
|
||||||
if (e.keyCode === 229) e.stopPropagation();
|
if (e.key === 'Process') e.stopPropagation();
|
||||||
},
|
},
|
||||||
true,
|
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'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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)'
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
divider: '#404040',
|
||||||
|
hoverBg: '#404040'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
menu: {
|
||||||
|
background: '#ffffff',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
color: '#333333',
|
||||||
|
shadow: '0 4px 16px rgba(0, 0, 0, 0.15)'
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
divider: '#f0f0f0',
|
||||||
|
hoverBg: '#d0d0d0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Theme detection and menu styles
|
||||||
|
function getTheme() {
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
return prefersDark ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMenuStyles(theme = getTheme()) {
|
||||||
|
return MENU_THEMES[theme] || MENU_THEMES.light;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
// 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'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create custom context menu
|
||||||
|
function createContextMenu() {
|
||||||
|
const contextMenu = document.createElement('div');
|
||||||
|
contextMenu.id = MENU_CONFIG.id;
|
||||||
|
const styles = getMenuStyles();
|
||||||
|
|
||||||
|
contextMenu.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
background: ${styles.menu.background};
|
||||||
|
border: ${styles.menu.border};
|
||||||
|
border-radius: ${MENU_CONFIG.borderRadius};
|
||||||
|
box-shadow: ${styles.menu.shadow};
|
||||||
|
padding: 4px 0;
|
||||||
|
min-width: ${MENU_CONFIG.minWidth};
|
||||||
|
font-family: ${MENU_CONFIG.fontFamily};
|
||||||
|
font-size: ${MENU_CONFIG.fontSize};
|
||||||
|
color: ${styles.menu.color};
|
||||||
|
z-index: ${MENU_CONFIG.zIndex};
|
||||||
|
display: none;
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
document.body.appendChild(contextMenu);
|
||||||
|
return contextMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMenuItem(text, onClick, divider = false) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
const styles = getMenuStyles();
|
||||||
|
|
||||||
|
item.style.cssText = `
|
||||||
|
padding: ${MENU_CONFIG.itemPadding};
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: ${MENU_CONFIG.itemLineHeight};
|
||||||
|
transition: ${MENU_CONFIG.itemTransition};
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: ${MENU_CONFIG.itemBorderRadius};
|
||||||
|
margin: 2px 4px;
|
||||||
|
border-bottom: ${divider ? `1px solid ${styles.item.divider}` : 'none'};
|
||||||
|
`;
|
||||||
|
item.textContent = text;
|
||||||
|
|
||||||
|
item.addEventListener('mouseenter', () => {
|
||||||
|
item.style.backgroundColor = styles.item.hoverBg;
|
||||||
|
});
|
||||||
|
|
||||||
|
item.addEventListener('mouseleave', () => {
|
||||||
|
item.style.backgroundColor = 'transparent';
|
||||||
|
});
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
contextMenu.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
if (rect.bottom > window.innerHeight) {
|
||||||
|
contextMenu.style.top = (y - rect.height) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideContextMenu() {
|
||||||
|
const contextMenu = document.getElementById(MENU_CONFIG.id);
|
||||||
|
if (contextMenu) {
|
||||||
|
contextMenu.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadImage(imageUrl) {
|
||||||
|
// Convert relative URLs to absolute
|
||||||
|
if (imageUrl.startsWith('/')) {
|
||||||
|
imageUrl = window.location.origin + imageUrl;
|
||||||
|
} else if (imageUrl.startsWith('./')) {
|
||||||
|
imageUrl = new URL(imageUrl, window.location.href).href;
|
||||||
|
} 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';
|
||||||
|
|
||||||
|
// Handle different URL types
|
||||||
|
if (imageUrl.startsWith('data:')) {
|
||||||
|
downloadFromDataUri(imageUrl, filename);
|
||||||
|
} 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,
|
||||||
|
filename: filename,
|
||||||
|
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' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for video tags
|
||||||
|
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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: 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;
|
||||||
|
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 }))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showContextMenu(event.clientX, event.clientY, menuItems);
|
||||||
|
}
|
||||||
|
// 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') {
|
||||||
|
hideContextMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
@@ -376,6 +712,40 @@ function setDefaultZoom() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFilenameFromUrl(url) {
|
function getFilenameFromUrl(url) {
|
||||||
|
try {
|
||||||
const urlPath = new URL(url).pathname;
|
const urlPath = new URL(url).pathname;
|
||||||
return urlPath.substring(urlPath.lastIndexOf("/") + 1);
|
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, '-');
|
||||||
|
|
||||||
|
// Detect image type from URL or data URI
|
||||||
|
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')) {
|
||||||
|
filename = `image-${timestamp}.jpg`;
|
||||||
|
} else if (url.includes('png')) {
|
||||||
|
filename = `image-${timestamp}.png`;
|
||||||
|
} else if (url.includes('gif')) {
|
||||||
|
filename = `image-${timestamp}.gif`;
|
||||||
|
} else if (url.includes('webp')) {
|
||||||
|
filename = `image-${timestamp}.webp`;
|
||||||
|
} 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, '-');
|
||||||
|
return `image-${timestamp}.png`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ pub enum MessageType {
|
|||||||
Failure,
|
Failure,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_download_message(message_type: MessageType) -> String {
|
|
||||||
|
pub fn get_download_message_with_lang(message_type: MessageType, language: Option<String>) -> String {
|
||||||
let default_start_message = "Start downloading~";
|
let default_start_message = "Start downloading~";
|
||||||
let chinese_start_message = "开始下载中~";
|
let chinese_start_message = "开始下载中~";
|
||||||
|
|
||||||
@@ -60,9 +61,19 @@ pub fn get_download_message(message_type: MessageType) -> String {
|
|||||||
let default_failure_message = "Download failed, please check your network connection~";
|
let default_failure_message = "Download failed, please check your network connection~";
|
||||||
let chinese_failure_message = "下载失败,请检查你的网络连接~";
|
let chinese_failure_message = "下载失败,请检查你的网络连接~";
|
||||||
|
|
||||||
env::var("LANG")
|
let is_chinese = language
|
||||||
.map(|lang| {
|
.as_ref()
|
||||||
if lang.starts_with("zh") {
|
.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"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
if is_chinese {
|
||||||
match message_type {
|
match message_type {
|
||||||
MessageType::Start => chinese_start_message,
|
MessageType::Start => chinese_start_message,
|
||||||
MessageType::Success => chinese_success_message,
|
MessageType::Success => chinese_success_message,
|
||||||
@@ -75,12 +86,6 @@ pub fn get_download_message(message_type: MessageType) -> String {
|
|||||||
MessageType::Failure => default_failure_message,
|
MessageType::Failure => default_failure_message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.unwrap_or_else(|_| match message_type {
|
|
||||||
MessageType::Start => default_start_message,
|
|
||||||
MessageType::Success => default_success_message,
|
|
||||||
MessageType::Failure => default_failure_message,
|
|
||||||
})
|
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user