修改了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

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# Dependencies
node_modules/
# Build outputs
src-tauri/target/
dist/
build/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Environment
.env
.env.local
# Tauri
src-tauri/icons/*.png
src-tauri/icons/*.ico
src-tauri/icons/*.icns
!src-tauri/icons/.gitkeep
# Application packages
*.app
*.dmg
*.msi
*.exe

View File

@@ -124,4 +124,4 @@ MIT
- [FFmpeg](https://ffmpeg.org/) - 强大的多媒体处理框架 - [FFmpeg](https://ffmpeg.org/) - 强大的多媒体处理框架
- [Tauri 2.0](https://v2.tauri.app/) - 下一代桌面应用开发框架 - [Tauri 2.0](https://v2.tauri.app/) - 下一代桌面应用开发框架
- [Tailwind CSS] () - - [Tailwind CSS](https://tailwindcss.com) - 只需书写 HTML 代码即可快速构建美观的网站

611
package-lock.json generated Normal file
View File

@@ -0,0 +1,611 @@
{
"name": "format-converter",
"version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "format-converter",
"version": "2.0.0",
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"lucide": "^0.563.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0",
"concurrently": "^8.2.2"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@tauri-apps/api": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.0.tgz",
"integrity": "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
"tauri": "tauri.js"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.10.0",
"@tauri-apps/cli-darwin-x64": "2.10.0",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0",
"@tauri-apps/cli-linux-arm64-gnu": "2.10.0",
"@tauri-apps/cli-linux-arm64-musl": "2.10.0",
"@tauri-apps/cli-linux-riscv64-gnu": "2.10.0",
"@tauri-apps/cli-linux-x64-gnu": "2.10.0",
"@tauri-apps/cli-linux-x64-musl": "2.10.0",
"@tauri-apps/cli-win32-arm64-msvc": "2.10.0",
"@tauri-apps/cli-win32-ia32-msvc": "2.10.0",
"@tauri-apps/cli-win32-x64-msvc": "2.10.0"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.0.tgz",
"integrity": "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.0.tgz",
"integrity": "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.0.tgz",
"integrity": "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.0.tgz",
"integrity": "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.0.tgz",
"integrity": "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.0.tgz",
"integrity": "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.0.tgz",
"integrity": "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.0.tgz",
"integrity": "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.0.tgz",
"integrity": "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.0.tgz",
"integrity": "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.0.tgz",
"integrity": "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/concurrently": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
"integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^2.30.0",
"lodash": "^4.17.21",
"rxjs": "^7.8.1",
"shell-quote": "^1.8.1",
"spawn-command": "0.0.2",
"supports-color": "^8.1.1",
"tree-kill": "^1.2.2",
"yargs": "^17.7.2"
},
"bin": {
"conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": "^14.13.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"dev": true,
"license": "MIT"
},
"node_modules/lucide": {
"version": "0.563.0",
"resolved": "https://registry.npmjs.org/lucide/-/lucide-0.563.0.tgz",
"integrity": "sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==",
"license": "ISC"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/spawn-command": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
"dev": true
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
}
}
}

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "format-converter",
"version": "2.0.0",
"description": "跨平台格式转换器 - 基于FFmpeg和Tauri 2.0",
"type": "module",
"scripts": {
"dev": "tauri dev",
"build": "tauri build",
"tauri": "tauri"
},
"keywords": [
"ffmpeg",
"converter",
"tauri",
"format"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@tauri-apps/cli": "^2.0.0",
"concurrently": "^8.2.2"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"lucide": "^0.563.0"
}
}

View File

@@ -0,0 +1,2 @@
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"

6754
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,38 @@
[package]
name = "torch-format-converter"
version = "2.0.0"
description = "火炬格式转换器 - 跨平台多媒体格式转换工具"
authors = [""]
license = "MIT"
repository = ""
edition = "2021"
[build-dependencies]
tauri-build = { version = "2.0", features = [] }
[dependencies]
tauri = { version = "2.0", features = ["devtools"] }
tauri-plugin-dialog = "2.0"
tauri-plugin-fs = "2.0"
tauri-plugin-shell = "2.0"
tauri-plugin-os = "2.0"
tauri-plugin-http = "2.0"
rfd = "0.15"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.0", features = ["full"] }
which = "6.0"
dirs = "5.0"
reqwest = { version = "0.12", features = ["stream"] }
zip = "2.1"
tar = "0.4"
xz2 = "0.1"
walkdir = "2.4"
uuid = { version = "1.0", features = ["v4"] }
base64 = "0.22"
tokio-stream = "0.1"
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp", "bmp"] }
lofty = "0.22"
[features]
default = []

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,368 @@
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use tokio::process::Command;
use tokio_stream::StreamExt;
const FFMPEG_VERSION: &str = "7.1";
#[derive(Debug, Clone)]
pub struct FFmpegInstaller;
impl FFmpegInstaller {
pub fn new() -> Self {
Self
}
/// 获取 FFmpeg 存储路径
pub fn get_ffmpeg_path() -> PathBuf {
let app_dir = Self::get_app_data_dir();
#[cfg(target_os = "windows")]
let ffmpeg_name = "ffmpeg.exe";
#[cfg(not(target_os = "windows"))]
let ffmpeg_name = "ffmpeg";
app_dir.join(ffmpeg_name)
}
/// 获取应用数据目录
fn get_app_data_dir() -> PathBuf {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
#[cfg(target_os = "macos")]
let app_dir = home.join("Library/Application Support/FormatConverter");
#[cfg(target_os = "windows")]
let app_dir = home.join("AppData/Local/FormatConverter");
#[cfg(target_os = "linux")]
let app_dir = home.join(".local/share/format-converter");
fs::create_dir_all(&app_dir).ok();
app_dir
}
/// 检查 FFmpeg 是否已安装
pub fn is_installed() -> bool {
// 1. 检查系统 PATH 中的 FFmpeg
if let Ok(output) = std::process::Command::new("which")
.arg("ffmpeg")
.output()
{
if output.status.success() && !output.stdout.is_empty() {
return true;
}
}
// 2. 检查应用目录
let app_ffmpeg = Self::get_ffmpeg_path();
if app_ffmpeg.exists() {
return true;
}
// 3. 检查系统常见路径
let common_paths = Self::get_system_ffmpeg_paths();
for path in common_paths {
if path.exists() {
return true;
}
}
false
}
/// 获取可能的系统 FFmpeg 路径
fn get_system_ffmpeg_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
#[cfg(target_os = "macos")]
{
// Homebrew 常见安装路径
paths.push(PathBuf::from("/opt/homebrew/bin/ffmpeg")); // Apple Silicon
paths.push(PathBuf::from("/usr/local/bin/ffmpeg")); // Intel
// 其他可能路径
paths.push(PathBuf::from("/usr/bin/ffmpeg"));
}
#[cfg(target_os = "linux")]
{
paths.push(PathBuf::from("/usr/bin/ffmpeg"));
paths.push(PathBuf::from("/usr/local/bin/ffmpeg"));
}
#[cfg(target_os = "windows")]
{
paths.push(PathBuf::from("C:\\ffmpeg\\bin\\ffmpeg.exe"));
if let Ok(program_files) = std::env::var("ProgramFiles") {
paths.push(PathBuf::from(&format!("{}\\ffmpeg\\bin\\ffmpeg.exe", program_files)));
}
}
paths
}
/// 获取 FFmpeg 版本
pub async fn get_version() -> Option<String> {
// 1. 尝试系统 PATH 中的 ffmpeg
if let Ok(output) = tokio::time::timeout(
std::time::Duration::from_secs(5),
Command::new("ffmpeg").args(["-version"]).output()
).await {
if let Ok(output) = output {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(first_line) = stdout.lines().next() {
return Some(first_line.to_string());
}
}
}
}
// 2. 尝试应用目录中的 ffmpeg
let ffmpeg_path = Self::get_ffmpeg_path();
if ffmpeg_path.exists() {
if let Some(cmd_str) = ffmpeg_path.to_str() {
if let Ok(Ok(output)) = tokio::time::timeout(
std::time::Duration::from_secs(5),
Command::new(cmd_str).args(["-version"]).output()
).await {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(first_line) = stdout.lines().next() {
return Some(first_line.to_string());
}
}
}
}
}
// 3. 尝试系统常见路径
let system_paths = Self::get_system_ffmpeg_paths();
for path in system_paths {
if path.exists() {
if let Some(cmd_str) = path.to_str() {
if let Ok(Ok(output)) = tokio::time::timeout(
std::time::Duration::from_secs(5),
Command::new(cmd_str).args(["-version"]).output()
).await {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(first_line) = stdout.lines().next() {
return Some(first_line.to_string());
}
}
}
}
}
}
None
}
/// 下载并安装 FFmpeg
pub async fn install<F>(progress_callback: F) -> Result<(), String>
where
F: Fn(f64, String) + Send + 'static,
{
let platform = Self::detect_platform()?;
let download_url = Self::get_download_url(&platform);
progress_callback(0.0, "准备下载 FFmpeg...".to_string());
// 创建临时目录
let temp_dir = std::env::temp_dir().join("format-converter-ffmpeg");
fs::create_dir_all(&temp_dir).map_err(|e: std::io::Error| format!("创建临时目录失败: {}", e))?;
// 下载文件
progress_callback(5.0, "正在下载 FFmpeg...".to_string());
let archive_path = temp_dir.join(format!("ffmpeg.{}", platform.extension()));
Self::download_file(&download_url, &archive_path, |downloaded, total| {
let percent = if total > 0 {
(downloaded as f64 / total as f64) * 40.0 + 5.0
} else {
5.0
};
progress_callback(percent, format!("正在下载 FFmpeg... {:.1}%", percent));
})
.await
.map_err(|e| format!("下载失败: {}", e))?;
// 解压
progress_callback(50.0, "正在解压...".to_string());
Self::extract_archive(&archive_path, &temp_dir, &platform)
.await
.map_err(|e| format!("解压失败: {}", e))?;
// 移动到目标位置
progress_callback(90.0, "正在安装...".to_string());
let ffmpeg_path = Self::get_ffmpeg_path();
let extracted_ffmpeg = Self::find_ffmpeg_binary(&temp_dir, &platform)
.ok_or("在解压文件中未找到 FFmpeg 可执行文件")?;
fs::copy(&extracted_ffmpeg, &ffmpeg_path)
.map_err(|e| format!("复制文件失败: {}", e))?;
// 设置可执行权限Unix
#[cfg(not(target_os = "windows"))]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&ffmpeg_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&ffmpeg_path, perms).unwrap();
}
// 清理临时文件
progress_callback(95.0, "正在清理...".to_string());
fs::remove_dir_all(&temp_dir).ok();
progress_callback(100.0, "FFmpeg 安装完成".to_string());
Ok(())
}
/// 检测平台
fn detect_platform() -> Result<Platform, String> {
#[cfg(target_os = "macos")]
{
// 检测架构
let arch = std::env::consts::ARCH;
match arch {
"aarch64" => Ok(Platform::MacOSArm64),
"x86_64" => Ok(Platform::MacOSX64),
_ => Err(format!("不支持的架构: {}", arch)),
}
}
#[cfg(target_os = "windows")]
{
let arch = std::env::consts::ARCH;
match arch {
"x86_64" => Ok(Platform::WindowsX64),
"aarch64" => Ok(Platform::WindowsArm64),
_ => Err(format!("不支持的架构: {}", arch)),
}
}
#[cfg(target_os = "linux")]
{
let arch = std::env::consts::ARCH;
match arch {
"x86_64" => Ok(Platform::LinuxX64),
"aarch64" => Ok(Platform::LinuxArm64),
_ => Err(format!("不支持的架构: {}", arch)),
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
Err("不支持的操作系统".to_string())
}
}
/// 获取下载 URL
fn get_download_url(platform: &Platform) -> String {
match platform {
Platform::MacOSArm64 => format!(
"https://evermeet.cx/pub/ffmpeg/ffmpeg-{}-macos-arm64.zip",
FFMPEG_VERSION
),
Platform::MacOSX64 => format!(
"https://evermeet.cx/pub/ffmpeg/ffmpeg-{}-macos-x86_64.zip",
FFMPEG_VERSION
),
Platform::WindowsX64 => format!(
"https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
),
Platform::WindowsArm64 => format!(
"https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
),
Platform::LinuxX64 => format!(
"https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"
),
Platform::LinuxArm64 => format!(
"https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz"
),
}
}
/// 下载文件
async fn download_file<F>(url: &str, path: &Path, progress: F) -> Result<(), String>
where
F: Fn(u64, u64),
{
let client = reqwest::Client::new();
let response = client
.get(url)
.timeout(std::time::Duration::from_secs(300))
.send()
.await
.map_err(|e| e.to_string())?;
let total_size = response.content_length().unwrap_or(0);
let mut file = fs::File::create(path).map_err(|e| e.to_string())?;
let mut downloaded: u64 = 0;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e: reqwest::Error| e.to_string())?;
file.write_all(&chunk).map_err(|e: std::io::Error| e.to_string())?;
downloaded += chunk.len() as u64;
progress(downloaded, total_size);
}
Ok(())
}
/// 解压归档文件
async fn extract_archive(
archive_path: &Path,
output_dir: &Path,
platform: &Platform,
) -> Result<(), String> {
match platform.extension().as_str() {
"zip" => {
let file = fs::File::open(archive_path).map_err(|e| e.to_string())?;
let mut archive = zip::ZipArchive::new(file).map_err(|e| e.to_string())?;
archive.extract(output_dir).map_err(|e| e.to_string())?;
}
"tar.xz" => {
let file = fs::File::open(archive_path).map_err(|e| e.to_string())?;
let tar = xz2::read::XzDecoder::new(file);
let mut archive = tar::Archive::new(tar);
archive.unpack(output_dir).map_err(|e| e.to_string())?;
}
_ => return Err("不支持的压缩格式".to_string()),
}
Ok(())
}
/// 查找 FFmpeg 可执行文件
fn find_ffmpeg_binary(dir: &Path, _platform: &Platform) -> Option<PathBuf> {
#[cfg(target_os = "windows")]
let binary_name = "ffmpeg.exe";
#[cfg(not(target_os = "windows"))]
let binary_name = "ffmpeg";
for entry in walkdir::WalkDir::new(dir) {
if let Ok(entry) = entry {
if entry.file_name() == binary_name {
return Some(entry.path().to_path_buf());
}
}
}
None
}
}
#[derive(Debug)]
enum Platform {
MacOSArm64,
MacOSX64,
WindowsX64,
WindowsArm64,
LinuxX64,
LinuxArm64,
}
impl Platform {
fn extension(&self) -> String {
match self {
Platform::MacOSArm64 | Platform::MacOSX64 | Platform::WindowsX64 | Platform::WindowsArm64 => "zip".to_string(),
Platform::LinuxX64 | Platform::LinuxArm64 => "tar.xz".to_string(),
}
}
}

1682
src-tauri/src/main.rs Normal file

File diff suppressed because it is too large Load Diff

79
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,79 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "火炬格式转换器",
"version": "2.0.0",
"identifier": "cn.meshel.formatconverter",
"build": {
"frontendDist": "../src"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "火炬格式转换器",
"width": 1100,
"height": 800,
"minWidth": 900,
"minHeight": 700,
"resizable": true,
"fullscreen": false,
"dragDropEnabled": true
}
],
"security": {
"csp": null,
"capabilities": [
{
"identifier": "main-capability",
"description": "Main application capabilities",
"windows": ["main"],
"permissions": [
"core:default",
"core:event:default",
"core:event:allow-listen",
"dialog:default",
"fs:default",
"shell:default",
"os:default",
"http:default"
]
}
]
}
},
"bundle": {
"active": true,
"targets": "all",
"category": "Utility",
"copyright": "",
"shortDescription": "火炬格式转换器",
"longDescription": "跨平台格式转换器 - 基于FFmpeg",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [],
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"exceptionDomain": "",
"signingIdentity": null,
"providerShortName": null,
"entitlements": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
},
"linux": {
"deb": {
"depends": []
}
}
},
"plugins": {}
}

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;
}

BIN
test.mp4 Executable file

Binary file not shown.