✨ Support downloading of blob scenes
This commit is contained in:
179
CLAUDE.md
179
CLAUDE.md
@@ -9,6 +9,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
- **Clear intent over clever code**: Prioritize readability and maintainability
|
- **Clear intent over clever code**: Prioritize readability and maintainability
|
||||||
- **Simple over complex**: Keep all implementations simple and straightforward - prioritize solving problems and ease of maintenance over complex solutions
|
- **Simple over complex**: Keep all implementations simple and straightforward - prioritize solving problems and ease of maintenance over complex solutions
|
||||||
|
|
||||||
|
## Claude Code Eight Honors and Eight Shames
|
||||||
|
|
||||||
|
- **Shame** in guessing APIs, **Honor** in careful research
|
||||||
|
- **Shame** in vague execution, **Honor** in seeking confirmation
|
||||||
|
- **Shame** in assuming business logic, **Honor** in human verification
|
||||||
|
- **Shame** in creating interfaces, **Honor** in reusing existing ones
|
||||||
|
- **Shame** in skipping validation, **Honor** in proactive testing
|
||||||
|
- **Shame** in breaking architecture, **Honor** in following specifications
|
||||||
|
- **Shame** in pretending to understand, **Honor** in honest ignorance
|
||||||
|
- **Shame** in blind modification, **Honor** in careful refactoring
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Pake transforms any webpage into a lightweight desktop app using Rust and Tauri. It's significantly lighter than Electron (~5M vs ~100M+) with better performance.
|
Pake transforms any webpage into a lightweight desktop app using Rust and Tauri. It's significantly lighter than Electron (~5M vs ~100M+) with better performance.
|
||||||
@@ -21,146 +32,42 @@ Pake transforms any webpage into a lightweight desktop app using Rust and Tauri.
|
|||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
### 1. Planning Phase
|
1. **Understand**: Study existing patterns in codebase
|
||||||
|
2. **Plan**: Break complex work into 3-5 stages
|
||||||
|
3. **Test**: Write tests first (when applicable)
|
||||||
|
4. **Implement**: Minimal working solution
|
||||||
|
5. **Refactor**: Optimize and clean up
|
||||||
|
|
||||||
Break complex work into 3-5 stages:
|
**Key Commands:**
|
||||||
|
|
||||||
1. Understand existing patterns in codebase
|
|
||||||
2. Plan implementation approach
|
|
||||||
3. Write tests first (when applicable)
|
|
||||||
4. Implement minimal working solution
|
|
||||||
5. Refactor and optimize
|
|
||||||
|
|
||||||
### 2. Implementation Flow
|
|
||||||
|
|
||||||
**Understanding First:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Explore codebase structure
|
pnpm test # Run comprehensive test suite
|
||||||
find src-tauri/src -name "*.rs" | head -10
|
pnpm run cli:build # Build CLI for testing
|
||||||
grep -r "window_config" src-tauri/src/
|
pnpm run dev # Development with hot reload
|
||||||
```
|
```
|
||||||
|
|
||||||
**Development Commands:**
|
**Testing:**
|
||||||
|
- Always run `pnpm test` before committing
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
pnpm i
|
|
||||||
|
|
||||||
# Development with hot reload (for testing app functionality)
|
|
||||||
pnpm run dev
|
|
||||||
|
|
||||||
# CLI development
|
|
||||||
pnpm run cli:dev
|
|
||||||
|
|
||||||
# Production build
|
|
||||||
pnpm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Testing and Validation
|
|
||||||
|
|
||||||
**Key Testing Commands:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run comprehensive test suite (unit + integration + builder)
|
|
||||||
pnpm test
|
|
||||||
|
|
||||||
# Build CLI for testing
|
|
||||||
pnpm run cli:build
|
|
||||||
|
|
||||||
# Debug build for development
|
|
||||||
pnpm run build:debug
|
|
||||||
|
|
||||||
# Multi-platform testing
|
|
||||||
pnpm run build:mac # macOS universal build
|
|
||||||
```
|
|
||||||
|
|
||||||
**Testing Checklist:**
|
|
||||||
|
|
||||||
- [ ] Run `npm test` for comprehensive validation (35 tests)
|
|
||||||
- [ ] Test on target platforms
|
|
||||||
- [ ] Verify injection system works
|
|
||||||
- [ ] Check system tray integration
|
|
||||||
- [ ] Validate window behavior
|
|
||||||
- [ ] Test with weekly.tw93.fun URL
|
|
||||||
- [ ] Verify remote icon functionality (https://cdn.tw93.fun/pake/weekly.icns)
|
|
||||||
|
|
||||||
**Testing Notes:**
|
|
||||||
|
|
||||||
- Do NOT use `PAKE_NO_CONFIG_OVERWRITE=1` - this environment variable is not implemented
|
|
||||||
- For CLI testing: `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 `pnpm run dev` to start development server with hot reload. This allows real-time testing of injected JavaScript changes without rebuilding the entire app.
|
- For app functionality testing: Use `pnpm run dev` for hot reload
|
||||||
- The dev server automatically reloads when you modify files in `src-tauri/src/inject/` directory
|
|
||||||
|
|
||||||
## Core Components
|
## Core Components
|
||||||
|
|
||||||
### CLI Tool (`bin/`)
|
- **CLI Tool** (`bin/`): Main entry point, builders, options processing
|
||||||
|
- **Tauri App** (`src-tauri/`): Rust application, window/tray management, injection logic
|
||||||
- `bin/cli.ts` - Main entry point with Commander.js
|
- **Config Files**: `pake.json`, `tauri.conf.json`, platform-specific configs
|
||||||
- `bin/builders/` - Platform-specific builders (Mac, Windows, Linux)
|
|
||||||
- `bin/options/` - CLI option processing and validation
|
|
||||||
- `bin/helpers/merge.ts` - Configuration merging (name setting at line 55)
|
|
||||||
|
|
||||||
### Tauri Application (`src-tauri/`)
|
|
||||||
|
|
||||||
- `src/lib.rs` - Application entry point
|
|
||||||
- `src/app/` - Core modules (window, tray, shortcuts)
|
|
||||||
- `src/inject/` - Web page injection logic
|
|
||||||
|
|
||||||
## Documentation Guidelines
|
## Documentation Guidelines
|
||||||
|
|
||||||
- **Main README**: Only include common, frequently-used parameters to avoid clutter
|
- **Main README**: Common parameters only
|
||||||
- **CLI Documentation** (`docs/cli-usage.md`): Include ALL parameters with detailed usage examples
|
- **CLI Documentation** (`docs/cli-usage.md`): ALL parameters with examples
|
||||||
- **Rare/Advanced Parameters**: Should have full documentation in CLI docs but minimal/no mention in main README
|
- **Rare parameters**: Full docs in CLI usage, minimal in main README
|
||||||
- **Examples of rare parameters**: `--title`, `--incognito`, `--system-tray-icon`, etc.
|
|
||||||
|
|
||||||
### Key Configuration Files
|
## Platform Specifics
|
||||||
|
|
||||||
- `pake.json` - App configuration
|
- **macOS**: `.icns` icons, universal builds with `--multi-arch`
|
||||||
- `tauri.conf.json` - Tauri settings
|
- **Windows**: `.ico` icons, requires Visual Studio Build Tools
|
||||||
- Platform configs: `tauri.{macos,windows,linux}.conf.json`
|
- **Linux**: `.png` icons, multiple formats (deb, AppImage, rpm)
|
||||||
|
|
||||||
## Problem-Solving Approach
|
|
||||||
|
|
||||||
**When stuck:**
|
|
||||||
|
|
||||||
1. **Limit attempts to 3** before stopping to reassess
|
|
||||||
2. **Document what doesn't work** and why
|
|
||||||
3. **Research alternative approaches** in similar projects
|
|
||||||
4. **Question assumptions** - is there a simpler way?
|
|
||||||
|
|
||||||
**Example debugging flow:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Check logs
|
|
||||||
pnpm run dev 2>&1 | grep -i error
|
|
||||||
|
|
||||||
# 2. Verify dependencies
|
|
||||||
cargo check --manifest-path=src-tauri/Cargo.toml
|
|
||||||
|
|
||||||
# 3. Test minimal reproduction
|
|
||||||
# Create simple test case isolating the issue
|
|
||||||
```
|
|
||||||
|
|
||||||
## Platform-Specific Development
|
|
||||||
|
|
||||||
### macOS
|
|
||||||
|
|
||||||
- Universal builds: `--multi-arch` flag
|
|
||||||
- Uses `.icns` icons
|
|
||||||
- Title bar customization available
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
- Requires Visual Studio Build Tools
|
|
||||||
- Uses `.ico` icons
|
|
||||||
- MSI installer support
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
- Multiple formats: deb, AppImage, rpm
|
|
||||||
- Requires `libwebkit2gtk` and dependencies
|
|
||||||
- Uses `.png` icons
|
|
||||||
|
|
||||||
## Quality Standards
|
## Quality Standards
|
||||||
|
|
||||||
@@ -170,15 +77,15 @@ cargo check --manifest-path=src-tauri/Cargo.toml
|
|||||||
- Use explicit types over implicit
|
- Use explicit types over implicit
|
||||||
- Write self-documenting code
|
- Write self-documenting code
|
||||||
- Follow existing patterns consistently
|
- Follow existing patterns consistently
|
||||||
|
- **NO Chinese comments** - Use English only
|
||||||
|
- **NO unnecessary comments** - For simple, obvious code, let the code speak for itself
|
||||||
|
|
||||||
**Git and Commit Guidelines:**
|
**Git Guidelines:**
|
||||||
|
|
||||||
- **NEVER commit code automatically** - User handles all git operations
|
- **NEVER commit automatically** - User handles all git operations
|
||||||
- **NEVER generate commit messages** - User writes their own commit messages
|
- **NEVER generate commit messages** - User writes their own
|
||||||
- **NEVER run npm publish automatically** - Always remind user to run it manually
|
- Only make code changes, user decides when/how to commit
|
||||||
- **NEVER execute git tag or push operations** - User handles all tag creation and GitHub pushes
|
- Always test before user commits
|
||||||
- Only make code changes, user decides when and how to commit
|
|
||||||
- Test before user commits
|
|
||||||
|
|
||||||
## Branch Strategy
|
## Branch Strategy
|
||||||
|
|
||||||
@@ -187,6 +94,6 @@ cargo check --manifest-path=src-tauri/Cargo.toml
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Node.js ≥22.0.0 (recommended LTS, older versions ≥18.0.0 may work)
|
- Node.js ≥22.0.0 (≥18.0.0 may work)
|
||||||
- Rust ≥1.89.0 (recommended stable, older versions ≥1.78.0 may work)
|
- Rust ≥1.89.0 (≥1.78.0 may work)
|
||||||
- Platform build tools (see CONTRIBUTING.md for details)
|
- Platform build tools (see CONTRIBUTING.md)
|
||||||
|
|||||||
80
dist/cli.js
vendored
80
dist/cli.js
vendored
@@ -328,6 +328,26 @@ async function combineFiles(files, output) {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateSafeFilename(name) {
|
||||||
|
return name
|
||||||
|
.replace(/[<>:"/\\|?*]/g, '_')
|
||||||
|
.replace(/\s+/g, '_')
|
||||||
|
.replace(/\.+$/g, '')
|
||||||
|
.slice(0, 255);
|
||||||
|
}
|
||||||
|
function generateLinuxPackageName(name) {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '')
|
||||||
|
.replace(/-+/g, '-');
|
||||||
|
}
|
||||||
|
function generateIdentifierSafeName(name) {
|
||||||
|
return name
|
||||||
|
.replace(/[^a-zA-Z0-9]/g, '')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
async function mergeConfig(url, options, tauriConf) {
|
async function mergeConfig(url, options, tauriConf) {
|
||||||
// Ensure .pake directory exists and copy source templates if needed
|
// Ensure .pake directory exists and copy source templates if needed
|
||||||
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
|
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
|
||||||
@@ -372,7 +392,7 @@ async function mergeConfig(url, options, tauriConf) {
|
|||||||
tauriConf.identifier = identifier;
|
tauriConf.identifier = identifier;
|
||||||
tauriConf.version = appVersion;
|
tauriConf.version = appVersion;
|
||||||
if (platform === 'linux') {
|
if (platform === 'linux') {
|
||||||
tauriConf.mainBinaryName = `pake-${name.toLowerCase()}`;
|
tauriConf.mainBinaryName = `pake-${generateIdentifierSafeName(name)}`;
|
||||||
}
|
}
|
||||||
if (platform == 'win32') {
|
if (platform == 'win32') {
|
||||||
tauriConf.bundle.windows.wix.language[0] = installerLanguage;
|
tauriConf.bundle.windows.wix.language[0] = installerLanguage;
|
||||||
@@ -418,8 +438,8 @@ async function mergeConfig(url, options, tauriConf) {
|
|||||||
// Remove hardcoded desktop files and regenerate with correct app name
|
// Remove hardcoded desktop files and regenerate with correct app name
|
||||||
delete tauriConf.bundle.linux.deb.files;
|
delete tauriConf.bundle.linux.deb.files;
|
||||||
// Generate correct desktop file configuration
|
// Generate correct desktop file configuration
|
||||||
const appNameLower = name.toLowerCase();
|
const appNameSafe = generateSafeFilename(name).toLowerCase();
|
||||||
const identifier = `com.pake.${appNameLower}`;
|
const identifier = `com.pake.${appNameSafe}`;
|
||||||
const desktopFileName = `${identifier}.desktop`;
|
const desktopFileName = `${identifier}.desktop`;
|
||||||
// Create desktop file content
|
// Create desktop file content
|
||||||
const desktopContent = `[Desktop Entry]
|
const desktopContent = `[Desktop Entry]
|
||||||
@@ -427,8 +447,8 @@ Version=1.0
|
|||||||
Type=Application
|
Type=Application
|
||||||
Name=${name}
|
Name=${name}
|
||||||
Comment=${name}
|
Comment=${name}
|
||||||
Exec=pake-${appNameLower}
|
Exec=pake-${appNameSafe}
|
||||||
Icon=${appNameLower}_512
|
Icon=${appNameSafe}_512
|
||||||
Categories=Network;WebBrowser;
|
Categories=Network;WebBrowser;
|
||||||
MimeType=text/html;text/xml;application/xhtml_xml;
|
MimeType=text/html;text/xml;application/xhtml_xml;
|
||||||
StartupNotify=true
|
StartupNotify=true
|
||||||
@@ -472,28 +492,29 @@ StartupNotify=true
|
|||||||
const platformIconMap = {
|
const platformIconMap = {
|
||||||
win32: {
|
win32: {
|
||||||
fileExt: '.ico',
|
fileExt: '.ico',
|
||||||
path: `png/${name.toLowerCase()}_256.ico`,
|
path: `png/${generateSafeFilename(name).toLowerCase()}_256.ico`,
|
||||||
defaultIcon: 'png/icon_256.ico',
|
defaultIcon: 'png/icon_256.ico',
|
||||||
message: 'Windows icon must be .ico and 256x256px.',
|
message: 'Windows icon must be .ico and 256x256px.',
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
fileExt: '.png',
|
fileExt: '.png',
|
||||||
path: `png/${name.toLowerCase()}_512.png`,
|
path: `png/${generateSafeFilename(name).toLowerCase()}_512.png`,
|
||||||
defaultIcon: 'png/icon_512.png',
|
defaultIcon: 'png/icon_512.png',
|
||||||
message: 'Linux icon must be .png and 512x512px.',
|
message: 'Linux icon must be .png and 512x512px.',
|
||||||
},
|
},
|
||||||
darwin: {
|
darwin: {
|
||||||
fileExt: '.icns',
|
fileExt: '.icns',
|
||||||
path: `icons/${name.toLowerCase()}.icns`,
|
path: `icons/${generateSafeFilename(name).toLowerCase()}.icns`,
|
||||||
defaultIcon: 'icons/icon.icns',
|
defaultIcon: 'icons/icon.icns',
|
||||||
message: 'macOS icon must be .icns type.',
|
message: 'macOS icon must be .icns type.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const iconInfo = platformIconMap[platform];
|
const iconInfo = platformIconMap[platform];
|
||||||
const exists = options.icon && (await fsExtra.pathExists(options.icon));
|
const resolvedIconPath = options.icon ? path.resolve(options.icon) : null;
|
||||||
|
const exists = resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));
|
||||||
if (exists) {
|
if (exists) {
|
||||||
let updateIconPath = true;
|
let updateIconPath = true;
|
||||||
let customIconExt = path.extname(options.icon).toLowerCase();
|
let customIconExt = path.extname(resolvedIconPath).toLowerCase();
|
||||||
if (customIconExt !== iconInfo.fileExt) {
|
if (customIconExt !== iconInfo.fileExt) {
|
||||||
updateIconPath = false;
|
updateIconPath = false;
|
||||||
logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`);
|
logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`);
|
||||||
@@ -503,10 +524,9 @@ StartupNotify=true
|
|||||||
const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
|
const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
|
||||||
tauriConf.bundle.resources = [iconInfo.path];
|
tauriConf.bundle.resources = [iconInfo.path];
|
||||||
// Avoid copying if source and destination are the same
|
// Avoid copying if source and destination are the same
|
||||||
const absoluteIconPath = path.resolve(options.icon);
|
|
||||||
const absoluteDestPath = path.resolve(iconPath);
|
const absoluteDestPath = path.resolve(iconPath);
|
||||||
if (absoluteIconPath !== absoluteDestPath) {
|
if (resolvedIconPath !== absoluteDestPath) {
|
||||||
await fsExtra.copy(options.icon, iconPath);
|
await fsExtra.copy(resolvedIconPath, iconPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (updateIconPath) {
|
if (updateIconPath) {
|
||||||
@@ -528,8 +548,8 @@ StartupNotify=true
|
|||||||
// 需要判断图标格式,默认只支持ico和png两种
|
// 需要判断图标格式,默认只支持ico和png两种
|
||||||
let iconExt = path.extname(systemTrayIcon).toLowerCase();
|
let iconExt = path.extname(systemTrayIcon).toLowerCase();
|
||||||
if (iconExt == '.png' || iconExt == '.ico') {
|
if (iconExt == '.png' || iconExt == '.ico') {
|
||||||
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}${iconExt}`);
|
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${generateSafeFilename(name).toLowerCase()}${iconExt}`);
|
||||||
trayIconPath = `png/${name.toLowerCase()}${iconExt}`;
|
trayIconPath = `png/${generateSafeFilename(name).toLowerCase()}${iconExt}`;
|
||||||
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -854,7 +874,7 @@ class BaseBuilder {
|
|||||||
const extension = process.platform === 'win32' ? '.exe' : '';
|
const extension = process.platform === 'win32' ? '.exe' : '';
|
||||||
// Linux uses the unique binary name we set in merge.ts
|
// Linux uses the unique binary name we set in merge.ts
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
return `pake-${appName.toLowerCase()}${extension}`;
|
return `pake-${generateIdentifierSafeName(appName)}${extension}`;
|
||||||
}
|
}
|
||||||
// Windows and macOS use 'pake' as binary name
|
// Windows and macOS use 'pake' as binary name
|
||||||
return `pake${extension}`;
|
return `pake${extension}`;
|
||||||
@@ -1169,12 +1189,9 @@ const API_KEYS = {
|
|||||||
logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
|
logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
|
||||||
brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
|
brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Generates platform-specific icon paths and handles copying for Windows
|
|
||||||
*/
|
|
||||||
function generateIconPath(appName, isDefault = false) {
|
function generateIconPath(appName, isDefault = false) {
|
||||||
const safeName = appName.toLowerCase().replace(/[^a-z0-9-_]/g, '_');
|
const safeName = isDefault ? 'icon' : generateSafeFilename(appName).toLowerCase();
|
||||||
const baseName = isDefault ? 'icon' : safeName;
|
const baseName = safeName;
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_256.ico`);
|
return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_256.ico`);
|
||||||
}
|
}
|
||||||
@@ -1237,7 +1254,7 @@ async function convertIconFormat(inputPath, appName) {
|
|||||||
const platformOutputDir = path.join(outputDir, 'converted-icons');
|
const platformOutputDir = path.join(outputDir, 'converted-icons');
|
||||||
await fsExtra.ensureDir(platformOutputDir);
|
await fsExtra.ensureDir(platformOutputDir);
|
||||||
const processedInputPath = await preprocessIcon(inputPath);
|
const processedInputPath = await preprocessIcon(inputPath);
|
||||||
const iconName = appName.toLowerCase();
|
const iconName = generateSafeFilename(appName).toLowerCase();
|
||||||
// Generate platform-specific format
|
// Generate platform-specific format
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
// Support multiple sizes for better Windows compatibility
|
// Support multiple sizes for better Windows compatibility
|
||||||
@@ -1513,8 +1530,8 @@ function resolveAppName(name, platform) {
|
|||||||
}
|
}
|
||||||
function isValidName(name, platform) {
|
function isValidName(name, platform) {
|
||||||
const platformRegexMapping = {
|
const platformRegexMapping = {
|
||||||
linux: /^[a-z0-9][a-z0-9-]*$/,
|
linux: /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/,
|
||||||
default: /^[a-zA-Z0-9][a-zA-Z0-9- ]*$/,
|
default: /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff- ]*$/,
|
||||||
};
|
};
|
||||||
const reg = platformRegexMapping[platform] || platformRegexMapping.default;
|
const reg = platformRegexMapping[platform] || platformRegexMapping.default;
|
||||||
return !!name && reg.test(name);
|
return !!name && reg.test(name);
|
||||||
@@ -1530,10 +1547,8 @@ async function handleOptions(options, url) {
|
|||||||
const namePrompt = await promptText(promptMessage, defaultName);
|
const namePrompt = await promptText(promptMessage, defaultName);
|
||||||
name = namePrompt || defaultName;
|
name = namePrompt || defaultName;
|
||||||
}
|
}
|
||||||
// Handle platform-specific name formatting
|
|
||||||
if (name && platform === 'linux') {
|
if (name && platform === 'linux') {
|
||||||
// Convert to lowercase and replace spaces with dashes for Linux
|
name = generateLinuxPackageName(name);
|
||||||
name = name.toLowerCase().replace(/\s+/g, '-');
|
|
||||||
}
|
}
|
||||||
if (!isValidName(name, platform)) {
|
if (!isValidName(name, platform)) {
|
||||||
const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
|
const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
|
||||||
@@ -1643,8 +1658,17 @@ program
|
|||||||
.addOption(new Option('--system-tray-icon <string>', 'Custom system tray icon')
|
.addOption(new Option('--system-tray-icon <string>', 'Custom system tray icon')
|
||||||
.default(DEFAULT_PAKE_OPTIONS.systemTrayIcon)
|
.default(DEFAULT_PAKE_OPTIONS.systemTrayIcon)
|
||||||
.hideHelp())
|
.hideHelp())
|
||||||
.addOption(new Option('--hide-on-close', 'Hide window on close instead of exiting (default: true for macOS, false for others)')
|
.addOption(new Option('--hide-on-close [boolean]', 'Hide window on close instead of exiting (default: true for macOS, false for others)')
|
||||||
.default(DEFAULT_PAKE_OPTIONS.hideOnClose)
|
.default(DEFAULT_PAKE_OPTIONS.hideOnClose)
|
||||||
|
.argParser((value) => {
|
||||||
|
if (value === undefined)
|
||||||
|
return true; // --hide-on-close without value
|
||||||
|
if (value === 'true')
|
||||||
|
return true;
|
||||||
|
if (value === 'false')
|
||||||
|
return false;
|
||||||
|
throw new Error('--hide-on-close must be true or false');
|
||||||
|
})
|
||||||
.hideHelp())
|
.hideHelp())
|
||||||
.addOption(new Option('--title <string>', 'Window title').hideHelp())
|
.addOption(new Option('--title <string>', 'Window title').hideHelp())
|
||||||
.addOption(new Option('--incognito', 'Launch app in incognito/private mode')
|
.addOption(new Option('--incognito', 'Launch app in incognito/private mode')
|
||||||
|
|||||||
62
src-tauri/src/inject/event.js
vendored
62
src-tauri/src/inject/event.js
vendored
@@ -252,27 +252,33 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function downloadFromDataUri(dataURI, filename) {
|
function downloadFromDataUri(dataURI, filename) {
|
||||||
const byteString = atob(dataURI.split(",")[1]);
|
try {
|
||||||
// write the bytes of the string to an ArrayBuffer
|
const byteString = atob(dataURI.split(",")[1]);
|
||||||
const bufferArray = new ArrayBuffer(byteString.length);
|
// write the bytes of the string to an ArrayBuffer
|
||||||
|
const bufferArray = new ArrayBuffer(byteString.length);
|
||||||
|
|
||||||
// create a view into the buffer
|
// create a view into the buffer
|
||||||
const binary = new Uint8Array(bufferArray);
|
const binary = new Uint8Array(bufferArray);
|
||||||
|
|
||||||
// set the bytes of the buffer to the correct values
|
// set the bytes of the buffer to the correct values
|
||||||
for (let i = 0; i < byteString.length; i++) {
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
binary[i] = byteString.charCodeAt(i);
|
binary[i] = byteString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the ArrayBuffer to a binary, and you're done
|
||||||
|
const userLanguage = navigator.language || navigator.userLanguage;
|
||||||
|
invoke("download_file_by_binary", {
|
||||||
|
params: {
|
||||||
|
filename,
|
||||||
|
binary: Array.from(binary),
|
||||||
|
language: userLanguage,
|
||||||
|
},
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Failed to download data URI file:', filename, error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to process data URI:', dataURI, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the ArrayBuffer to a binary, and you're done
|
|
||||||
const userLanguage = navigator.language || navigator.userLanguage;
|
|
||||||
invoke("download_file_by_binary", {
|
|
||||||
params: {
|
|
||||||
filename,
|
|
||||||
binary: Array.from(binary),
|
|
||||||
language: userLanguage,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFromBlobUrl(blobUrl, filename) {
|
function downloadFromBlobUrl(blobUrl, filename) {
|
||||||
@@ -284,7 +290,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
binary,
|
binary,
|
||||||
language: userLanguage,
|
language: userLanguage,
|
||||||
},
|
},
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Failed to download blob file:', filename, error);
|
||||||
});
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Failed to convert blob to binary:', blobUrl, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,8 +333,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
anchorElement.download || e.metaKey || e.ctrlKey || isDownloadableFile(url);
|
anchorElement.download || e.metaKey || e.ctrlKey || isDownloadableFile(url);
|
||||||
|
|
||||||
const handleExternalLink = (url) => {
|
const handleExternalLink = (url) => {
|
||||||
|
// Don't try to open blob: or data: URLs with shell
|
||||||
|
if (isSpecialDownload(url)) {
|
||||||
|
console.warn('Cannot open special URL with shell:', url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
invoke("plugin:shell|open", {
|
invoke("plugin:shell|open", {
|
||||||
path: url,
|
path: url,
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Failed to open URL with shell:', url, error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -351,6 +369,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const detectAnchorElementClick = (e) => {
|
const detectAnchorElementClick = (e) => {
|
||||||
|
// Safety check: ensure e.target exists and is an Element with closest method
|
||||||
|
if (!e.target || typeof e.target.closest !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const anchorElement = e.target.closest("a");
|
const anchorElement = e.target.closest("a");
|
||||||
|
|
||||||
if (anchorElement && anchorElement.href) {
|
if (anchorElement && anchorElement.href) {
|
||||||
@@ -701,7 +723,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for parent elements with background images
|
// Check for parent elements with background images
|
||||||
const parentWithBg = target.closest('[style*="background-image"]');
|
const parentWithBg = target && typeof target.closest === 'function' ? target.closest('[style*="background-image"]') : null;
|
||||||
if (parentWithBg) {
|
if (parentWithBg) {
|
||||||
const bgImage = parentWithBg.style.backgroundImage;
|
const bgImage = parentWithBg.style.backgroundImage;
|
||||||
const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
|
const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
|
||||||
@@ -770,7 +792,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const mediaInfo = getMediaInfo(target);
|
const mediaInfo = getMediaInfo(target);
|
||||||
|
|
||||||
// Check for links (but not if it's media)
|
// Check for links (but not if it's media)
|
||||||
const linkElement = target.closest("a");
|
const linkElement = target && typeof target.closest === 'function' ? target.closest("a") : null;
|
||||||
const isLink = linkElement && linkElement.href && !mediaInfo.isMedia;
|
const isLink = linkElement && linkElement.href && !mediaInfo.isMedia;
|
||||||
|
|
||||||
// Only show custom menu for media or links
|
// Only show custom menu for media or links
|
||||||
|
|||||||
Reference in New Issue
Block a user