修改了readme文件
This commit is contained in:
338
src/index.html
Normal file
338
src/index.html
Normal 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">×</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">×</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">×</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">×</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
1637
src/main.js
Normal file
File diff suppressed because it is too large
Load Diff
1747
src/style.css
Normal file
1747
src/style.css
Normal file
File diff suppressed because it is too large
Load Diff
123
src/thumbnail-helper.js
Normal file
123
src/thumbnail-helper.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user