修改了readme文件

This commit is contained in:
2026-02-06 12:39:11 +06:00
parent ff4b6a149a
commit d7cfb044d5
20 changed files with 26378 additions and 1 deletions

338
src/index.html Normal file
View File

@@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>火炬格式转换器 - Format Converter 0.1.0</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<!-- 首页 -->
<div id="home-page" class="page active">
<!-- 加载遮罩层 -->
<div id="loading-overlay" class="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-text">正在分析文件...</div>
<div class="loading-subtext">生成缩略图中,请稍候</div>
</div>
<div class="home-content">
<!-- 拖放区域 -->
<div class="drop-zone" id="drop-zone">
<div class="drop-icon">
<svg viewBox="0 0 24 24" width="64" height="64">
<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"/>
</svg>
</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 class="formats-info">
<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 class="status-bar">
<div class="status-left">
<span id="encoder-status" class="status-indicator loading" title="">
<span class="status-dot"></span>
<span class="status-text">检测编码器...</span>
</span>
</div>
<div class="status-right">
<a href="https://www.meshel.cn" target="_blank" class="copyright-link">
©️ meshel.cn
</a>
</div>
</div>
</div>
<!-- FFmpeg 安装页面 -->
<div id="install-page" class="page">
<div class="install-content">
<div class="install-icon">📦</div>
<h2>正在安装 FFmpeg</h2>
<p class="install-desc">首次运行需要下载编码器组件</p>
<div class="install-progress">
<div class="progress-bar">
<div class="progress-fill" id="install-progress-fill"></div>
</div>
<span class="progress-text" id="install-progress-text">0%</span>
</div>
<p class="install-message" id="install-message">准备下载...</p>
</div>
</div>
<!-- 参数配置页面 -->
<div id="config-page" class="page">
<div class="config-header">
<button class="btn-back" id="btn-back">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
</svg>
返回
</button>
<h1>转换设置</h1>
<div class="header-actions">
<button class="btn-icon" id="btn-settings" title="设置">
<svg viewBox="0 0 24 24" width="20" height="20">
<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"/>
</svg>
</button>
</div>
</div>
<div class="config-content">
<!-- 文件类型卡片列表 -->
<div class="file-type-cards" id="file-type-cards">
<!-- 动态生成 -->
</div>
</div>
</div>
<!-- 模板选择弹窗 -->
<div id="template-modal" class="modal">
<div class="modal-content template-modal-content">
<div class="modal-header">
<h3>选择输出格式</h3>
<button class="btn-close" id="close-template-modal">&times;</button>
</div>
<div class="modal-body">
<div class="template-layout">
<div class="template-sidebar">
<div class="template-list" id="template-list">
<!-- 动态生成 -->
</div>
<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 class="modal-footer">
<button class="btn-secondary" id="btn-cancel-template">取消</button>
<button class="btn-primary" id="btn-confirm-template">确定</button>
</div>
</div>
</div>
<!-- 文件列表弹窗 -->
<div id="files-modal" class="modal">
<div class="modal-content files-modal-content">
<div class="modal-header">
<h3 id="files-modal-title">文件列表</h3>
<button class="btn-close" id="close-files-modal">&times;</button>
</div>
<div class="modal-body">
<div class="files-list-container">
<div class="files-list-header">
<div class="file-col-thumb">预览</div>
<div class="file-col-name">文件名</div>
<div class="file-col-format">格式</div>
<div class="file-col-codec">编码参数</div>
<div class="file-col-size">大小</div>
</div>
<div class="files-list" id="files-list">
<!-- 动态生成 -->
</div>
</div>
</div>
</div>
</div>
<!-- 输出文件夹设置弹窗 -->
<div id="output-modal" class="modal">
<div class="modal-content output-modal-content">
<div class="modal-header">
<h3>输出设置</h3>
<button class="btn-close" id="close-output-modal">&times;</button>
</div>
<div class="modal-body">
<div class="output-options">
<label class="radio-label">
<input type="radio" name="output-location" value="same" checked id="output-same">
<span>与原文件相同目录(推荐)</span>
</label>
<label class="radio-label">
<input type="radio" name="output-location" value="custom" id="output-custom">
<span>自定义输出目录</span>
</label>
</div>
<div class="output-folder-input" id="output-folder-section" style="display: none;">
<input type="text" id="output-folder-path" placeholder="选择输出文件夹..." readonly>
<button class="btn-secondary" id="btn-select-output">浏览...</button>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" id="btn-cancel-output">取消</button>
<button class="btn-primary" id="btn-confirm-output">确定</button>
</div>
</div>
</div>
<!-- 设置弹窗 -->
<div id="settings-modal" class="modal">
<div class="modal-content settings-modal-content">
<div class="modal-header">
<h3>设置</h3>
<button class="btn-close" id="close-settings-modal">&times;</button>
</div>
<div class="modal-body settings-body">
<!-- 左侧菜单 -->
<div class="settings-sidebar">
<div class="settings-menu-item active" data-section="general">
<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>
</div>
<div class="settings-menu-item" data-section="output">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
<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>
</div>
<div class="settings-menu-item" data-section="notification">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
<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>
</div>
<div class="settings-menu-item" data-section="about">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
<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>
</div>
</div>
<!-- 右侧内容区 -->
<div class="settings-content">
<!-- 通用设置 -->
<div class="settings-section active" data-section="general">
<h4>通用设置</h4>
<div class="setting-item">
<label class="checkbox-label">
<input type="checkbox" id="setting-open-folder" checked>
<span>转换完成后自动打开输出文件夹</span>
</label>
</div>
<div class="setting-item">
<label class="checkbox-label">
<input type="checkbox" id="setting-show-notification">
<span>显示系统通知</span>
</label>
</div>
</div>
<!-- 输出设置 -->
<div class="settings-section" data-section="output">
<h4>默认输出位置</h4>
<div class="setting-item vertical">
<label class="radio-label">
<input type="radio" name="default-output" value="same" id="setting-output-same" checked>
<span>与原文件相同目录(推荐)</span>
</label>
</div>
<div class="setting-item vertical">
<label class="radio-label">
<input type="radio" name="default-output" value="custom" id="setting-output-custom">
<span>自定义目录</span>
</label>
<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 class="settings-section" data-section="notification">
<h4>通知设置</h4>
<div class="setting-item">
<label class="checkbox-label">
<input type="checkbox" id="setting-notify-complete" checked>
<span>转换完成时通知</span>
</label>
</div>
<div class="setting-item">
<label class="checkbox-label">
<input type="checkbox" id="setting-notify-error" checked>
<span>转换失败时通知</span>
</label>
</div>
</div>
<!-- 关于 -->
<div class="settings-section" data-section="about">
<div class="about-content settings-about">
<div class="about-logo">
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5">
<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>
<h2 class="about-title">火炬格式转换器</h2>
<p class="about-version">版本 2.0.0</p>
<p class="about-desc">简单易用的多媒体格式转换工具,支持视频、音频、图片等 50+ 种格式互相转换。</p>
<div class="about-features">
<div class="feature-tag">批量转换</div>
<div class="feature-tag">格式丰富</div>
<div class="feature-tag">简洁高效</div>
</div>
<a href="https://www.meshel.cn" target="_blank" class="about-link">
<span>访问官网</span>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<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>
<p class="about-copyright">© 2024-2025 meshel.cn · All rights reserved</p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" id="btn-cancel-settings">取消</button>
<button class="btn-primary" id="btn-save-settings">保存</button>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="main.js"></script>
<!-- Tauri 2.0 API 初始化 -->
<script>
// 手动初始化 Tauri API
window.__TAURI_INTERNALS__ = window.__TAURI_INTERNALS__ || {};
// 等待 Tauri 运行时加载
if (!window.__TAURI__) {
console.log('等待 Tauri 运行时...');
}
</script>
</body>
</html>

1637
src/main.js Normal file

File diff suppressed because it is too large Load Diff

1747
src/style.css Normal file

File diff suppressed because it is too large Load Diff

123
src/thumbnail-helper.js Normal file
View File

@@ -0,0 +1,123 @@
/**
* 缩略图生成辅助函数
* 当后端 FFmpeg 生成失败时,使用浏览器原生能力作为兜底
*/
/**
* 使用浏览器原生 API 生成视频缩略图
* @param {File} file - 视频文件对象
* @param {number} seekTime - 截取时间点(秒)
* @returns {Promise<string>} base64 图片数据
*/
export async function generateVideoThumbnailNative(file, seekTime = 0.1) {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置视频源
const url = URL.createObjectURL(file);
video.src = url;
video.muted = true;
video.playsInline = true;
video.addEventListener('loadeddata', () => {
// seek 到指定时间
video.currentTime = Math.min(seekTime, video.duration * 0.1 || 0.1);
});
video.addEventListener('seeked', () => {
try {
// 计算缩放后的尺寸(最大宽度 320
const maxWidth = 320;
const scale = Math.min(1, maxWidth / video.videoWidth);
canvas.width = video.videoWidth * scale;
canvas.height = video.videoHeight * scale;
// 绘制帧
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 转换为 base64
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
URL.revokeObjectURL(url);
resolve(dataUrl);
} catch (e) {
URL.revokeObjectURL(url);
reject(e);
}
});
video.addEventListener('error', (e) => {
URL.revokeObjectURL(url);
reject(new Error('视频加载失败'));
});
// 开始加载
video.load();
});
}
/**
* 使用浏览器原生 API 生成图片缩略图
* @param {File} file - 图片文件对象
* @returns {Promise<string>} base64 图片数据
*/
export async function generateImageThumbnailNative(file) {
return new Promise((resolve, reject) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const url = URL.createObjectURL(file);
img.onload = () => {
try {
// 计算缩放后的尺寸(最大宽度 320
const maxWidth = 320;
const scale = Math.min(1, maxWidth / img.naturalWidth);
canvas.width = img.naturalWidth * scale;
canvas.height = img.naturalHeight * scale;
// 绘制图片
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// 转换为 base64
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
URL.revokeObjectURL(url);
resolve(dataUrl);
} catch (e) {
URL.revokeObjectURL(url);
reject(e);
}
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('图片加载失败'));
};
img.src = url;
});
}
/**
* 智能缩略图生成
* 优先使用后端 FFmpeg失败时尝试浏览器原生方案
*/
export async function generateThumbnailSmart(filePath, fileType, fileObj = null) {
// 首先尝试使用后端 FFmpeg如果有 file 对象的话)
if (fileObj && fileObj instanceof File) {
try {
if (fileType === 'video') {
return await generateVideoThumbnailNative(fileObj);
} else if (fileType === 'image') {
return await generateImageThumbnailNative(fileObj);
}
} catch (e) {
console.log('浏览器原生缩略图生成失败,使用后端:', e);
}
}
// 返回 null让后端处理
return null;
}