删除冗余代码,美化UI界面
This commit is contained in:
50
CHANGELOG.md
Normal file
50
CHANGELOG.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# 更新日志
|
||||||
|
|
||||||
|
## [2.0.0] - 2026-02-06
|
||||||
|
|
||||||
|
### 🎉 重大更新
|
||||||
|
|
||||||
|
#### UI 重构
|
||||||
|
- ✅ 完全迁移到 **Tailwind CSS**,移除 90% 的自定义 CSS(从 1800+ 行减少到 199 行)
|
||||||
|
- ✅ 使用 **Lucide Icons** 替换所有内联 SVG
|
||||||
|
- ✅ 现代化的玻璃态效果和动画
|
||||||
|
- ✅ 完整的响应式设计
|
||||||
|
|
||||||
|
#### 性能优化
|
||||||
|
- ✅ 取消缩略图生成,改为懒加载
|
||||||
|
- ✅ 图片使用 base64 懒加载
|
||||||
|
- ✅ 编码信息异步获取
|
||||||
|
- ✅ 文件分析速度提升 50-100 倍
|
||||||
|
|
||||||
|
#### 新功能
|
||||||
|
- ✅ FFmpeg 自动安装引导
|
||||||
|
- ✅ 安装进度实时显示
|
||||||
|
- ✅ 批量文件处理优化
|
||||||
|
- ✅ 智能输出路径设置
|
||||||
|
|
||||||
|
#### 技术栈升级
|
||||||
|
- ✅ Tauri 2.0
|
||||||
|
- ✅ Tailwind CSS 3.x
|
||||||
|
- ✅ Lucide Icons
|
||||||
|
- ✅ ES6+ 模块化
|
||||||
|
|
||||||
|
### 📝 文档更新
|
||||||
|
- ✅ 重写 README.md,更清晰的项目说明
|
||||||
|
- ✅ 添加 node_modules 说明
|
||||||
|
- ✅ 添加详细的项目结构说明
|
||||||
|
- ✅ 添加常见问题解答
|
||||||
|
|
||||||
|
### 🗑️ 清理
|
||||||
|
- ✅ 删除所有重构临时文件
|
||||||
|
- ✅ 删除备份文件
|
||||||
|
- ✅ 删除重构脚本和文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.0.0] - 2025-XX-XX
|
||||||
|
|
||||||
|
### 初始版本
|
||||||
|
- 基础的格式转换功能
|
||||||
|
- 支持视频、音频、图片格式
|
||||||
|
- 自定义 CSS 样式
|
||||||
|
- 内联 SVG 图标
|
||||||
136
OPTIMIZATION.md
136
OPTIMIZATION.md
@@ -1,136 +0,0 @@
|
|||||||
# 性能优化说明
|
|
||||||
|
|
||||||
## 优化目标
|
|
||||||
解决添加大量文件时加载缓慢的问题,特别是缩略图生成和编码信息获取导致的性能瓶颈。
|
|
||||||
|
|
||||||
## 优化方案
|
|
||||||
|
|
||||||
### 1. 完全移除缩略图生成
|
|
||||||
- **视频文件**:不再生成缩略图,直接显示视频图标
|
|
||||||
- **音频文件**:不再提取封面,直接显示音频图标
|
|
||||||
- **图片文件**:直接显示原图(使用 Tauri 的 `convertFileSrc` API)
|
|
||||||
|
|
||||||
### 2. 编码信息懒加载
|
|
||||||
- **文件分析阶段**:只获取文件名、大小、类型等基本信息
|
|
||||||
- **进入配置页面后**:异步加载编码信息(分辨率、编码器、比特率等)
|
|
||||||
- **打开文件列表时**:按需加载还未获取的编码信息
|
|
||||||
- **并发控制**:每次并发加载 5 个文件,避免阻塞
|
|
||||||
|
|
||||||
### 3. 优化文件分析流程
|
|
||||||
- 移除 `analyze_files` 中的缩略图生成逻辑
|
|
||||||
- 移除 `analyze_files` 中的编码信息获取逻辑
|
|
||||||
- 只保留最基本的文件系统信息读取
|
|
||||||
- 文件分析速度提升 **50-100 倍**(取决于文件数量和类型)
|
|
||||||
|
|
||||||
### 4. 简化 UI 显示
|
|
||||||
- 预览网格:图片显示原图,视频/音频显示图标
|
|
||||||
- 文件列表:编码信息显示"加载中...",加载完成后自动更新
|
|
||||||
- 保留所有编码信息显示(分辨率、编码器、比特率等)
|
|
||||||
|
|
||||||
## 性能对比
|
|
||||||
|
|
||||||
### 优化前
|
|
||||||
- 10 个视频文件:~15-30 秒(生成缩略图 + 获取编码信息)
|
|
||||||
- 50 个音频文件:~30-60 秒(提取封面 + 获取编码信息)
|
|
||||||
- 100 个图片文件:~20-40 秒(生成缩略图 + 获取分辨率)
|
|
||||||
|
|
||||||
### 优化后
|
|
||||||
- 10 个视频文件:**~0.1-0.5 秒**(只读取文件系统信息)
|
|
||||||
- 50 个音频文件:**~0.2-0.8 秒**(只读取文件系统信息)
|
|
||||||
- 100 个图片文件:**~0.3-1 秒**(只读取文件系统信息)
|
|
||||||
|
|
||||||
编码信息在后台异步加载,不阻塞用户操作。
|
|
||||||
|
|
||||||
## 用户体验改进
|
|
||||||
|
|
||||||
1. **即时响应**:选择文件后 **立即** 进入配置页面(< 1 秒)
|
|
||||||
2. **清晰的图标**:使用 Lucide 风格的 SVG 图标,美观且加载快
|
|
||||||
3. **图片原图显示**:图片文件直接显示原图,画质更好
|
|
||||||
4. **渐进式加载**:编码信息在后台加载,加载完成后自动更新显示
|
|
||||||
5. **按需加载**:只在打开文件列表时才加载该类型文件的编码信息
|
|
||||||
|
|
||||||
## 技术细节
|
|
||||||
|
|
||||||
### 前端优化
|
|
||||||
```javascript
|
|
||||||
// 文件分析后立即进入配置页面
|
|
||||||
state.files = analyzedFiles;
|
|
||||||
renderFileTypeCards();
|
|
||||||
switchPage('config-page');
|
|
||||||
|
|
||||||
// 异步懒加载编码信息(不阻塞 UI)
|
|
||||||
lazyLoadCodecInfo();
|
|
||||||
|
|
||||||
// 图片直接使用原图
|
|
||||||
if (type === 'image' && file.path) {
|
|
||||||
const fileUrl = window.__TAURI__.core.convertFileSrc(file.path);
|
|
||||||
return `<img src="${fileUrl}" alt="${file.name}" loading="lazy">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 视频/音频显示图标
|
|
||||||
const icon = previewIcons[type];
|
|
||||||
return `<div class="preview-item ${type}">${icon}</div>`;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 后端优化
|
|
||||||
```rust
|
|
||||||
// 只获取基本文件信息
|
|
||||||
let metadata = fs::metadata(&path)?;
|
|
||||||
let size = metadata.len();
|
|
||||||
|
|
||||||
// 完全取消缩略图生成
|
|
||||||
let thumbnail = None;
|
|
||||||
|
|
||||||
// 编码信息也改为懒加载
|
|
||||||
let codec_info = None;
|
|
||||||
|
|
||||||
// 提供单独的命令按需获取编码信息
|
|
||||||
#[tauri::command]
|
|
||||||
async fn get_file_info(path: String, file_type: FileType) -> Result<Option<FileCodecInfo>, String> {
|
|
||||||
Ok(get_file_codec_info(&path, &file_type).await)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 加载策略
|
|
||||||
|
|
||||||
### 阶段 1:文件分析(< 1 秒)
|
|
||||||
- ✅ 读取文件名、大小、扩展名
|
|
||||||
- ✅ 识别文件类型
|
|
||||||
- ✅ 立即显示配置页面
|
|
||||||
|
|
||||||
### 阶段 2:后台加载(异步,不阻塞)
|
|
||||||
- 🔄 并发加载编码信息(每次 5 个)
|
|
||||||
- 🔄 加载完成后自动更新显示
|
|
||||||
- 🔄 用户可以同时进行其他操作
|
|
||||||
|
|
||||||
### 阶段 3:按需加载
|
|
||||||
- 🔄 打开文件列表时,加载该类型文件的编码信息
|
|
||||||
- 🔄 已加载的信息会被缓存,不会重复加载
|
|
||||||
|
|
||||||
## 保留的功能
|
|
||||||
|
|
||||||
虽然移除了缩略图和同步编码信息获取,但以下功能完全保留:
|
|
||||||
- ✅ 文件编码信息显示(异步加载)
|
|
||||||
- ✅ 文件大小显示
|
|
||||||
- ✅ 文件格式识别
|
|
||||||
- ✅ 所有转换功能
|
|
||||||
- ✅ 批量处理能力
|
|
||||||
|
|
||||||
## 未来可选优化
|
|
||||||
|
|
||||||
如果需要进一步优化,可以考虑:
|
|
||||||
1. **缓存编码信息**:将编码信息缓存到本地数据库
|
|
||||||
2. **更快的元数据读取**:使用更轻量的库读取基本信息
|
|
||||||
3. **虚拟滚动**:文件列表使用虚拟滚动,只渲染可见部分
|
|
||||||
4. **Web Worker**:在 Web Worker 中处理编码信息
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
通过完全移除同步操作,我们实现了:
|
|
||||||
- **50-100 倍**的初始加载速度提升
|
|
||||||
- **即时响应**(< 1 秒进入配置页面)
|
|
||||||
- **更好的用户体验**(不阻塞 UI)
|
|
||||||
- **更低的资源占用**(按需加载)
|
|
||||||
- **保留所有核心功能**(异步加载)
|
|
||||||
|
|
||||||
这是一个以用户体验为导向的优化方案,特别适合需要批量处理大量文件的场景。无论添加多少文件,初始加载都是瞬间完成的。
|
|
||||||
274
README.md
274
README.md
@@ -1,127 +1,241 @@
|
|||||||
# 火炬格式转换器 0.1.0
|
# 火炬格式转换器 2.0.0
|
||||||
|
|
||||||
基于 FFmpeg 和 Tauri 2.0 的跨平台媒体格式转换工具。
|
<div align="center">
|
||||||
|
|
||||||
## 特性
|
**基于 FFmpeg 和 Tauri 2.0 的跨平台媒体格式转换工具**
|
||||||
|
|
||||||
- 🎬 支持多种视频格式:MP4、AVI、MKV、MOV、WebM 等
|
[](https://v2.tauri.app/)
|
||||||
- 🎵 支持多种音频格式:MP3、WAV、AAC、FLAC、OGG 等
|
[](https://www.rust-lang.org/)
|
||||||
- ⚙️ 丰富的转换参数:编码器、分辨率、帧率、比特率等
|
[](LICENSE)
|
||||||
- 📊 实时进度显示
|
|
||||||
- 🖥️ 跨平台支持:Windows、macOS、Linux
|
|
||||||
- 🚀 基于 Rust + Tauri 2.0,性能优异
|
|
||||||
|
|
||||||
## 系统要求
|
</div>
|
||||||
|
|
||||||
### 必需依赖
|
## ✨ 特性
|
||||||
|
|
||||||
|
- 🎬 **多格式支持** - 支持视频、音频、图片等多种格式转换
|
||||||
|
- ⚙️ **丰富参数** - 自定义编码器、分辨率、帧率、比特率等
|
||||||
|
- 🚀 **高性能** - 基于 Rust + Tauri 2.0,原生性能
|
||||||
|
- 🎨 **现代化 UI** - 使用 Tailwind CSS + Lucide 图标
|
||||||
|
- 📊 **实时进度** - 转换进度实时显示
|
||||||
|
- 🖥️ **跨平台** - 支持 Windows、macOS、Linux
|
||||||
|
- 🔄 **批量转换** - 支持多文件批量处理
|
||||||
|
- 💾 **智能输出** - 自动检测或自定义输出路径
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 系统要求
|
||||||
|
|
||||||
|
#### 必需依赖
|
||||||
|
|
||||||
1. **FFmpeg** - 核心转换引擎
|
1. **FFmpeg** - 核心转换引擎
|
||||||
- Windows: 从 [ffmpeg.org](https://ffmpeg.org/download.html) 下载并添加到 PATH
|
- Windows: 从 [ffmpeg.org](https://ffmpeg.org/download.html) 下载并添加到 PATH
|
||||||
- macOS: `brew install ffmpeg`
|
- macOS: `brew install ffmpeg`
|
||||||
- Linux: `sudo apt install ffmpeg` 或对应包管理器
|
- Linux: `sudo apt install ffmpeg`
|
||||||
|
|
||||||
2. **Rust** - 编译 Tauri 后端
|
2. **Rust** - 编译 Tauri 后端
|
||||||
- 从 [rustup.rs](https://rustup.rs/) 安装
|
- 安装:`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
|
||||||
|
- 或访问 [rustup.rs](https://rustup.rs/)
|
||||||
|
|
||||||
3. **Node.js** - 前端构建工具
|
3. **Node.js** - 前端构建工具
|
||||||
- 从 [nodejs.org](https://nodejs.org/) 安装 (推荐 v18+)
|
- 推荐版本:v18 或更高
|
||||||
|
- 下载:[nodejs.org](https://nodejs.org/)
|
||||||
|
|
||||||
### Tauri 2.0 系统依赖
|
#### Tauri 系统依赖
|
||||||
|
|
||||||
请参考 [Tauri 2.0 官方文档](https://v2.tauri.app/start/prerequisites/) 安装对应平台的系统依赖。
|
请参考 [Tauri 2.0 官方文档](https://v2.tauri.app/start/prerequisites/) 安装对应平台的系统依赖。
|
||||||
|
|
||||||
## 快速开始
|
### 安装步骤
|
||||||
|
|
||||||
### 1. 克隆项目
|
1. **克隆项目**
|
||||||
|
```bash
|
||||||
|
git clone <your-repo>
|
||||||
|
cd format-converter
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
2. **安装依赖**
|
||||||
git clone <your-repo>
|
```bash
|
||||||
cd format-converter
|
npm install
|
||||||
|
```
|
||||||
|
> 这会自动下载并安装所有 Node.js 依赖到 `node_modules` 文件夹
|
||||||
|
|
||||||
|
3. **开发模式运行**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **构建发布版本**
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
> 构建完成后,安装包位于 `src-tauri/target/release/bundle/` 目录
|
||||||
|
|
||||||
|
## 📖 使用说明
|
||||||
|
|
||||||
|
### 基本流程
|
||||||
|
|
||||||
|
1. **添加文件**
|
||||||
|
- 点击拖放区域选择文件
|
||||||
|
- 或直接拖放文件到窗口
|
||||||
|
|
||||||
|
2. **配置转换参数**
|
||||||
|
- 选择目标格式(MP4、MP3、JPG 等)
|
||||||
|
- 设置编码参数(可选)
|
||||||
|
- 选择输出位置
|
||||||
|
|
||||||
|
3. **开始转换**
|
||||||
|
- 点击"开始转换"按钮
|
||||||
|
- 查看实时进度
|
||||||
|
|
||||||
|
4. **完成**
|
||||||
|
- 转换完成后自动打开输出文件夹(可选)
|
||||||
|
|
||||||
|
### 支持的格式
|
||||||
|
|
||||||
|
#### 视频格式
|
||||||
|
- **输入**: MP4, AVI, MKV, MOV, WebM, FLV, WMV, MPEG 等
|
||||||
|
- **输出**: MP4, MKV, MOV, AVI, WebM, FLV, WMV
|
||||||
|
|
||||||
|
#### 音频格式
|
||||||
|
- **输入**: MP3, WAV, AAC, FLAC, OGG, WMA, M4A 等
|
||||||
|
- **输出**: MP3, AAC, FLAC, WAV, OGG, Opus, WMA
|
||||||
|
|
||||||
|
#### 图片格式
|
||||||
|
- **输入**: JPG, PNG, WebP, GIF, BMP, TIFF, ICO 等
|
||||||
|
- **输出**: JPG, PNG, WebP, GIF, BMP, TIFF, ICO
|
||||||
|
|
||||||
|
## 🏗️ 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
format-converter/
|
||||||
|
├── src/ # 前端源代码
|
||||||
|
│ ├── index.html # 主页面(使用 Tailwind CSS)
|
||||||
|
│ ├── style.css # 自定义样式(动画和特效)
|
||||||
|
│ ├── main.js # 前端逻辑(ES6 模块)
|
||||||
|
│ └── thumbnail-helper.js # 缩略图辅助工具
|
||||||
|
├── src-tauri/ # Tauri 后端
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── main.rs # Rust 后端主程序
|
||||||
|
│ │ └── ffmpeg_installer.rs # FFmpeg 自动安装器
|
||||||
|
│ ├── icons/ # 应用图标
|
||||||
|
│ ├── Cargo.toml # Rust 依赖配置
|
||||||
|
│ └── tauri.conf.json # Tauri 配置
|
||||||
|
├── node_modules/ # Node.js 依赖(自动生成,不要提交到 Git)
|
||||||
|
├── package.json # Node.js 项目配置
|
||||||
|
├── package-lock.json # 依赖版本锁定文件
|
||||||
|
├── README.md # 项目说明文档
|
||||||
|
└── TESTING_FFMPEG.md # FFmpeg 测试指南
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 安装依赖
|
### 关键文件说明
|
||||||
|
|
||||||
```bash
|
- **`node_modules/`** - Node.js 依赖包文件夹,由 `npm install` 自动生成,包含所有前端依赖(Tailwind CSS、Lucide 等)。不要手动修改,也不要提交到 Git。
|
||||||
npm install
|
- **`package.json`** - 定义项目依赖和脚本命令
|
||||||
```
|
- **`package-lock.json`** - 锁定依赖版本,确保团队成员使用相同版本
|
||||||
|
- **`src/index.html`** - 主页面,使用 Tailwind CSS CDN 和 Lucide 图标
|
||||||
|
- **`src/style.css`** - 精简的自定义样式,只包含动画和特殊效果(199 行)
|
||||||
|
- **`src/main.js`** - 前端业务逻辑,使用 ES6 模块
|
||||||
|
- **`src-tauri/src/main.rs`** - Rust 后端,处理文件分析、格式转换等
|
||||||
|
|
||||||
### 3. 开发模式运行
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- **HTML5** - 页面结构
|
||||||
|
- **Tailwind CSS** - 现代化 CSS 框架(utility-first)
|
||||||
|
- **Lucide Icons** - 轻量级图标库
|
||||||
|
- **Vanilla JavaScript** - ES6+ 模块化
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- **Rust** - 系统编程语言
|
||||||
|
- **Tauri 2.0** - 桌面应用框架
|
||||||
|
- **FFmpeg** - 多媒体处理引擎
|
||||||
|
|
||||||
|
### 开发工具
|
||||||
|
- **npm** - 包管理器
|
||||||
|
- **Cargo** - Rust 包管理器
|
||||||
|
|
||||||
|
## 🔧 开发指南
|
||||||
|
|
||||||
|
### 开发模式
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 构建发布版本
|
这会启动 Tauri 开发服务器,支持热重载。
|
||||||
|
|
||||||
|
### 调试
|
||||||
|
|
||||||
|
- **后端日志**: 在终端中查看 Rust 输出
|
||||||
|
- **前端调试**: 按 F12 打开 DevTools
|
||||||
|
- **Tauri API**: 使用 `console.log` 调试前端逻辑
|
||||||
|
|
||||||
|
### 构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
构建完成后,安装包位于 `src-tauri/target/release/bundle/` 目录。
|
构建产物位于 `src-tauri/target/release/bundle/`:
|
||||||
|
- **Windows**: `.msi` 或 `.exe`
|
||||||
|
- **macOS**: `.dmg` 或 `.app`
|
||||||
|
- **Linux**: `.deb`, `.AppImage` 等
|
||||||
|
|
||||||
## 使用说明
|
## 📝 脚本命令
|
||||||
|
|
||||||
1. **选择文件**:点击"选择文件"按钮或拖拽文件到窗口
|
```bash
|
||||||
2. **设置参数**:
|
# 开发模式(热重载)
|
||||||
- 选择输出格式
|
npm run dev
|
||||||
- 配置编码器、分辨率、帧率等(可选)
|
|
||||||
- 选择输出路径
|
|
||||||
3. **开始转换**:点击"开始转换"按钮
|
|
||||||
4. **查看进度**:在任务列表中查看转换进度
|
|
||||||
|
|
||||||
## 支持的格式
|
# 构建发布版本
|
||||||
|
npm run build
|
||||||
|
|
||||||
### 视频格式
|
# 运行 Tauri CLI
|
||||||
- **MP4** - 通用视频格式,兼容性好(支持 H.264、HEVC 编码)
|
npm run tauri
|
||||||
- **AVI** - Windows 视频格式
|
|
||||||
- **MKV** - Matroska 格式,支持多音轨
|
|
||||||
- **MOV** - QuickTime 视频格式
|
|
||||||
- **WebM** - 网页视频格式
|
|
||||||
|
|
||||||
### 音频格式
|
|
||||||
- **MP3** - 通用音频格式
|
|
||||||
- **WAV** - 无损音频格式
|
|
||||||
- **AAC** - 高级音频编码
|
|
||||||
- **FLAC** - 无损音频压缩
|
|
||||||
- **OGG** - 开源音频格式
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
format-converter/
|
|
||||||
├── src/ # 前端源代码
|
|
||||||
│ ├── index.html # 主页面
|
|
||||||
│ ├── style.css # 自定义样式文件
|
|
||||||
│ └── main.js # 前端逻辑 (ES6 模块)
|
|
||||||
├── src-tauri/ # Tauri 2.0 后端
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── main.rs # Rust 后端代码
|
|
||||||
│ │ └── ffmpeg_installer.rs # FFmpeg 安装器
|
|
||||||
│ ├── Cargo.toml # Rust 依赖
|
|
||||||
│ └── tauri.conf.json # Tauri 2.0 配置
|
|
||||||
├── package.json # Node.js 配置
|
|
||||||
└── README.md # 项目说明
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 技术栈
|
## 🐛 常见问题
|
||||||
|
|
||||||
- **前端**: HTML5 + Tailwind CSS + ES6 JavaScript (模块化)
|
### Q: FFmpeg 未安装怎么办?
|
||||||
- **后端**: Rust + Tauri 2.0
|
A: 应用会自动检测 FFmpeg,如果未安装会显示安装引导。您也可以手动安装:
|
||||||
- **核心引擎**: FFmpeg
|
- macOS: `brew install ffmpeg`
|
||||||
- **插件系统**: Tauri 2.0 插件架构
|
- Windows: 下载并添加到 PATH
|
||||||
|
- Linux: `sudo apt install ffmpeg`
|
||||||
|
|
||||||
### 调试
|
### Q: 图标不显示?
|
||||||
|
A: 确保网络连接正常,Lucide CDN 需要联网加载。或检查浏览器控制台是否有错误。
|
||||||
|
|
||||||
- 后端日志会在终端中输出
|
### Q: 转换失败?
|
||||||
- 前端控制台可以通过 DevTools 查看(开发模式下按 F12)
|
A: 检查:
|
||||||
- 使用 `console.log` 进行前端调试
|
1. FFmpeg 是否正确安装
|
||||||
|
2. 输入文件是否损坏
|
||||||
|
3. 输出路径是否有写入权限
|
||||||
|
4. 查看终端日志获取详细错误信息
|
||||||
|
|
||||||
## 许可证
|
### Q: node_modules 文件夹很大?
|
||||||
|
A: 这是正常的,`node_modules` 包含所有前端依赖。不要提交到 Git(已在 `.gitignore` 中)。
|
||||||
|
|
||||||
MIT
|
## 📄 许可证
|
||||||
|
|
||||||
## 致谢
|
MIT License - 详见 [LICENSE](LICENSE) 文件
|
||||||
|
|
||||||
|
## 🙏 致谢
|
||||||
|
|
||||||
- [FFmpeg](https://ffmpeg.org/) - 强大的多媒体处理框架
|
- [FFmpeg](https://ffmpeg.org/) - 强大的多媒体处理框架
|
||||||
- [Tauri 2.0](https://v2.tauri.app/) - 下一代桌面应用开发框架
|
- [Tauri](https://tauri.app/) - 现代化桌面应用框架
|
||||||
- [Tailwind CSS](https://tailwindcss.com) - 只需书写 HTML 代码即可快速构建美观的网站
|
- [Tailwind CSS](https://tailwindcss.com/) - 实用优先的 CSS 框架
|
||||||
|
- [Lucide](https://lucide.dev/) - 精美的开源图标库
|
||||||
|
|
||||||
|
## 📮 联系方式
|
||||||
|
|
||||||
|
- 官网: [meshel.cn](https://www.meshel.cn)
|
||||||
|
- 问题反馈: [GitHub Issues](https://github.com/your-repo/issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**火炬格式转换器** - 让格式转换更简单
|
||||||
|
|
||||||
|
Made with ❤️ by Meshel
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
247
TESTING_FFMPEG.md
Normal file
247
TESTING_FFMPEG.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# FFmpeg 安装测试指南
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
测试在没有 FFmpeg 的电脑上全新安装该软件后,第一次启动时的 FFmpeg 自动安装流程。
|
||||||
|
|
||||||
|
## 测试方法
|
||||||
|
|
||||||
|
### 方法 1:临时重命名 FFmpeg(推荐)
|
||||||
|
|
||||||
|
如果你的电脑已经安装了 FFmpeg,可以临时重命名它来模拟未安装的情况:
|
||||||
|
|
||||||
|
#### macOS/Linux:
|
||||||
|
```bash
|
||||||
|
# 1. 查找 FFmpeg 位置
|
||||||
|
which ffmpeg
|
||||||
|
|
||||||
|
# 2. 临时重命名(需要 sudo)
|
||||||
|
# 如果在 /usr/local/bin/ffmpeg
|
||||||
|
sudo mv /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg.backup
|
||||||
|
sudo mv /usr/local/bin/ffprobe /usr/local/bin/ffprobe.backup
|
||||||
|
|
||||||
|
# 如果在 /opt/homebrew/bin/ffmpeg (Apple Silicon)
|
||||||
|
sudo mv /opt/homebrew/bin/ffmpeg /opt/homebrew/bin/ffmpeg.backup
|
||||||
|
sudo mv /opt/homebrew/bin/ffprobe /opt/homebrew/bin/ffprobe.backup
|
||||||
|
|
||||||
|
# 3. 测试完成后恢复
|
||||||
|
sudo mv /usr/local/bin/ffmpeg.backup /usr/local/bin/ffmpeg
|
||||||
|
sudo mv /usr/local/bin/ffprobe.backup /usr/local/bin/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows:
|
||||||
|
```cmd
|
||||||
|
# 1. 查找 FFmpeg 位置
|
||||||
|
where ffmpeg
|
||||||
|
|
||||||
|
# 2. 临时重命名
|
||||||
|
ren "C:\path\to\ffmpeg.exe" "ffmpeg.exe.backup"
|
||||||
|
ren "C:\path\to\ffprobe.exe" "ffprobe.exe.backup"
|
||||||
|
|
||||||
|
# 3. 测试完成后恢复
|
||||||
|
ren "C:\path\to\ffmpeg.exe.backup" "ffmpeg.exe"
|
||||||
|
ren "C:\path\to\ffprobe.exe.backup" "ffprobe.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 2:删除应用数据目录
|
||||||
|
|
||||||
|
删除应用存储的 FFmpeg:
|
||||||
|
|
||||||
|
#### macOS:
|
||||||
|
```bash
|
||||||
|
rm -rf ~/Library/Application\ Support/FormatConverter/ffmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows:
|
||||||
|
```cmd
|
||||||
|
del /F /Q "%LOCALAPPDATA%\FormatConverter\ffmpeg.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux:
|
||||||
|
```bash
|
||||||
|
rm -rf ~/.local/share/format-converter/ffmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 3:使用虚拟机或容器
|
||||||
|
|
||||||
|
最彻底的测试方法:
|
||||||
|
1. 使用虚拟机(VirtualBox、VMware、Parallels)
|
||||||
|
2. 使用 Docker 容器
|
||||||
|
3. 使用云端测试环境
|
||||||
|
|
||||||
|
## 测试流程
|
||||||
|
|
||||||
|
### 1. 启动应用
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 观察初始化过程
|
||||||
|
|
||||||
|
应该看到以下流程:
|
||||||
|
|
||||||
|
1. **应用启动**
|
||||||
|
- 显示主页面
|
||||||
|
- 底部状态栏显示"检测编码器..."
|
||||||
|
|
||||||
|
2. **检测 FFmpeg**
|
||||||
|
- 控制台输出:`正在检测 FFmpeg...`
|
||||||
|
- 控制台输出:`调用 check_ffmpeg_status...`
|
||||||
|
|
||||||
|
3. **弹出安装引导**
|
||||||
|
- 如果未检测到 FFmpeg,自动弹出安装引导弹窗
|
||||||
|
- 弹窗标题:🔧 需要安装 FFmpeg
|
||||||
|
- 显示两个选项:
|
||||||
|
- 🚀 自动安装(推荐)
|
||||||
|
- 📝 手动安装
|
||||||
|
|
||||||
|
4. **点击"开始安装"**
|
||||||
|
- 弹窗关闭
|
||||||
|
- 切换到安装页面
|
||||||
|
- 显示进度条和安装信息
|
||||||
|
|
||||||
|
5. **安装过程**
|
||||||
|
- 0-5%: 准备下载 FFmpeg...
|
||||||
|
- 5-50%: 正在下载 FFmpeg... (显示下载进度)
|
||||||
|
- 50-90%: 正在解压...
|
||||||
|
- 90-95%: 正在安装...
|
||||||
|
- 95-100%: 正在清理...
|
||||||
|
- 100%: FFmpeg 安装完成
|
||||||
|
|
||||||
|
6. **安装完成**
|
||||||
|
- 自动返回主页面
|
||||||
|
- 底部状态栏显示"编码器"(绿色点)
|
||||||
|
- 鼠标悬停显示 FFmpeg 版本信息
|
||||||
|
|
||||||
|
### 3. 验证安装
|
||||||
|
|
||||||
|
安装完成后,验证功能:
|
||||||
|
|
||||||
|
1. **选择文件**
|
||||||
|
- 点击或拖拽文件到主页面
|
||||||
|
- 应该能正常分析文件
|
||||||
|
|
||||||
|
2. **转换测试**
|
||||||
|
- 选择一个小文件进行转换
|
||||||
|
- 观察转换过程是否正常
|
||||||
|
|
||||||
|
3. **检查安装位置**
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
ls -la ~/Library/Application\ Support/FormatConverter/
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
dir "%LOCALAPPDATA%\FormatConverter\"
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
ls -la ~/.local/share/format-converter/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 预期结果
|
||||||
|
|
||||||
|
### 成功场景
|
||||||
|
- ✅ 自动检测到 FFmpeg 未安装
|
||||||
|
- ✅ 弹出安装引导弹窗
|
||||||
|
- ✅ 点击"开始安装"后显示安装页面
|
||||||
|
- ✅ 显示实时下载和安装进度
|
||||||
|
- ✅ 安装完成后自动返回主页面
|
||||||
|
- ✅ 状态栏显示"编码器"(绿色)
|
||||||
|
- ✅ 可以正常使用转换功能
|
||||||
|
|
||||||
|
### 失败场景处理
|
||||||
|
- ❌ 网络连接失败:显示错误信息,提示检查网络
|
||||||
|
- ❌ 下载失败:显示错误信息,提供重试选项
|
||||||
|
- ❌ 解压失败:显示错误信息,提示手动安装
|
||||||
|
- ❌ 权限不足:显示错误信息,提示以管理员身份运行
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 点击"稍后安装"后怎么办?
|
||||||
|
A: 弹窗关闭,但状态栏仍显示"编码器未安装"。用户可以:
|
||||||
|
- 手动安装 FFmpeg 到系统 PATH
|
||||||
|
- 重启应用,会再次弹出安装引导
|
||||||
|
|
||||||
|
### Q: 安装失败怎么办?
|
||||||
|
A: 应用会显示错误信息。用户可以:
|
||||||
|
1. 检查网络连接
|
||||||
|
2. 点击重试
|
||||||
|
3. 选择手动安装 FFmpeg
|
||||||
|
|
||||||
|
### Q: 如何手动安装 FFmpeg?
|
||||||
|
A:
|
||||||
|
- **macOS**: `brew install ffmpeg`
|
||||||
|
- **Windows**: 从 [ffmpeg.org](https://ffmpeg.org/download.html) 下载并添加到 PATH
|
||||||
|
- **Linux**: `sudo apt install ffmpeg` 或对应包管理器
|
||||||
|
|
||||||
|
### Q: 安装后在哪里?
|
||||||
|
A:
|
||||||
|
- **macOS**: `~/Library/Application Support/FormatConverter/ffmpeg`
|
||||||
|
- **Windows**: `%LOCALAPPDATA%\FormatConverter\ffmpeg.exe`
|
||||||
|
- **Linux**: `~/.local/share/format-converter/ffmpeg`
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### 查看控制台日志
|
||||||
|
```bash
|
||||||
|
# 开发模式下,控制台会输出详细日志
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 查看后端日志
|
||||||
|
# 在终端中查看 Rust 输出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动触发安装
|
||||||
|
在浏览器控制台中执行:
|
||||||
|
```javascript
|
||||||
|
// 显示安装引导
|
||||||
|
document.getElementById('ffmpeg-install-guide').classList.add('active');
|
||||||
|
|
||||||
|
// 直接开始安装
|
||||||
|
installFFmpeg();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查 FFmpeg 状态
|
||||||
|
在浏览器控制台中执行:
|
||||||
|
```javascript
|
||||||
|
// 检查 FFmpeg 状态
|
||||||
|
window.tauriInvoke('check_ffmpeg_status').then(console.log);
|
||||||
|
|
||||||
|
// 获取 FFmpeg 版本
|
||||||
|
// 返回: [installed: boolean, version: string | null]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试清单
|
||||||
|
|
||||||
|
- [ ] 应用启动时自动检测 FFmpeg
|
||||||
|
- [ ] 未安装时弹出安装引导弹窗
|
||||||
|
- [ ] 安装引导 UI 显示正常
|
||||||
|
- [ ] 点击"开始安装"切换到安装页面
|
||||||
|
- [ ] 显示实时下载进度
|
||||||
|
- [ ] 显示解压和安装进度
|
||||||
|
- [ ] 安装完成后返回主页面
|
||||||
|
- [ ] 状态栏显示"编码器"(绿色)
|
||||||
|
- [ ] 可以正常选择和转换文件
|
||||||
|
- [ ] 点击"稍后安装"可以关闭弹窗
|
||||||
|
- [ ] 安装失败时显示错误信息
|
||||||
|
- [ ] 重启应用后不再弹出安装引导(已安装)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **网络要求**: 自动安装需要网络连接,下载大小约 50-100 MB
|
||||||
|
2. **权限要求**: 某些系统可能需要管理员权限
|
||||||
|
3. **磁盘空间**: 确保有足够的磁盘空间(至少 200 MB)
|
||||||
|
4. **防火墙**: 确保防火墙允许应用访问网络
|
||||||
|
5. **代理设置**: 如果使用代理,可能需要配置系统代理
|
||||||
|
|
||||||
|
## 恢复测试环境
|
||||||
|
|
||||||
|
测试完成后,记得恢复 FFmpeg:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS/Linux
|
||||||
|
sudo mv /usr/local/bin/ffmpeg.backup /usr/local/bin/ffmpeg
|
||||||
|
sudo mv /usr/local/bin/ffprobe.backup /usr/local/bin/ffprobe
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
ren "C:\path\to\ffmpeg.exe.backup" "ffmpeg.exe"
|
||||||
|
ren "C:\path\to\ffprobe.exe.backup" "ffprobe.exe"
|
||||||
|
```
|
||||||
500
src/index.html
500
src/index.html
@@ -3,336 +3,338 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>火炬格式转换器 - Format Converter 0.1.0</title>
|
<title>火炬格式转换器 - Format Converter 2.0.0</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: { DEFAULT: '#3b82f6', dark: '#2563eb' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style type="text/tailwindcss">
|
||||||
|
@layer utilities {
|
||||||
|
.glass-effect { @apply backdrop-blur-md bg-opacity-95; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-slate-900 text-slate-100 overflow-hidden">
|
||||||
<div id="app">
|
<div id="app" class="w-screen h-screen overflow-hidden">
|
||||||
<!-- 首页 -->
|
|
||||||
|
<!-- ========== 首页 ========== -->
|
||||||
<div id="home-page" class="page active">
|
<div id="home-page" class="page active">
|
||||||
<!-- 加载遮罩层 -->
|
<!-- 加载遮罩 -->
|
||||||
<div id="loading-overlay" class="loading-overlay">
|
<div id="loading-overlay" class="fixed inset-0 bg-slate-900/95 backdrop-blur-sm flex flex-col items-center justify-center z-50 opacity-0 invisible transition-all duration-300">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner mb-4"></div>
|
||||||
<div class="loading-text">正在分析文件...</div>
|
<div class="text-xl font-medium mb-2" id="loading-text">正在分析文件...</div>
|
||||||
<div class="loading-subtext">生成缩略图中,请稍候</div>
|
<div class="text-sm text-slate-400" id="loading-subtext"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="home-content">
|
<div class="flex flex-col h-full">
|
||||||
<!-- 拖放区域 -->
|
<div class="flex-1 flex items-center justify-center p-8">
|
||||||
<div class="drop-zone" id="drop-zone">
|
<div id="drop-zone" class="w-full max-w-2xl border-2 border-dashed border-slate-600 rounded-2xl p-16 text-center cursor-pointer transition-all duration-200 hover:border-primary hover:bg-primary/5">
|
||||||
<div class="drop-icon">
|
<div class="mb-6 text-primary"><i data-lucide="cloud-upload" class="w-16 h-16 mx-auto"></i></div>
|
||||||
<svg viewBox="0 0 24 24" width="64" height="64">
|
<h2 class="text-2xl font-semibold mb-2">拖入文件或点击选择</h2>
|
||||||
<path fill="currentColor" d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
|
<p class="text-slate-400">支持批量选择多个文件</p>
|
||||||
</svg>
|
<input type="file" id="file-input" multiple hidden accept="video/*,audio/*,image/*">
|
||||||
</div>
|
</div>
|
||||||
<h2 class="drop-title">拖入文件或点击选择</h2>
|
|
||||||
<p class="drop-subtitle">支持批量选择多个文件</p>
|
|
||||||
<input type="file" id="file-input" multiple hidden accept="video/*,audio/*,image/*">
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="px-8 pb-6 text-center">
|
||||||
<!-- 支持的格式说明 -->
|
<h3 class="text-lg font-medium text-slate-200 mb-2">支持的文件类型</h3>
|
||||||
<div class="formats-info">
|
<p class="text-sm text-slate-400">mp4, avi, mkv, mov, webm, mp3, wav, flac, aac, ogg, jpg, png, webp, gif, bmp... <span class="text-primary">等 50+ 种格式互相转换</span></p>
|
||||||
<h3>支持的文件类型</h3>
|
|
||||||
<p class="formats-list">mp4, avi, mkv, mov, webm, mp3, wav, flac, aac, ogg, jpg, png, webp, gif, bmp... <span id="format-count">等 50+ 种格式互相转换</span></p>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex items-center justify-between px-6 py-3 bg-slate-800/50 border-t border-slate-700">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<!-- 底部状态栏 -->
|
<span id="encoder-status" class="status-indicator flex items-center gap-2 text-sm">
|
||||||
<div class="status-bar">
|
<span class="status-dot"></span>
|
||||||
<div class="status-left">
|
<span class="status-text">检测编码器...</span>
|
||||||
<span id="encoder-status" class="status-indicator loading" title="">
|
</span>
|
||||||
<span class="status-dot"></span>
|
</div>
|
||||||
<span class="status-text">检测编码器...</span>
|
<a href="https://www.meshel.cn" target="_blank" class="text-sm text-slate-400 hover:text-primary transition-colors">©️ meshel.cn</a>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-right">
|
|
||||||
<a href="https://www.meshel.cn" target="_blank" class="copyright-link">
|
|
||||||
©️ meshel.cn
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- FFmpeg 安装页面 -->
|
<!-- ========== 安装页面 ========== -->
|
||||||
<div id="install-page" class="page">
|
<div id="install-page" class="page">
|
||||||
<div class="install-content">
|
<div class="flex items-center justify-center h-full p-8">
|
||||||
<div class="install-icon">📦</div>
|
<div class="text-center max-w-md">
|
||||||
<h2>正在安装 FFmpeg</h2>
|
<div class="text-6xl mb-6">📦</div>
|
||||||
<p class="install-desc">首次运行需要下载编码器组件</p>
|
<h2 class="text-3xl font-bold mb-3">正在安装 FFmpeg</h2>
|
||||||
<div class="install-progress">
|
<p class="text-slate-400 mb-8">首次运行需要下载编码器组件</p>
|
||||||
<div class="progress-bar">
|
<div class="mb-6">
|
||||||
<div class="progress-fill" id="install-progress-fill"></div>
|
<div class="w-full bg-slate-700 rounded-full h-3 overflow-hidden mb-2">
|
||||||
|
<div id="install-progress-fill" class="progress-fill bg-primary h-full rounded-full" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<span id="install-progress-text" class="text-2xl font-bold text-primary">0%</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="progress-text" id="install-progress-text">0%</span>
|
<p id="install-message" class="text-sm text-slate-400">准备下载...</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="install-message" id="install-message">准备下载...</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 参数配置页面 -->
|
<!-- ========== 配置页面 ========== -->
|
||||||
<div id="config-page" class="page">
|
<div id="config-page" class="page">
|
||||||
<div class="config-header">
|
<div class="flex flex-col h-full">
|
||||||
<button class="btn-back" id="btn-back">
|
<!-- 头部 -->
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20">
|
<div class="flex items-center justify-between px-6 py-4 bg-slate-800/50 border-b border-slate-700">
|
||||||
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
|
<div class="flex items-center gap-4">
|
||||||
</svg>
|
<button id="btn-back" class="flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors">
|
||||||
返回
|
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
||||||
</button>
|
<span>返回</span>
|
||||||
<h1>转换设置</h1>
|
</button>
|
||||||
<div class="header-actions">
|
<h1 class="text-xl font-semibold">转换设置</h1>
|
||||||
<button class="btn-icon" id="btn-settings" title="设置">
|
</div>
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20">
|
<button id="btn-settings" class="p-2 rounded-lg hover:bg-slate-700 transition-colors">
|
||||||
<path fill="currentColor" d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/>
|
<i data-lucide="settings" class="w-5 h-5"></i>
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-content">
|
<!-- 内容区 -->
|
||||||
<!-- 文件类型卡片列表 -->
|
<div class="flex-1 overflow-y-auto p-6">
|
||||||
<div class="file-type-cards" id="file-type-cards">
|
<div id="file-type-cards" class="grid grid-cols-1 gap-6"></div>
|
||||||
<!-- 动态生成 -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模板选择弹窗 -->
|
<!-- ========== 模板弹窗 ========== -->
|
||||||
<div id="template-modal" class="modal">
|
<div id="template-modal" class="modal fixed inset-0 bg-black/70 flex items-center justify-center z-50 opacity-0 invisible transition-all duration-300">
|
||||||
<div class="modal-content template-modal-content">
|
<div class="modal-content bg-slate-800 rounded-xl border border-slate-700 w-full max-w-3xl max-h-[90vh] flex flex-col">
|
||||||
<div class="modal-header">
|
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-700">
|
||||||
<h3>选择输出格式</h3>
|
<h3 class="text-lg font-semibold">选择输出格式</h3>
|
||||||
<button class="btn-close" id="close-template-modal">×</button>
|
<button id="close-template-modal" class="p-1 hover:bg-slate-700 rounded transition-colors">
|
||||||
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
<div class="template-layout">
|
<div class="w-64 border-r border-slate-700 p-4 overflow-y-auto">
|
||||||
<div class="template-sidebar">
|
<div id="template-list" class="space-y-2"></div>
|
||||||
<div class="template-list" id="template-list">
|
<div class="mt-4 pt-4 border-t border-slate-700 space-y-2">
|
||||||
<!-- 动态生成 -->
|
<button id="btn-add-template" class="w-full text-left px-3 py-2 text-sm text-primary hover:bg-slate-700 rounded transition-colors">+ 新建模板</button>
|
||||||
</div>
|
<button id="btn-delete-template" class="w-full text-left px-3 py-2 text-sm text-red-400 hover:bg-slate-700 rounded transition-colors">删除</button>
|
||||||
<div class="template-actions">
|
|
||||||
<button class="btn-text" id="btn-add-template">+ 新建模板</button>
|
|
||||||
<button class="btn-text btn-danger" id="btn-delete-template">删除</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="template-config" id="template-config">
|
|
||||||
<!-- 参数配置 -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-1 p-6 overflow-y-auto">
|
||||||
|
<div id="template-config"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-slate-700">
|
||||||
<button class="btn-secondary" id="btn-cancel-template">取消</button>
|
<button id="btn-cancel-template" class="px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors">取消</button>
|
||||||
<button class="btn-primary" id="btn-confirm-template">确定</button>
|
<button id="btn-confirm-template" class="px-4 py-2 rounded-lg bg-primary hover:bg-primary-dark transition-colors">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件列表弹窗 -->
|
<!-- ========== 文件列表弹窗 ========== -->
|
||||||
<div id="files-modal" class="modal">
|
<div id="files-modal" class="modal fixed inset-0 bg-black/70 flex items-center justify-center z-50 opacity-0 invisible transition-all duration-300">
|
||||||
<div class="modal-content files-modal-content">
|
<div class="modal-content bg-slate-800 rounded-xl border border-slate-700 w-full max-w-4xl max-h-[90vh] flex flex-col">
|
||||||
<div class="modal-header">
|
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-700">
|
||||||
<h3 id="files-modal-title">文件列表</h3>
|
<h3 id="files-modal-title" class="text-lg font-semibold">文件列表</h3>
|
||||||
<button class="btn-close" id="close-files-modal">×</button>
|
<button id="close-files-modal" class="p-1 hover:bg-slate-700 rounded transition-colors">
|
||||||
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<div class="files-list-container">
|
<div class="grid grid-cols-[80px_1fr_80px_200px_100px] gap-4 px-6 py-3 text-sm text-slate-400 border-b border-slate-700 sticky top-0 bg-slate-800">
|
||||||
<div class="files-list-header">
|
<div>预览</div>
|
||||||
<div class="file-col-thumb">预览</div>
|
<div>文件名</div>
|
||||||
<div class="file-col-name">文件名</div>
|
<div>格式</div>
|
||||||
<div class="file-col-format">格式</div>
|
<div>编码参数</div>
|
||||||
<div class="file-col-codec">编码参数</div>
|
<div>大小</div>
|
||||||
<div class="file-col-size">大小</div>
|
|
||||||
</div>
|
|
||||||
<div class="files-list" id="files-list">
|
|
||||||
<!-- 动态生成 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="files-list" class="divide-y divide-slate-700"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输出文件夹设置弹窗 -->
|
<!-- ========== 输出设置弹窗 ========== -->
|
||||||
<div id="output-modal" class="modal">
|
<div id="output-modal" class="modal fixed inset-0 bg-black/70 flex items-center justify-center z-50 opacity-0 invisible transition-all duration-300">
|
||||||
<div class="modal-content output-modal-content">
|
<div class="modal-content bg-slate-800 rounded-xl border border-slate-700 w-full max-w-lg">
|
||||||
<div class="modal-header">
|
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-700">
|
||||||
<h3>输出设置</h3>
|
<h3 class="text-lg font-semibold">输出设置</h3>
|
||||||
<button class="btn-close" id="close-output-modal">×</button>
|
<button id="close-output-modal" class="p-1 hover:bg-slate-700 rounded transition-colors">
|
||||||
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="p-6 space-y-4">
|
||||||
<div class="output-options">
|
<label class="flex items-center gap-3 p-4 border border-slate-700 rounded-lg cursor-pointer hover:border-primary transition-colors">
|
||||||
<label class="radio-label">
|
<input type="radio" name="output-location" value="same" checked id="output-same" class="w-4 h-4">
|
||||||
<input type="radio" name="output-location" value="same" checked id="output-same">
|
<span>与原文件相同目录(推荐)</span>
|
||||||
<span>与原文件相同目录(推荐)</span>
|
</label>
|
||||||
</label>
|
<label class="flex items-center gap-3 p-4 border border-slate-700 rounded-lg cursor-pointer hover:border-primary transition-colors">
|
||||||
<label class="radio-label">
|
<input type="radio" name="output-location" value="custom" id="output-custom" class="w-4 h-4">
|
||||||
<input type="radio" name="output-location" value="custom" id="output-custom">
|
<span>自定义输出目录</span>
|
||||||
<span>自定义输出目录</span>
|
</label>
|
||||||
</label>
|
<div id="output-folder-section" class="hidden flex gap-2">
|
||||||
</div>
|
<input type="text" id="output-folder-path" placeholder="选择输出文件夹..." readonly class="flex-1 px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg">
|
||||||
<div class="output-folder-input" id="output-folder-section" style="display: none;">
|
<button id="btn-select-output" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg transition-colors">浏览...</button>
|
||||||
<input type="text" id="output-folder-path" placeholder="选择输出文件夹..." readonly>
|
|
||||||
<button class="btn-secondary" id="btn-select-output">浏览...</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-slate-700">
|
||||||
<button class="btn-secondary" id="btn-cancel-output">取消</button>
|
<button id="btn-cancel-output" class="px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors">取消</button>
|
||||||
<button class="btn-primary" id="btn-confirm-output">确定</button>
|
<button id="btn-confirm-output" class="px-4 py-2 rounded-lg bg-primary hover:bg-primary-dark transition-colors">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 设置弹窗 -->
|
<!-- ========== 设置弹窗 ========== -->
|
||||||
<div id="settings-modal" class="modal">
|
<div id="settings-modal" class="modal fixed inset-0 bg-black/70 flex items-center justify-center z-50 opacity-0 invisible transition-all duration-300">
|
||||||
<div class="modal-content settings-modal-content">
|
<div class="modal-content bg-slate-800 rounded-xl border border-slate-700 w-full max-w-3xl max-h-[90vh] flex flex-col">
|
||||||
<div class="modal-header">
|
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-700">
|
||||||
<h3>设置</h3>
|
<h3 class="text-lg font-semibold">设置</h3>
|
||||||
<button class="btn-close" id="close-settings-modal">×</button>
|
<button id="close-settings-modal" class="p-1 hover:bg-slate-700 rounded transition-colors">
|
||||||
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body settings-body">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
<!-- 左侧菜单 -->
|
<div class="w-48 border-r border-slate-700 p-4">
|
||||||
<div class="settings-sidebar">
|
<div class="settings-menu-item active flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-700 transition-colors" data-section="general">
|
||||||
<div class="settings-menu-item active" data-section="general">
|
<i data-lucide="settings" class="w-4 h-4"></i>
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<circle cx="12" cy="12" r="3"/>
|
|
||||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
||||||
</svg>
|
|
||||||
<span>通用</span>
|
<span>通用</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-menu-item" data-section="output">
|
<div class="settings-menu-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-700 transition-colors" data-section="output">
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
<i data-lucide="download" class="w-4 h-4"></i>
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
||||||
<polyline points="7 10 12 15 17 10"/>
|
|
||||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
|
||||||
</svg>
|
|
||||||
<span>输出设置</span>
|
<span>输出设置</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-menu-item" data-section="notification">
|
<div class="settings-menu-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-700 transition-colors" data-section="notification">
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
<i data-lucide="bell" class="w-4 h-4"></i>
|
||||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
|
||||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
|
||||||
</svg>
|
|
||||||
<span>通知</span>
|
<span>通知</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-menu-item" data-section="about">
|
<div class="settings-menu-item flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-700 transition-colors" data-section="about">
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
<i data-lucide="info" class="w-4 h-4"></i>
|
||||||
<circle cx="12" cy="12" r="10"/>
|
|
||||||
<line x1="12" y1="16" x2="12" y2="12"/>
|
|
||||||
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
|
||||||
</svg>
|
|
||||||
<span>关于</span>
|
<span>关于</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-1 p-6 overflow-y-auto">
|
||||||
<!-- 右侧内容区 -->
|
|
||||||
<div class="settings-content">
|
|
||||||
<!-- 通用设置 -->
|
<!-- 通用设置 -->
|
||||||
<div class="settings-section active" data-section="general">
|
<div class="settings-section active space-y-4" data-section="general">
|
||||||
<h4>通用设置</h4>
|
<h4 class="text-lg font-medium mb-4">通用设置</h4>
|
||||||
<div class="setting-item">
|
<label class="flex items-center gap-3">
|
||||||
<label class="checkbox-label">
|
<input type="checkbox" id="setting-open-folder" checked class="w-4 h-4">
|
||||||
<input type="checkbox" id="setting-open-folder" checked>
|
<span>转换完成后自动打开输出文件夹</span>
|
||||||
<span>转换完成后自动打开输出文件夹</span>
|
</label>
|
||||||
</label>
|
<label class="flex items-center gap-3">
|
||||||
</div>
|
<input type="checkbox" id="setting-show-notification" class="w-4 h-4">
|
||||||
<div class="setting-item">
|
<span>显示系统通知</span>
|
||||||
<label class="checkbox-label">
|
</label>
|
||||||
<input type="checkbox" id="setting-show-notification">
|
|
||||||
<span>显示系统通知</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输出设置 -->
|
<!-- 输出设置 -->
|
||||||
<div class="settings-section" data-section="output">
|
<div class="settings-section hidden space-y-4" data-section="output">
|
||||||
<h4>默认输出位置</h4>
|
<h4 class="text-lg font-medium mb-4">默认输出位置</h4>
|
||||||
<div class="setting-item vertical">
|
<label class="flex items-center gap-3">
|
||||||
<label class="radio-label">
|
<input type="radio" name="default-output" value="same" id="setting-output-same" checked class="w-4 h-4">
|
||||||
<input type="radio" name="default-output" value="same" id="setting-output-same" checked>
|
<span>与原文件相同目录(推荐)</span>
|
||||||
<span>与原文件相同目录(推荐)</span>
|
</label>
|
||||||
</label>
|
<label class="flex items-center gap-3">
|
||||||
</div>
|
<input type="radio" name="default-output" value="custom" id="setting-output-custom" class="w-4 h-4">
|
||||||
<div class="setting-item vertical">
|
<span>自定义目录</span>
|
||||||
<label class="radio-label">
|
</label>
|
||||||
<input type="radio" name="default-output" value="custom" id="setting-output-custom">
|
<div id="setting-custom-path-wrapper" class="hidden flex gap-2 ml-7">
|
||||||
<span>自定义目录</span>
|
<input type="text" id="setting-custom-folder" placeholder="点击选择文件夹..." readonly class="flex-1 px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg">
|
||||||
</label>
|
<button id="btn-setting-browse" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg transition-colors">浏览</button>
|
||||||
<div class="setting-custom-path" id="setting-custom-path-wrapper" style="display: none;">
|
|
||||||
<input type="text" id="setting-custom-folder" class="setting-input" placeholder="点击选择文件夹..." readonly>
|
|
||||||
<button class="btn-secondary btn-small" id="btn-setting-browse">浏览</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 通知设置 -->
|
<!-- 通知设置 -->
|
||||||
<div class="settings-section" data-section="notification">
|
<div class="settings-section hidden space-y-4" data-section="notification">
|
||||||
<h4>通知设置</h4>
|
<h4 class="text-lg font-medium mb-4">通知设置</h4>
|
||||||
<div class="setting-item">
|
<label class="flex items-center gap-3">
|
||||||
<label class="checkbox-label">
|
<input type="checkbox" id="setting-notify-complete" checked class="w-4 h-4">
|
||||||
<input type="checkbox" id="setting-notify-complete" checked>
|
<span>转换完成时通知</span>
|
||||||
<span>转换完成时通知</span>
|
</label>
|
||||||
</label>
|
<label class="flex items-center gap-3">
|
||||||
</div>
|
<input type="checkbox" id="setting-notify-error" checked class="w-4 h-4">
|
||||||
<div class="setting-item">
|
<span>转换失败时通知</span>
|
||||||
<label class="checkbox-label">
|
</label>
|
||||||
<input type="checkbox" id="setting-notify-error" checked>
|
|
||||||
<span>转换失败时通知</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 关于 -->
|
<!-- 关于 -->
|
||||||
<div class="settings-section" data-section="about">
|
<div class="settings-section hidden" data-section="about">
|
||||||
<div class="about-content settings-about">
|
<div class="text-center py-8">
|
||||||
<div class="about-logo">
|
<div class="text-primary mb-4">
|
||||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5">
|
<i data-lucide="layers" class="w-16 h-16 mx-auto"></i>
|
||||||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
|
||||||
<path d="M2 17l10 5 10-5"/>
|
|
||||||
<path d="M2 12l10 5 10-5"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 class="about-title">火炬格式转换器</h2>
|
<h2 class="text-2xl font-bold mb-2">火炬格式转换器</h2>
|
||||||
<p class="about-version">版本 2.0.0</p>
|
<p class="text-slate-400 mb-4">版本 2.0.0</p>
|
||||||
<p class="about-desc">简单易用的多媒体格式转换工具,支持视频、音频、图片等 50+ 种格式互相转换。</p>
|
<p class="text-sm text-slate-400 mb-6 max-w-md mx-auto">简单易用的多媒体格式转换工具,支持视频、音频、图片等 50+ 种格式互相转换。</p>
|
||||||
<div class="about-features">
|
<div class="flex items-center justify-center gap-2 mb-6">
|
||||||
<div class="feature-tag">批量转换</div>
|
<span class="px-3 py-1 bg-slate-700 rounded-full text-sm">批量转换</span>
|
||||||
<div class="feature-tag">格式丰富</div>
|
<span class="px-3 py-1 bg-slate-700 rounded-full text-sm">格式丰富</span>
|
||||||
<div class="feature-tag">简洁高效</div>
|
<span class="px-3 py-1 bg-slate-700 rounded-full text-sm">简洁高效</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="https://www.meshel.cn" target="_blank" class="about-link">
|
<a href="https://www.meshel.cn" target="_blank" class="inline-flex items-center gap-2 text-primary hover:underline">
|
||||||
<span>访问官网</span>
|
<span>访问官网</span>
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
<i data-lucide="external-link" class="w-4 h-4"></i>
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
||||||
<polyline points="15 3 21 3 21 9"/>
|
|
||||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
<p class="about-copyright">© 2024-2025 meshel.cn · All rights reserved</p>
|
<p class="text-xs text-slate-500 mt-8">© 2024-2025 meshel.cn · All rights reserved</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-slate-700">
|
||||||
<button class="btn-secondary" id="btn-cancel-settings">取消</button>
|
<button id="btn-cancel-settings" class="px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors">取消</button>
|
||||||
<button class="btn-primary" id="btn-save-settings">保存</button>
|
<button id="btn-save-settings" class="px-4 py-2 rounded-lg bg-primary hover:bg-primary-dark transition-colors">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========== FFmpeg 安装引导弹窗 ========== -->
|
||||||
|
<div id="ffmpeg-install-guide" class="modal fixed inset-0 bg-black/70 flex items-center justify-center z-50 opacity-0 invisible transition-all duration-300">
|
||||||
|
<div class="modal-content bg-slate-800 rounded-xl border border-slate-700 w-full max-w-lg">
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-700">
|
||||||
|
<h3 class="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<span>🔧</span>
|
||||||
|
<span>需要安装 FFmpeg</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 space-y-4">
|
||||||
|
<p class="text-slate-300">FFmpeg 是一个强大的多媒体处理工具,用于视频、音频和图片的格式转换。</p>
|
||||||
|
<p class="text-slate-300">首次使用需要下载并安装 FFmpeg 组件(约 50-100 MB)。</p>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex gap-4 p-4 bg-slate-700/50 rounded-lg border border-slate-600 hover:border-primary transition-colors">
|
||||||
|
<div class="text-3xl">🚀</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-1">自动安装(推荐)</h4>
|
||||||
|
<p class="text-sm text-slate-400">应用将自动下载并安装 FFmpeg,无需手动操作</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4 p-4 bg-slate-700/50 rounded-lg border border-slate-600">
|
||||||
|
<div class="text-3xl">📝</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium mb-1">手动安装</h4>
|
||||||
|
<p class="text-sm text-slate-400">如果您已经安装了 FFmpeg,请确保它在系统 PATH 中,然后重启应用</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start gap-2 p-3 bg-primary/10 border border-primary/20 rounded-lg text-sm">
|
||||||
|
<i data-lucide="info" class="w-4 h-4 text-primary flex-shrink-0 mt-0.5"></i>
|
||||||
|
<span class="text-slate-300">安装过程需要网络连接,请耐心等待</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-slate-700">
|
||||||
|
<button id="btn-skip-install" class="px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors">稍后安装</button>
|
||||||
|
<button id="btn-start-install" class="px-4 py-2 rounded-lg bg-primary hover:bg-primary-dark transition-colors">开始安装</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript" src="main.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
<!-- Tauri 2.0 API 初始化 -->
|
|
||||||
<script>
|
<script>
|
||||||
// 手动初始化 Tauri API
|
// 初始化 Lucide 图标
|
||||||
window.__TAURI_INTERNALS__ = window.__TAURI_INTERNALS__ || {};
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (typeof lucide !== 'undefined') {
|
||||||
// 等待 Tauri 运行时加载
|
lucide.createIcons();
|
||||||
if (!window.__TAURI__) {
|
}
|
||||||
console.log('等待 Tauri 运行时...');
|
});
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
598
src/main.js
598
src/main.js
@@ -180,6 +180,7 @@ async function initFFmpeg() {
|
|||||||
updateEncoderStatus('ready', version || 'FFmpeg 已就绪');
|
updateEncoderStatus('ready', version || 'FFmpeg 已就绪');
|
||||||
} else {
|
} else {
|
||||||
updateEncoderStatus('error', '编码器未安装');
|
updateEncoderStatus('error', '编码器未安装');
|
||||||
|
// 不自动显示安装引导,只在用户尝试转换时提示
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检测 FFmpeg 失败:', error);
|
console.error('检测 FFmpeg 失败:', error);
|
||||||
@@ -187,6 +188,14 @@ async function initFFmpeg() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showFFmpegInstallGuide() {
|
||||||
|
// 显示安装引导弹窗(仅在用户主动操作时调用)
|
||||||
|
const modal = document.getElementById('ffmpeg-install-guide');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateEncoderStatus(status, text) {
|
function updateEncoderStatus(status, text) {
|
||||||
const dot = elements.encoderStatus.querySelector('.status-dot');
|
const dot = elements.encoderStatus.querySelector('.status-dot');
|
||||||
const textEl = elements.encoderStatus.querySelector('.status-text');
|
const textEl = elements.encoderStatus.querySelector('.status-text');
|
||||||
@@ -367,6 +376,23 @@ function setupOtherEventListeners() {
|
|||||||
state.fileGroups = {};
|
state.fileGroups = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FFmpeg 安装引导
|
||||||
|
const btnStartInstall = document.getElementById('btn-start-install');
|
||||||
|
const btnSkipInstall = document.getElementById('btn-skip-install');
|
||||||
|
|
||||||
|
if (btnStartInstall) {
|
||||||
|
btnStartInstall.addEventListener('click', () => {
|
||||||
|
document.getElementById('ffmpeg-install-guide').classList.remove('active');
|
||||||
|
installFFmpeg();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnSkipInstall) {
|
||||||
|
btnSkipInstall.addEventListener('click', () => {
|
||||||
|
document.getElementById('ffmpeg-install-guide').classList.remove('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 模板弹窗
|
// 模板弹窗
|
||||||
elements.btnCloseTemplate.addEventListener('click', closeTemplateModal);
|
elements.btnCloseTemplate.addEventListener('click', closeTemplateModal);
|
||||||
elements.btnCancelTemplate.addEventListener('click', closeTemplateModal);
|
elements.btnCancelTemplate.addEventListener('click', closeTemplateModal);
|
||||||
@@ -440,18 +466,21 @@ async function handleFilePaths(paths) {
|
|||||||
|
|
||||||
state.files = analyzedFiles;
|
state.files = analyzedFiles;
|
||||||
groupFilesByType();
|
groupFilesByType();
|
||||||
renderFileTypeCards();
|
|
||||||
|
// 先隐藏加载动画
|
||||||
|
hideLoading();
|
||||||
|
|
||||||
|
// 然后切换页面并渲染
|
||||||
switchPage('config-page');
|
switchPage('config-page');
|
||||||
|
renderFileTypeCards();
|
||||||
|
|
||||||
// 异步懒加载编码信息和图片(不阻塞 UI)
|
// 异步懒加载编码信息和图片(不阻塞 UI)
|
||||||
lazyLoadCodecInfo();
|
lazyLoadCodecInfo();
|
||||||
lazyLoadImages();
|
lazyLoadImages();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('分析文件失败:', error);
|
console.error('分析文件失败:', error);
|
||||||
alert('分析文件失败: ' + (error.message || error));
|
|
||||||
} finally {
|
|
||||||
// 隐藏加载动画
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
alert('分析文件失败: ' + (error.message || error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,13 +499,12 @@ function groupFilesByType() {
|
|||||||
function renderFileTypeCards() {
|
function renderFileTypeCards() {
|
||||||
elements.fileTypeCards.innerHTML = '';
|
elements.fileTypeCards.innerHTML = '';
|
||||||
|
|
||||||
// SVG 图标定义(Lucide 风格)
|
|
||||||
const typeIcons = {
|
const typeIcons = {
|
||||||
video: `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2" ry="2"/></svg>`,
|
video: 'video',
|
||||||
audio: `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`,
|
audio: 'music',
|
||||||
image: `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
|
image: 'image',
|
||||||
document: `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>`,
|
document: 'file-text',
|
||||||
other: `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>`,
|
other: 'package',
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeNames = {
|
const typeNames = {
|
||||||
@@ -489,7 +517,7 @@ function renderFileTypeCards() {
|
|||||||
|
|
||||||
for (const [type, files] of Object.entries(state.fileGroups)) {
|
for (const [type, files] of Object.entries(state.fileGroups)) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'file-type-card';
|
card.className = 'relative bg-slate-800 rounded-xl overflow-hidden mb-4';
|
||||||
card.dataset.type = type;
|
card.dataset.type = type;
|
||||||
|
|
||||||
// 获取当前设置
|
// 获取当前设置
|
||||||
@@ -516,65 +544,65 @@ function renderFileTypeCards() {
|
|||||||
<div class="card-overlay-text" id="overlay-text-${type}">准备转换...</div>
|
<div class="card-overlay-text" id="overlay-text-${type}">准备转换...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="flex">
|
||||||
<!-- 左侧:原文件信息(70%) -->
|
<!-- 左侧:原文件信息(70%) -->
|
||||||
<div class="card-left">
|
<div class="flex-[7] p-6 border-r border-slate-700">
|
||||||
<div class="card-header-row">
|
<div class="flex items-center gap-4 mb-4">
|
||||||
<div class="card-icon ${type}">${typeIcons[type] || typeIcons.other}</div>
|
<div class="text-primary"><i data-lucide="${typeIcons[type] || typeIcons.other}" class="w-8 h-8"></i></div>
|
||||||
<div class="card-info">
|
<div class="flex-1">
|
||||||
<div class="card-title">${typeNames[type]}</div>
|
<div class="text-lg font-semibold text-slate-100">${typeNames[type]}</div>
|
||||||
<div class="card-count" ${hasMultipleFiles ? 'style="text-decoration: underline;"' : ''}>${files.length} 个文件</div>
|
<div class="text-sm text-slate-400 ${hasMultipleFiles ? 'underline cursor-pointer hover:text-primary' : ''}" data-count="${type}">${files.length} 个文件</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="previews-section">
|
<div class="mb-4">
|
||||||
<div class="previews-grid">
|
<div class="grid grid-cols-3 gap-3">
|
||||||
${renderPreviews(files.slice(0, 3), type)}
|
${renderPreviews(files.slice(0, 3), type)}
|
||||||
${files.length > 3 ? `
|
${files.length > 3 ? `
|
||||||
<button class="view-all-btn" data-type="${type}">
|
<button class="flex flex-col items-center justify-center gap-2 p-4 bg-slate-700 rounded-lg hover:bg-slate-600 transition-colors cursor-pointer" data-view-all="${type}">
|
||||||
<span>+${files.length - 3}</span>
|
<span class="text-2xl font-bold text-primary">+${files.length - 3}</span>
|
||||||
<span>查看全部</span>
|
<span class="text-xs text-slate-300">查看全部</span>
|
||||||
</button>
|
</button>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 左侧底部进度条 -->
|
<!-- 左侧底部进度条 -->
|
||||||
<div class="left-progress" id="progress-${type}" style="display: none;">
|
<div class="hidden" id="progress-${type}">
|
||||||
<div class="left-progress-bar">
|
<div class="w-full bg-slate-700 rounded-full h-2 mb-2">
|
||||||
<div class="left-progress-fill" id="progress-fill-${type}"></div>
|
<div class="bg-primary h-2 rounded-full transition-all duration-300" id="progress-fill-${type}" style="width: 0%"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="left-progress-text" id="progress-text-${type}">准备中...</div>
|
<div class="text-xs text-slate-400" id="progress-text-${type}">准备中...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:目标格式(30%) -->
|
<!-- 右侧:目标格式(30%) -->
|
||||||
<div class="card-right">
|
<div class="flex-[3] p-6 flex flex-col">
|
||||||
<div class="card-right-header">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="card-right-title">输出设置</div>
|
<div class="text-sm font-semibold text-slate-300">输出设置</div>
|
||||||
<button class="btn-more-settings" data-type="${type}" title="更多设置">
|
<button class="p-1 hover:bg-slate-700 rounded transition-colors" data-more="${type}" title="更多设置">
|
||||||
<i data-lucide="more-vertical" class="icon-18"></i>
|
<i data-lucide="more-vertical" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="target-format-section">
|
<div class="mb-4">
|
||||||
<div class="target-format-label">目标格式</div>
|
<div class="text-xs text-slate-400 mb-2">目标格式</div>
|
||||||
<span class="target-format-badge" data-type="${type}">${currentSettings.output_format.toUpperCase()}</span>
|
<span class="inline-block px-3 py-1 bg-primary text-white text-sm font-semibold rounded-lg cursor-pointer hover:bg-blue-600 transition-colors" data-format="${type}">${currentSettings.output_format.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="params-list">
|
<div class="mb-4 flex-1">
|
||||||
${paramsHtml}
|
${paramsHtml}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="output-location-section" data-type="${type}">
|
<div class="mb-4 cursor-pointer hover:bg-slate-700 p-2 rounded transition-colors" data-output="${type}">
|
||||||
<div class="output-location-label">输出位置</div>
|
<div class="text-xs text-slate-400 mb-1">输出位置</div>
|
||||||
<div class="output-location-value" id="output-location-${type}">
|
<div class="text-sm text-slate-300 flex items-center gap-2" id="output-location-${type}">
|
||||||
${getOutputLocationHtml(type)}
|
${getOutputLocationHtml(type)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-actions">
|
<div>
|
||||||
<button class="btn-convert" data-type="${type}" data-task-id="" id="btn-convert-${type}">
|
<button class="w-full px-4 py-2 bg-primary hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors" data-convert="${type}" data-task-id="" id="btn-convert-${type}">
|
||||||
<span class="btn-text">开始转换</span>
|
<span class="btn-text">开始转换</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -585,26 +613,26 @@ function renderFileTypeCards() {
|
|||||||
elements.fileTypeCards.appendChild(card);
|
elements.fileTypeCards.appendChild(card);
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
card.querySelector('.target-format-badge').addEventListener('click', () => openTemplateModal(type));
|
card.querySelector(`[data-format="${type}"]`).addEventListener('click', () => openTemplateModal(type));
|
||||||
|
|
||||||
// 点击文件数量显示文件列表弹窗
|
// 点击文件数量显示文件列表弹窗
|
||||||
const cardCount = card.querySelector('.card-count');
|
const cardCount = card.querySelector(`[data-count="${type}"]`);
|
||||||
if (hasMultipleFiles) {
|
if (hasMultipleFiles) {
|
||||||
cardCount.addEventListener('click', () => openFilesModal(type));
|
cardCount.addEventListener('click', () => openFilesModal(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
card.querySelector('.view-all-btn')?.addEventListener('click', () => openFilesModal(type));
|
card.querySelector(`[data-view-all="${type}"]`)?.addEventListener('click', () => openFilesModal(type));
|
||||||
|
|
||||||
const convertBtn = card.querySelector('.btn-convert');
|
const convertBtn = card.querySelector(`[data-convert="${type}"]`);
|
||||||
convertBtn.addEventListener('click', () => toggleConversion(type, convertBtn));
|
convertBtn.addEventListener('click', () => toggleConversion(type, convertBtn));
|
||||||
|
|
||||||
// 更多设置按钮(三个点)
|
// 更多设置按钮(三个点)
|
||||||
card.querySelector('.btn-more-settings')?.addEventListener('click', () => {
|
card.querySelector(`[data-more="${type}"]`)?.addEventListener('click', () => {
|
||||||
openTemplateModal(type);
|
openTemplateModal(type);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 输出位置点击修改
|
// 输出位置点击修改
|
||||||
card.querySelector('.output-location-section')?.addEventListener('click', () => {
|
card.querySelector(`[data-output="${type}"]`)?.addEventListener('click', () => {
|
||||||
openOutputModal(type);
|
openOutputModal(type);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -629,7 +657,7 @@ function getOutputLocationHtml(type) {
|
|||||||
// 生成参数列表HTML
|
// 生成参数列表HTML
|
||||||
function generateParamsList(params, type) {
|
function generateParamsList(params, type) {
|
||||||
if (!params || Object.keys(params).length === 0) {
|
if (!params || Object.keys(params).length === 0) {
|
||||||
return '<div class="param-item"><span class="param-name">保持原参数</span></div>';
|
return '<div class="text-xs text-slate-400">保持原参数</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
const paramNames = {
|
const paramNames = {
|
||||||
@@ -644,7 +672,7 @@ function generateParamsList(params, type) {
|
|||||||
quality: '质量',
|
quality: '质量',
|
||||||
};
|
};
|
||||||
|
|
||||||
let html = '';
|
let html = '<div class="space-y-2">';
|
||||||
for (const [key, value] of Object.entries(params)) {
|
for (const [key, value] of Object.entries(params)) {
|
||||||
if (value && paramNames[key]) {
|
if (value && paramNames[key]) {
|
||||||
let displayValue = value;
|
let displayValue = value;
|
||||||
@@ -655,23 +683,23 @@ function generateParamsList(params, type) {
|
|||||||
displayValue = value;
|
displayValue = value;
|
||||||
}
|
}
|
||||||
html += `
|
html += `
|
||||||
<div class="param-item">
|
<div class="flex justify-between items-center text-xs">
|
||||||
<span class="param-name">${paramNames[key]}</span>
|
<span class="text-slate-400">${paramNames[key]}</span>
|
||||||
<span class="param-value">${displayValue}</span>
|
<span class="text-slate-200 font-medium">${displayValue}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
return html || '<div class="param-item"><span class="param-name">保持原参数</span></div>';
|
return html || '<div class="text-xs text-slate-400">保持原参数</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPreviews(files, type) {
|
function renderPreviews(files, type) {
|
||||||
// SVG 图标定义
|
|
||||||
const previewIcons = {
|
const previewIcons = {
|
||||||
video: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2"/></svg>`,
|
video: 'video',
|
||||||
audio: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="16" r="2.5"/></svg>`,
|
audio: 'music',
|
||||||
image: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.5"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
|
image: 'image',
|
||||||
};
|
};
|
||||||
|
|
||||||
return files.map(file => {
|
return files.map(file => {
|
||||||
@@ -679,16 +707,16 @@ function renderPreviews(files, type) {
|
|||||||
if (type === 'image' && file.path) {
|
if (type === 'image' && file.path) {
|
||||||
// 如果已经有缓存的图片数据,直接使用
|
// 如果已经有缓存的图片数据,直接使用
|
||||||
if (file.imageData) {
|
if (file.imageData) {
|
||||||
return `<div class="preview-item" data-file-id="${file.id}"><img src="${file.imageData}" alt="${file.name}" loading="lazy"></div>`;
|
return `<div class="aspect-square bg-slate-700 rounded-lg overflow-hidden" data-file-id="${file.id}"><img src="${file.imageData}" alt="${file.name}" loading="lazy" class="w-full h-full object-cover"></div>`;
|
||||||
}
|
}
|
||||||
// 否则显示占位符,稍后异步加载
|
// 否则显示占位符,稍后异步加载
|
||||||
const icon = previewIcons.image;
|
const icon = previewIcons.image;
|
||||||
return `<div class="preview-item image" data-file-id="${file.id}" data-loading="true">${icon}</div>`;
|
return `<div class="aspect-square bg-slate-700 rounded-lg flex items-center justify-center text-slate-400" data-file-id="${file.id}" data-loading="true"><i data-lucide="${icon}" class="w-8 h-8"></i></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视频和音频:只显示图标,不生成缩略图
|
// 视频和音频:只显示图标,不生成缩略图
|
||||||
const icon = previewIcons[type] || previewIcons.image;
|
const icon = previewIcons[type] || 'image';
|
||||||
return `<div class="preview-item ${type}" data-file-id="${file.id}">${icon}</div>`;
|
return `<div class="aspect-square bg-slate-700 rounded-lg flex items-center justify-center text-slate-400" data-file-id="${file.id}"><i data-lucide="${icon}" class="w-8 h-8"></i></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,6 +808,11 @@ function renderTemplateList() {
|
|||||||
|
|
||||||
// 渲染当前设置的配置界面
|
// 渲染当前设置的配置界面
|
||||||
renderTemplateConfig();
|
renderTemplateConfig();
|
||||||
|
|
||||||
|
// 初始化 Lucide 图标
|
||||||
|
if (typeof lucide !== 'undefined') {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将模板参数应用到当前设置
|
// 将模板参数应用到当前设置
|
||||||
@@ -1016,96 +1049,46 @@ function renderTemplateConfig() {
|
|||||||
configHTML += `</div>`;
|
configHTML += `</div>`;
|
||||||
elements.templateConfig.innerHTML = configHTML;
|
elements.templateConfig.innerHTML = configHTML;
|
||||||
|
|
||||||
// 绑定复选框事件
|
|
||||||
bindCheckboxEvents(type);
|
|
||||||
// 绑定参数变更事件
|
// 绑定参数变更事件
|
||||||
bindParamChangeEvents(type);
|
bindParamChangeEvents(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定复选框事件
|
// 统一的参数绑定(合并复选框和参数变更)
|
||||||
function bindCheckboxEvents(type) {
|
|
||||||
const checkboxMap = {
|
|
||||||
video: ['enable-video-codec', 'enable-audio-codec', 'enable-resolution', 'enable-bitrate', 'enable-frame-rate', 'enable-audio-bitrate'],
|
|
||||||
audio: ['enable-audio-codec', 'enable-audio-bitrate', 'enable-sample-rate', 'enable-channels'],
|
|
||||||
image: ['enable-quality']
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkboxes = checkboxMap[type] || [];
|
|
||||||
|
|
||||||
checkboxes.forEach(checkboxId => {
|
|
||||||
const checkbox = document.getElementById(checkboxId);
|
|
||||||
if (checkbox) {
|
|
||||||
checkbox.addEventListener('change', (e) => {
|
|
||||||
const paramName = checkboxId.replace('enable-', '').replace(/-/g, '_');
|
|
||||||
const selectId = checkboxId.replace('enable-', 'cfg-');
|
|
||||||
const select = document.getElementById(selectId);
|
|
||||||
|
|
||||||
if (select) {
|
|
||||||
select.disabled = !e.target.checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新当前设置
|
|
||||||
const type = state.currentFileType;
|
|
||||||
if (!state.currentSettings[type].params) {
|
|
||||||
state.currentSettings[type].params = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target.checked) {
|
|
||||||
// 启用参数,设置默认值
|
|
||||||
const defaultValue = select ? select.value : null;
|
|
||||||
if (defaultValue) {
|
|
||||||
state.currentSettings[type].params[paramName] = defaultValue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 禁用参数,删除该参数
|
|
||||||
delete state.currentSettings[type].params[paramName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定参数变更事件
|
|
||||||
function bindParamChangeEvents(type) {
|
function bindParamChangeEvents(type) {
|
||||||
const paramIds = {
|
const config = elements.templateConfig;
|
||||||
video: ['cfg-format', 'cfg-video-codec', 'cfg-audio-codec', 'cfg-resolution', 'cfg-bitrate', 'cfg-frame-rate', 'cfg-audio-bitrate'],
|
|
||||||
audio: ['cfg-format', 'cfg-audio-codec', 'cfg-audio-bitrate', 'cfg-sample-rate', 'cfg-channels'],
|
|
||||||
image: ['cfg-format', 'cfg-quality']
|
|
||||||
};
|
|
||||||
|
|
||||||
const ids = paramIds[type] || [];
|
// 使用事件委托处理所有复选框
|
||||||
|
config.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', (e) => {
|
||||||
|
const paramName = e.target.id.replace('enable-', '').replace(/-/g, '_');
|
||||||
|
const select = document.getElementById(e.target.id.replace('enable-', 'cfg-'));
|
||||||
|
if (select) select.disabled = !e.target.checked;
|
||||||
|
|
||||||
ids.forEach(id => {
|
if (!state.currentSettings[type].params) state.currentSettings[type].params = {};
|
||||||
const element = document.getElementById(id);
|
|
||||||
if (element) {
|
|
||||||
element.addEventListener('change', (e) => {
|
|
||||||
const paramName = id.replace('cfg-', '').replace(/-/g, '_');
|
|
||||||
const type = state.currentFileType;
|
|
||||||
|
|
||||||
if (id === 'cfg-format') {
|
if (e.target.checked && select) {
|
||||||
// 输出格式特殊处理
|
state.currentSettings[type].params[paramName] = paramName === 'quality' ? parseInt(select.value) : select.value;
|
||||||
state.currentSettings[type].output_format = e.target.value;
|
} else {
|
||||||
} else {
|
delete state.currentSettings[type].params[paramName];
|
||||||
// 其他参数
|
}
|
||||||
if (!state.currentSettings[type].params) {
|
});
|
||||||
state.currentSettings[type].params = {};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// 检查对应的复选框是否选中
|
// 使用事件委托处理所有下拉框
|
||||||
const checkboxId = 'enable-' + id.replace('cfg-', '');
|
config.querySelectorAll('select').forEach(select => {
|
||||||
const checkbox = document.getElementById(checkboxId);
|
select.addEventListener('change', (e) => {
|
||||||
|
const paramName = e.target.id.replace('cfg-', '').replace(/-/g, '_');
|
||||||
|
|
||||||
if (!checkbox || checkbox.checked) {
|
if (e.target.id === 'cfg-format') {
|
||||||
// 处理数字类型
|
state.currentSettings[type].output_format = e.target.value;
|
||||||
if (paramName === 'quality') {
|
} else {
|
||||||
state.currentSettings[type].params[paramName] = parseInt(e.target.value);
|
if (!state.currentSettings[type].params) state.currentSettings[type].params = {};
|
||||||
} else {
|
const checkbox = document.getElementById('enable-' + e.target.id.replace('cfg-', ''));
|
||||||
state.currentSettings[type].params[paramName] = e.target.value;
|
if (!checkbox || checkbox.checked) {
|
||||||
}
|
state.currentSettings[type].params[paramName] = paramName === 'quality' ? parseInt(e.target.value) : e.target.value;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,9 +1152,9 @@ function openFilesModal(fileType) {
|
|||||||
|
|
||||||
// 文件列表中的 SVG 图标
|
// 文件列表中的 SVG 图标
|
||||||
const fileListIcons = {
|
const fileListIcons = {
|
||||||
video: `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2"/></svg>`,
|
video: 'video',
|
||||||
audio: `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="16" r="2.5"/></svg>`,
|
audio: 'music',
|
||||||
image: `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
|
image: 'image',
|
||||||
};
|
};
|
||||||
|
|
||||||
elements.filesList.innerHTML = files.map(file => {
|
elements.filesList.innerHTML = files.map(file => {
|
||||||
@@ -1188,8 +1171,8 @@ function openFilesModal(fileType) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 视频和音频:只显示图标,不生成缩略图
|
// 视频和音频:只显示图标,不生成缩略图
|
||||||
const icon = fileListIcons[fileType] || fileListIcons.image;
|
const icon = fileListIcons[fileType] || 'image';
|
||||||
thumbnail = `<span class="file-list-icon ${fileType}">${icon}</span>`;
|
thumbnail = `<span class="file-list-icon ${fileType}"><i data-lucide="${icon}" class="w-6 h-6"></i></span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编码信息显示(如果还没加载,显示加载中)
|
// 编码信息显示(如果还没加载,显示加载中)
|
||||||
@@ -1302,15 +1285,32 @@ function closeSettingsModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function switchSettingsTab(section) {
|
function switchSettingsTab(section) {
|
||||||
|
console.log('切换设置标签到:', section);
|
||||||
|
|
||||||
// 更新菜单项状态
|
// 更新菜单项状态
|
||||||
elements.settingMenuItems.forEach(item => {
|
elements.settingMenuItems.forEach(item => {
|
||||||
item.classList.toggle('active', item.dataset.section === section);
|
if (item.dataset.section === section) {
|
||||||
|
item.classList.add('active');
|
||||||
|
} else {
|
||||||
|
item.classList.remove('active');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新内容区域显示
|
// 更新内容区域显示
|
||||||
elements.settingSections.forEach(sec => {
|
elements.settingSections.forEach(sec => {
|
||||||
sec.classList.toggle('active', sec.dataset.section === section);
|
if (sec.dataset.section === section) {
|
||||||
|
sec.classList.remove('hidden');
|
||||||
|
sec.classList.add('active');
|
||||||
|
} else {
|
||||||
|
sec.classList.add('hidden');
|
||||||
|
sec.classList.remove('active');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始化 Lucide 图标
|
||||||
|
if (typeof lucide !== 'undefined') {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSettingsCustomFolder() {
|
function toggleSettingsCustomFolder() {
|
||||||
@@ -1329,6 +1329,26 @@ async function selectSettingsCustomFolder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 设置管理 ============
|
||||||
|
const DEFAULT_SETTINGS = {
|
||||||
|
defaultOutput: 'same',
|
||||||
|
customFolder: '',
|
||||||
|
openFolder: true,
|
||||||
|
showNotification: false,
|
||||||
|
notifyComplete: true,
|
||||||
|
notifyError: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadAppSettings() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('torch_converter_settings');
|
||||||
|
return saved ? { ...DEFAULT_SETTINGS, ...JSON.parse(saved) } : DEFAULT_SETTINGS;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载设置失败:', e);
|
||||||
|
return DEFAULT_SETTINGS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
const settings = {
|
const settings = {
|
||||||
defaultOutput: elements.settingOutputCustom.checked ? 'custom' : 'same',
|
defaultOutput: elements.settingOutputCustom.checked ? 'custom' : 'same',
|
||||||
@@ -1339,57 +1359,22 @@ function saveSettings() {
|
|||||||
notifyError: elements.settingNotifyError.checked,
|
notifyError: elements.settingNotifyError.checked,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存到 localStorage
|
|
||||||
localStorage.setItem('torch_converter_settings', JSON.stringify(settings));
|
localStorage.setItem('torch_converter_settings', JSON.stringify(settings));
|
||||||
|
|
||||||
// 应用设置
|
|
||||||
applySettings(settings);
|
applySettings(settings);
|
||||||
|
|
||||||
closeSettingsModal();
|
closeSettingsModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAppSettings() {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem('torch_converter_settings');
|
|
||||||
if (saved) {
|
|
||||||
return JSON.parse(saved);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('加载设置失败:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认设置
|
|
||||||
return {
|
|
||||||
defaultOutput: 'same',
|
|
||||||
customFolder: '',
|
|
||||||
openFolder: true,
|
|
||||||
showNotification: false,
|
|
||||||
notifyComplete: true,
|
|
||||||
notifyError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySettings(settings) {
|
function applySettings(settings) {
|
||||||
// 应用默认输出设置到所有文件类型
|
|
||||||
if (settings.defaultOutput === 'custom' && settings.customFolder) {
|
if (settings.defaultOutput === 'custom' && settings.customFolder) {
|
||||||
Object.keys(state.outputSettings).forEach(type => {
|
Object.keys(state.outputSettings).forEach(type => {
|
||||||
state.outputSettings[type] = {
|
state.outputSettings[type] = { useSameAsSource: false, customPath: settings.customFolder };
|
||||||
useSameAsSource: false,
|
|
||||||
customPath: settings.customFolder
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (state.files.length > 0) renderFileTypeCards();
|
||||||
// 重新渲染卡片以反映新设置
|
|
||||||
if (state.files.length > 0) {
|
|
||||||
renderFileTypeCards();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化设置
|
|
||||||
function initSettings() {
|
function initSettings() {
|
||||||
const settings = loadAppSettings();
|
applySettings(loadAppSettings());
|
||||||
applySettings(settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmOutputSettings() {
|
function confirmOutputSettings() {
|
||||||
@@ -1424,11 +1409,6 @@ function updateOutputLocationDisplay(type) {
|
|||||||
} else {
|
} else {
|
||||||
locationEl.innerHTML = `<i data-lucide="folder" class="icon-14"></i><span>与原文件相同目录</span>`;
|
locationEl.innerHTML = `<i data-lucide="folder" class="icon-14"></i><span>与原文件相同目录</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新初始化 Lucide 图标
|
|
||||||
if (typeof lucide !== 'undefined') {
|
|
||||||
lucide.createIcons();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 转换控制 ============
|
// ============ 转换控制 ============
|
||||||
@@ -1524,64 +1504,33 @@ async function toggleConversion(fileType, btn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 左侧进度条控制
|
// 统一的UI控制函数
|
||||||
function showLeftProgress(fileType) {
|
function toggleElement(id, show, className = 'hidden') {
|
||||||
const progress = document.getElementById(`progress-${fileType}`);
|
const el = document.getElementById(id);
|
||||||
if (progress) {
|
if (el) el.classList.toggle(className, !show);
|
||||||
progress.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideLeftProgress(fileType) {
|
function updateProgress(fileType, progress, message) {
|
||||||
const progress = document.getElementById(`progress-${fileType}`);
|
|
||||||
if (progress) {
|
|
||||||
progress.style.display = 'none';
|
|
||||||
}
|
|
||||||
// 重置进度
|
|
||||||
const fill = document.getElementById(`progress-fill-${fileType}`);
|
|
||||||
if (fill) {
|
|
||||||
fill.style.width = '0%';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLeftProgress(fileType, progress, message) {
|
|
||||||
const fill = document.getElementById(`progress-fill-${fileType}`);
|
const fill = document.getElementById(`progress-fill-${fileType}`);
|
||||||
const text = document.getElementById(`progress-text-${fileType}`);
|
const text = document.getElementById(`progress-text-${fileType}`);
|
||||||
|
if (fill) fill.style.width = `${progress}%`;
|
||||||
if (fill) {
|
if (text && message) text.textContent = message;
|
||||||
fill.style.width = `${progress}%`;
|
|
||||||
}
|
|
||||||
if (text && message) {
|
|
||||||
text.textContent = message;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 玻璃覆盖层控制
|
function updateOverlay(fileType, text, show) {
|
||||||
function showOverlay(fileType, text) {
|
|
||||||
const overlay = document.getElementById(`overlay-${fileType}`);
|
const overlay = document.getElementById(`overlay-${fileType}`);
|
||||||
const overlayText = document.getElementById(`overlay-text-${fileType}`);
|
const overlayText = document.getElementById(`overlay-text-${fileType}`);
|
||||||
|
if (overlay) overlay.classList.toggle('active', show);
|
||||||
if (overlay) {
|
if (overlayText && text) overlayText.textContent = text;
|
||||||
overlay.classList.add('active');
|
|
||||||
}
|
|
||||||
if (overlayText && text) {
|
|
||||||
overlayText.textContent = text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideOverlay(fileType) {
|
// 兼容旧函数名
|
||||||
const overlay = document.getElementById(`overlay-${fileType}`);
|
const showLeftProgress = (type) => toggleElement(`progress-${type}`, true);
|
||||||
if (overlay) {
|
const hideLeftProgress = (type) => { toggleElement(`progress-${type}`, false); updateProgress(type, 0); };
|
||||||
overlay.classList.remove('active');
|
const updateLeftProgress = updateProgress;
|
||||||
}
|
const showOverlay = (type, text) => updateOverlay(type, text, true);
|
||||||
}
|
const hideOverlay = (type) => updateOverlay(type, '', false);
|
||||||
|
const updateOverlayText = (type, text) => updateOverlay(type, text, true);
|
||||||
function updateOverlayText(fileType, text) {
|
|
||||||
const overlayText = document.getElementById(`overlay-text-${fileType}`);
|
|
||||||
if (overlayText && text) {
|
|
||||||
overlayText.textContent = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 工具函数 ============
|
// ============ 工具函数 ============
|
||||||
function formatFileSize(bytes) {
|
function formatFileSize(bytes) {
|
||||||
@@ -1636,142 +1585,59 @@ async function loadTemplates() {
|
|||||||
// 图片文件直接显示原图,无需生成缩略图
|
// 图片文件直接显示原图,无需生成缩略图
|
||||||
// 这样可以大幅提升加载速度,特别是处理大量文件时
|
// 这样可以大幅提升加载速度,特别是处理大量文件时
|
||||||
|
|
||||||
// ============ 懒加载编码信息 ============
|
// ============ 统一的懒加载系统 ============
|
||||||
|
async function batchLoad(items, loadFn, concurrency = 5) {
|
||||||
|
if (!items.length) return;
|
||||||
|
console.log(`批量加载 ${items.length} 项,并发数: ${concurrency}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i += concurrency) {
|
||||||
|
await Promise.all(items.slice(i, i + concurrency).map(loadFn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function lazyLoadCodecInfo() {
|
async function lazyLoadCodecInfo() {
|
||||||
console.log('开始懒加载编码信息...');
|
const filesToLoad = state.files.filter(f => !f.codec_info && ['video', 'audio', 'image'].includes(f.file_type));
|
||||||
|
await batchLoad(filesToLoad, async (file) => {
|
||||||
// 需要获取编码信息的文件
|
try {
|
||||||
const filesToLoad = state.files.filter(file =>
|
file.codec_info = await window.tauriInvoke('get_file_info', { path: file.path, fileType: file.file_type });
|
||||||
!file.codec_info && (file.file_type === 'video' || file.file_type === 'audio' || file.file_type === 'image')
|
document.querySelectorAll(`.file-list-item[data-file-id="${file.id}"] .file-list-codec`).forEach(el => {
|
||||||
);
|
const info = getCodecInfoTextForFile(file);
|
||||||
|
el.textContent = el.title = info;
|
||||||
if (filesToLoad.length === 0) {
|
});
|
||||||
console.log('没有需要加载的编码信息');
|
console.log(`✅ 编码信息: ${file.name}`);
|
||||||
return;
|
} catch (e) { console.warn(`⚠️ 编码信息失败: ${file.name}`, e); }
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`需要加载 ${filesToLoad.length} 个文件的编码信息`);
|
|
||||||
|
|
||||||
// 并发加载编码信息(限制并发数为 5,比缩略图可以更多)
|
|
||||||
const concurrency = 5;
|
|
||||||
for (let i = 0; i < filesToLoad.length; i += concurrency) {
|
|
||||||
const batch = filesToLoad.slice(i, i + concurrency);
|
|
||||||
await Promise.all(batch.map(file => loadSingleCodecInfo(file)));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('所有编码信息加载完成');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSingleCodecInfo(file) {
|
|
||||||
try {
|
|
||||||
const codecInfo = await window.tauriInvoke('get_file_info', {
|
|
||||||
path: file.path,
|
|
||||||
fileType: file.file_type
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新文件对象
|
|
||||||
file.codec_info = codecInfo;
|
|
||||||
|
|
||||||
// 更新 DOM 显示(如果文件列表弹窗已打开)
|
|
||||||
updateCodecInfoInDOM(file);
|
|
||||||
|
|
||||||
console.log(`✅ 编码信息加载成功: ${file.name}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`⚠️ 编码信息加载失败: ${file.name}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCodecInfoInDOM(file) {
|
|
||||||
// 查找文件列表中对应的元素
|
|
||||||
const fileListItems = document.querySelectorAll(`.file-list-item[data-file-id="${file.id}"]`);
|
|
||||||
fileListItems.forEach(item => {
|
|
||||||
const codecEl = item.querySelector('.file-list-codec');
|
|
||||||
if (codecEl && file.codec_info) {
|
|
||||||
const codecInfo = getCodecInfoTextForFile(file);
|
|
||||||
codecEl.textContent = codecInfo;
|
|
||||||
codecEl.title = codecInfo;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为指定的文件列表加载编码信息
|
|
||||||
async function loadCodecInfoForFiles(files) {
|
async function loadCodecInfoForFiles(files) {
|
||||||
const filesToLoad = files.filter(file => !file.codec_info);
|
await batchLoad(files.filter(f => !f.codec_info), async (file) => {
|
||||||
|
try {
|
||||||
if (filesToLoad.length === 0) {
|
file.codec_info = await window.tauriInvoke('get_file_info', { path: file.path, fileType: file.file_type });
|
||||||
return;
|
document.querySelectorAll(`.file-list-item[data-file-id="${file.id}"] .file-list-codec`).forEach(el => {
|
||||||
}
|
const info = getCodecInfoTextForFile(file);
|
||||||
|
el.textContent = el.title = info;
|
||||||
console.log(`为文件列表加载 ${filesToLoad.length} 个编码信息`);
|
});
|
||||||
|
} catch (e) { console.warn(`⚠️ ${file.name}`, e); }
|
||||||
// 并发加载
|
});
|
||||||
const concurrency = 5;
|
|
||||||
for (let i = 0; i < filesToLoad.length; i += concurrency) {
|
|
||||||
const batch = filesToLoad.slice(i, i + concurrency);
|
|
||||||
await Promise.all(batch.map(file => loadSingleCodecInfo(file)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 懒加载图片 ============
|
|
||||||
async function lazyLoadImages() {
|
async function lazyLoadImages() {
|
||||||
console.log('开始懒加载图片...');
|
const imagesToLoad = state.files.filter(f => f.file_type === 'image' && !f.imageData);
|
||||||
|
await batchLoad(imagesToLoad, async (file) => {
|
||||||
// 需要加载的图片文件
|
try {
|
||||||
const imagesToLoad = state.files.filter(file =>
|
file.imageData = await window.tauriInvoke('read_image_as_base64', { path: file.path });
|
||||||
file.file_type === 'image' && !file.imageData
|
document.querySelectorAll(`[data-file-id="${file.id}"]`).forEach(el => {
|
||||||
);
|
if (el.dataset.loading === 'true') {
|
||||||
|
el.innerHTML = `<img src="${file.imageData}" alt="" loading="lazy">`;
|
||||||
if (imagesToLoad.length === 0) {
|
el.dataset.loading = 'false';
|
||||||
console.log('没有需要加载的图片');
|
}
|
||||||
return;
|
el.querySelectorAll('.file-list-thumbnail').forEach(thumb => {
|
||||||
}
|
thumb.innerHTML = `<img src="${file.imageData}" alt="" loading="lazy">`;
|
||||||
|
});
|
||||||
console.log(`需要加载 ${imagesToLoad.length} 个图片`);
|
});
|
||||||
|
console.log(`✅ 图片: ${file.name}`);
|
||||||
// 并发加载图片(限制并发数为 3)
|
} catch (e) { console.warn(`⚠️ 图片失败: ${file.name}`, e); }
|
||||||
const concurrency = 3;
|
}, 3);
|
||||||
for (let i = 0; i < imagesToLoad.length; i += concurrency) {
|
|
||||||
const batch = imagesToLoad.slice(i, i + concurrency);
|
|
||||||
await Promise.all(batch.map(file => loadSingleImage(file)));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('所有图片加载完成');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSingleImage(file) {
|
|
||||||
try {
|
|
||||||
const imageData = await window.tauriInvoke('read_image_as_base64', {
|
|
||||||
path: file.path
|
|
||||||
});
|
|
||||||
|
|
||||||
// 缓存图片数据
|
|
||||||
file.imageData = imageData;
|
|
||||||
|
|
||||||
// 更新 DOM 显示
|
|
||||||
updateImageInDOM(file.id, imageData);
|
|
||||||
|
|
||||||
console.log(`✅ 图片加载成功: ${file.name}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`⚠️ 图片加载失败: ${file.name}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateImageInDOM(fileId, imageData) {
|
|
||||||
// 更新预览网格中的图片
|
|
||||||
const previewItems = document.querySelectorAll(`.preview-item[data-file-id="${fileId}"]`);
|
|
||||||
previewItems.forEach(item => {
|
|
||||||
if (item.dataset.loading === 'true') {
|
|
||||||
item.innerHTML = `<img src="${imageData}" alt="" loading="lazy">`;
|
|
||||||
item.dataset.loading = 'false';
|
|
||||||
item.classList.remove('image');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新文件列表弹窗中的图片
|
|
||||||
const fileListItems = document.querySelectorAll(`.file-list-item[data-file-id="${fileId}"] .file-list-thumbnail`);
|
|
||||||
fileListItems.forEach(item => {
|
|
||||||
item.innerHTML = `<img src="${imageData}" alt="" loading="lazy">`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCodecInfoTextForFile(file) {
|
function getCodecInfoTextForFile(file) {
|
||||||
|
|||||||
1761
src/style.css
1761
src/style.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user