diff --git a/.justfile b/.justfile new file mode 100644 index 0000000..62bd8d8 --- /dev/null +++ b/.justfile @@ -0,0 +1,32 @@ +# UPX-Tools 常用命令,运行 `just` 或 `just --list` 查看全部 + +[private] +default: + @just --list + +# 开发模式运行(热重载,先编译一次 Tailwind CSS) +dev: css + cargo tauri dev + +# 构建 CSS、安装包和便携版 +build: css + npm run build + +# Tailwind CSS:just css 构建,just css watch 监听 +css mode='build': + npm run {{ mode }}:css + +# 统一格式化前端和 Rust +fmt: + npm run format + cargo fmt --manifest-path src-tauri/Cargo.toml + +# 只读检查:前端 lint/格式、Rust 格式/clippy +check: + npm run check + cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check + cargo clippy --manifest-path src-tauri/Cargo.toml + +# 清理 Rust 构建产物 +clean: + cargo clean --manifest-path src-tauri/Cargo.toml diff --git a/CLAUDE.md b/CLAUDE.md index f7103a3..ab8386a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,28 +14,30 @@ This file provides guidance to Claude Code (claude.ai/code) when working in this ### 应用架构 - **单窗口应用**:自定义标题栏(无边框窗口 `decorations: false`) - **拖放支持**:支持文件/文件夹拖放到窗口,自动判断操作区域 -- **多线程批处理**:根据 CPU 核心数动态调整并发数 -- **配置持久化**:配置保存在程序目录的 `upx_gui_config.json` +- **分块并发批处理**:前端按启动时的 CPU 逻辑处理器数计算每批并发上限,后端执行单文件 UPX 命令 +- **配置持久化**:保存到可执行文件同目录的 `upx_gui_config.json`,需要该目录可写 - **便携版支持**:使用 `include_bytes!` 嵌入 UPX,运行时释放到临时目录 ## 常用命令 ```bash -# 开发模式运行(热重载) +# 开发模式运行(先生成 CSS;也可使用 just dev) +npm run build:css cargo tauri dev -# 编译发行版 +# 编译安装包及 release 主程序(不会自动构建 CSS) cargo tauri build # 编译产物位置 # src-tauri/target/release/bundle/ -# 生成 MSI、NSIS 和 Portable 三种格式 +# Tauri 目标为 MSI、NSIS;Portable 由 npm run post-build 复制主程序 # 前端代码检查 npm run lint # 检查代码问题 npm run lint:fix # 自动修复 npm run format # 格式化代码 -npm run check # 完整检查 +npm run format:check # 检查前端格式 +npm run check # 前端 lint + 格式检查,不包含 Rust # Tailwind CSS 构建 npm run build:css # 编译并压缩 Tailwind CSS @@ -43,13 +45,27 @@ npm run watch:css # 监听模式编译 Tailwind CSS # 完整构建(包含便携版复制) npm run build # 编译 Tauri 并复制便携版到 bundle 目录 + +# 可选 just 快捷入口(6 个常用入口,底层 npm/Cargo 命令仍可单独运行) +just # 显示菜单,默认配方不在列表中显示 +just dev # 构建 CSS + Tauri 开发模式 +just build # 构建 CSS + 安装包 + 便携版复制 +just css # 构建 CSS;just css watch 监听 +just fmt # 前端 Prettier + Rust 格式化 +just check # 前端 lint/格式 + Rust 格式检查/clippy,不含测试 +just clean # 清理 Rust 构建产物 + +# Rust 检查与格式化(从项目根目录执行) +cargo fmt --manifest-path src-tauri/Cargo.toml +cargo clippy --manifest-path src-tauri/Cargo.toml +cargo test --manifest-path src-tauri/Cargo.toml ``` ## 代码结构 ### 前端 (ui/) - `index.html` - 主界面,包含双操作按钮布局、设置弹窗、更新弹窗 -- `js/main.js` - 前端逻辑(约 900 行),包含: +- `js/main.js` - 前端逻辑,包含: - 拖放检测与区域判断(通过按钮位置缓存 `cachedButtonRects`) - 动态批处理(基于 CPU 核心数) - 配置的保存/加载 @@ -57,41 +73,69 @@ npm run build # 编译 Tauri 并复制便携版到 bundle 目录 - 更新检查与下载 - `css/style.css` - 主样式,包含 shadcn/ui CSS 变量、自定义按钮、弹窗动画 - `css/main.css` - Tailwind 入口文件(需编译) +- `css/tailwind.css` - `npm run build:css` 生成的样式;Tauri 构建钩子为空,开发、构建或发布前需显式更新 ### 后端 (src-tauri/src/) -- `main.rs` - 唯一的 Rust 源文件(约 500 行),采用模块化结构: - - **数据结构定义**:`UpxOptions`、`ScanFolderOptions`、`AppConfig`、`UpdateInfo` +- `main.rs` - 唯一的 Rust 源文件,按功能分区: + - **数据结构定义**:`UpxOptions`、`ScanFolderOptions`、`AppConfig`;更新区定义 `GitHubRelease`、`GitHubAsset`、`UpdateInfo` - **路径解析**:`get_upx_path()` 多级查找(安装版 → 开发版 → 便携版嵌入资源) - **命令构建辅助**:`build_compress_args()`、`build_decompress_args()` - **输出处理**:`filter_output_lines()`、`parse_upx_error()` 智能过滤和错误解析 - **Tauri Commands**: - `process_upx` - 执行压缩/解压(支持 tokio 阻塞任务) - - `scan_folder` - 递归扫描 exe/dll 文件 + - `scan_folder` - 扫描 exe/dll 文件,按选项决定是否递归 - `get_upx_version` - 获取 UPX 版本 - `refresh_icon_cache` - 刷新 Windows 图标缓存 - - `check_update` / `download_and_install` - 检查和下载 GitHub 更新 + - `check_update` / `download_and_install` - 检查 GitHub 更新,下载到临时目录并启动文件,不确认安装完成 - `save_config` / `load_config` - 配置持久化 ### 构建配置 - `src-tauri/tauri.conf.json` - Tauri 配置,定义窗口属性、权限、打包设置 - `src-tauri/Cargo.toml` - Rust 依赖,release profile 优化(strip、lto、opt-level=z) -- `.github/workflows/release.yml` - GitHub Actions 自动发布,生成 MSI、NSIS、Portable 三种安装包 +- `.justfile` - 可选的开发、CSS 构建、检查和格式化快捷入口 +- `.github/workflows/ci.yml` - Rust 格式检查、严格 clippy 和测试 +- `.github/workflows/build.yml` - Windows x64 MSI/NSIS 构建与 artifact 上传 +- `.github/workflows/release.yml` - `v*` tag 自动发布 MSI、NSIS 安装包和 Portable 便携版;不运行前端检查或 CSS 构建 ## 重要细节 ### UPX 路径解析(三级查找) 1. **安装版**:`程序目录/_up_/upx/upx.exe` -2. **开发版**:`../upx/upx.exe`(相对于项目根目录) +2. **开发路径**:当前工作目录下的 `../upx/upx.exe`(通常在 `src-tauri/` 中运行) 3. **便携版**:从嵌入的 `EMBEDDED_UPX` 释放到 `%TEMP%/upx-gui-portable/upx.exe` +`EMBEDDED_UPX` 对所有构建无条件嵌入。释放结果通过 `OnceLock>` 缓存;已有临时文件仅按长度判断是否复用,不校验内容哈希。仓库当前 `upx/upx.exe` 为 UPX 5.1.0,可用 `--version` 核对。 + ### 编码处理 -UPX 在 Windows 上输出 GBK 编码,使用 `encoding_rs::GBK` 转换为 UTF-8 +后端使用 `encoding_rs::GBK` 解码 UPX 的标准输出和标准错误,再进行过滤和错误解析。 ### 批处理并发策略 ```javascript +cpuCores = navigator.hardwareConcurrency || 4 batchSize = Math.max(2, Math.min(cpuCores * 2, 16)) ``` -最小 2,最大 16,默认为 CPU 核心数的 2 倍 +初始化时计算一次,每块上限 2–16,无法获取逻辑处理器数时按 4 计算(上限 8)。`processBatchFiles` 对每块使用 `Promise.all`,等待整块完成后再处理下一块;不是运行中自适应调度,也不是整个应用的全局并发限额。 + +### 处理语义与配置 +- 操作按钮打开多选 EXE/DLL 文件对话框,没有目录选择;文件夹通过拖放添加。落在操作按钮内立即处理,落在其他区域暂存后再点击按钮。 +- 覆盖设置仅影响压缩;未覆盖时逐文件请求保存路径,默认追加 `_packed`。脱壳始终以相同输入输出路径覆盖原文件。 +- 备份为输入完整路径追加 `.bak`,压缩/脱壳及另存压缩都会创建;已有备份会被覆盖,备份失败则停止该文件处理。 +- 压缩滑条 10 映射为 `best`,配置中仍保存整数 10;`--ultra-brute` 优先于压缩级别。LZMA 和极限压缩只参与压缩参数,强制选项在两种模式中均传 `--force`。 +- 点击「保存设置」或设置遮罩时保存;修改控件或关闭应用不会保存。字段和默认值见 README 设置表;缺少文件时后端返回默认配置,读取/解析失败时前端保留默认 UI。 +- `AppConfig` 只有 `auto_check_update` 字段设有 serde 缺字段默认值(true);不要假定缺少其他字段的旧 JSON 也能加载。 + +### 更新流程 +- 更新仓库固定为 `Y-ASLant/UPX-Tools`,当前版本来自 Cargo 包版本;检查 `/releases/latest`,不是完整 SemVer 比较。 +- 默认启动时约 1 秒后自动检查,亦可手动检查;有更新显示应用内弹窗,不自动打开浏览器。后端筛选 portable/setup/MSI,前端只展示 portable/setup EXE。 +- `GITHUB_TOKEN` 仅作为检查 API 的 Bearer token,资产下载不带它。下载到系统临时目录 `upx-tools-update/` 后用 Windows `cmd /C start` 启动,便携版不会原位替换旧程序,当前应用不会自动退出。 +- 下载进度条为模拟动画,不是网络进度;流程没有资产哈希/签名验证,也不等待或验证安装结果。 + +### 当前实现限制(维护时注意) +- `processUpx` 和单文件 handler 捕获错误后不重新抛出;批处理会把这些失败以及取消保存位置计入成功。应以单文件日志判断结果。 +- 拖放路径没有去重,也没有全局批处理锁;扫描先应用于所有路径,直接拖文件可能先出现「不是文件夹」错误再处理该文件。按钮外单文件/多文件暂存值也未互相清除。 +- 目录读取失败或部分目录项错误可能被忽略,扫描结果不保证完整;递归扫描没有已访问目录集合。 +- 配置保存错误仅写控制台,按钮仍可提示「设置已保存」;图标刷新忽略系统命令错误,返回成功不保证实际清理完成。 +- 关闭更新弹窗不会取消下载;下载失败路径未清除模拟进度定时器。 ### 日志系统 - 最大日志条数:1000 条 @@ -136,10 +180,12 @@ batchSize = Math.max(2, Math.min(cpuCores * 2, 16)) ## 版本发布 -1. 修改 `src-tauri/Cargo.toml` 和 `package.json` 中的版本号 -2. 推送 tag:`git tag v1.x.x && git push origin v1.x.x` -3. GitHub Actions 自动构建并发布 Release -4. 生成三种安装包: +1. 同步修改 `src-tauri/Cargo.toml`、`package.json` 和 `src-tauri/tauri.conf.json` 中的版本号;当前版本均为 `1.5.0`,更新检查使用 `env!("CARGO_PKG_VERSION")` +2. 在本地运行前端检查并更新 CSS;GitHub Actions 不安装 Node.js,也不运行 Tailwind 构建 +3. 推送 tag:`git tag v1.x.x && git push origin v1.x.x` +4. GitHub Actions 在 Windows x64 上构建并发布 Release,资产版本名取自 tag: - `UPX-Tools-{version}-x64.msi` - MSI 安装包 - - `UPX-Tools-{version}-x64-setup.exe` - NSIS 安装包 - - `UPX-Tools-{version}-x64-portable.exe` - 便携版(单文件,内嵌 UPX) + - `UPX-Tools-{version}-x64-setup.exe` - NSIS 安装包(当前用户安装) + - `UPX-Tools-{version}-x64-portable.exe` - 便携版(release 主程序副本,单文件内嵌 UPX,仍依赖 WebView2) + +构建环境要求 Windows、Rust stable/MSVC、Microsoft C++ Build Tools、WebView2 和 Tauri CLI 2;前端使用受支持的 Node.js LTS。安装 CLI 使用 `cargo install tauri-cli --version "^2.0.0" --locked`,npm CLI 不替代现有脚本中的 `cargo tauri`。 diff --git a/README.md b/README.md index c0e17f2..ccf2661 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # UPX-Tools -一个基于 Tauri 开发的 UPX 压缩工具图形界面,小而美。 +基于 Tauri 2 的 Windows UPX 可视化加壳/脱壳工具。前端使用原生 HTML、JavaScript 和 Tailwind CSS,支持安装版与单文件便携版。 ## 应用截图 @@ -24,20 +24,15 @@ ## 功能特性 -- 界面美观:基于 shadcn/ui 设计风格,简洁直观的操作界面 -- 体积轻量:应用程序体积小,启动速度快,资源占用低 -- 便携版本:支持单文件便携版,无需安装即可运行 -- 文件压缩:支持对 EXE 和 DLL 文件进行压缩加壳 -- 文件解压:支持对已压缩的文件进行解压脱壳 -- 批量处理:支持选择文件夹批量处理多个文件 -- 压缩级别:提供 1-9 和 best 多个压缩级别选项 -- 极限压缩:支持 ultra-brute 模式获得最高压缩率 -- 强制压缩:可处理带有 GUARD_CF 保护的文件 -- 文件备份:可选择在处理前自动备份原文件 -- 递归处理:支持包含子文件夹的批量处理 -- 图标刷新:内置 Windows 图标缓存清理功能 -- 检查更新:一键检查 GitHub 最新版本 -- 实时日志:显示详细的处理过程和结果信息 +- 设计风格:原生前端采用 shadcn/ui 风格的样式,不依赖 React 组件库 +- 便携版本:支持单文件便携版,内嵌 UPX,无需安装应用本身 +- 加壳/脱壳:选择 EXE、DLL 文件,或扫描文件夹中的 EXE、DLL +- 批量处理:按 CPU 逻辑核心数分批并发执行,支持可选的子目录扫描 +- 压缩选项:1–9、best、LZMA、ultra-brute 和 `--force` +- 原文件保护:可启用处理前备份;压缩可选择覆盖或另存为 +- 图标刷新:清理 Windows 图标缓存并重启资源管理器 +- 检查更新:启动自动检查或手动检查,应用内选择便携版或安装版下载 +- 执行日志:显示单文件处理结果和错误提示,限制日志数量以避免持续累积 ## 技术栈 @@ -47,7 +42,7 @@ ## 下载安装 -前往 [Releases](https://github.com/YuSanllworking/UPX-GUI/releases) 页面下载最新版本: +前往 [Releases](https://github.com/Y-ASLant/UPX-Tools/releases) 页面下载最新版本: - **便携版**:下载 `*-portable.exe`,双击即可运行,无需安装 - **安装版**:下载 `*.msi` 或 `*-setup.exe`,运行安装程序 @@ -58,8 +53,8 @@ 1. 启动应用程序 2. 点击左侧 "加壳压缩" 或右侧 "脱壳解压" 按钮 -3. 选择要处理的文件或文件夹 -4. 等待处理完成,查看执行日志 +3. 在文件对话框中选择一个或多个 EXE/DLL;文件夹请通过拖放添加 +4. 等待处理完成,查看逐文件日志;当前批量汇总可能把失败或取消操作计为成功,不应仅依赖汇总数量 ### 拖放操作 @@ -67,147 +62,203 @@ - 拖放到加壳区域:自动开始压缩 - 拖放到脱壳区域:自动开始解压 - 支持同时拖放多个文件或文件夹 +- 拖放到按钮以外的窗口区域:暂存文件,再点击所需操作按钮开始处理 +- 文件夹仅扫描 EXE/DLL,默认不包含子目录;需要递归时先启用「包含子文件夹」 +- 拖放路径没有去重,避免同时拖入文件夹和其中的同一文件;运行中重复操作也可能启动独立批次 ### 设置选项 点击右上角齿轮图标打开设置面板: -- 压缩级别:1-9 或 best,数字越大压缩率越高但速度越慢 -- 覆盖原文件:直接替换源文件,不生成新文件 -- 备份原文件:处理前自动创建 .bak 备份 -- 极限压缩:使用 ultra-brute 模式,获得最高压缩率但耗时较长 -- 包含子文件夹:批量处理时递归扫描所有子目录 -- 强制压缩:强制处理受保护的文件,如带 GUARD_CF 的程序 +| 选项 | 默认值 | 行为 | +| --- | --- | --- | +| 压缩级别 | 9 | 1–9 或 best;选择 best 时使用 `--best` | +| 覆盖原文件 | 开启 | **仅控制压缩**;关闭后选择输出路径,默认文件名为 `原文件名_packed.exe`(DLL 保留 `.dll`) | +| 备份原文件 | 关闭 | 压缩和脱壳前复制为完整文件名追加 `.bak`,如 `app.exe.bak`;已有同名备份会被覆盖 | +| LZMA | 关闭 | 压缩时追加 `--lzma` | +| 极限压缩 | 关闭 | 使用 `--ultra-brute`,优先于所选压缩级别;可能耗时较长 | +| 包含子文件夹 | 关闭 | 扫描文件夹时递归查找 EXE、DLL | +| 强制压缩 | 关闭 | 追加 `--force`(脱壳也会传入);不保证所有受保护文件都能处理 | +| 启动时检查更新 | 开启 | 启动加载配置后延迟约 1 秒检查最新版本 | -**配置自动保存**:所有设置会在关闭设置面板时自动保存,下次启动应用时会自动加载上次的配置。配置文件保存在应用程序目录下的 `upx_gui_config.json` 文件中。 +**脱壳始终覆盖原文件**,不受「覆盖原文件」选项影响。未开启覆盖的批量压缩会为每个文件请求输出路径,而不是统一输出到一个目录。建议处理重要文件前启用备份或另行保存副本。 + +点击「保存设置」或设置弹窗遮罩时保存配置并关闭面板,下次启动加载。单独修改选项或关闭应用窗口不会写入配置。配置文件位于**应用可执行文件所在目录**的 `upx_gui_config.json`,不是用户的 AppData 目录;该目录必须可写,否则无法持久化设置。当前保存失败只记录到控制台,「设置已保存」提示不能保证写入成功。 ### 检查更新 -点击右上角下载图标按钮,可检查 GitHub 仓库是否有新版本发布,如果有新版本会自动打开下载页面。 +点击右上角下载图标,查询 `Y-ASLant/UPX-Tools` 的最新 GitHub Release。发现更新后显示应用内弹窗,包含版本信息、更新说明和便携版/安装版下载按钮,**不会自动打开下载网页**。MSI 可从 Releases 页面手动下载。 -> **提示**:如果频繁检查更新遇到 API 限制,可设置环境变量 `GITHUB_TOKEN` 提高请求配额。 +选择下载项后,文件保存到系统临时目录 `upx-tools-update/`,随后启动下载的 EXE:安装版启动安装程序,便携版启动新程序。当前应用不会自动退出,旧便携版也不会被自动替换;便携版用户需自行替换原文件。 + +弹窗进度条是模拟进度,不代表实际下载百分比。当前下载流程没有校验发布资产的哈希或签名;仅从可信的官方发布源获取程序。 + +> **提示**:可设置环境变量 `GITHUB_TOKEN` 提高检查更新的 GitHub API 配额;需让启动的应用进程继承该变量。它仅用于更新检查请求,不用于发布资产下载。 ### 刷新图标缓存 -点击右上角刷新图标按钮,可清理 Windows 图标缓存并重启资源管理器。 +点击右上角刷新图标按钮会立即尝试清理图标缓存并重启资源管理器,没有确认对话框。任务栏和桌面可能短暂消失;后端会忽略部分系统命令错误,因此界面提示完成并不保证所有缓存都已清理。 ## 开发环境搭建 ### 环境要求 -- Node.js 16+ -- Rust 1.70+ -- Windows 10/11 +- Windows 10/11;现有构建与发布工作流面向 Windows x64 +- Node.js:推荐使用仍受支持的 LTS 版本;ESLint 9 要求 `^18.18.0 || ^20.9.0 || >=21.1.0`,不支持原文档中的 Node.js 16 +- Rust stable,使用 MSVC 工具链;项目未声明固定的最低 Rust 版本 +- Microsoft C++ Build Tools,安装「使用 C++ 的桌面开发」工作负载 +- Microsoft Edge WebView2 Runtime(应用和便携版均需要) +- Tauri CLI 2;下列构建命令使用 Cargo CLI,npm 全局安装的 CLI 不能替代 `cargo tauri` +- PowerShell:`npm run post-build` 使用它复制便携版 +- 可选:[just](https://github.com/casey/just),用于执行 `.justfile` 中的快捷命令 + +Windows 依赖安装细节见 [Tauri 官方环境要求](https://v2.tauri.app/start/prerequisites/)。 ### 安装依赖 +在项目根目录执行: + ```bash -# 安装前端开发依赖(ESLint + Prettier) +# 安装前端检查、格式化和 CSS 构建依赖 npm install -# 安装 Tauri CLI -cargo install tauri-cli - -# 或使用 npm -npm install -g @tauri-apps/cli +# 与 GitHub Actions 一致,安装 Tauri CLI 2 +cargo install tauri-cli --version "^2.0.0" --locked ``` +确保 `upx/upx.exe` 存在:Rust 的 `include_bytes!` 在编译时读取它,安装包也将它作为资源打包。当前仓库内的 UPX 可执行文件版本为 **5.1.0**,可运行 `upx/upx.exe --version` 核对。 + +### 开发与 CSS 构建 + +```bash +# 先生成 Tailwind 样式,再启动 Tauri 开发模式 +npm run build:css +cargo tauri dev + +# 修改 HTML/JS 中的 Tailwind 类时,在另一个终端监听 +npm run watch:css +``` + +前端直接从 `ui/` 加载,没有单独的前端开发服务器。`tauri.conf.json` 的 `beforeDevCommand` 和 `beforeBuildCommand` 均为空,因此直接执行 `cargo tauri dev`、`cargo tauri build` 或 `npm run build` **不会自动编译 CSS**。 + +安装 just 后,常用操作统一为以下 6 个入口;`just` 或 `just --list` 显示菜单: + +| 命令 | 作用 | +| --- | --- | +| `just dev` | 构建一次 CSS,再启动 Tauri 开发模式 | +| `just build` | 构建 CSS、MSI/NSIS 安装包并复制便携版 | +| `just css` / `just css watch` | 构建 CSS / 在另一个终端监听 CSS | +| `just fmt` | 格式化前端和 Rust | +| `just check` | 只读检查前端 lint/格式、Rust 格式/clippy | +| `just clean` | 清理 Rust 构建产物 | + +单独运行 ESLint 自动修复等细项时,使用下面的 npm/Cargo 命令,不再提供重复的 just 入口。 + ### 代码检查与格式化 -项目使用 ESLint 和 Prettier 进行代码质量控制: - ```bash -# 检查代码问题 -npm run lint +npm run lint # ESLint,任何 warning 都使检查失败 +npm run lint:fix # ESLint 自动修复 +npm run format # 格式化 ui/ 中的 JS、HTML、CSS +npm run format:check # 检查前端格式 +npm run check # 前端 lint + 格式检查,不包含 Rust -# 自动修复代码问题 -npm run lint:fix - -# 格式化代码 -npm run format - -# 检查代码格式 -npm run format:check - -# 完整检查(lint + format) -npm run check +cargo fmt --manifest-path src-tauri/Cargo.toml +cargo clippy --manifest-path src-tauri/Cargo.toml +cargo test --manifest-path src-tauri/Cargo.toml ``` -**代码规范**: -- 缩进:4 空格 -- 字符串:单引号 -- 结尾:无分号 +`just check` 依次执行 `npm run check`、Rust 格式检查和 Rust clippy,不包含测试;任一步失败立即停止,不修改文件。与 CI 一致的 Rust 严格检查为: + +```bash +cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check +cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings +``` + +前端规范:4 空格缩进、单引号优先、不使用行尾分号。修改前端代码后运行 `npm run format`;修改 Rust 后运行 `cargo fmt`。 ### 项目结构 -``` +```text UPX-Tools/ -├── ui/ # 前端代码 -│ ├── index.html # 主页面 -│ ├── css/ # 样式文件 -│ └── js/ # JavaScript 代码 -├── src-tauri/ # Rust 后端 -│ ├── src/ # Rust 源码 -│ ├── Cargo.toml # Rust 依赖配置 -│ └── tauri.conf.json # Tauri 配置 -├── upx/ # UPX 工具 -│ └── upx.exe # UPX 可执行文件 -├── eslint.config.js # ESLint v9 配置 -├── .prettierrc.json # Prettier 配置 -└── package.json # npm 依赖配置 +├── ui/ +│ ├── index.html # 主窗口、设置和更新弹窗 +│ ├── js/main.js # 拖放、批处理、配置、日志、更新交互 +│ ├── css/main.css # Tailwind 入口 +│ ├── css/tailwind.css # build:css 生成的样式 +│ ├── css/style.css # 自定义样式 +│ └── font/ # 本地字体 +├── src-tauri/ +│ ├── src/main.rs # UPX 执行、扫描、配置和更新命令 +│ ├── Cargo.toml # Rust 依赖和应用版本 +│ └── tauri.conf.json # 窗口、权限、资源和安装包配置 +├── upx/upx.exe # 运行资源及编译时嵌入的 UPX +├── icons/ # 应用与安装程序图标 +├── img/ # 文档截图 +├── .github/workflows/ # Rust 检查、安装包构建、版本发布 +├── .justfile # 可选命令快捷入口 +├── tailwind.config.js # Tailwind 扫描范围与主题 +├── eslint.config.js # ESLint 9 配置 +├── .prettierrc.json # Prettier 配置 +└── package.json # npm 依赖、检查与构建脚本 ``` -## 编译方法 - -### 开发模式运行 - -```bash -# 进入项目目录 -cd UPX-Tools - -# 启动开发服务器 -cargo tauri dev -``` +## 编译与发布 ### 生成发行版本 ```bash -# 完整编译(推荐,包含便携版) +# 先更新 CSS,再生成安装包并复制便携版 +npm run build:css npm run build -# 或仅编译安装包 +# 只生成安装包及 release 主程序,不执行 Portable 目录复制 cargo tauri build ``` -### 编译产物 +`npm run build` 等价于 `cargo tauri build && npm run post-build`,不会自动编译 CSS;使用 `just build` 则会先构建 CSS,再调用 `npm run build`。 -编译完成后的文件位于 `src-tauri/target/release/bundle/`: +### 本地编译产物 -``` -bundle/ -├── Portable/ -│ └── UPX-Tools.exe # 便携版(单文件,内嵌 UPX) -├── msi/ -│ └── *.msi # MSI 安装包 -└── nsis/ - └── *.exe # NSIS 安装包 +```text +src-tauri/target/release/ +├── UPX-Tools.exe # 主程序,包含嵌入的 UPX +└── bundle/ + ├── Portable/ + │ └── UPX-Tools.exe # post-build 复制的便携版 + ├── msi/ + │ └── *.msi # MSI 安装包 + └── nsis/ + └── *.exe # NSIS 安装包 ``` -### 版本说明 +Portable 并非 Tauri 的第三种打包目标,而是同一个 release 主程序的副本;便携版不需要单独编译,但仍依赖系统 WebView2 Runtime。 -| 版本类型 | 说明 | -|---------|------| -| 便携版 | 单个 EXE 文件,内嵌 UPX,无需安装,运行时自动释放到临时目录 | -| MSI 安装包 | 传统 Windows Installer 格式,支持静默安装 | -| NSIS 安装包 | 现代化安装程序,支持自定义安装选项 | +UPX 查找顺序: -### 注意事项 +1. 可执行文件目录下的 `_up_/upx/upx.exe`(安装包资源) +2. 当前工作目录下的 `../upx/upx.exe`(开发路径) +3. 将嵌入的 UPX 写入系统临时目录 `upx-gui-portable/upx.exe` -1. 首次编译需要下载依赖,时间较长 -2. 确保 upx 目录下存在 upx.exe 文件 -3. 便携版会将 upx.exe 内嵌到程序中,运行时自动释放 -4. 安装版会将 upx.exe 打包到安装目录 -5. 发行版默认开启编译器优化,体积更小速度更快 +Release profile 使用 `strip`、`opt-level = "z"`、LTO、单个 codegen unit 和 `panic = "abort"` 优化体积。 + +### 版本与 GitHub Actions + +发布前同步修改 **三个**版本字段:`package.json`、`src-tauri/Cargo.toml`、`src-tauri/tauri.conf.json`。更新检查的当前版本来自 Cargo 包版本。 + +推送 `v{version}` tag 后,`release.yml` 在 Windows 上构建,并发布: + +| 文件 | 类型 | +| --- | --- | +| `UPX-Tools-{version}-x64-portable.exe` | 单文件便携版 | +| `UPX-Tools-{version}-x64.msi` | MSI 安装包 | +| `UPX-Tools-{version}-x64-setup.exe` | NSIS 安装包(当前用户安装) | + +其他工作流: + +- `ci.yml`:面向 `main`/`master` 的 PR、`v*` tag,以及提交消息包含 `build:` 或 `tag` 的分支推送,运行 Rust 格式检查、严格 clippy 和测试。 +- `build.yml`:面向 `main`/`master` 的 PR 或提交消息包含 `build:` 的分支推送,构建并上传 MSI/NSIS artifact,不复制便携版。 +- 当前工作流不安装 Node.js,也不运行前端检查或 Tailwind 构建;发布前需在本地检查前端并更新 `ui/css/tailwind.css`。 ## 相关链接 diff --git a/ui/css/tailwind.css b/ui/css/tailwind.css index a191713..82addc4 100644 --- a/ui/css/tailwind.css +++ b/ui/css/tailwind.css @@ -1,662 +1 @@ -*, -:after, -:before { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgba(59, 130, 246, 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} -::backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgba(59, 130, 246, 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} /*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/ -*, -:after, -:before { - box-sizing: border-box; - border: 0 solid #e5e7eb; -} -:after, -:before { - --tw-content: ''; -} -:host, -html { - line-height: 1.5; - -webkit-text-size-adjust: 100%; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - font-family: - YASLant, - -apple-system, - BlinkMacSystemFont, - Segoe UI, - Roboto, - sans-serif; - font-feature-settings: normal; - font-variation-settings: normal; - -webkit-tap-highlight-color: transparent; -} -body { - margin: 0; - line-height: inherit; -} -hr { - height: 0; - color: inherit; - border-top-width: 1px; -} -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} -a { - color: inherit; - text-decoration: inherit; -} -b, -strong { - font-weight: bolder; -} -code, -kbd, -pre, -samp { - font-family: - ui-monospace, - SFMono-Regular, - Menlo, - Monaco, - Consolas, - Liberation Mono, - Courier New, - monospace; - font-feature-settings: normal; - font-variation-settings: normal; - font-size: 1em; -} -small { - font-size: 80%; -} -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} -sub { - bottom: -0.25em; -} -sup { - top: -0.5em; -} -table { - text-indent: 0; - border-color: inherit; - border-collapse: collapse; -} -button, -input, -optgroup, -select, -textarea { - font-family: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - font-size: 100%; - font-weight: inherit; - line-height: inherit; - letter-spacing: inherit; - color: inherit; - margin: 0; - padding: 0; -} -button, -select { - text-transform: none; -} -button, -input:where([type='button']), -input:where([type='reset']), -input:where([type='submit']) { - -webkit-appearance: button; - background-color: transparent; - background-image: none; -} -:-moz-focusring { - outline: auto; -} -:-moz-ui-invalid { - box-shadow: none; -} -progress { - vertical-align: baseline; -} -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} -[type='search'] { - -webkit-appearance: textfield; - outline-offset: -2px; -} -::-webkit-search-decoration { - -webkit-appearance: none; -} -::-webkit-file-upload-button { - -webkit-appearance: button; - font: inherit; -} -summary { - display: list-item; -} -blockquote, -dd, -dl, -figure, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -p, -pre { - margin: 0; -} -fieldset { - margin: 0; -} -fieldset, -legend { - padding: 0; -} -menu, -ol, -ul { - list-style: none; - margin: 0; - padding: 0; -} -dialog { - padding: 0; -} -textarea { - resize: vertical; -} -input::-moz-placeholder, -textarea::-moz-placeholder { - opacity: 1; - color: #9ca3af; -} -input::placeholder, -textarea::placeholder { - opacity: 1; - color: #9ca3af; -} -[role='button'], -button { - cursor: pointer; -} -:disabled { - cursor: default; -} -audio, -canvas, -embed, -iframe, -img, -object, -svg, -video { - display: block; - vertical-align: middle; -} -img, -video { - max-width: 100%; - height: auto; -} -[hidden]:where(:not([hidden='until-found'])) { - display: none; -} -.fixed { - position: fixed; -} -.relative { - position: relative; -} -.inset-0 { - inset: 0; -} -.z-50 { - z-index: 50; -} -.col-span-2 { - grid-column: span 2 / span 2; -} -.mx-4 { - margin-left: 1rem; - margin-right: 1rem; -} -.mx-auto { - margin-left: auto; - margin-right: auto; -} -.mb-1 { - margin-bottom: 0.25rem; -} -.mb-3 { - margin-bottom: 0.75rem; -} -.mb-5 { - margin-bottom: 1.25rem; -} -.mt-0\.5 { - margin-top: 0.125rem; -} -.mt-3 { - margin-top: 0.75rem; -} -.mt-6 { - margin-top: 1.5rem; -} -.block { - display: block; -} -.flex { - display: flex; -} -.grid { - display: grid; -} -.hidden { - display: none; -} -.h-10 { - height: 2.5rem; -} -.h-2 { - height: 0.5rem; -} -.h-4 { - height: 1rem; -} -.h-9 { - height: 2.25rem; -} -.h-\[calc\(100vh-2\.5rem\)\] { - height: calc(100vh - 2.5rem); -} -.h-full { - height: 100%; -} -.h-screen { - height: 100vh; -} -.min-h-0 { - min-height: 0; -} -.w-10 { - width: 2.5rem; -} -.w-2 { - width: 0.5rem; -} -.w-4 { - width: 1rem; -} -.w-9 { - width: 2.25rem; -} -.w-full { - width: 100%; -} -.min-w-0 { - min-width: 0; -} -.flex-1 { - flex: 1 1 0%; -} -.flex-shrink-0 { - flex-shrink: 0; -} -.cursor-pointer { - cursor: pointer; -} -.select-none { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} -.resize { - resize: both; -} -.appearance-none { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} -.grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} -.flex-col { - flex-direction: column; -} -.items-start { - align-items: flex-start; -} -.items-center { - align-items: center; -} -.justify-center { - justify-content: center; -} -.justify-between { - justify-content: space-between; -} -.gap-0\.5 { - gap: 0.125rem; -} -.gap-3 { - gap: 0.75rem; -} -.gap-4 { - gap: 1rem; -} -.space-y-5 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(1.25rem * (1 - var(--tw-space-y-reverse))); - margin-bottom: calc(1.25rem * var(--tw-space-y-reverse)); -} -.overflow-hidden { - overflow: hidden; -} -.overflow-y-auto { - overflow-y: auto; -} -.overflow-x-hidden { - overflow-x: hidden; -} -.rounded { - border-radius: 0.25rem; -} -.rounded-full { - border-radius: 9999px; -} -.rounded-lg { - border-radius: var(--radius); -} -.rounded-md { - border-radius: calc(var(--radius) - 2px); -} -.rounded-sm { - border-radius: var(--radius-btn); -} -.border { - border-width: 1px; -} -.border-b { - border-bottom-width: 1px; -} -.border-border { - border-color: hsl(var(--border)); -} -.border-input { - border-color: hsl(var(--input)); -} -.bg-background { - background-color: hsl(var(--background)); -} -.bg-card { - background-color: hsl(var(--card)); -} -.bg-foreground\/80 { - background-color: hsl(var(--foreground) / 0.8); -} -.bg-green-500 { - --tw-bg-opacity: 1; - background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1)); -} -.bg-muted { - background-color: hsl(var(--muted)); -} -.bg-primary { - background-color: hsl(var(--primary)); -} -.bg-secondary\/50 { - background-color: hsl(var(--secondary) / 0.5); -} -.p-0 { - padding: 0; -} -.p-3 { - padding: 0.75rem; -} -.p-4 { - padding: 1rem; -} -.p-5 { - padding: 1.25rem; -} -.p-6 { - padding: 1.5rem; -} -.px-3 { - padding-left: 0.75rem; - padding-right: 0.75rem; -} -.px-5 { - padding-left: 1.25rem; - padding-right: 1.25rem; -} -.px-6 { - padding-left: 1.5rem; - padding-right: 1.5rem; -} -.py-2\.5 { - padding-top: 0.625rem; - padding-bottom: 0.625rem; -} -.py-6 { - padding-top: 1.5rem; - padding-bottom: 1.5rem; -} -.text-center { - text-align: center; -} -.font-mono { - font-family: - ui-monospace, - SFMono-Regular, - Menlo, - Monaco, - Consolas, - Liberation Mono, - Courier New, - monospace; -} -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; -} -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} -.text-xs { - font-size: 0.75rem; - line-height: 1rem; -} -.font-medium { - font-weight: 500; -} -.font-semibold { - font-weight: 600; -} -.text-foreground { - color: hsl(var(--foreground)); -} -.text-muted-foreground { - color: hsl(var(--muted-foreground)); -} -.text-primary { - color: hsl(var(--primary)); -} -.text-primary-foreground { - color: hsl(var(--primary-foreground)); -} -.text-primary-foreground\/50 { - color: hsl(var(--primary-foreground) / 0.5); -} -.outline-none { - outline: 2px solid transparent; - outline-offset: 2px; -} -.backdrop-blur-sm { - --tw-backdrop-blur: blur(4px); - backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) - var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) - var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) - var(--tw-backdrop-sepia); -} -.transition-all { - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 0.15s; -} -.transition-colors { - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 0.15s; -} -.hover\:border-primary:hover { - border-color: hsl(var(--primary)); -} -.hover\:bg-accent:hover { - background-color: hsl(var(--accent)); -} -.hover\:bg-destructive\/10:hover { - background-color: hsl(var(--destructive) / 0.1); -} -.hover\:text-muted-foreground:hover { - color: hsl(var(--muted-foreground)); -} -.focus\:outline-none:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} -.group:hover .group-hover\:text-destructive { - color: hsl(var(--destructive)); -} -.group:hover .group-hover\:text-primary { - color: hsl(var(--primary)); -} +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:YASLant,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:0}.z-50{z-index:50}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-5{margin-bottom:1.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-2{height:.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[calc\(100vh-2\.5rem\)\]{height:calc(100vh - 2.5rem)}.h-full{height:100%}.h-screen{height:100vh}.max-h-28{max-height:7rem}.min-h-0{min-height:0}.w-10{width:2.5rem}.w-2{width:.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-full{width:100%}.flex-1{flex:1 1 0%}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:var(--radius-btn)}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-border{border-color:hsl(var(--border))}.border-border\/50{border-color:hsl(var(--border)/.5)}.bg-background{background-color:hsl(var(--background))}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-foreground\/80{background-color:hsl(var(--foreground)/.8)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:rgba(34,197,94,.1)}.bg-muted{background-color:hsl(var(--muted))}.bg-primary{background-color:hsl(var(--primary))}.bg-secondary\/50{background-color:hsl(var(--secondary)/.5)}.p-0{padding:0}.p-3{padding:.75rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-foreground{color:hsl(var(--foreground))}.text-foreground\/70{color:hsl(var(--foreground)/.7)}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-muted-foreground\/50{color:hsl(var(--muted-foreground)/.5)}.text-primary{color:hsl(var(--primary))}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:border-primary:hover{border-color:hsl(var(--primary))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/10:hover{background-color:hsl(var(--destructive)/.1)}.hover\:text-destructive:hover{color:hsl(var(--destructive))}.hover\:text-muted-foreground:hover{color:hsl(var(--muted-foreground))}.hover\:underline:hover{text-decoration-line:underline}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.group:hover .group-hover\:text-destructive{color:hsl(var(--destructive))}.group:hover .group-hover\:text-primary{color:hsl(var(--primary))} \ No newline at end of file