diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5317633 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cmake.sourceDirectory": "D:/CODE/NuGet C++ Pack/cpp_nuget_pack/windows", + "terminal.integrated.stickyScroll.enabled": false, + "cmake.autoDetectSourceDirectory": false +} \ No newline at end of file diff --git a/README.md b/README.md index 68da04c..d4c2980 100644 --- a/README.md +++ b/README.md @@ -1,154 +1,23 @@ -# C++ NuGet 打包工具 +
+HWOPT -把 C++ 头文件、源码与库可视化为原生 NuGet 包的 Windows 桌面工具。通过图形界面完成包信息编辑、目录映射、依赖与编译设置,并一键导出 Visual Studio C++ 工程可直接引用的 `.nupkg`,或供 CMake `find_package` 消费的配置包。 +# CCPPP — C/C++ 包管理工具 -- 平台:仅 Windows 桌面 -- 界面:全中文、Win11 风格(fluent_ui)、Fluent 默认配色(浅色/深色两族,跟随系统) -- 数据:配置保存在程序目录下的 `config/`,便携、无需安装 +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +![Status](https://img.shields.io/badge/Status-WIP-yellow) -## 功能特性 +[English](README_EN.md) -| 功能 | 说明 | -| ---- | ---- | -| 包管理 | 添加文件夹(自动扫描后填写包信息)、编辑版本/作者/许可证/描述(包 ID 不可改)、删除包、重新映射源目录 | -| 文件管理 | 目录树展示包内文件(目录大小按后代聚合);双击用系统默认程序打开;lib/dll/pdb/exe 按路径显示 Release/Debug 构建标签 | -| 构建 | 源目录根部含 `build.py` 时「文件管理」页显示「构建」按钮:自动拉取 git 源码到 `cache/`(二次构建原位硬重置,预构建包复用本地缓存不重复下载)并执行构建脚本(`SRC_PATH`/`BUILD_OUT` 环境变量;源码就绪后、执行构建脚本前清空包源目录产物(保留 `build.py`/`icon.*`/钩子脚本/根级 `.git` 与许可证)),成功后自动重新映射;构建前自动准备环境——检测本机编译器(默认优先 ICX > clang-cl > MSVC > MinGW,可在设置调整)、捕获编译器环境(vcvars/setvars)、按需下载 CMake/Ninja 到 `tools/`;`build.py` 头部可声明 `# tool`(需自动下载的环境工具,如 NASM/Perl)与 `# option`(构建选项,工具栏内随选随存、构建时经 `CNP_OPTION_*` 下发);分类辅助模块 `cnp_build_support.py` 随构建释放,脚本可经 `PYTHONPATH` import 完成 CMake 装配与产物分类;`build.py` 不进入打包产物;产物按 `release/`、`debug/` 分层分类(源码构建自动注入 AVX2、Release 最高优化与 LTO(可退化)、多线程构建参数);构建输出实时显示于进度对话框右侧面板(ERROR/WARNING/INFO 高亮);源目录根部 `pre.bat`/`post.bat` 与头部 `# depends` 自动注册为系统条目(编译前/后命令与依赖,界面禁改删);`# source: none` 支持预构建配方;含仓库的包在侧栏显示 git/更新徽标,文件管理页显示当前与最新版本号 | -| 依赖管理 | 从现有包选择依赖,自定义 NuGet 版本范围并实时校验;指向不存在包的依赖显示「缺失」 | -| 编译设置 | 宏定义、编译前/后命令(可从包内选择脚本、插入 MSBuild 常用宏)、附加库目录、附加库;条目可按 ALL/Release/Debug 分组;「节点脚本」分区提供可视化节点编辑器(节点图随包分发并自动执行,见「节点脚本」) | -| 打包设置 | 选择打包格式(NuGet / CMake);预览包内文件树与文本内容;按所选格式导出(缺失依赖时弹窗提醒,可继续;导出前校验节点脚本与包内可执行二进制,提示后仍可继续/取消) | -| 历史记录 | 时间线记录创建、版本变更、重新映射、打包导出四类事件(上限 100 条,可删除) | -| 依赖关系图 | 全部包的依赖关系可视化:可拖拽平移、滚轮缩放;缺失依赖红色标注,当前包高亮 | -| 设置 | 打包输出目录;编译器优先级(ICX/clang-cl/MSVC/MinGW,检测本机版本、可排序与重新检测;检测结果缓存至 `detectedCompilers`,有缓存时免检测直接显示、「重新检测」刷新并写回);SKILL.md 生成(内置模板写出,供分发给 AI 插件);主题模式(系统/深色/浅色),即时生效并持久化 | +
-## 打包产物 +CCPPP (C/C++ PackTool) 是一个图形化带有低代码节点编辑器的包管理工具,用于从本地或远程仓库构建并生成 NuGet/Cmake 包,使用 SKILL 快速从远程仓库生成对应包 -导出文件命名为 `<包ID>.<版本>.nupkg`(版本去掉 `+build` 后缀),内部布局: +*** +*~~**啊这个这个,0百万元以内最好用的包管理,遥遥领先**~~* -| 内容 | 包内路径 | 说明 | -| ---- | -------- | ---- | -| 头文件 / 模块 | `build/native/include/<源目录名>/...` | 剥离开头 `include/` 后再套一层源目录名作命名空间(源目录已自带同名目录时不再叠加,避免重复层),消费者可写 `#include <源目录名/foo.h>` 防冲突;源目录缺失时回退包名 | -| lib / dll / pdb | `build/native/lib/...` | 剥离开头 `lib/`、`bin/` | -| 其余文件 | `build/native/files/...` | 保留原相对路径,全部入包(含 `.exe` 等可执行二进制,导出时提示随包分发) | -| 包清单 | `<包ID>.nuspec` | 许可证用 SPDX 表达式;依赖组 `native0.0`;恒定声明包图标 | -| MSBuild 集成 | `build/native/<包ID>.targets` | 为消费者工程提供编译集成,见下 | -| 节点脚本 | `build/native/files/scripts/.ps1` | 由节点编辑器生成(UTF-8 BOM);`` 形如 `script_1`;无节点脚本时不产生 | -| 包图标 | `images/icon.png` | 从包图标自动转换,等比缩放至最长边不超过 128;缺失时使用默认纸箱图标 | +## 项目状态 +**开发中** -`<包ID>.targets` 为消费者工程提供: +## 交流群 -- 自动把 `build/native/include` 追加到包含路径 -- 写入宏定义、附加库目录与附加库(按 ALL/Release/Debug 条件分组;包内 `.lib` 按路径自动识别配置,同时补充库目录与库名) -- 编译前/后命令以自定义目标自动执行:`CnpPreBuild_<包ID清洗>_`(`BeforeTargets="ClCompile"`)/ `CnpPostBuild_<包ID清洗>_`(`AfterTargets="Build"`),每命令一条 `Exec`(工作目录 `$(ProjectDir)`,ALL/Release/Debug 条件在目标级,Release/Debug 组以 `_Release`/`_Debug` 后缀区分) -- 包内 dll/pdb 由 `DeployPkgRuntimeBinaries` 目标在构建后硬链接到 `$(OutDir)`(失败回退为拷贝,并登记 `FileWrites` 供清理) -- 包内包含 `.asm` 时条件导入 VS 的 `masm.props`/`masm.targets`,生成 `MASM` 项 -- 包内包含 `.rc` 时生成 `ResourceCompile` 项 -- 节点脚本以每个触发时机一个目标自动执行:`CnpScripts_<包ID清洗>__Pre` / `..._Post`(`hash8` 为包 ID 的 SHA-1 前 8 位,防跨包目标重名覆盖),见「节点脚本」 - -> 命令注入修复(自定义 `Exec` 目标)随导出生成于 `.targets`,不回溯既有包:已导出包需重新打包才会带上修复,旧包内仍为旧机制。 - -### 节点脚本 - -在「编译设置」→「节点脚本」分区打开可视化节点编辑器(内置 51 种节点);节点图随包分发,导出 NuGet 包时自动生成执行目标: - -- **生成脚本**:`build/native/files/scripts/.ps1`(`` 形如 `script_1`,ASCII 安全文件名),以 UTF-8 BOM 写入,保证 PowerShell 5.1 正确解析脚本中的中文;脚本入口先将 `PSModulePath` 全量归一为 `$PSHOME\Modules`(不保留继承的自定义模块路径),避免中间进程链(如 PowerShell 7)的模块路径遮蔽 5.1 自带模块 -- **执行目标**:每个触发时机一个目标——`CnpScripts_<包ID清洗>__Pre` 早于消费者 `PreBuildEvent` 与编译;`..._Post` 晚于运行时二进制部署(包内无 dll/pdb 时为构建之后)。`<包ID清洗>` 将包 ID 中非 `[A-Za-z0-9_]` 字符替换为 `_`;`hash8` 避免不同包的目标重名静默覆盖 -- **任务**:每个脚本一条 `Exec`,以 `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File` 调用;顺序与编辑器脚本列表一致,失败即中止且只按退出码判定(不把 stderr 文本当失败);按脚本的 ALL/Release/Debug 标签生成构建条件 -- **节点语义**:`创建硬链接`仅支持同一卷上的文件——跨卷或目录目标会报错;`由十六进制写`的十六进制串忽略空白与连字符后,须为偶数长度且仅含十六进制字符,奇数长度或非法字符时脚本报错中断(十六进制串与字节数组整体驻留内存,不适用于大文件);`查找工具`用 `Get-Command -CommandType Application` 探测(防别名误中),未命中再按候选路径展开环境变量探测,结果分「是否找到」/「路径」两输出(每次输出各探测一次);`下载文件`/`上传文件`用 `Net.WebClient`(上传方法默认 PUT);`包内脚本文件`输出 `Join-Path $env:CNP_PackageRoot` 组合的包内路径;`运行包内脚本`按扩展名分派解释器(ps1 → `powershell.exe -File`、py → `python`、bat/cmd/exe 直调),参数按行拆分、工作目录临时切换、失败默认中止(可关) -- **变量系统**:`写入数值变量`/`读取数值变量`/`写入文本变量`/`读取文本变量` 四个节点读写脚本变量(发射为 `$var_<名称> = 值` / `$var_<名称>`);名称须以字母或下划线开头且仅含字母、数字、下划线;变量名大小写不敏感,同一名称只能用于一种类型,混用会报校验错误。脚本顶部自动为用到的变量注入初始化——数值 `0`、文本 `''`,按名称字典序;初始化收集口径为**生成路径上的变量**(不可达节点不参与生成),读取永不为 `$null`。变量为动态作用域(循环体内赋值对外可见),且每次脚本执行在独立 `powershell.exe` 进程中进行——变量不跨脚本共享。变量配合 `while`/`branch` 与算术、比较即可表达任意计算(图灵完备可达) -- **环境变量**:恒传 `CNP_PackageRoot`(包根 `build/native/`);节点中用到的 MSBuild 宏按白名单转成 `CNP_<宏名>`(如 `CNP_OutDir`);`$(SolutionDir)` 在单项目构建下为 `*Undefined*` -- **CMake 格式不接入节点脚本**:CMake 预览与导出产物均不包含脚本条目 - -> **安全声明**:节点脚本与手写编译前/后命令一样,会在**消费方构建机**上自动执行(供应链语义);「运行程序」节点即任意命令执行面,请仅消费可信来源的包。包内若含可执行二进制(`.exe`),导出时会提示其随包分发并被脚本在构建时调用,同样请确认来源可信。AES 加解密与代码签名节点的口令请优先使用「口令环境变量名」方式——直接填写的口令**字面量会随包分发**。 -> -> **执行策略提示**:脚本以 `-ExecutionPolicy Bypass` 启动,该参数**无法覆盖企业组策略(GPO)**强制的脚本执行限制;受管控环境请先确认策略允许。 - -### CMake 配置包 - -选择 CMake 格式导出时生成 `<包ID>-<版本>-cmake.zip`;解压后将目录加入 `CMAKE_PREFIX_PATH` 即可在 CMake 工程中消费: - -```cmake -find_package(<包名> CONFIG REQUIRED) -target_link_libraries(app PRIVATE <包名>::<包名>) -``` - -| 内容 | 包内路径 | 说明 | -| ---- | -------- | ---- | -| 头文件 / 模块 | `include/<源目录名>/...` | 与 NuGet 产物相同的命名空间策略(源目录已自带同名目录时不再叠加),`#include <源目录名/foo.h>` | -| lib / dll / pdb | `lib/...` | 剥离开头 `lib/`、`bin/`,保留 Release/Debug 子目录结构 | -| 其余文件 | `files/...` | 保留原相对路径 | -| CMake 配置 | `lib/cmake/<包名>/` | `Config.cmake`、`Targets.cmake`(`<包名>::<包名>` INTERFACE IMPORTED 目标、`.lib` 按 Debug/Release 分组条件链接)与 `ConfigVersion.cmake`(版本为数字点分时生成) | - -> DLL 不参与链接,需消费方自行拷贝到运行目录(例如 `$` 或手动复制)。 -> -> v1 不接入节点脚本:CMake 预览与导出产物均不包含节点脚本条目(仅 NuGet 格式支持)。 - -## 构建与运行 - -环境要求: - -- Windows 10/11 -- Flutter stable(建议 3.44.0 及以上,Dart SDK `^3.13.2`) -- Visual Studio C++ 工具链(含 Windows SDK,用于构建 Windows 桌面产物) - -```text -flutter pub get -flutter run -d windows # 本地运行 -flutter analyze # 静态检查(要求零问题) -flutter test # 运行测试 -flutter build windows --release # 产物位于 build/windows/x64/runner/Release/ -``` - -## 配置文件 - -配置保存在工作目录相对的 `config/`(便携,程序目录即数据目录): - -```text -config/ -├── config.yaml # 全局设置 -└── packs/ - └── <包 ID>.yaml # 每个包一个文件(同 ID 覆盖) -``` - -`config.yaml` 保存打包输出目录、编译器优先级、编译器检测结果缓存(`detectedCompilers`:设置页有缓存时免检测直接显示、「重新检测」刷新并写回;构建优先复用缓存,缺失/失效时自动重检并写回)与主题设置。包 YAML 字段: - -| 字段 | 说明 | -| ---- | ---- | -| `name` / `version` / `author` | 必填;`name` 同时决定配置文件名与 NuGet 包 ID | -| `description` / `license` / `iconPath` / `sourcePath` | 可选;`license` 为 SPDX 表达式 | -| `files` | 文件快照(`path`/`size`),由扫描与重新映射写入 | -| `dependencies` | 依赖列表(`name`/`version`,版本范围;`system: true` 为 `# depends` 自动注册) | -| `commands` | 编译前/后命令(`command`/`type`/`buildModel`;`system: true` 为 `pre.bat`/`post.bat` 自动注册) | -| `macros` | 宏定义(`value`/`buildModel`) | -| `libDirectories` | 附加库目录(`path`/`buildModel`) | -| `libraries` | 附加库(`name`/`buildModel`) | -| `buildOptions` | 构建选项(`{<名称>: <值>}`,由 `build.py` 头部 `# option` 声明;空省略) | -| `sourceVersion` | 上次构建记录的仓库版本(git tag 或短哈希;空省略) | -| `history` | 历史记录(`time`/`type`/`message`,上限 100 条) | -| `scripts` | 节点脚本(`id`/`name`/`trigger`/`buildModel` + 节点图 `nodes`/`edges`/视口;单条损坏仅丢弃该脚本) | - -其中 `buildModel` 取值为 ALL / Release / Debug。配置文件损坏或缺必填字段不会导致启动失败,启动后会以悬浮提示列出问题文件。 - -执行「构建」时下载的源码缓存在工作目录的 `cache/`(可随时删除;再次构建会按需重新下载);构建环境准备按需下载的工具缓存在 `tools/`(同为本地缓存;本机已有可用的 CMake(≥ 3.25)/Ninja/Python 时优先直接使用,否则自动解析官方最新版本下载最小版;编译器完全缺失时下载 LLVM clang 兜底;构建子进程使用受控 TMP 目录 `tools/.tmp/`);`build.py` 头部 `# tool` 声明的环境工具同样下载到 `tools/<名称>/`,构建前释放的 `cnp_build_support.py` 也在 `tools/`(均可随时删除)。 - -## 版本与发布 - -- 版本号采用「年份.年内发布数量」方案:`26.1` 表示 2026 年第 1 次发布,年内依次递增(`26.2`、`26.3`…),次年从 `27.1` 重新计数;更早的 `1.0.x` 为历史版本号。 -- 版本号唯一来源是 `pubspec.yaml` 的 `version:`(格式 `26.1.0+<构建号>`,第三段固定为 0);发布时同步更新 `lib/app_info.dart` 的 `appVersion`(去掉 `+build` 与末尾 `.0`,即 `26.1.0+1` → `26.1`),两者一致性由 `test/app_info_test.dart` 强制校验。 -- `dev` 分支:推送触发 CI(静态检查 → 测试 → Windows Release 构建),只验证不发布。 -- `master` 分支:验证通过后打包 `dist/cpp_nuget_pack-<版本>-win-x86_64.zip`,并创建 GitHub Release(tag `v<版本>`,如 `v26.1`)。 -- 因此修改版本号并推送到 `master` 即等于发布;没有发布意图时不要改动版本号。 - -## 技术栈 - -| 组件 | 用途 | -| ---- | ---- | -| Flutter / Dart | 应用框架(仅 Windows 桌面) | -| [fluent_ui](https://pub.dev/packages/fluent_ui) | Win11 风格控件 | -| [flutter_svg](https://pub.dev/packages/flutter_svg) | SVG 图标渲染 | -| [yaml](https://pub.dev/packages/yaml) / [yaml_edit](https://pub.dev/packages/yaml_edit) | YAML 配置解析与生成 | -| [archive](https://pub.dev/packages/archive) | `.nupkg` / CMake 配置包(ZIP)组装 | -| [file_selector](https://pub.dev/packages/file_selector) | 系统原生目录选择 / 保存位置对话框 | - -## 第三方声明 - -目录树图标来自 [Catppuccin Icons for VSCode](https://github.com/catppuccin/vscode-icons)(v1.26.0,MIT 许可)的图标子集;完整第三方组件许可声明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。 +- QQ: 112986834 \ No newline at end of file diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..e69de29 diff --git a/assets/icons/logo.png b/assets/icons/logo.png new file mode 100644 index 0000000..bda7d12 Binary files /dev/null and b/assets/icons/logo.png differ diff --git a/lib/app_info.dart b/lib/app_info.dart index 512b5b5..2f6578a 100644 --- a/lib/app_info.dart +++ b/lib/app_info.dart @@ -1,8 +1,8 @@ -const String appName = 'C++ NuGet 打包工具'; +const String appName = 'CCPPP'; /// 与 pubspec.yaml 的 version 同步(去掉 +build 与末尾 .0,如 26.1.0+1 → 26.1);不一致时 test/app_info_test.dart 会失败。 -const String appVersion = '26.3'; +const String appVersion = '26.3 WIP'; -const String appDescription = '将 C++ 头文件、源码与库可视化为原生 NuGet 包的 Windows 桌面工具。'; +const String appDescription = '啊这个这个,0百万元以内最好用的包管理,遥遥领先'; const String appRepositoryUrl = 'https://github.com/1992724048/cpp-pack-tool'; diff --git a/lib/controls/build_options.dart b/lib/controls/build_options.dart index f3a7f40..5eefb24 100644 --- a/lib/controls/build_options.dart +++ b/lib/controls/build_options.dart @@ -7,15 +7,8 @@ import 'package:fluent_ui/fluent_ui.dart'; /// 选项按 `build.py` 声明顺序渲染;值变更经 [onChange] 上报(`null` 表示移除保存键); /// [saving] 为保存挂起标志,为 true 时全部控件禁用(fluent 禁用态)。 class BuildOptionsPanel extends StatefulWidget { - const BuildOptionsPanel({ - super.key, - required this.options, - required this.values, - required this.expanded, - required this.onToggleExpanded, - required this.saving, - required this.onChange, - }); + const BuildOptionsPanel( + {super.key, required this.options, required this.values, required this.expanded, required this.onToggleExpanded, required this.saving, required this.onChange}); final List options; @@ -49,33 +42,31 @@ class _BuildOptionsPanelState extends State { final FluentThemeData theme = FluentTheme.of(context); return Padding( key: const Key('buildOptionsSection'), - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox( - height: 28, + Card( + margin: EdgeInsetsGeometry.fromLTRB(0, 3, 0, 0), + padding: EdgeInsetsGeometry.fromLTRB(5, 2, 5, 2), child: Row( children: [ Text( '构建选项', style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: theme.resources.textFillColorPrimary, - ), + fontSize: 13, fontWeight: FontWeight.w600, color: theme.resources.textFillColorPrimary), ), + SizedBox(width: 5), + Text(('(${widget.options.length} 项)'), style: TextStyle(color: FluentTheme + .of(context) + .resources + .textFillColorSecondary)), const Spacer(), _buildToggle(), ], ), ), if (widget.expanded) ...[ - Container( - height: 1, - color: theme.resources.dividerStrokeColorDefault, - ), - const SizedBox(height: 8), ConstrainedBox( constraints: const BoxConstraints(maxHeight: _maxListHeight), child: Scrollbar( @@ -86,15 +77,10 @@ class _BuildOptionsPanelState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - for ( - int index = 0; - index < widget.options.length; - index++ - ) ...[ - if (index > 0) const SizedBox(height: 4), - _buildOptionRow(theme, widget.options[index]), + for (int index = 0; index < widget.options.length; index++) ...[ + Card(margin: EdgeInsetsGeometry.fromLTRB(0, 3, 0, 0), padding: EdgeInsetsGeometry.fromLTRB( + 0, 0, 0, 0), child: _buildOptionRow(theme, widget.options[index])), ], - const SizedBox(height: 8), ], ), ), @@ -108,18 +94,13 @@ class _BuildOptionsPanelState extends State { Widget _buildToggle() { return SizedBox( - width: 24, - height: 24, + width: 26, + height: 26, child: Tooltip( message: widget.expanded ? '收起构建选项' : '展开构建选项', - child: IconButton( - key: const Key('buildOptionsToggle'), - icon: Icon( - widget.expanded ? FluentIcons.chevron_down : FluentIcons.chevron_up, - size: 14, - ), - onPressed: widget.onToggleExpanded, - ), + child: IconButton(key: const Key('buildOptionsToggle'), + icon: Icon(widget.expanded ? FluentIcons.chevron_down : FluentIcons.chevron_up, size: 15), + onPressed: widget.onToggleExpanded), ), ); } @@ -129,12 +110,10 @@ class _BuildOptionsPanelState extends State { case BuildOptionControl.dropdown: return _buildDropdownRow(theme, option); case BuildOptionControl.checkbox: - return _CheckboxOptionRow( - option: option, - value: effectiveBuildOptionValue(option, widget.values), - enabled: !widget.saving, - onChanged: widget.onChange, - ); + return _CheckboxOptionRow(option: option, + value: effectiveBuildOptionValue(option, widget.values), + enabled: !widget.saving, + onChanged: widget.onChange); case BuildOptionControl.multiselect: return _buildMultiSelectRow(theme, option); } @@ -145,82 +124,75 @@ class _BuildOptionsPanelState extends State { return SizedBox( key: Key('buildOptionRow_${option.name}'), height: 40, - child: Row( - children: [ - Expanded(child: _buildOptionTitle(theme, option.name)), - SizedBox( - width: _optionFieldWidth, - child: FluentTheme( - data: FluentTheme.of(context).copyWith( - visualDensity: comboBoxDensity, - ), - child: ComboBox( - key: Key('buildOption_${option.name}'), - value: value, - isExpanded: true, - onChanged: widget.saving - ? null - : (String? selected) { - if (selected != null && selected != value) { - widget.onChange(option.name, selected); - } - }, - items: >[ - for (final String item in option.values) - ComboBoxItem( - value: item, - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text( - item, - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: Padding( + padding: EdgeInsetsGeometry.fromLTRB(5, 0, 5, 0), + child: Row( + children: [ + Expanded(child: _buildOptionTitle(theme, option.name)), + SizedBox( + width: _optionFieldWidth, + child: FluentTheme( + data: FluentTheme.of(context).copyWith(visualDensity: comboBoxDensity), + child: ComboBox( + key: Key('buildOption_${option.name}'), + value: value, + isExpanded: true, + onChanged: widget.saving + ? null + : (String? selected) { + if (selected != null && selected != value) { + widget.onChange(option.name, selected); + } + }, + items: >[ + for (final String item in option.values) + ComboBoxItem( + value: item, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text(item, maxLines: 1, overflow: TextOverflow.ellipsis), ), ), - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), ); } Widget _buildMultiSelectRow(FluentThemeData theme, BuildScriptOption option) { - final Set selected = multiSelectSelection( - option.values, - widget.values[option.name], - ); - return ConstrainedBox( - key: Key('buildOptionRow_${option.name}'), - constraints: const BoxConstraints(minHeight: 40), - child: Row( - children: [ - Expanded(child: _buildOptionTitle(theme, option.name)), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: _multiSelectMaxWidth), - child: Wrap( - key: Key('buildOption_${option.name}'), - alignment: WrapAlignment.end, - spacing: 16, - runSpacing: 6, - children: [ - for (final String value in option.values) - _buildMultiSelectItem(option, value, selected.contains(value)), - ], + final Set selected = multiSelectSelection(option.values, widget.values[option.name]); + return Padding( + padding: EdgeInsetsGeometry.fromLTRB(5, 0, 5, 0), + child: ConstrainedBox( + key: Key('buildOptionRow_${option.name}'), + constraints: const BoxConstraints(minHeight: 40), + child: Row( + children: [ + Expanded(child: _buildOptionTitle(theme, option.name)), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: _multiSelectMaxWidth), + child: Wrap( + key: Key('buildOption_${option.name}'), + alignment: WrapAlignment.end, + spacing: 16, + runSpacing: 6, + children: [ + for (final String value in option.values) _buildMultiSelectItem( + option, value, selected.contains(value)) + ], + ), ), - ), - ], + ], + ), ), ); } - Widget _buildMultiSelectItem( - BuildScriptOption option, - String value, - bool checked, - ) { + Widget _buildMultiSelectItem(BuildScriptOption option, String value, bool checked) { return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -228,10 +200,7 @@ class _BuildOptionsPanelState extends State { key: Key('buildOptionValue_${option.name}_$value'), checked: checked, semanticLabel: value, - onChanged: widget.saving - ? null - : (bool? next) => - _toggleMultiSelectValue(option, value, next ?? !checked), + onChanged: widget.saving ? null : (bool? next) => _toggleMultiSelectValue(option, value, next ?? !checked), ), const SizedBox(width: 8), Text(value, style: _optionTextStyle(FluentTheme.of(context))), @@ -239,19 +208,11 @@ class _BuildOptionsPanelState extends State { ); } - void _toggleMultiSelectValue( - BuildScriptOption option, - String value, - bool checked, - ) { - final Set current = multiSelectSelection( - option.values, - widget.values[option.name], - ); + void _toggleMultiSelectValue(BuildScriptOption option, String value, bool checked) { + final Set current = multiSelectSelection(option.values, widget.values[option.name]); final List selected = [ for (final String candidate in option.values) - if (candidate == value ? checked : current.contains(candidate)) - candidate, + if (candidate == value ? checked : current.contains(candidate)) candidate, ]; widget.onChange(option.name, selected.join(';')); } @@ -261,21 +222,13 @@ class _BuildOptionsPanelState extends State { TextStyle _optionTextStyle(FluentThemeData theme) => TextStyle(fontSize: 13, color: theme.resources.textFillColorPrimary); -Widget _buildOptionTitle(FluentThemeData theme, String name) => Text( - name, - overflow: TextOverflow.ellipsis, - style: _optionTextStyle(theme), -); +Widget _buildOptionTitle(FluentThemeData theme, String name) => + Text(name, overflow: TextOverflow.ellipsis, style: _optionTextStyle(theme)); /// 布尔选项行:整行可点(悬停背景 + click 光标),Checkbox 自身保留键盘切换; /// 行外层与 Checkbox 手势竞技场由内层胜出,单击只切换一次。 class _CheckboxOptionRow extends StatefulWidget { - const _CheckboxOptionRow({ - required this.option, - required this.value, - required this.enabled, - required this.onChanged, - }); + const _CheckboxOptionRow({required this.option, required this.value, required this.enabled, required this.onChanged}); final BuildScriptOption option; final String value; @@ -310,23 +263,19 @@ class _CheckboxOptionRowState extends State<_CheckboxOptionRow> { behavior: HitTestBehavior.opaque, onTap: _toggle, child: Container( + margin: EdgeInsetsGeometry.all(0), + padding: EdgeInsetsGeometry.fromLTRB(5, 0, 5, 0), key: Key('buildOptionRow_${widget.option.name}'), constraints: const BoxConstraints(minHeight: 40), - decoration: BoxDecoration( - color: _hovered - ? theme.resources.controlFillColorSecondary - : Colors.transparent, - borderRadius: BorderRadius.circular(4), - ), + decoration: BoxDecoration(color: _hovered ? theme.resources.controlFillColorSecondary : Colors.transparent, + borderRadius: BorderRadius.circular(4)), child: Row( children: [ Expanded(child: _buildOptionTitle(theme, widget.option.name)), - Checkbox( - key: Key('buildOption_${widget.option.name}'), - checked: _checked, - semanticLabel: widget.option.name, - onChanged: widget.enabled ? (bool? _) => _toggle() : null, - ), + Checkbox(key: Key('buildOption_${widget.option.name}'), + checked: _checked, + semanticLabel: widget.option.name, + onChanged: widget.enabled ? (bool? _) => _toggle() : null), ], ), ), @@ -339,12 +288,7 @@ class _CheckboxOptionRowState extends State<_CheckboxOptionRow> { /// /// 保存口径:默认 = 键不存在([onChanged] 收到 null);MD / MT = 显式写入。 class RuntimeLibrarySelector extends StatelessWidget { - const RuntimeLibrarySelector({ - super.key, - required this.value, - required this.enabled, - required this.onChanged, - }); + const RuntimeLibrarySelector({super.key, required this.value, required this.enabled, required this.onChanged}); /// 当前保存值:`MD` / `MT`;null 显示「默认(跟随配方)」。 final String? value; @@ -353,111 +297,74 @@ class RuntimeLibrarySelector extends StatelessWidget { final ValueChanged onChanged; static const String _defaultItemValue = 'default'; - static const double _width = 168; + static const double _width = 75; @override Widget build(BuildContext context) { - return SizedBox( - width: _width, - child: ComboBox( - key: const Key('buildRuntimeSelector'), - value: value ?? _defaultItemValue, - isExpanded: true, - onChanged: enabled - ? (String? selected) => onChanged( - selected == null || selected == _defaultItemValue ? null : selected, - ) - : null, - items: const >[ - ComboBoxItem( - value: _defaultItemValue, - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text( - '默认(跟随配方)', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), + final TextStyle boldPhrase = const TextStyle(fontWeight: FontWeight.w600); + return Tooltip( + richMessage: TextSpan( + children: [ + TextSpan(text: '运行库(MSVC CRT)', style: boldPhrase), + const TextSpan(text: '\n· '), + TextSpan(text: '默认', style: boldPhrase), + const TextSpan( + text: + ':由 build.py 配方的编译参数决定;' + '需要覆盖时再显式选择 MD / MT。', ), - ComboBoxItem( - value: 'MD', - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text( - 'MD(动态运行库)', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), + const TextSpan(text: '\n· '), + TextSpan(text: 'MD(动态运行库)', style: boldPhrase), + const TextSpan( + text: + ':CRT 以共享 dll 提供,多个模块共用同一份,产物体积更小;' + '运行需目标机器安装匹配的 VC++ Redistributable。', ), - ComboBoxItem( - value: 'MT', - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text( - 'MT(静态运行库)', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), + const TextSpan(text: '\n· '), + TextSpan(text: 'MT(静态运行库)', style: boldPhrase), + const TextSpan( + text: + ':CRT 静态链入每个模块,无运行时依赖、部署最简单;' + '各模块各自持有一份,总体积更大。', ), + const TextSpan(text: '\n· Debug 构建自动使用 d 变体(MDd / MTd),无需手动切换。'), + const TextSpan(text: '\n· 推荐:优先 MD 并随包分发共享 dll + lib,可显著减小体积;需要免依赖分发时选 MT。'), ], ), - ); - } -} - -/// 运行库帮助入口:「?」按钮悬停显示反色富文本 Tooltip(只读说明)。 -class RuntimeLibraryHelpButton extends StatelessWidget { - const RuntimeLibraryHelpButton({super.key}); - - @override - Widget build(BuildContext context) { - final TextStyle boldPhrase = const TextStyle(fontWeight: FontWeight.w600); - return SizedBox( - width: 28, - height: 28, - child: Tooltip( - richMessage: TextSpan( - children: [ - TextSpan(text: '运行库(MSVC CRT)', style: boldPhrase), - const TextSpan(text: '\n· '), - TextSpan(text: '默认(跟随配方)', style: boldPhrase), - const TextSpan( - text: - ':由 build.py 配方的编译参数决定;' - '需要覆盖时再显式选择 MD / MT。', - ), - const TextSpan(text: '\n· '), - TextSpan(text: 'MD(动态运行库)', style: boldPhrase), - const TextSpan( - text: - ':CRT 以共享 dll 提供,多个模块共用同一份,产物体积更小;' - '运行需目标机器安装匹配的 VC++ Redistributable。', + style: const TooltipThemeData(maxWidth: 360, showDuration: Duration(seconds: 30)), + child: SizedBox( + width: _width, + height: 29, + child: ComboBox( + key: const Key('buildRuntimeSelector'), + value: value ?? _defaultItemValue, + isExpanded: true, + onChanged: enabled ? (String? selected) => + onChanged(selected == null || selected == _defaultItemValue ? null : selected) : null, + items: const >[ + ComboBoxItem( + value: _defaultItemValue, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text('默认', maxLines: 1, overflow: TextOverflow.ellipsis), + ), ), - const TextSpan(text: '\n· '), - TextSpan(text: 'MT(静态运行库)', style: boldPhrase), - const TextSpan( - text: - ':CRT 静态链入每个模块,无运行时依赖、部署最简单;' - '各模块各自持有一份,总体积更大。', + ComboBoxItem( + value: 'MD', + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text('MD', maxLines: 1, overflow: TextOverflow.ellipsis), + ), ), - const TextSpan(text: '\n· Debug 构建自动使用 d 变体(MDd / MTd),无需手动切换。'), - const TextSpan( - text: '\n· 推荐:优先 MD 并随包分发共享 dll + lib,可显著减小体积;需要免依赖分发时选 MT。', + ComboBoxItem( + value: 'MT', + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text('MT', maxLines: 1, overflow: TextOverflow.ellipsis), + ), ), ], ), - style: const TooltipThemeData( - maxWidth: 360, - showDuration: Duration(seconds: 30), - ), - child: IconButton( - key: const Key('buildRuntimeHelp'), - icon: const Icon(FluentIcons.help, size: 16), - onPressed: () {}, - ), ), ); } diff --git a/lib/controls/pack_manage.dart b/lib/controls/pack_manage.dart index 07bdd39..dc40df6 100644 --- a/lib/controls/pack_manage.dart +++ b/lib/controls/pack_manage.dart @@ -46,40 +46,12 @@ class _PackManageState extends State { int _index = 0; late final ValueNotifier _pack; late final ValueNotifier _packagingBuilder; - late final List _tabs; @override void initState() { super.initState(); _pack = ValueNotifier(widget.pack); _packagingBuilder = ValueNotifier(widget.packagingBuilder); - _tabs = [ - Tab( - icon: Svgs.showPermitCard, - text: const Text('包信息'), - body: _body(_packInfoBody()), - ), - Tab( - icon: Svgs.fileExplorer, - text: const Text('文件管理'), - body: _body(_packFilesBody()), - ), - Tab( - icon: Svgs.inventoryFlow, - text: const Text('依赖管理'), - body: _body(_packDependenciesBody()), - ), - Tab( - icon: Svgs.projectSetup, - text: const Text('编译设置'), - body: _body(_packCompileBody()), - ), - Tab( - icon: Svgs.boxSettings, - text: const Text('打包设置'), - body: _body(_packPackagingBody()), - ), - ]; } @override @@ -103,22 +75,20 @@ class _PackManageState extends State { Widget _packInfoBody() { return ValueListenableBuilder( valueListenable: _pack, - builder: (BuildContext context, PackModel pack, Widget? child) => - PackInfo(pack: pack, onSave: widget.onSave), + builder: (BuildContext context, PackModel pack, Widget? child) => PackInfo(pack: pack, onSave: widget.onSave), ); } Widget _packFilesBody() { return ValueListenableBuilder( valueListenable: _pack, - builder: (BuildContext context, PackModel pack, Widget? child) => - PackFiles( - pack: pack, - onBuildPack: widget.onBuildPack, - onSave: widget.onSave, - loadLatestVersion: widget.loadLatestVersion, - loadHeader: widget.loadHeader ?? loadBuildScriptHeader, - ), + builder: (BuildContext context, PackModel pack, Widget? child) => PackFiles( + pack: pack, + onBuildPack: widget.onBuildPack, + onSave: widget.onSave, + loadLatestVersion: widget.loadLatestVersion, + loadHeader: widget.loadHeader ?? loadBuildScriptHeader, + ), ); } @@ -126,11 +96,7 @@ class _PackManageState extends State { return ValueListenableBuilder( valueListenable: _pack, builder: (BuildContext context, PackModel pack, Widget? child) => - PackDependencies( - pack: pack, - allPacks: widget.allPacks, - onSave: widget.onSave, - ), + PackDependencies(pack: pack, allPacks: widget.allPacks, onSave: widget.onSave), ); } @@ -138,32 +104,22 @@ class _PackManageState extends State { return ValueListenableBuilder( valueListenable: _pack, builder: (BuildContext context, PackModel pack, Widget? child) => - PackCompileSettings( - pack: pack, - onSave: widget.onSave, - pickDirectory: widget.pickDirectory, - ), + PackCompileSettings(pack: pack, onSave: widget.onSave, pickDirectory: widget.pickDirectory), ); } Widget _packPackagingBody() { return ValueListenableBuilder( valueListenable: _pack, - builder: (BuildContext context, PackModel pack, Widget? child) => - ValueListenableBuilder( - valueListenable: _packagingBuilder, - builder: - ( - BuildContext context, - PackageBuilder? builder, - Widget? child, - ) => PackPackaging( - pack: pack, - selectedBuilder: builder, - onBuilderChanged: widget.onPackagingBuilderChanged, - onSave: widget.onSave, - ), - ), + builder: (BuildContext context, PackModel pack, Widget? child) => ValueListenableBuilder( + valueListenable: _packagingBuilder, + builder: (BuildContext context, PackageBuilder? builder, Widget? child) => PackPackaging( + pack: pack, + selectedBuilder: builder, + onBuilderChanged: widget.onPackagingBuilderChanged, + onSave: widget.onSave, + ), + ), ); } @@ -171,14 +127,52 @@ class _PackManageState extends State { return Builder( builder: (BuildContext context) { final theme = FluentTheme.of(context); - return Container( - decoration: BoxDecoration(color: theme.cardColor), - child: child, - ); + return child; }, ); } + List _buildTabs(BuildContext context) { + final accent = FluentTheme.of(context).accentColor; + return [ + Tab( + icon: Svgs.showPermitCard, + text: const Text('包信息'), + body: _body(_packInfoBody()), + selectedBackgroundColor: WidgetStateColor.resolveWith((states) => FluentTheme.of(context).selectionColor), + selectedForegroundColor: WidgetStateColor.resolveWith((states) => Colors.white), + ), + Tab( + icon: Svgs.fileExplorer, + text: const Text('文件管理'), + body: _body(_packFilesBody()), + selectedBackgroundColor: WidgetStateColor.resolveWith((states) => FluentTheme.of(context).selectionColor), + selectedForegroundColor: WidgetStateColor.resolveWith((states) => Colors.white), + ), + Tab( + icon: Svgs.inventoryFlow, + text: const Text('依赖管理'), + body: _body(_packDependenciesBody()), + selectedBackgroundColor: WidgetStateColor.resolveWith((states) => FluentTheme.of(context).selectionColor), + selectedForegroundColor: WidgetStateColor.resolveWith((states) => Colors.white), + ), + Tab( + icon: Svgs.projectSetup, + text: const Text('编译设置'), + body: _body(_packCompileBody()), + selectedBackgroundColor: WidgetStateColor.resolveWith((states) => FluentTheme.of(context).selectionColor), + selectedForegroundColor: WidgetStateColor.resolveWith((states) => Colors.white), + ), + Tab( + icon: Svgs.boxSettings, + text: const Text('打包设置'), + body: _body(_packPackagingBody()), + selectedBackgroundColor: WidgetStateColor.resolveWith((states) => FluentTheme.of(context).selectionColor), + selectedForegroundColor: WidgetStateColor.resolveWith((states) => Colors.white), + ), + ]; + } + @override Widget build(BuildContext context) { return Padding( @@ -186,7 +180,7 @@ class _PackManageState extends State { child: TabView( currentIndex: _index, onChanged: (int index) => setState(() => _index = index), - tabs: _tabs, + tabs: _buildTabs(context), ), ); } diff --git a/lib/main.dart b/lib/main.dart index 8282e97..d8991ff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,8 +31,8 @@ import 'package:cpp_nuget_pack/util/system_entries.dart'; import 'package:cpp_nuget_pack/widgets/floating_toast.dart'; import 'package:cpp_nuget_pack/widgets/library_card.dart'; import 'package:file_selector/file_selector.dart'; -import 'package:flutter/services.dart'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; import 'app_info.dart'; import 'controls/add_directory_dialog.dart'; @@ -92,7 +92,7 @@ class _PackToolState extends State { @override Widget build(BuildContext context) { return FluentApp( - title: 'C++ Pack Tool', + title: 'C++ Pack', themeMode: switch (_settings.themeMode) { ThemeModeSetting.system => ThemeMode.system, ThemeModeSetting.dark => ThemeMode.dark, @@ -100,11 +100,7 @@ class _PackToolState extends State { }, theme: buildTheme(Brightness.light), darkTheme: buildTheme(Brightness.dark), - home: MainLayout( - store: widget.store, - settings: _settings, - onSaveSettings: _saveSettings, - ), + home: MainLayout(store: widget.store, settings: _settings, onSaveSettings: _saveSettings), ); } } @@ -140,8 +136,7 @@ Future _preparePackBuildEnvironment( onCompilersDetected: onCompilersDetected, onDownloadProgress: onDownloadProgress, proxy: proxy, - loadSupportModule: () => - rootBundle.loadString('assets/build/cnp_build_support.py'), + loadSupportModule: () => rootBundle.loadString('assets/build/cnp_build_support.py'), ); } @@ -179,16 +174,8 @@ class MainLayout extends StatefulWidget { final PackStore store; final SettingsModel settings; final Future Function(SettingsModel settings) onSaveSettings; - final Future Function( - PackModel pack, - String outputDirectory, - ) - exportPackage; - final Future Function( - PackModel pack, - String outputDirectory, - ) - exportCmakePackage; + final Future Function(PackModel pack, String outputDirectory) exportPackage; + final Future Function(PackModel pack, String outputDirectory) exportCmakePackage; final PackBuildRunner buildPack; final PackBuildEnvironmentPreparer? prepareBuildEnv; @@ -248,8 +235,7 @@ class _MainLayoutState extends State { final Map> _repoIconQueries = >{}; /// 仓库地址 → 远端最新 tag 查询 Future(去重同一仓库的并发查询)。 - final Map> _latestTagQueries = - >{}; + final Map> _latestTagQueries = >{}; /// 仓库地址 → 已完成的远端最新 tag;查询失败/无可用 tag 时为 null。 final Map _latestTags = {}; @@ -286,12 +272,7 @@ class _MainLayoutState extends State { packs = result.packs; errors.addAll(result.errors); } catch (error) { - errors.add( - PackLoadError( - fileName: widget.store.rootPath, - message: error.toString(), - ), - ); + errors.add(PackLoadError(fileName: widget.store.rootPath, message: error.toString())); } if (!mounted) { return; @@ -303,13 +284,9 @@ class _MainLayoutState extends State { }); _resolveRepos(); if (errors.isNotEmpty) { - WidgetsBinding.instance.addPostFrameCallback( - (_) => _showLoadErrorsToast(errors), - ); + WidgetsBinding.instance.addPostFrameCallback((_) => _showLoadErrorsToast(errors)); } - WidgetsBinding.instance.addPostFrameCallback( - (_) => unawaited(_fixPlaceholderAuthors()), - ); + WidgetsBinding.instance.addPostFrameCallback((_) => unawaited(_fixPlaceholderAuthors())); } /// 占位作者批量修正:默认作者非空且自身不是占位值时逐个替换并写盘; @@ -319,9 +296,7 @@ class _MainLayoutState extends State { return; } final String defaultAuthor = widget.settings.defaultAuthor.trim(); - if (defaultAuthor.isEmpty || - isPlaceholderAuthor(defaultAuthor) || - _packs.isEmpty) { + if (defaultAuthor.isEmpty || isPlaceholderAuthor(defaultAuthor) || _packs.isEmpty) { return; } _fixingAuthors = true; @@ -346,12 +321,10 @@ class _MainLayoutState extends State { if (updatedPacks.isNotEmpty && mounted) { setState(() { final Map replacements = { - for (final PackModel pack in updatedPacks) - pack.name.toLowerCase(): pack, + for (final PackModel pack in updatedPacks) pack.name.toLowerCase(): pack, }; for (int index = 0; index < _packs.length; index++) { - final PackModel? replacement = - replacements[_packs[index].name.toLowerCase()]; + final PackModel? replacement = replacements[_packs[index].name.toLowerCase()]; if (replacement != null) { _packs[index] = replacement; } @@ -383,11 +356,7 @@ class _MainLayoutState extends State { ); return; } - showFloatingToast( - context, - '已按默认作者修正 $fixed 个包的作者', - type: FloatingToastType.info, - ); + showFloatingToast(context, '已按默认作者修正 $fixed 个包的作者', type: FloatingToastType.info); } /// 全字段拷贝并替换作者(`PackModel.author` 为 final,只能重建)。 @@ -416,10 +385,7 @@ class _MainLayoutState extends State { /// 写盘前的作者兜底:占位作者替换为默认作者(静默)。 PackModel _withResolvedAuthor(PackModel pack) { - final String resolved = resolveDefaultAuthor( - pack.author, - widget.settings.defaultAuthor, - ); + final String resolved = resolveDefaultAuthor(pack.author, widget.settings.defaultAuthor); return resolved == pack.author ? pack : _withAuthor(pack, resolved); } @@ -427,9 +393,7 @@ class _MainLayoutState extends State { if (!mounted) { return; } - final String details = errors - .map((PackLoadError error) => error.toString()) - .join('\n'); + final String details = errors.map((PackLoadError error) => error.toString()).join('\n'); showFloatingToast( context, '${errors.length} 个配置加载失败\n$details', @@ -449,19 +413,13 @@ class _MainLayoutState extends State { final Future> scanFuture = widget.scanFiles(path); final PackModel? pack = await showDialog( context: context, - builder: (_) => AddDirectoryDialog( - directoryPath: path, - scanFuture: scanFuture, - initialAuthor: widget.settings.defaultAuthor, - ), + builder: (_) => + AddDirectoryDialog(directoryPath: path, scanFuture: scanFuture, initialAuthor: widget.settings.defaultAuthor), ); if (pack == null || !mounted) { return; } - final int totalSize = pack.files.fold( - 0, - (int sum, FileModel file) => sum + file.size, - ); + final int totalSize = pack.files.fold(0, (int sum, FileModel file) => sum + file.size); pack.history = appendHistoryEntry( pack.history, HistoryModel( @@ -497,12 +455,7 @@ class _MainLayoutState extends State { builder: (BuildContext dialogContext) => ContentDialog( title: const Text('保存失败'), content: Text('包「${pack.name}」保存失败:$error'), - actions: [ - Button( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('确定'), - ), - ], + actions: [Button(onPressed: () => Navigator.pop(dialogContext), child: const Text('确定'))], ), ); return false; @@ -516,9 +469,7 @@ class _MainLayoutState extends State { void _upsertPack(PackModel pack) { setState(() { - final int existing = _packs.indexWhere( - (PackModel item) => item.name.toLowerCase() == pack.name.toLowerCase(), - ); + final int existing = _packs.indexWhere((PackModel item) => item.name.toLowerCase() == pack.name.toLowerCase()); if (existing >= 0) { _packs[existing] = pack; } else { @@ -578,9 +529,7 @@ class _MainLayoutState extends State { return; } final String? resolvedRepo = repo; - final RepoLocation? location = resolvedRepo == null - ? null - : parseRepoLocation(resolvedRepo); + final RepoLocation? location = resolvedRepo == null ? null : parseRepoLocation(resolvedRepo); final String? previousRepo = _packRepos[name]; setState(() { _packRepos[name] = resolvedRepo; @@ -601,10 +550,7 @@ class _MainLayoutState extends State { /// 解析并缓存侧栏头像:同一仓库 URL 复用进行中的查询,成功后写入 [_repoIcons]。 Future _resolveRepoAvatar(String packName, String repoUrl) async { - final Future query = _repoIconQueries.putIfAbsent( - repoUrl, - () => _queryRepoIcon(repoUrl), - ); + final Future query = _repoIconQueries.putIfAbsent(repoUrl, () => _queryRepoIcon(repoUrl)); String? path; try { path = await query; @@ -619,8 +565,7 @@ class _MainLayoutState extends State { Future _queryRepoIcon(String repoUrl) async { try { - final Future Function(String repoUrl)? loader = - widget.loadRepoIcon; + final Future Function(String repoUrl)? loader = widget.loadRepoIcon; if (loader != null) { return await loader(repoUrl); } @@ -657,16 +602,12 @@ class _MainLayoutState extends State { Future _queryLatestTag(String repoUrl) async { List? tags; try { - final Future?> Function(String repoUrl)? loader = - widget.loadRemoteTags; + final Future?> Function(String repoUrl)? loader = widget.loadRemoteTags; if (loader != null) { tags = await loader(repoUrl); } else { final ProxyResolution proxy = await _proxyResolution(); - tags = await listRemoteTags( - repoUrl, - environment: proxyEnvironmentOverrides(proxy), - ); + tags = await listRemoteTags(repoUrl, environment: proxyEnvironmentOverrides(proxy)); } } catch (_) { tags = null; @@ -685,9 +626,7 @@ class _MainLayoutState extends State { } final String? current = pack.sourceVersion; final String? latest = _latestTags[repo]; - if (current != null && - latest != null && - compareTagVersions(latest, current) > 0) { + if (current != null && latest != null && compareTagVersions(latest, current) > 0) { return RepoBadge.update; } return RepoBadge.git; @@ -725,12 +664,7 @@ class _MainLayoutState extends State { if (!mounted) { return; } - showFloatingToast( - context, - '删除失败:$error', - type: FloatingToastType.error, - duration: const Duration(seconds: 5), - ); + showFloatingToast(context, '删除失败:$error', type: FloatingToastType.error, duration: const Duration(seconds: 5)); return; } if (!mounted) { @@ -805,11 +739,7 @@ class _MainLayoutState extends State { final Future> scanFuture = widget.scanFiles(sourcePath); await showDialog( context: context, - builder: (_) => RemapPackDialog( - pack: pack, - scanFuture: scanFuture, - onApply: _applyRemap, - ), + builder: (_) => RemapPackDialog(pack: pack, scanFuture: scanFuture, onApply: _applyRemap), ); } @@ -817,22 +747,12 @@ class _MainLayoutState extends State { pack = _withResolvedAuthor(pack); final PackModel? previous = _findPack(pack.name); if (previous != null) { - final Set oldPaths = { - for (final FileModel file in previous.files) file.path.toLowerCase(), - }; - final Set newPaths = { - for (final FileModel file in pack.files) file.path.toLowerCase(), - }; + final Set oldPaths = {for (final FileModel file in previous.files) file.path.toLowerCase()}; + final Set newPaths = {for (final FileModel file in pack.files) file.path.toLowerCase()}; final int added = newPaths.difference(oldPaths).length; final int removed = oldPaths.difference(newPaths).length; - final int oldSize = previous.files.fold( - 0, - (int sum, FileModel file) => sum + file.size, - ); - final int newSize = pack.files.fold( - 0, - (int sum, FileModel file) => sum + file.size, - ); + final int oldSize = previous.files.fold(0, (int sum, FileModel file) => sum + file.size); + final int newSize = pack.files.fold(0, (int sum, FileModel file) => sum + file.size); if (added != 0 || removed != 0 || oldSize != newSize) { pack.history = appendHistoryEntry( pack.history, @@ -866,11 +786,7 @@ class _MainLayoutState extends State { } catch (_) { header = null; } - return applySystemEntries( - pack, - header: header, - resolvePackVersion: (String name) => _findPack(name)?.version, - ).pack; + return applySystemEntries(pack, header: header, resolvePackVersion: (String name) => _findPack(name)?.version).pack; } void _selectPackagingBuilder(PackageBuilder builder) { @@ -926,17 +842,13 @@ class _MainLayoutState extends State { onSourceVersion: onSourceVersion, gitGlobalArguments: gitGlobalArguments, ), - prepare: - ( - PackModel pack, { - ToolDownloadProgressCallback? onDownloadProgress, - }) => prepare( - pack, - compilerPriority: widget.settings.compilerPriority, - cachedCompilers: widget.settings.detectedCompilers, - onCompilersDetected: _persistDetectedCompilers, - onDownloadProgress: onDownloadProgress, - ), + prepare: (PackModel pack, {ToolDownloadProgressCallback? onDownloadProgress}) => prepare( + pack, + compilerPriority: widget.settings.compilerPriority, + cachedCompilers: widget.settings.detectedCompilers, + onCompilersDetected: _persistDetectedCompilers, + onDownloadProgress: onDownloadProgress, + ), scanFiles: widget.scanFiles, onApply: _applyRemap, fixIncludes: widget.fixIncludes, @@ -967,10 +879,7 @@ class _MainLayoutState extends State { } /// 记录构建失败条目(失败会话关闭时落盘一次);保存失败仅提示、不阻断。 - Future _recordBuildFailure( - PackModel pack, - HistoryModel entry, - ) async { + Future _recordBuildFailure(PackModel pack, HistoryModel entry) async { final PackModel? current = _findPack(pack.name); if (current == null) { return; @@ -1042,14 +951,9 @@ class _MainLayoutState extends State { return; } final PackModel pack = _packs[selected]; - final PackageBuilder builder = effectivePackagingBuilder( - pack, - _packagingBuilder, - ); + final PackageBuilder builder = effectivePackagingBuilder(pack, _packagingBuilder); final bool isCmake = builder.id == _cmakeBuilderId; - final String? outputDirectory = isCmake - ? widget.settings.cmakeOutputDirectory - : widget.settings.outputDirectory; + final String? outputDirectory = isCmake ? widget.settings.cmakeOutputDirectory : widget.settings.outputDirectory; if (outputDirectory == null || outputDirectory.isEmpty) { showFloatingToast( context, @@ -1060,20 +964,12 @@ class _MainLayoutState extends State { return; } if (pack.sourcePath == null) { - showFloatingToast( - context, - '该包缺少源目录信息,无法打包', - type: FloatingToastType.error, - duration: const Duration(seconds: 5), - ); + showFloatingToast(context, '该包缺少源目录信息,无法打包', type: FloatingToastType.error, duration: const Duration(seconds: 5)); return; } final List missing = _missingDependencies(pack); if (missing.isNotEmpty) { - final bool proceed = await showMissingDependenciesDialog( - context, - missing: missing, - ); + final bool proceed = await showMissingDependenciesDialog(context, missing: missing); if (!proceed || !mounted) { return; } @@ -1086,8 +982,7 @@ class _MainLayoutState extends State { ? const [] : collectExecutableWarnings(plan); final List issues = [ - if (plan != null && builder.id == _nugetBuilderId) - ...collectPackagingIssues(pack, plan), + if (plan != null && builder.id == _nugetBuilderId) ...collectPackagingIssues(pack, plan), ...executableWarnings, ]; if (issues.isNotEmpty) { @@ -1100,13 +995,8 @@ class _MainLayoutState extends State { return; } } - final Future Function( - PackModel pack, - String outputDirectory, - ) - exportPackage = builder.id == _cmakeBuilderId - ? widget.exportCmakePackage - : widget.exportPackage; + final Future Function(PackModel pack, String outputDirectory) exportPackage = + builder.id == _cmakeBuilderId ? widget.exportCmakePackage : widget.exportPackage; await showDialog( context: context, builder: (_) => PackExportDialog( @@ -1118,10 +1008,7 @@ class _MainLayoutState extends State { ); } - Future _buildPackagingPlan( - PackageBuilder builder, - PackModel pack, - ) async { + Future _buildPackagingPlan(PackageBuilder builder, PackModel pack) async { try { return await builder.buildPlan(pack); } catch (_) { @@ -1147,11 +1034,7 @@ class _MainLayoutState extends State { Future _recordExport(PackModel pack, PackageExportResult result) async { pack.history = appendHistoryEntry( pack.history, - HistoryModel( - time: widget.now(), - type: HistoryType.exported, - message: '打包导出:${result.outputPath}', - ), + HistoryModel(time: widget.now(), type: HistoryType.exported, message: '打包导出:${result.outputPath}'), ); try { await widget.store.savePack(pack); @@ -1178,11 +1061,7 @@ class _MainLayoutState extends State { if (selected == null || selected < 0 || selected >= _packs.length) { return; } - await showPackHistoryDialog( - context, - pack: _packs[selected], - onSave: _savePack, - ); + await showPackHistoryDialog(context, pack: _packs[selected], onSave: _savePack); } Future _openDependencyGraph() async { @@ -1195,9 +1074,7 @@ class _MainLayoutState extends State { void _sortPacks() { _packs.sort((PackModel first, PackModel second) { - final int insensitive = first.name.toLowerCase().compareTo( - second.name.toLowerCase(), - ); + final int insensitive = first.name.toLowerCase().compareTo(second.name.toLowerCase()); if (insensitive != 0) { return insensitive; } @@ -1227,64 +1104,55 @@ class _MainLayoutState extends State { @override Widget build(BuildContext context) { return NavigationView( - titleBar: TitleBar( - isBackButtonVisible: false, - title: const Center(widthFactor: 1.0, child: Text('C++ NuGet 打包工具')), - ), paneBodyBuilder: _buildPaneBody, + titleBar: SizedBox.shrink(), pane: NavigationPane( selected: _selected, onChanged: (int newIndex) => setState(() => _selected = newIndex), - header: Row( - mainAxisAlignment: MainAxisAlignment.start, + header: Column( + mainAxisSize: .min, + spacing: 0, children: [ - Tooltip( - message: '添加文件夹', - child: IconButton(icon: Svgs.addFolder, onPressed: _addFolder), - ), - Tooltip( - message: '删除文件夹', - child: IconButton( - icon: Svgs.deleteFolder, - onPressed: _hasSelectedPack ? _deleteSelectedPack : null, - ), - ), - Tooltip( - message: '重新映射', - child: IconButton( - icon: Svgs.mapAsDrive, - onPressed: _hasSelectedPack ? _remapSelectedPack : null, - ), - ), - Tooltip( - message: '打包文件夹', - child: IconButton( - icon: Svgs.moveToFolder, - onPressed: _hasSelectedPack ? _packSelectedPack : null, - ), - ), - Tooltip( - message: '历史记录', - child: IconButton( - icon: Svgs.historyFolder, - onPressed: _hasSelectedPack ? _historySelectedPack : null, - ), - ), - Tooltip( - message: '依赖关系图', - child: IconButton( - icon: Svgs.internetConnection, - onPressed: _hasSelectedPack ? _openDependencyGraph : null, - ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Tooltip( + message: '添加文件夹', + child: IconButton(icon: Svgs.addFolder, onPressed: _addFolder), + ), + Tooltip( + message: '删除文件夹', + child: IconButton(icon: Svgs.deleteFolder, onPressed: _hasSelectedPack ? _deleteSelectedPack : null), + ), + Tooltip( + message: '重新映射', + child: IconButton(icon: Svgs.mapAsDrive, onPressed: _hasSelectedPack ? _remapSelectedPack : null), + ), + Tooltip( + message: '打包文件夹', + child: IconButton(icon: Svgs.moveToFolder, onPressed: _hasSelectedPack ? _packSelectedPack : null), + ), + Tooltip( + message: '历史记录', + child: IconButton( + icon: Svgs.historyFolder, + onPressed: _hasSelectedPack ? _historySelectedPack : null, + ), + ), + Tooltip( + message: '依赖关系图', + child: IconButton( + icon: Svgs.internetConnection, + onPressed: _hasSelectedPack ? _openDependencyGraph : null, + ), + ), + ], ), + Divider(style: DividerThemeData(horizontalMargin: .fromLTRB(0, 4, 8, 0)),), ], ), displayMode: PaneDisplayMode.expanded, - size: NavigationPaneSize( - openMaxWidth: 260, - openMinWidth: 260, - compactWidth: 50, - ), + size: NavigationPaneSize(openMaxWidth: 260, openMinWidth: 260, compactWidth: 50), items: PackList.buildCards( _packs, onSave: _savePack, @@ -1296,10 +1164,7 @@ class _MainLayoutState extends State { repoBadgeFor: _repoBadgeFor, repoIconFor: (PackModel pack) { final String name = pack.name.toLowerCase(); - return ( - platform: _repoPlatforms[name], - avatarPath: _repoIcons[name], - ); + return (platform: _repoPlatforms[name], avatarPath: _repoIcons[name]); }, loadHeader: widget.loadBuildHeader, ), @@ -1315,12 +1180,7 @@ class _MainLayoutState extends State { detectCompilers: widget.detectCompilers, ), ), - LibraryItem( - icon: Svgs.info, - title: '关于', - version: appVersion, - body: const About(), - ), + LibraryItem(icon: Svgs.info, title: '关于', version: appVersion, body: const About()), ], ), ); diff --git a/lib/models/pack_model.dart b/lib/models/pack_model.dart index 8b1707d..e7df2d4 100644 --- a/lib/models/pack_model.dart +++ b/lib/models/pack_model.dart @@ -37,16 +37,8 @@ class PackModel { static List packs = []; - PackModel({ - required this.name, - required this.version, - required this.author, - this.description, - this.license, - this.iconPath, - this.sourcePath, - this.sourceVersion, - }); + PackModel( + {required this.name, required this.version, required this.author, this.description, this.license, this.iconPath, this.sourcePath, this.sourceVersion}); Map toMap() { return { @@ -58,43 +50,22 @@ class PackModel { if (iconPath != null) 'iconPath': iconPath, if (sourcePath != null) 'sourcePath': sourcePath, if (sourceVersion != null) 'sourceVersion': sourceVersion, - 'files': >[ - for (final FileModel file in files) file.toMap(), - ], - 'dependencies': >[ - for (final DependencyModel dependency in dependencies) - dependency.toMap(), - ], - 'commands': >[ - for (final CmdModel command in commands) command.toMap(), - ], - 'macros': >[ - for (final MacroModel macro in macros) macro.toMap(), - ], + 'files': >[for (final FileModel file in files) file.toMap()], + 'dependencies': >[for (final DependencyModel dependency in dependencies) dependency.toMap()], + 'commands': >[for (final CmdModel command in commands) command.toMap()], + 'macros': >[for (final MacroModel macro in macros) macro.toMap()], 'libDirectories': >[ - for (final LibDirModel libDirectory in libDirectories) - libDirectory.toMap(), - ], - 'libraries': >[ - for (final LibraryModel library in libraries) library.toMap(), - ], - 'history': >[ - for (final HistoryModel entry in history) entry.toMap(), + for (final LibDirModel libDirectory in libDirectories) libDirectory.toMap() ], - 'scripts': >[ - for (final ScriptProjectModel script in scripts) script.toMap(), - ], - if (buildOptions.isNotEmpty) - 'buildOptions': {...buildOptions}, - if (enabledFormats.isNotEmpty) - 'enabledFormats': [...enabledFormats], + 'libraries': >[for (final LibraryModel library in libraries) library.toMap()], + 'history': >[for (final HistoryModel entry in history) entry.toMap()], + 'scripts': >[for (final ScriptProjectModel script in scripts) script.toMap()], + if (buildOptions.isNotEmpty) 'buildOptions': {...buildOptions}, + if (enabledFormats.isNotEmpty) 'enabledFormats': [...enabledFormats], }; } - factory PackModel.fromMap( - Map map, { - List? warnings, - }) { + factory PackModel.fromMap(Map map, {List? warnings}) { final PackModel pack = PackModel( name: _requiredString(map, 'name'), version: _requiredString(map, 'version'), @@ -106,42 +77,27 @@ class PackModel { sourceVersion: _optionalSourceVersion(map), ); - pack.files.addAll([ - for (final Map item in _mapList(map, 'files')) - FileModel.fromMap(item), - ]); + pack.files.addAll( + [for (final Map item in _mapList(map, 'files')) FileModel.fromMap(item)]); pack.dependencies.addAll([ - for (final Map item in _mapList(map, 'dependencies')) - DependencyModel.fromMap(item), - ]); - pack.commands.addAll([ - for (final Map item in _mapList(map, 'commands')) - CmdModel.fromMap(item), - ]); - pack.macros.addAll([ - for (final Map item in _mapList(map, 'macros')) - MacroModel.fromMap(item), + for (final Map item in _mapList(map, 'dependencies')) DependencyModel.fromMap(item) ]); + pack.commands.addAll( + [for (final Map item in _mapList(map, 'commands')) CmdModel.fromMap(item)]); + pack.macros.addAll( + [for (final Map item in _mapList(map, 'macros')) MacroModel.fromMap(item)]); pack.libDirectories.addAll([ - for (final Map item in _mapList(map, 'libDirectories')) - LibDirModel.fromMap(item), - ]); - pack.libraries.addAll([ - for (final Map item in _mapList(map, 'libraries')) - LibraryModel.fromMap(item), - ]); - pack.history.addAll([ - for (final Map item in _mapList(map, 'history')) - HistoryModel.fromMap(item), + for (final Map item in _mapList(map, 'libDirectories')) LibDirModel.fromMap(item) ]); + pack.libraries.addAll( + [for (final Map item in _mapList(map, 'libraries')) LibraryModel.fromMap(item)]); + pack.history.addAll( + [for (final Map item in _mapList(map, 'history')) HistoryModel.fromMap(item)]); final Set scriptIds = {}; for (final Map item in _mapList(map, 'scripts')) { try { - final ScriptProjectModel script = ScriptProjectModel.fromMap( - item, - warnings: warnings, - ); + final ScriptProjectModel script = ScriptProjectModel.fromMap(item, warnings: warnings); if (!scriptIds.add(script.id)) { warnings?.add('脚本 id 重复,已丢弃:${script.id}'); continue; @@ -189,8 +145,7 @@ Map _stringStringMap(Map map, String key) { } return { for (final MapEntry entry in value.entries) - if (entry.key is String && entry.value is String) - entry.key as String: entry.value as String, + if (entry.key is String && entry.value is String) entry.key as String: entry.value as String, }; } diff --git a/lib/pages/about.dart b/lib/pages/about.dart index 066a962..c83d9fe 100644 --- a/lib/pages/about.dart +++ b/lib/pages/about.dart @@ -4,7 +4,6 @@ import 'package:cpp_nuget_pack/util/svgs.dart'; import 'package:cpp_nuget_pack/widgets/floating_toast.dart'; import 'package:cpp_nuget_pack/widgets/tag.dart'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter_svg/flutter_svg.dart'; class About extends StatefulWidget { const About({super.key, this.openUrl = openExternalUrl}); @@ -19,12 +18,7 @@ class _AboutState extends State { Future _openRepository() async { final bool opened = await widget.openUrl(appRepositoryUrl); if (!opened && mounted) { - showFloatingToast( - context, - '无法打开链接', - type: FloatingToastType.error, - duration: const Duration(seconds: 5), - ); + showFloatingToast(context, '无法打开链接', type: FloatingToastType.error, duration: const Duration(seconds: 5)); } } @@ -34,56 +28,37 @@ class _AboutState extends State { final Color secondaryColor = theme.resources.textFillColorSecondary; return SizedBox.expand( key: const Key('aboutPage'), - child: Container( - decoration: BoxDecoration(color: theme.cardColor), - child: Center( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset( - 'assets/icons/cardboard_box.svg', - width: 64, - height: 64, + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset('assets/icons/logo.png', width: 128, height: 128), + const SizedBox(height: 16), + Text(appName, style: theme.typography.titleLarge?.copyWith(fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + Tag(text: 'v$appVersion', fontSize: 11), + const SizedBox(height: 16), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Text( + appDescription, + textAlign: TextAlign.center, + style: TextStyle(color: secondaryColor), ), - const SizedBox(height: 16), - Text( - appName, - style: theme.typography.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - ), + ), + const SizedBox(height: 24), + FilledButton( + key: const Key('aboutRepoButton'), + onPressed: _openRepository, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [Svgs.openBox, const SizedBox(width: 8), const Text('项目主页')], ), - const SizedBox(height: 8), - Tag(text: 'v$appVersion', fontSize: 11), - const SizedBox(height: 16), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 420), - child: Text( - appDescription, - textAlign: TextAlign.center, - style: TextStyle(color: secondaryColor), - ), - ), - const SizedBox(height: 24), - FilledButton( - key: const Key('aboutRepoButton'), - onPressed: _openRepository, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Svgs.openBox, - const SizedBox(width: 8), - const Text('项目主页'), - ], - ), - ), - const SizedBox(height: 24), - Text( - '第三方组件许可见仓库 THIRD_PARTY_NOTICES.md', - style: TextStyle(color: secondaryColor, fontSize: 12), - ), - ], - ), + ), + const SizedBox(height: 24), + Text('第三方组件许可见仓库 THIRD_PARTY_NOTICES.md', style: TextStyle(color: secondaryColor, fontSize: 12)), + ], ), ), ), diff --git a/lib/pages/pack_compile_settings.dart b/lib/pages/pack_compile_settings.dart index 46ac64d..9a19725 100644 --- a/lib/pages/pack_compile_settings.dart +++ b/lib/pages/pack_compile_settings.dart @@ -45,11 +45,7 @@ class _PackCompileSettingsState extends State { bool _saving = false; Future _addMacro() async { - final CompileEntryResult? result = await _openEntryDialog( - title: '添加宏定义', - label: '宏定义', - hintText: 'MY_MACRO=1', - ); + final CompileEntryResult? result = await _openEntryDialog(title: '添加宏定义', label: '宏定义', hintText: 'MY_MACRO=1'); if (result == null || !mounted) { return; } @@ -82,10 +78,7 @@ class _PackCompileSettingsState extends State { pack, macros: [ for (int i = 0; i < pack.macros.length; i++) - if (i == index) - MacroModel(value: result.text, buildModel: result.buildModel) - else - pack.macros[i], + if (i == index) MacroModel(value: result.text, buildModel: result.buildModel) else pack.macros[i], ], ), '已保存', @@ -121,11 +114,7 @@ class _PackCompileSettingsState extends State { pack, commands: [ ...pack.commands, - CmdModel( - command: result.text, - type: type, - buildModel: result.buildModel, - ), + CmdModel(command: result.text, type: type, buildModel: result.buildModel), ], ), '已添加', @@ -152,11 +141,7 @@ class _PackCompileSettingsState extends State { commands: [ for (int i = 0; i < pack.commands.length; i++) if (i == index) - CmdModel( - command: result.text, - type: command.type, - buildModel: result.buildModel, - ) + CmdModel(command: result.text, type: command.type, buildModel: result.buildModel) else pack.commands[i], ], @@ -218,10 +203,7 @@ class _PackCompileSettingsState extends State { pack, libDirectories: [ for (int i = 0; i < pack.libDirectories.length; i++) - if (i == index) - LibDirModel(path: result.text, buildModel: result.buildModel) - else - pack.libDirectories[i], + if (i == index) LibDirModel(path: result.text, buildModel: result.buildModel) else pack.libDirectories[i], ], ), '已保存', @@ -242,11 +224,7 @@ class _PackCompileSettingsState extends State { } Future _addLibrary() async { - final CompileEntryResult? result = await _openEntryDialog( - title: '添加附加库', - label: '库名称', - hintText: 'mylib.lib', - ); + final CompileEntryResult? result = await _openEntryDialog(title: '添加附加库', label: '库名称', hintText: 'mylib.lib'); if (result == null || !mounted) { return; } @@ -279,10 +257,7 @@ class _PackCompileSettingsState extends State { pack, libraries: [ for (int i = 0; i < pack.libraries.length; i++) - if (i == index) - LibraryModel(name: result.text, buildModel: result.buildModel) - else - pack.libraries[i], + if (i == index) LibraryModel(name: result.text, buildModel: result.buildModel) else pack.libraries[i], ], ), '已保存', @@ -335,10 +310,7 @@ class _PackCompileSettingsState extends State { ]; } - Future _persist( - PackModel Function(PackModel pack) update, - String message, - ) async { + Future _persist(PackModel Function(PackModel pack) update, String message) async { if (_saving) { return; } @@ -450,11 +422,13 @@ class _PackCompileSettingsState extends State { Widget build(BuildContext context) { return SizedBox.expand( child: Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Divider(), + SizedBox(height: 5), _buildSection( title: '宏定义', columnLabel: '宏定义', @@ -518,14 +492,8 @@ class _PackCompileSettingsState extends State { }) { return _buildSectionFrame( title: title, - action: FilledButton( - key: addKey, - onPressed: _saving ? null : onAdd, - child: const Text('添加'), - ), - body: entries.isEmpty - ? _buildEmptyText(emptyText) - : _buildEntryList(entries, columnLabel), + action: Button(key: addKey, onPressed: _saving ? null : onAdd, child: const Text('添加')), + body: _buildEntryList(entries, columnLabel), ); } @@ -534,62 +502,52 @@ class _PackCompileSettingsState extends State { return _buildSectionFrame( sectionKey: const Key('nodeScriptsSection'), title: '节点脚本', - action: FilledButton( + action: Button( key: const Key('openScriptEditorButton'), onPressed: _saving ? null : _openScriptEditor, child: const Text('打开节点编辑器'), ), - body: scripts.isEmpty - ? _buildEmptyText('暂无节点脚本') - : _buildScriptList(scripts), + body: _buildScriptList(scripts), ); } - Widget _buildSectionFrame({ - required String title, - required Widget action, - required Widget body, - Key? sectionKey, - }) { + Widget _buildSectionFrame({required String title, required Widget action, required Widget body, Key? sectionKey}) { return Column( - key: sectionKey, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Text( - title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: FluentTheme.of(context).typography.body?.color, - ), + Padding( + padding: .fromLTRB(0, 8, 0, 0), + child: Column( + key: sectionKey, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: FluentTheme.of(context).typography.body?.color, + ), + ), + ), + action, + ], ), - ), - action, - ], + ], + ), ), - const SizedBox(height: 4), + SizedBox(height: 5), body, ], ); } - Widget _buildEmptyText(String text) { - return Text( - text, - style: TextStyle( - color: FluentTheme.of(context).resources.textFillColorSecondary, - ), - ); - } - void _openScriptEditor() { Navigator.of(context).push( FluentPageRoute( - builder: (_) => - ScriptEditorPage(pack: widget.pack, onSave: widget.onSave), + builder: (_) => ScriptEditorPage(pack: widget.pack, onSave: widget.onSave), ), ); } @@ -597,17 +555,17 @@ class _PackCompileSettingsState extends State { Widget _buildScriptList(List scripts) { return _buildTable( header: _buildScriptHeaderRow(), - rows: [ - for (final ScriptProjectModel script in scripts) - _buildScriptRow(script), - ], + rows: scripts.isEmpty + ? [SizedBox.shrink()] + : [for (final ScriptProjectModel script in scripts) _buildScriptRow(script)], ); } Widget _buildScriptHeaderRow() { final TextStyle style = _headerTextStyle(); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + return Card( + margin: const .fromLTRB(0, 0, 0, 2), + padding: const .fromLTRB(8, 8, 8, 8), child: Row( children: [ Expanded(child: Text('名称', style: style)), @@ -630,8 +588,9 @@ class _PackCompileSettingsState extends State { Widget _buildScriptRow(ScriptProjectModel script) { final (String statusText, Color statusColor) = _scriptStatus(script); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + return Card( + margin: const .fromLTRB(0, 0, 0, 2), + padding: const .fromLTRB(8, 8, 8, 8), child: Row( children: [ Expanded(child: Text(script.name, overflow: TextOverflow.ellipsis)), @@ -658,10 +617,7 @@ class _PackCompileSettingsState extends State { SizedBox( width: _statusColumnWidth, child: Center( - child: Text( - statusText, - style: TextStyle(fontSize: 12, color: statusColor), - ), + child: Text(statusText, style: TextStyle(fontSize: 12, color: statusColor)), ), ), ], @@ -672,9 +628,7 @@ class _PackCompileSettingsState extends State { (String, Color) _scriptStatus(ScriptProjectModel script) { final Brightness brightness = FluentTheme.of(context).brightness; final List diagnostics = GraphValidator.validate(script); - final int errors = diagnostics - .where((ScriptDiagnostic diagnostic) => diagnostic.isError) - .length; + final int errors = diagnostics.where((ScriptDiagnostic diagnostic) => diagnostic.isError).length; final int warnings = diagnostics.length - errors; if (errors > 0) { return ('$errors 个错误', AppColors.critical(brightness)); @@ -688,42 +642,21 @@ class _PackCompileSettingsState extends State { Widget _buildEntryList(List<_CompileEntry> entries, String columnLabel) { return _buildTable( header: _buildHeaderRow(columnLabel), - rows: [ - for (final _CompileEntry entry in entries) _buildEntryRow(entry), - ], + rows: entries.isEmpty + ? [SizedBox.shrink()] + : [for (final _CompileEntry entry in entries) _buildEntryRow(entry)], ); } Widget _buildTable({required Widget header, required List rows}) { - final FluentThemeData theme = FluentTheme.of(context); - final DividerThemeData dividerTheme = theme.dividerTheme; - return FluentTheme( - data: theme.copyWith( - // 全局分割线自带 8px 水平边距,覆盖为 0 使线与列表内容同宽 - dividerTheme: DividerThemeData( - decoration: dividerTheme.decoration, - verticalMargin: dividerTheme.verticalMargin, - horizontalMargin: EdgeInsets.zero, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - header, - const Divider(), - for (int index = 0; index < rows.length; index++) ...[ - if (index > 0) const Divider(), - rows[index], - ], - ], - ), - ); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [header, ...rows]); } Widget _buildHeaderRow(String columnLabel) { final TextStyle style = _headerTextStyle(); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + return Card( + margin: const .fromLTRB(0, 0, 0, 2), + padding: const .fromLTRB(8, 8, 8, 8), child: Row( children: [ Expanded(child: Text(columnLabel, style: style)), @@ -741,30 +674,24 @@ class _PackCompileSettingsState extends State { } TextStyle _headerTextStyle() { - return TextStyle( - fontSize: 12, - color: FluentTheme.of(context).resources.textFillColorSecondary, - ); + return TextStyle(fontSize: 12, color: FluentTheme.of(context).resources.textFillColorSecondary); } Widget _buildEntryRow(_CompileEntry entry) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + return Card( + margin: const .fromLTRB(0, 0, 0, 2), + padding: const .fromLTRB(8, 2, 8, 2), child: Row( children: [ Expanded( child: Row( children: [ - Flexible( - child: Text(entry.text, overflow: TextOverflow.ellipsis), - ), + Flexible(child: Text(entry.text, overflow: TextOverflow.ellipsis)), if (entry.isSystem) ...[ const SizedBox(width: 5), Tag( text: '系统', - color: FluentTheme.of( - context, - ).resources.solidBackgroundFillColorQuarternary, + color: FluentTheme.of(context).resources.solidBackgroundFillColorBaseAlt, fontSize: 10, ), ], @@ -794,14 +721,13 @@ class _PackCompileSettingsState extends State { onPressed: _saving || entry.isSystem ? null : entry.onEdit, ), ), + SizedBox(width: 2), Tooltip( message: entry.isSystem ? _systemLockTooltip : '删除', child: IconButton( key: entry.deleteKey, icon: const Icon(FluentIcons.delete, size: 16), - onPressed: _saving || entry.isSystem - ? null - : entry.onDelete, + onPressed: _saving || entry.isSystem ? null : entry.onDelete, ), ), ], diff --git a/lib/pages/pack_dependencies.dart b/lib/pages/pack_dependencies.dart index 4df3655..9c346c1 100644 --- a/lib/pages/pack_dependencies.dart +++ b/lib/pages/pack_dependencies.dart @@ -12,12 +12,7 @@ const double _actionColumnWidth = 80; const String _systemLockTooltip = '由构建管线注册,禁止修改/删除'; class PackDependencies extends StatefulWidget { - const PackDependencies({ - super.key, - required this.pack, - required this.allPacks, - required this.onSave, - }); + const PackDependencies({super.key, required this.pack, required this.allPacks, required this.onSave}); final PackModel pack; final List allPacks; @@ -33,14 +28,11 @@ class _PackDependenciesState extends State { List get _candidates { final String self = widget.pack.name.toLowerCase(); final Set added = { - for (final DependencyModel dependency in widget.pack.dependencies) - dependency.name.toLowerCase(), + for (final DependencyModel dependency in widget.pack.dependencies) dependency.name.toLowerCase(), }; return [ for (final PackModel pack in widget.allPacks) - if (pack.name.toLowerCase() != self && - !added.contains(pack.name.toLowerCase())) - pack, + if (pack.name.toLowerCase() != self && !added.contains(pack.name.toLowerCase())) pack, ]; } @@ -52,10 +44,7 @@ class _PackDependenciesState extends State { if (result == null || !mounted) { return; } - await _persist([ - ...widget.pack.dependencies, - result, - ], '已添加'); + await _persist([...widget.pack.dependencies, result], '已添加'); } Future _edit(DependencyModel dependency) async { @@ -68,9 +57,7 @@ class _PackDependenciesState extends State { } final List updated = [ for (final DependencyModel item in widget.pack.dependencies) - item.name.toLowerCase() == dependency.name.toLowerCase() - ? result - : item, + item.name.toLowerCase() == dependency.name.toLowerCase() ? result : item, ]; await _persist(updated, '已保存'); } @@ -83,10 +70,7 @@ class _PackDependenciesState extends State { await _persist(updated, '已删除'); } - Future _persist( - List dependencies, - String message, - ) async { + Future _persist(List dependencies, String message) async { if (_saving) { return; } @@ -106,21 +90,29 @@ class _PackDependenciesState extends State { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FilledButton( - key: const Key('addDependencyButton'), - onPressed: _saving ? null : _add, - child: const Text('添加依赖'), - ), - const SizedBox(height: 12), - Expanded( - child: widget.pack.dependencies.isEmpty - ? _buildEmptyGuide() - : _buildDependencyList(), + Divider(), + Padding( + padding: const EdgeInsets.fromLTRB(8, 5, 8, 0), + child: Row( + children: [ + Button( + key: const Key('addDependencyButton'), + onPressed: _saving ? null : _add, + child: const Row( + mainAxisSize: .min, + children: [Icon(FluentIcons.add), SizedBox(width: 5), Text('添加依赖')], + ), + ), + Spacer(), + ], + ), ), + const SizedBox(height: 5), + Expanded(child: widget.pack.dependencies.isEmpty ? _buildEmptyGuide() : _buildDependencyList()), ], ), ); @@ -129,143 +121,112 @@ class _PackDependenciesState extends State { Widget _buildDependencyList() { final FluentThemeData theme = FluentTheme.of(context); final DividerThemeData dividerTheme = theme.dividerTheme; - return FluentTheme( - data: theme.copyWith( - // 全局分割线自带 8px 水平边距,覆盖为 0 使线与列表内容同宽 - dividerTheme: DividerThemeData( - decoration: dividerTheme.decoration, - verticalMargin: dividerTheme.verticalMargin, - horizontalMargin: EdgeInsets.zero, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeaderRow(), - const Divider(), - Expanded( - child: ListView.separated( - itemCount: widget.pack.dependencies.length, - separatorBuilder: (BuildContext context, int index) => - const Divider(), - itemBuilder: (BuildContext context, int index) => - _buildRow(context, widget.pack.dependencies[index]), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderRow(), + Expanded( + child: ListView.builder( + itemCount: widget.pack.dependencies.length, + itemBuilder: (BuildContext context, int index) => _buildRow(context, widget.pack.dependencies[index]), ), - ], - ), + ), + ], ); } Widget _buildHeaderRow() { - final TextStyle style = TextStyle( - fontSize: 12, - color: FluentTheme.of(context).resources.textFillColorSecondary, - ); + final TextStyle style = TextStyle(fontSize: 12, color: FluentTheme.of(context).resources.textFillColorSecondary); return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - Expanded(child: Text('包名', style: style)), - SizedBox( - width: _versionColumnWidth, - child: Text('版本范围', style: style), - ), - SizedBox( - width: _actionColumnWidth, - child: Text('操作', style: style, textAlign: TextAlign.center), - ), - ], + padding: const EdgeInsets.fromLTRB(8, 0, 8, 2), + child: Card( + child: Row( + children: [ + Expanded(child: Text('包名', style: style)), + SizedBox( + width: _versionColumnWidth, + child: Text('版本范围', style: style), + ), + SizedBox( + width: _actionColumnWidth, + child: Text('操作', style: style, textAlign: TextAlign.center), + ), + ], + ), ), ); } Widget _buildEmptyGuide() { return const Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [Text('暂无依赖'), SizedBox(height: 6), Text('点击「添加依赖」开始')], - ), + child: Column(mainAxisSize: MainAxisSize.min, children: [Text('暂无依赖'), SizedBox(height: 6), Text('点击「添加依赖」开始')]), ); } Widget _buildRow(BuildContext context, DependencyModel dependency) { final FluentThemeData theme = FluentTheme.of(context); final bool missing = !widget.allPacks.any( - (PackModel pack) => - pack.name.toLowerCase() == dependency.name.toLowerCase(), + (PackModel pack) => pack.name.toLowerCase() == dependency.name.toLowerCase(), ); return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - Expanded( - child: Row( - children: [ - Flexible( - child: Text(dependency.name, overflow: TextOverflow.ellipsis), - ), - if (dependency.system) ...[ - const SizedBox(width: 5), - Tag( - text: '系统', - color: theme.resources.solidBackgroundFillColorQuarternary, - fontSize: 10, - ), + padding: const EdgeInsets.fromLTRB(8, 2, 8, 2), + child: Card( + padding: EdgeInsetsGeometry.fromLTRB(8, 2, 8, 2), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Flexible(child: Text(dependency.name, overflow: TextOverflow.ellipsis)), + if (dependency.system) ...[ + const SizedBox(width: 5), + Tag(text: '系统', color: theme.resources.solidBackgroundFillColorQuarternary, fontSize: 10), + ], + if (missing) ...[const SizedBox(width: 5), Tag(text: '缺失', color: MarkerColors.red, fontSize: 10)], ], - if (missing) ...[ - const SizedBox(width: 5), - Tag(text: '缺失', color: MarkerColors.red, fontSize: 10), - ], - ], + ), ), - ), - SizedBox( - width: _versionColumnWidth, - child: Text( - dependency.version, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: theme.resources.textFillColorSecondary), + SizedBox( + width: _versionColumnWidth, + child: Text( + dependency.version, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: theme.resources.textFillColorSecondary), + ), ), - ), - SizedBox( - width: _actionColumnWidth, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Tooltip( - message: dependency.system ? _systemLockTooltip : '编辑', - child: IconButton( - key: Key('dependencyEditButton_${dependency.name}'), - icon: const Icon(FluentIcons.edit, size: 16), - onPressed: _saving || dependency.system - ? null - : () => _edit(dependency), + SizedBox( + width: _actionColumnWidth, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Tooltip( + message: dependency.system ? _systemLockTooltip : '编辑', + child: IconButton( + key: Key('dependencyEditButton_${dependency.name}'), + icon: const Icon(FluentIcons.edit, size: 16), + onPressed: _saving || dependency.system ? null : () => _edit(dependency), + ), ), - ), - Tooltip( - message: dependency.system ? _systemLockTooltip : '删除', - child: IconButton( - key: Key('dependencyDeleteButton_${dependency.name}'), - icon: const Icon(FluentIcons.delete, size: 16), - onPressed: _saving || dependency.system - ? null - : () => _remove(dependency), + SizedBox(width: 3), + Tooltip( + message: dependency.system ? _systemLockTooltip : '删除', + child: IconButton( + key: Key('dependencyDeleteButton_${dependency.name}'), + icon: const Icon(FluentIcons.delete, size: 16), + onPressed: _saving || dependency.system ? null : () => _remove(dependency), + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ); } } -PackModel _withDependencies( - PackModel pack, - List dependencies, -) { +PackModel _withDependencies(PackModel pack, List dependencies) { return PackModel( name: pack.name, version: pack.version, diff --git a/lib/pages/pack_files.dart b/lib/pages/pack_files.dart index 6a0838b..ec52c3e 100644 --- a/lib/pages/pack_files.dart +++ b/lib/pages/pack_files.dart @@ -49,6 +49,7 @@ class PackFiles extends StatefulWidget { class _PackFilesState extends State { static const double _treeIconSize = 18; + // TreeView 行内容原有效高度 18,按用户确认加高 8 后为 26(行容器另加 4)。 static const double _treeRowContentMinHeight = 26; static const Set _buildLabelTypes = { @@ -56,6 +57,7 @@ class _PackFilesState extends State { FileType.dll, FileType.pdb, FileType.executable, + FileType.source }; static final RegExp _pathSeparator = RegExp(r'[/\\]'); @@ -90,8 +92,7 @@ class _PackFilesState extends State { if (before.name != after.name || before.sourcePath != after.sourcePath) { return true; } - return findBuildScript(before.files)?.path != - findBuildScript(after.files)?.path; + return findBuildScript(before.files)?.path != findBuildScript(after.files)?.path; } Future _refreshHeader() async { @@ -112,8 +113,7 @@ class _PackFilesState extends State { Future _refreshLatestVersion() async { final String? repo = _header?.repo; - final Future Function(String repoUrl)? loader = - widget.loadLatestVersion; + final Future Function(String repoUrl)? loader = widget.loadLatestVersion; final int loadId = ++_latestLoadId; if (repo == null) { setState(() { @@ -148,10 +148,8 @@ class _PackFilesState extends State { }); } - static List _pathSegments(String path) => path - .split(_pathSeparator) - .where((String segment) => segment.isNotEmpty) - .toList(); + static List _pathSegments(String path) => + path.split(_pathSeparator).where((String segment) => segment.isNotEmpty).toList(); static int _compareNames(String first, String second) { final int insensitive = first.toLowerCase().compareTo(second.toLowerCase()); @@ -171,13 +169,8 @@ class _PackFilesState extends State { } _DirNode parent = root; for (final String segment in segments.take(segments.length - 1)) { - final String childPath = parent.path.isEmpty - ? segment - : '${parent.path}/$segment'; - parent = parent.children.putIfAbsent( - segment, - () => _DirNode(name: segment, path: childPath), - ); + final String childPath = parent.path.isEmpty ? segment : '${parent.path}/$segment'; + parent = parent.children.putIfAbsent(segment, () => _DirNode(name: segment, path: childPath)); parent.size += file.size; parent.fileCount += 1; } @@ -189,51 +182,33 @@ class _PackFilesState extends State { List _buildTreeItems(_DirNode node, Color sizeColor) { final List items = []; final List<_DirNode> dirs = node.children.values.toList() - ..sort( - (_DirNode first, _DirNode second) => - _compareNames(first.name, second.name), - ); + ..sort((_DirNode first, _DirNode second) => _compareNames(first.name, second.name)); for (final _DirNode dir in dirs) { final bool expanded = _expandedDirs.contains(dir.path); items.add( TreeViewItem( value: dir.path, - leading: _buildIcon( - dir.name, - isDirectory: true, - isExpanded: expanded, - ), + leading: _buildIcon(dir.name, isDirectory: true, isExpanded: expanded), expanded: expanded, content: _buildRow( dir.name, formatBytes(dir.size), sizeColor, - label: Text( - '(${dir.fileCount} 个文件)', - style: TextStyle(color: sizeColor), - ), + label: Text('(${dir.fileCount} 个文件)', style: TextStyle(color: sizeColor)), ), children: _buildTreeItems(dir, sizeColor), ), ); } final List files = node.files.toList() - ..sort( - (FileModel first, FileModel second) => - _compareNames(first.name, second.name), - ); + ..sort((FileModel first, FileModel second) => _compareNames(first.name, second.name)); for (final FileModel file in files) { items.add( TreeViewItem( leading: _buildIcon(file.name), content: GestureDetector( onDoubleTap: () => _openFile(file), - child: _buildRow( - file.name, - formatBytes(file.size), - sizeColor, - label: _buildBuildLabel(file), - ), + child: _buildRow(file.name, formatBytes(file.size), sizeColor, label: _buildBuildLabel(file)), ), ), ); @@ -241,29 +216,17 @@ class _PackFilesState extends State { return items; } - Widget _buildIcon( - String name, { - bool isDirectory = false, - bool isExpanded = false, - }) { + Widget _buildIcon(String name, {bool isDirectory = false, bool isExpanded = false}) { return SvgPicture.asset( - iconAssetFor( - brightness: FluentTheme.of(context).brightness, - name: name, - isDirectory: isDirectory, - isExpanded: isExpanded, - ), + iconAssetFor(brightness: FluentTheme + .of(context) + .brightness, name: name, isDirectory: isDirectory, isExpanded: isExpanded), width: _treeIconSize, height: _treeIconSize, ); } - static Widget _buildRow( - String name, - String size, - Color sizeColor, { - Widget? label, - }) { + static Widget _buildRow(String name, String size, Color sizeColor, {Widget? label}) { final Widget nameText = Text(name, overflow: TextOverflow.ellipsis); return ConstrainedBox( constraints: const BoxConstraints(minHeight: _treeRowContentMinHeight), @@ -283,6 +246,7 @@ class _PackFilesState extends State { ), const SizedBox(width: 8), Text(size, style: TextStyle(color: sizeColor)), + const SizedBox(width: 16), ], ), ); @@ -296,24 +260,14 @@ class _PackFilesState extends State { if (label == null) { return null; } - return Tag( - text: label, - color: label == releaseBuildLabel - ? MarkerColors.green - : MarkerColors.orange, - fontSize: 10, - ); + return Tag(text: label, color: label == releaseBuildLabel ? MarkerColors.green : MarkerColors.orange, fontSize: 10); } Future _openFile(FileModel file) async { final String? sourcePath = widget.pack.sourcePath; if (sourcePath == null) { - showFloatingToast( - context, - '该包缺少源目录信息,无法打开文件', - type: FloatingToastType.error, - duration: const Duration(seconds: 5), - ); + showFloatingToast(context, '该包缺少源目录信息,无法打开文件', type: FloatingToastType.error, + duration: const Duration(seconds: 5)); return; } final bool opened = await widget.openFile(joinPath(sourcePath, file.path)); @@ -322,18 +276,11 @@ class _PackFilesState extends State { } if (!opened) { showFloatingToast( - context, - '无法打开文件:${file.name}', - type: FloatingToastType.error, - duration: const Duration(seconds: 5), - ); + context, '无法打开文件:${file.name}', type: FloatingToastType.error, duration: const Duration(seconds: 5)); } } - Future _onItemInvoked( - TreeViewItem item, - TreeViewItemInvokeReason reason, - ) async { + Future _onItemInvoked(TreeViewItem item, TreeViewItemInvokeReason reason) async { if (reason != TreeViewItemInvokeReason.pressed) { return; } @@ -360,28 +307,29 @@ class _PackFilesState extends State { } } - Widget _buildToolbar({ - required String? openRepoUrl, - required bool canBuild, - required bool showVersionChip, - }) { + Widget _buildToolbar({required String? openRepoUrl, required bool canBuild, required bool showVersionChip}) { return Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Wrap( - spacing: 16, - runSpacing: 8, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - if (openRepoUrl != null) _buildOpenRepoButton(openRepoUrl), - if (canBuild) - FilledButton( - key: const Key('buildPackButton'), - onPressed: () => widget.onBuildPack!(widget.pack), - child: const Text('构建'), + padding: const EdgeInsets.fromLTRB(8, 3, 8, 0), + child: Card( + padding: EdgeInsetsGeometry.all(5), + child: Row( + children: [ + Wrap( + spacing: 5, + runSpacing: 0, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (canBuild) FilledButton(key: const Key('buildPackButton'), + onPressed: () => widget.onBuildPack!(widget.pack), + child: const Text('构建')), + if (openRepoUrl != null) _buildOpenRepoButton(openRepoUrl), + if (canBuild) _buildRuntimeGroup(), + ], ), - if (canBuild) _buildRuntimeGroup(), - if (showVersionChip) _buildVersionChip(), - ], + Spacer(), + if (showVersionChip) ...[_buildVersionChip()], + ], + ), ), ); } @@ -389,18 +337,7 @@ class _PackFilesState extends State { Widget _buildOpenRepoButton(String url) { return Tooltip( message: url, - child: Button( - key: const Key('openRepoButton'), - onPressed: () => _openRepo(url), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(FluentIcons.open_in_new_window, size: 14), - SizedBox(width: 6), - Text('打开远程仓库'), - ], - ), - ), + child: Button(key: const Key('openRepoButton'), onPressed: () => _openRepo(url), child: Text('远程仓库')), ); } @@ -410,12 +347,7 @@ class _PackFilesState extends State { return; } if (!opened) { - showFloatingToast( - context, - '无法打开链接', - type: FloatingToastType.error, - duration: const Duration(seconds: 5), - ); + showFloatingToast(context, '无法打开链接', type: FloatingToastType.error, duration: const Duration(seconds: 5)); } } @@ -424,14 +356,6 @@ class _PackFilesState extends State { return Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - '运行库', - style: TextStyle( - fontSize: 12, - color: FluentTheme.of(context).resources.textFillColorSecondary, - ), - ), - const SizedBox(width: 8), RuntimeLibrarySelector( value: runtimeValue, enabled: widget.onSave != null && !_savingOption, @@ -441,30 +365,23 @@ class _PackFilesState extends State { } }, ), - const SizedBox(width: 6), - const RuntimeLibraryHelpButton(), ], ); } /// 运行库保存值(`MD` / `MT` 大写归一);缺失或非法显示「默认(跟随配方)」。 String? _savedRuntimeLibrary() { - final String? normalized = normalizeRuntimeLibrary( - widget.pack.buildOptions[runtimeOptionName], - ); + final String? normalized = normalizeRuntimeLibrary(widget.pack.buildOptions[runtimeOptionName]); return normalized?.toUpperCase(); } /// 版本胶囊:纯展示(不可点击、不参与 Tab 序),四态见设计规格 §5.3。 Widget _buildVersionChip() { - final String current = widget.pack.sourceVersion ?? '—'; + final String current = widget.pack.sourceVersion ?? '不可用'; final bool querying = !_latestLoaded; final String? latestVersion = _latestVersion; - final String latestText = querying ? '查询中…' : (latestVersion ?? '—'); - final bool hasUpdate = - !querying && - latestVersion != null && - compareTagVersions(latestVersion, current) > 0; + final String latestText = querying ? '查询中…' : (latestVersion ?? '不可用'); + final bool hasUpdate = !querying && latestVersion != null && compareTagVersions(latestVersion, current) > 0; final String latestTooltip; if (querying) { latestTooltip = '最新版本:查询中…'; @@ -473,65 +390,38 @@ class _PackFilesState extends State { } else if (latestVersion != null) { latestTooltip = '最新版本:$latestText(已是最新)'; } else { - latestTooltip = '最新版本:—'; + latestTooltip = '最新版本:不可用'; } final FluentThemeData theme = FluentTheme.of(context); final Color valueColor = theme.resources.textFillColorPrimary; final Color placeholderColor = theme.resources.textFillColorTertiary; - final TextStyle labelStyle = TextStyle( - fontSize: 12, - color: theme.resources.textFillColorSecondary, - ); + final TextStyle labelStyle = TextStyle(fontSize: 12, color: theme.resources.textFillColorSecondary); return Container( key: const Key('packRepoVersionLabel'), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( - color: theme.resources.solidBackgroundFillColorQuarternary, - borderRadius: BorderRadius.circular(999), - ), + color: theme.resources.solidBackgroundFillColorSecondary, borderRadius: BorderRadius.circular(999)), child: Tooltip( message: '当前版本:$current\n$latestTooltip', child: Row( mainAxisSize: MainAxisSize.min, children: [ if (hasUpdate) ...[ - const Icon( - FluentIcons.update_restore, - size: 12, - color: MarkerColors.green, - ), - const SizedBox(width: 6), + const Icon(FluentIcons.update_restore, size: 12, color: MarkerColors.green), + const SizedBox(width: 6) ], Text('当前', style: labelStyle), const SizedBox(width: 4), - Text( - current, - style: TextStyle( - fontSize: 12, - color: widget.pack.sourceVersion == null - ? placeholderColor - : valueColor, - ), - ), + Text(current, style: TextStyle( + fontSize: 12, color: widget.pack.sourceVersion == null ? placeholderColor : valueColor)), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: Icon( - FluentIcons.chevron_right, - size: 12, - color: placeholderColor, - ), + child: Icon(FluentIcons.chevron_right, size: 12, color: placeholderColor), ), Text('最新', style: labelStyle), const SizedBox(width: 4), - Text( - latestText, - style: TextStyle( - fontSize: 12, - color: querying || latestVersion == null - ? placeholderColor - : valueColor, - ), - ), + Text(latestText, style: TextStyle( + fontSize: 12, color: querying || latestVersion == null ? placeholderColor : valueColor)), ], ), ), @@ -560,60 +450,44 @@ class _PackFilesState extends State { if (saved) { showFloatingToast(context, '已保存'); } else { - showFloatingToast( - context, - '保存失败', - type: FloatingToastType.error, - duration: const Duration(seconds: 5), - ); + showFloatingToast(context, '保存失败', type: FloatingToastType.error, duration: const Duration(seconds: 5)); } } @override Widget build(BuildContext context) { - final bool canBuild = - widget.onBuildPack != null && - findBuildScript(widget.pack.files) != null; + final bool canBuild = widget.onBuildPack != null && findBuildScript(widget.pack.files) != null; final String? openRepoUrl = openableRepoWebUrl(_header?.repo ?? ''); final bool showVersionChip = _header?.repo != null; - final List options = canBuild && widget.onSave != null - ? (_header?.options ?? const []) - : const []; - final Color sizeColor = FluentTheme.of(context) + final List options = canBuild && widget.onSave != null ? (_header?.options ?? + const []) : const []; + final Color sizeColor = FluentTheme + .of(context) .resources .textFillColorSecondary; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (openRepoUrl != null || canBuild || showVersionChip) - _buildToolbar( - openRepoUrl: openRepoUrl, - canBuild: canBuild, - showVersionChip: showVersionChip, - ), + Divider(), + if (openRepoUrl != null || canBuild || showVersionChip) _buildToolbar( + openRepoUrl: openRepoUrl, canBuild: canBuild, showVersionChip: showVersionChip), if (options.isNotEmpty) BuildOptionsPanel( options: options, values: widget.pack.buildOptions, expanded: _optionsExpanded, - onToggleExpanded: () => - setState(() => _optionsExpanded = !_optionsExpanded), + onToggleExpanded: () => setState(() => _optionsExpanded = !_optionsExpanded), saving: _savingOption, onChange: _changeBuildOption, ), Expanded( child: widget.pack.files.isEmpty ? const Center(child: Text('该包暂无文件')) - : TreeView( - items: _buildTreeItems( - _buildTree(widget.pack.files), - sizeColor, - ), - onItemInvoked: _onItemInvoked, - onItemExpandToggle: _onExpandToggle, - shrinkWrap: false, - scrollPrimary: false, - ), + : TreeView(items: _buildTreeItems(_buildTree(widget.pack.files), sizeColor), + onItemInvoked: _onItemInvoked, + onItemExpandToggle: _onExpandToggle, + shrinkWrap: false, + scrollPrimary: false), ), ], ); diff --git a/lib/pages/pack_packaging.dart b/lib/pages/pack_packaging.dart index adb50f2..1672c99 100644 --- a/lib/pages/pack_packaging.dart +++ b/lib/pages/pack_packaging.dart @@ -43,11 +43,7 @@ class _PackPackagingState extends State { if (widget.builders.isEmpty) { return null; } - return effectivePackagingBuilder( - widget.pack, - widget.selectedBuilder ?? _localBuilder, - all: widget.builders, - ); + return effectivePackagingBuilder(widget.pack, widget.selectedBuilder ?? _localBuilder, all: widget.builders); } PackageBuilder? _findBuilder(String id) { @@ -70,11 +66,7 @@ class _PackPackagingState extends State { /// 该包当前启用的格式 id(规范序);未记录时回退全部。 List _enabledFormatIds() => [ - for (final PackageBuilder builder in enabledBuildersFor( - widget.pack, - all: widget.builders, - )) - builder.id, + for (final PackageBuilder builder in enabledBuildersFor(widget.pack, all: widget.builders)) builder.id, ]; Future _modifyFormats() async { @@ -115,12 +107,7 @@ class _PackPackagingState extends State { showFloatingToast(context, '已保存'); return; } - showFloatingToast( - context, - '保存失败', - type: FloatingToastType.error, - duration: const Duration(seconds: 5), - ); + showFloatingToast(context, '保存失败', type: FloatingToastType.error, duration: const Duration(seconds: 5)); } /// 全字段拷贝并仅替换启用格式集合。 @@ -212,11 +199,7 @@ class _PackPackagingState extends State { children: [ Text( '打包格式', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: theme.typography.body?.color, - ), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: theme.typography.body?.color), ), const SizedBox(height: 8), Row( @@ -233,34 +216,23 @@ class _PackPackagingState extends State { isExpanded: true, onChanged: _selectBuilder, items: >[ - for (final PackageBuilder builder - in enabledBuildersFor( - widget.pack, - all: widget.builders, - )) - ComboBoxItem( - value: builder.id, - child: Text(builder.displayName), - ), + for (final PackageBuilder builder in enabledBuildersFor(widget.pack, all: widget.builders)) + ComboBoxItem(value: builder.id, child: Text(builder.displayName)), ], ), ), ), - const SizedBox(width: 12), + const SizedBox(width: 8), Button( key: const Key('modifyEnabledFormatsButton'), - onPressed: widget.onSave == null || _savingFormats - ? null - : _modifyFormats, - child: const Text('修改启用格式…'), + onPressed: widget.onSave == null || _savingFormats ? null : _modifyFormats, + child: SizedBox(height: 22, child: const Text('修改启用格式…'),), ), ], ), const SizedBox(height: 8), - Text( - '仅列出该包已启用的格式;可点击「修改启用格式…」调整。', - style: TextStyle(color: theme.resources.textFillColorSecondary), - ), + Text('仅列出该包已启用的格式;可点击「修改启用格式…」调整。', + style: TextStyle(color: theme.resources.textFillColorSecondary)), const SizedBox(height: 16), FilledButton( key: const Key('packPreviewButton'), @@ -268,15 +240,10 @@ class _PackPackagingState extends State { child: const Text('预览打包内容'), ), const SizedBox(height: 16), - Text( - _descriptionFor(_builder), - style: TextStyle(color: theme.resources.textFillColorSecondary), - ), + Text(_descriptionFor(_builder), style: TextStyle(color: theme.resources.textFillColorSecondary)), const SizedBox(height: 8), - Text( - '点击「预览打包内容」可查看包内完整文件列表与文件内容。', - style: TextStyle(color: theme.resources.textFillColorSecondary), - ), + Text('点击「预览打包内容」可查看包内完整文件列表与文件内容。', + style: TextStyle(color: theme.resources.textFillColorSecondary)), ], ), ), diff --git a/lib/pages/script_editor.dart b/lib/pages/script_editor.dart index 180617a..4fe6277 100644 --- a/lib/pages/script_editor.dart +++ b/lib/pages/script_editor.dart @@ -26,14 +26,6 @@ const double _nodeLibraryWidth = 232; const double _inspectorWidth = 280; const double _titleMaxWidth = 240; -/// 节点编辑器全屏页:顶栏(返回 / 标题 / 项目选择器与新建、重命名、删除 / -/// 保存状态 / 生成预览)、左侧节点库、中间画布、右侧检查器与底部输出面板。 -/// -/// 布局数值与分隔线取自视觉规范 §1/§3(顶栏 48、左 232、右 280、底部收起 34、 -/// 1px `surface2` 分隔)。无脚本项目时画布区为空态引导(§5.8,含「新建脚本项目」 -/// 按钮)、两侧面板整体禁用。项目 CRUD 见 §8.1-8.3,切换/排序见 §10.5/§10.6: -/// 切换前先收口视口防抖并 flush 保存队列;控制器变更与视口变化经 400ms 防抖保存 -/// 队列写回(§10.1/§10.4),返回按钮在队列清空后 pop,保存失败时弹确认对话框(§8.4)。 class ScriptEditorPage extends StatefulWidget { const ScriptEditorPage({ super.key, @@ -47,14 +39,10 @@ class ScriptEditorPage extends StatefulWidget { final PackModel pack; final Future Function(PackModel pack) onSave; - /// 代码生成器;null 时由生成预览(T9)回退 `PowerShell5Generator()`。 final ScriptCodeGenerator? generator; - /// 包内路径建议(检查器 T8 使用);null 时由检查器经 `NuGetPackageBuilder` - /// 计划条目转为 `build/native/` 相对路径。 final List? packagePaths; - /// 保存与视口写回的防抖时长(§10.1/§10.4,默认 400ms);测试注入更短值。 final Duration debounce; @override @@ -64,8 +52,7 @@ class ScriptEditorPage extends StatefulWidget { class _ScriptEditorPageState extends State { GraphEditorController? _controller; final TransformationController _transformation = TransformationController(); - final GlobalKey _outputPanelKey = - GlobalKey(); + final GlobalKey _outputPanelKey = GlobalKey(); Size? _canvasViewportSize; late final EditorSaveQueue _saveQueue; @@ -73,13 +60,10 @@ class _ScriptEditorPageState extends State { Object? _lastSaveError; bool _backing = false; - /// 免脏加载标记:控制器 `loadProject` 的选中清空/诊断重算通知不是编辑, - /// 不应进入保存队列(切换/重命名/元数据变更共用)。 bool _suppressSaveMark = false; bool get _hasProject => _controller != null; - /// 会话内的脚本项目工作列表(就地增删改);保存时经 `_buildUpdatedPack` 拷贝。 List get _scripts => widget.pack.scripts; @override @@ -111,7 +95,7 @@ class _ScriptEditorPageState extends State { Widget build(BuildContext context) { return Container( key: const Key('nodeEditorPage'), - color: FluentTheme.of(context).scaffoldBackgroundColor, + color: FluentTheme.of(context).menuColor, child: Column( children: [ _buildTopBar(), @@ -137,11 +121,7 @@ class _ScriptEditorPageState extends State { message: '返回包管理', child: IconButton( icon: _backing - ? const SizedBox( - width: 16, - height: 16, - child: ProgressRing(strokeWidth: 2), - ) + ? const SizedBox(width: 16, height: 16, child: ProgressRing(strokeWidth: 2)) : const Icon(FluentIcons.back, size: 16), onPressed: _backing ? null : _handleBack, ), @@ -192,7 +172,6 @@ class _ScriptEditorPageState extends State { ); } - /// 顶栏动作区(Spacer 之后,§3.1):保存状态在「生成预览」之前。 Widget _buildTopBarActions() { return Row( key: const Key('editorTopBarActions'), @@ -222,8 +201,6 @@ class _ScriptEditorPageState extends State { ); } - /// 顶栏项目选择器(§3.1/§3.2):宽 200、高 32;按钮态显示项目名, - /// 下拉项含项目名 + 触发时机与构建标签;无项目时禁用。 Widget _buildScriptProjectSelector() { final List scripts = _scripts; return SizedBox( @@ -298,11 +275,7 @@ class _ScriptEditorPageState extends State { child: SizedBox( width: 32, height: 32, - child: IconButton( - key: buttonKey, - icon: Icon(icon, size: 16), - onPressed: onPressed, - ), + child: IconButton(key: buttonKey, icon: Icon(icon, size: 16), onPressed: onPressed), ), ); } @@ -315,9 +288,7 @@ class _ScriptEditorPageState extends State { panelKey: const Key('nodeLibraryPanel'), width: _nodeLibraryWidth, border: Border(right: BorderSide(color: FluentTheme.of(context).resources.dividerStrokeColorDefault)), - child: NodeLibraryPanel( - onAddNode: _hasProject ? _addNodeFromLibrary : null, - ), + child: NodeLibraryPanel(onAddNode: _hasProject ? _addNodeFromLibrary : null), ), Expanded(child: _buildCanvasArea()), _buildSidePanel( @@ -354,10 +325,7 @@ class _ScriptEditorPageState extends State { child: IgnorePointer( ignoring: !_hasProject, child: DecoratedBox( - decoration: BoxDecoration( - color: FluentTheme.of(context).scaffoldBackgroundColor, - border: border, - ), + decoration: BoxDecoration(color: FluentTheme.of(context).scaffoldBackgroundColor, border: border), child: child, ), ), @@ -374,11 +342,7 @@ class _ScriptEditorPageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - FluentIcons.power_shell, - size: 40, - color: FluentTheme.of(context).resources.textFillColorSecondary, - ), + Icon(FluentIcons.power_shell, size: 40, color: FluentTheme.of(context).resources.textFillColorSecondary), const SizedBox(height: 16), Text( '尚未创建脚本项目', @@ -418,7 +382,6 @@ class _ScriptEditorPageState extends State { ); } - /// 节点库点击添加(§4.2):落点基于画布可见视口中心的场景坐标。 void _addNodeFromLibrary(ScriptNodeTypeDescriptor descriptor) { final GraphEditorController? controller = _controller; if (controller == null) { @@ -434,7 +397,6 @@ class _ScriptEditorPageState extends State { ); } - /// 画布可见视口中心对应的场景坐标;与画布共用同一 [TransformationController]。 Offset _viewportCenterScene() { final Size? viewport = _canvasViewportSize; if (viewport == null) { @@ -443,11 +405,8 @@ class _ScriptEditorPageState extends State { return _transformation.toScene(viewport.center(Offset.zero)); } - /// 新建项目(§8.1):对话框收集名称与触发/构建配置;id 取 `script_N` - /// 最大序号 +1,自动放置入口节点 (40, 60),追加后选中并走保存队列。 Future _handleCreateScriptProject() async { - final NewScriptProjectRequest? request = - await showCreateScriptProjectDialog(context, existing: _scripts); + final NewScriptProjectRequest? request = await showCreateScriptProjectDialog(context, existing: _scripts); if (request == null || !mounted) { return; } @@ -466,37 +425,26 @@ class _ScriptEditorPageState extends State { showFloatingToast(context, '已创建'); } - /// 顶栏「重命名」(§8.2):对话框返回新名称后走 [_handleRenameScriptProject] - /// (检查器内联提交共用同一入口)。 Future _openRenameScriptProjectDialog() async { final GraphEditorController? controller = _controller; if (controller == null) { return; } final ScriptProjectModel project = controller.project; - final String? name = await showRenameScriptProjectDialog( - context, - project: project, - existing: _scripts, - ); + final String? name = await showRenameScriptProjectDialog(context, project: project, existing: _scripts); if (name == null || !mounted) { return; } _handleRenameScriptProject(project, name); } - /// 删除项目(§8.3):确认后移除;选择按「保持索引位 → 越界回退末项 → - /// 空列表回空态」调整(§8.3/§10.5)。 Future _handleDeleteScriptProject() async { final GraphEditorController? controller = _controller; if (controller == null) { return; } final ScriptProjectModel project = controller.project; - final bool confirmed = await showDeleteScriptProjectDialog( - context, - project: project, - ); + final bool confirmed = await showDeleteScriptProjectDialog(context, project: project); if (!confirmed || !mounted) { return; } @@ -511,7 +459,6 @@ class _ScriptEditorPageState extends State { showFloatingToast(context, '已删除'); } - /// 切换项目入口(顶栏下拉 / 执行顺序行):先 flush 视口与保存队列再加载新图。 void _handleSelectScriptProject(ScriptProjectModel project) { if (identical(project, _controller?.project)) { return; @@ -519,8 +466,6 @@ class _ScriptEditorPageState extends State { unawaited(_switchToScriptProject(project)); } - /// 切换项目(§10.5):保存队列 flush 后加载新图(画布恢复其 viewport、 - /// 清空选中、诊断与底部面板随新图重算);切换不写历史记录。 Future _switchToScriptProject(ScriptProjectModel next) async { final GraphEditorController? controller = _controller; if (controller == null || identical(controller.project, next)) { @@ -539,14 +484,11 @@ class _ScriptEditorPageState extends State { setState(() {}); } - /// 项目切换/替换前的焦点收口:让检查器字段失焦并等焦点变更微任务落地, - /// 未提交文本经失焦提交写回原项目(否则会残留或提交到新项目上)。 Future _settleFocusBeforeProjectChange() async { FocusManager.instance.primaryFocus?.unfocus(); await null; } - /// 重命名(检查器内联与顶栏对话框共用):写回列表、同步控制器并保存。 void _handleRenameScriptProject(ScriptProjectModel project, String name) { if (_replaceScriptProject(project, name: name) == null) { return; @@ -555,20 +497,14 @@ class _ScriptEditorPageState extends State { showFloatingToast(context, '已重命名'); } - void _handleTriggerChanged( - ScriptProjectModel project, - ScriptTrigger trigger, - ) { + void _handleTriggerChanged(ScriptProjectModel project, ScriptTrigger trigger) { if (_replaceScriptProject(project, trigger: trigger) == null) { return; } _saveQueue.markDirty(); } - void _handleBuildModelChanged( - ScriptProjectModel project, - BuildModel buildModel, - ) { + void _handleBuildModelChanged(ScriptProjectModel project, BuildModel buildModel) { if (_replaceScriptProject(project, buildModel: buildModel) == null) { return; } @@ -583,7 +519,6 @@ class _ScriptEditorPageState extends State { _moveScriptProject(project, 1); } - /// 执行顺序排序(§10.6):列表顺序即同组执行顺序;重排保持当前选中并即时保存。 void _moveScriptProject(ScriptProjectModel project, int delta) { final List scripts = _scripts; final int index = _indexOfScript(project); @@ -598,13 +533,7 @@ class _ScriptEditorPageState extends State { _saveQueue.markDirty(); } - /// 激活项目:无控制器时创建(含可选入口节点),否则免脏加载后追加入口节点。 - /// - /// 激活前先收口视口防抖,避免矩阵尾段变更(fling 惯性终点等)随切换丢失。 - Future _activateScriptProject( - ScriptProjectModel project, { - bool addEntryNode = false, - }) async { + Future _activateScriptProject(ScriptProjectModel project, {bool addEntryNode = false}) async { await _settleFocusBeforeProjectChange(); if (!mounted) { return; @@ -628,9 +557,6 @@ class _ScriptEditorPageState extends State { setState(() {}); } - /// 移除列表项并调整选择:保持索引位、越界回退末项、空列表回空态。 - /// - /// 移除前先收口视口防抖,删除/新建与切换统一收口(矩阵尾段变更不随操作丢失)。 Future _removeScriptAt(int index) async { await _settleFocusBeforeProjectChange(); if (!mounted) { @@ -650,7 +576,6 @@ class _ScriptEditorPageState extends State { _saveQueue.markDirty(); } - /// 回到无项目空态:注销监听并释放控制器(画布/检查器/输出面板随之为 null)。 void _detachController() { final GraphEditorController? controller = _controller; _controller = null; @@ -659,10 +584,6 @@ class _ScriptEditorPageState extends State { setState(() {}); } - /// 免脏加载:`loadProject` 的选中清空/诊断重算通知不视为编辑。 - /// - /// [preserveSelection] 为 true 时加载后恢复原选中(节点/连线仍在时): - /// 元数据变更(重命名/触发/构建标签)不改变图,不应打断编辑焦点。 void _loadScriptProject( GraphEditorController controller, ScriptProjectModel project, { @@ -685,12 +606,6 @@ class _ScriptEditorPageState extends State { } } - /// 替换列表中项目实例(名称/触发/构建配置不可变,需全字段拷贝); - /// 无实际变化或找不到时返回 null。当前项目同步免脏加载。 - /// - /// 替换前先收口视口防抖(同切换路径),避免矩阵领先模型时(fling 惯性期/ - /// T9 居中防抖窗)免脏加载把矩阵重置回陈旧模型造成回跳与尾段变更丢失; - /// 免脏加载恢复原选中,元数据变更不打断编辑焦点。 ScriptProjectModel? _replaceScriptProject( ScriptProjectModel project, { String? name, @@ -721,7 +636,6 @@ class _ScriptEditorPageState extends State { return updated; } - /// 全字段拷贝(节点/连线/视口保持引用与数值),仅替换可变元数据。 ScriptProjectModel _copyScriptProject( ScriptProjectModel source, { String? name, @@ -745,8 +659,7 @@ class _ScriptEditorPageState extends State { int _indexOfScript(ScriptProjectModel project) { final List scripts = _scripts; for (int index = 0; index < scripts.length; index++) { - if (identical(scripts[index], project) || - scripts[index].id == project.id) { + if (identical(scripts[index], project) || scripts[index].id == project.id) { return index; } } @@ -764,12 +677,10 @@ class _ScriptEditorPageState extends State { ); } - /// 顶栏「生成预览」(§10.8):交给底部面板编译并呈现。 void _generatePreview() { _outputPanelKey.currentState?.generatePreview(); } - /// 顶栏保存状态四态(§3.3):未保存 / 保存中 / 已保存 / 保存失败 + 重试。 Widget _buildSaveStatus() { final EditorSaveQueue queue = _saveQueue; final FluentThemeData theme = FluentTheme.of(context); @@ -784,38 +695,22 @@ class _ScriptEditorPageState extends State { mainAxisSize: MainAxisSize.min, children: switch (queue.status) { EditorSaveStatus.unsaved => [ - Icon( - FluentIcons.save, - size: 14, - color: theme.resources.textFillColorTertiary, - ), + Icon(FluentIcons.save, size: 14, color: theme.resources.textFillColorTertiary), const SizedBox(width: 6), Text('未保存', style: statusStyle), ], EditorSaveStatus.saving => [ - const SizedBox( - width: 14, - height: 14, - child: ProgressRing(strokeWidth: 2), - ), + const SizedBox(width: 14, height: 14, child: ProgressRing(strokeWidth: 2)), const SizedBox(width: 6), Text('保存中…', style: statusStyle), ], EditorSaveStatus.saved => [ - Icon( - FluentIcons.check_mark, - size: 14, - color: AppColors.success(theme.brightness), - ), + Icon(FluentIcons.check_mark, size: 14, color: AppColors.success(theme.brightness)), const SizedBox(width: 6), Text('已保存', style: statusStyle), ], EditorSaveStatus.failed => [ - Icon( - FluentIcons.error_badge, - size: 14, - color: AppColors.critical(theme.brightness), - ), + Icon(FluentIcons.error_badge, size: 14, color: AppColors.critical(theme.brightness)), const SizedBox(width: 6), Text('保存失败', style: statusStyle), const SizedBox(width: 8), @@ -838,8 +733,6 @@ class _ScriptEditorPageState extends State { } } - /// 控制器任意变更(参数/节点/选中/视口写回)统一进入防抖保存(§10.4); - /// 项目切换/重命名/元数据变更的免脏加载(见 `_suppressSaveMark`)除外。 void _handleControllerChanged() { if (_suppressSaveMark) { return; @@ -847,22 +740,14 @@ class _ScriptEditorPageState extends State { _saveQueue.markDirty(); } - /// 视口矩阵变化(平移/缩放/fling/T9 程序化居中)重置防抖计时(§10.1)。 void _handleTransformationChanged() { if (_controller == null) { return; } _viewportDebounceTimer?.cancel(); - _viewportDebounceTimer = Timer( - widget.debounce, - _persistViewportFromTransformation, - ); + _viewportDebounceTimer = Timer(widget.debounce, _persistViewportFromTransformation); } - /// 把变换矩阵的视口写回控制器,经控制器通知进入保存队列。 - /// - /// 与模型当前值(1e-9 容差)比较后再写:画布 `_lastSyncedViewport` 对账 - /// 会在模型被外部更新时回写同一矩阵,直接写入会自触发保存环路。 void _persistViewportFromTransformation() { _viewportDebounceTimer = null; final GraphEditorController? controller = _controller; @@ -874,9 +759,7 @@ class _ScriptEditorPageState extends State { final double y = matrix.storage[13]; final double scale = matrix.getMaxScaleOnAxis(); final ScriptProjectModel project = controller.project; - if (_sameViewport(x, project.viewX) && - _sameViewport(y, project.viewY) && - _sameViewport(scale, project.viewScale)) { + if (_sameViewport(x, project.viewX) && _sameViewport(y, project.viewY) && _sameViewport(scale, project.viewScale)) { return; } controller.setViewport(x: x, y: y, scale: scale); @@ -884,14 +767,11 @@ class _ScriptEditorPageState extends State { bool _sameViewport(double left, double right) => (left - right).abs() < 1e-9; - /// 立即收口视口:取消防抖计时并把当前矩阵写回项目(切换/返回前调用), - /// 避免 400ms 窗口内的矩阵尾段变更(fling 惯性终点等)随操作丢失。 void _flushViewportWriteBack() { _viewportDebounceTimer?.cancel(); _persistViewportFromTransformation(); } - /// 保存回调(队列串行调用):构造新包并交给 [ScriptEditorPage.onSave]。 Future _saveProject() async { try { final bool saved = await widget.onSave(_buildUpdatedPack()); @@ -903,13 +783,10 @@ class _ScriptEditorPageState extends State { } } - /// 跳过防抖立即 flush 保存队列(画布 `Ctrl+S` 与顶栏「重试」共用)。 void _flushSaveQueue() { _saveQueue.flush(); } - /// 返回(§8.4):先收口视口并 flush + 等待队列清空;失败未清时弹确认对话框 - /// (重试 / 仍返回),重试成功后照常返回。 Future _handleBack() async { if (_backing) { return; @@ -947,9 +824,7 @@ class _ScriptEditorPageState extends State { Future _showSaveFailedDialog() async { final String projectName = _controller?.project.name ?? widget.pack.name; final Object? error = _lastSaveError; - final String body = error == null - ? '脚本项目「$projectName」保存失败' - : '脚本项目「$projectName」保存失败:${formatError(error)}'; + final String body = error == null ? '脚本项目「$projectName」保存失败' : '脚本项目「$projectName」保存失败:${formatError(error)}'; final bool? retry = await showDialog( context: context, builder: (BuildContext dialogContext) => ContentDialog( @@ -974,7 +849,6 @@ class _ScriptEditorPageState extends State { return retry ?? false; } - /// 全字段拷贝 + 当前控制器项目替换 `scripts` 中所属项(其余列表原样引用)。 PackModel _buildUpdatedPack() { final ScriptProjectModel? current = _controller?.project; return PackModel( @@ -1008,9 +882,7 @@ class _ScriptEditorPageState extends State { if (index < 0) { return [...scripts, current]; } - final List updated = List.of( - scripts, - ); + final List updated = List.of(scripts); updated[index] = current; return updated; } diff --git a/lib/pages/setting.dart b/lib/pages/setting.dart index b8f2e52..aadc4c4 100644 --- a/lib/pages/setting.dart +++ b/lib/pages/setting.dart @@ -37,22 +37,19 @@ class Setting extends StatefulWidget { } class _SettingState extends State { - static const Map _themeModeLabels = - { - ThemeModeSetting.system: '系统', - ThemeModeSetting.dark: '深色', - ThemeModeSetting.light: '浅色', - }; - - static const Map _proxyModeLabels = - { - ProxyModeSetting.off: '关闭(直连)', - ProxyModeSetting.auto: '自动检测', - ProxyModeSetting.manual: '手动设置', - }; - - List _detected = - const []; + static const Map _themeModeLabels = { + ThemeModeSetting.system: '系统', + ThemeModeSetting.dark: '深色', + ThemeModeSetting.light: '浅色', + }; + + static const Map _proxyModeLabels = { + ProxyModeSetting.off: '关闭', + ProxyModeSetting.auto: '自动检测', + ProxyModeSetting.manual: '手动设置', + }; + + List _detected = const []; bool _detecting = true; late final TextEditingController _defaultAuthorController; late final FocusNode _defaultAuthorFocusNode; @@ -65,20 +62,13 @@ class _SettingState extends State { @override void initState() { super.initState(); - _defaultAuthorController = TextEditingController( - text: widget.settings.defaultAuthor, - ); + _defaultAuthorController = TextEditingController(text: widget.settings.defaultAuthor); _defaultAuthorFocusNode = FocusNode()..addListener(_onAuthorFocusChanged); - _proxyHostController = TextEditingController( - text: widget.settings.proxyHost, - ); + _proxyHostController = TextEditingController(text: widget.settings.proxyHost); _proxyHostFocusNode = FocusNode()..addListener(_onProxyHostFocusChanged); - _proxyPortController = TextEditingController( - text: widget.settings.proxyPort?.toString() ?? '', - ); + _proxyPortController = TextEditingController(text: widget.settings.proxyPort?.toString() ?? ''); _proxyPortFocusNode = FocusNode()..addListener(_onProxyPortFocusChanged); - final List cached = - widget.settings.detectedCompilers; + final List cached = widget.settings.detectedCompilers; if (cached.isEmpty) { _detectCompilers(showLoading: false); } else { @@ -90,16 +80,13 @@ class _SettingState extends State { @override void didUpdateWidget(covariant Setting oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.settings.defaultAuthor != widget.settings.defaultAuthor && - !_defaultAuthorFocusNode.hasFocus) { + if (oldWidget.settings.defaultAuthor != widget.settings.defaultAuthor && !_defaultAuthorFocusNode.hasFocus) { _defaultAuthorController.text = widget.settings.defaultAuthor; } - if (oldWidget.settings.proxyHost != widget.settings.proxyHost && - !_proxyHostFocusNode.hasFocus) { + if (oldWidget.settings.proxyHost != widget.settings.proxyHost && !_proxyHostFocusNode.hasFocus) { _proxyHostController.text = widget.settings.proxyHost; } - if (oldWidget.settings.proxyPort != widget.settings.proxyPort && - !_proxyPortFocusNode.hasFocus) { + if (oldWidget.settings.proxyPort != widget.settings.proxyPort && !_proxyPortFocusNode.hasFocus) { _proxyPortController.text = widget.settings.proxyPort?.toString() ?? ''; } } @@ -245,21 +232,13 @@ class _SettingState extends State { showFloatingToast(context, '已保存'); } - SettingsModel _directorySettings({ - required String? outputDirectory, - required String? cmakeOutputDirectory, - }) { - return widget.settings.copyWith( - outputDirectory: outputDirectory, - cmakeOutputDirectory: cmakeOutputDirectory, - ); + SettingsModel _directorySettings({required String? outputDirectory, required String? cmakeOutputDirectory}) { + return widget.settings.copyWith(outputDirectory: outputDirectory, cmakeOutputDirectory: cmakeOutputDirectory); } - SettingsModel _defaultAuthorSettings(String defaultAuthor) => - widget.settings.copyWith(defaultAuthor: defaultAuthor); + SettingsModel _defaultAuthorSettings(String defaultAuthor) => widget.settings.copyWith(defaultAuthor: defaultAuthor); - SettingsModel _themeSettings({ThemeModeSetting? themeMode}) => - widget.settings.copyWith(themeMode: themeMode); + SettingsModel _themeSettings({ThemeModeSetting? themeMode}) => widget.settings.copyWith(themeMode: themeMode); SettingsModel _compilerSettings(List compilerPriority) => widget.settings.copyWith(compilerPriority: compilerPriority); @@ -268,9 +247,7 @@ class _SettingState extends State { widget.settings.copyWith(detectedCompilers: detected); void _moveCompiler(int index, int offset) { - final List priority = List.of( - widget.settings.compilerPriority, - ); + final List priority = List.of(widget.settings.compilerPriority); final int target = index + offset; if (target < 0 || target >= priority.length) { return; @@ -285,40 +262,22 @@ class _SettingState extends State { if (path == null || !mounted) { return; } - await _apply( - _directorySettings( - outputDirectory: path, - cmakeOutputDirectory: widget.settings.cmakeOutputDirectory, - ), - ); + await _apply(_directorySettings(outputDirectory: path, cmakeOutputDirectory: widget.settings.cmakeOutputDirectory)); } - Future _clearDirectory() => _apply( - _directorySettings( - outputDirectory: null, - cmakeOutputDirectory: widget.settings.cmakeOutputDirectory, - ), - ); + Future _clearDirectory() => + _apply(_directorySettings(outputDirectory: null, cmakeOutputDirectory: widget.settings.cmakeOutputDirectory)); Future _pickCmakeDirectory() async { final String? path = await widget.pickDirectory(); if (path == null || !mounted) { return; } - await _apply( - _directorySettings( - outputDirectory: widget.settings.outputDirectory, - cmakeOutputDirectory: path, - ), - ); + await _apply(_directorySettings(outputDirectory: widget.settings.outputDirectory, cmakeOutputDirectory: path)); } - Future _clearCmakeDirectory() => _apply( - _directorySettings( - outputDirectory: widget.settings.outputDirectory, - cmakeOutputDirectory: null, - ), - ); + Future _clearCmakeDirectory() => + _apply(_directorySettings(outputDirectory: widget.settings.outputDirectory, cmakeOutputDirectory: null)); Future _generateSkill() async { final String? path = await widget.pickSaveFile(_skillFileName); @@ -348,13 +307,10 @@ class _SettingState extends State { @override Widget build(BuildContext context) { - final bool manualProxy = - widget.settings.proxyMode == ProxyModeSetting.manual; + final bool manualProxy = widget.settings.proxyMode == ProxyModeSetting.manual; return SettingsPage( title: '设置', - description: const Text( - '配置打包输出目录、默认作者、外观、编译器优先级、网络代理与 AI 技能输出。', - ), + description: const Text('配置打包输出目录、默认作者、外观、编译器优先级、网络代理与 AI 技能输出。'), children: [ SettingsGroup( header: '打包', @@ -378,29 +334,14 @@ class _SettingState extends State { ), ], ), - SettingsGroup( - header: '新包默认值', - children: [_buildDefaultAuthorCard()], - ), - SettingsGroup( - header: '外观', - children: [_buildThemeModeCard()], - ), - SettingsGroup( - header: '编译器', - children: [_buildCompilerCard()], - ), + SettingsGroup(header: '新包默认值', children: [_buildDefaultAuthorCard()]), + SettingsGroup(header: '外观', children: [_buildThemeModeCard()]), + SettingsGroup(header: '编译器', children: [_buildCompilerCard()]), SettingsGroup( header: '网络', - children: [ - _buildProxyModeCard(), - if (manualProxy) _buildProxyManualCard(), - ], - ), - SettingsGroup( - header: 'AI 技能', - children: [_buildSkillCard()], + children: [_buildProxyModeCard(), if (manualProxy) _buildProxyManualCard()], ), + SettingsGroup(header: 'AI 技能', children: [_buildSkillCard()]), ], ); } @@ -414,14 +355,9 @@ class _SettingState extends State { required VoidCallback onClear, }) { return SettingsCard( + padding: const .fromLTRB(8, 8, 8, 8), header: Text(header), - content: _buildPathControls( - path: path, - pickKey: pickKey, - clearKey: clearKey, - onPick: onPick, - onClear: onClear, - ), + content: _buildPathControls(path: path, pickKey: pickKey, clearKey: clearKey, onPick: onPick, onClear: onClear), ); } @@ -451,21 +387,16 @@ class _SettingState extends State { ), ), Button(key: pickKey, onPressed: onPick, child: const Text('选择目录…')), - Button( - key: clearKey, - onPressed: path == null ? null : onClear, - child: const Text('清除'), - ), + Button(key: clearKey, onPressed: path == null ? null : onClear, child: const Text('清除')), ], ); } Widget _buildDefaultAuthorCard() { return SettingsCard( + padding: const .fromLTRB(8, 8, 8, 8), header: const Text('默认作者'), - description: const Text( - '新包将预填此值;作者为空或占位(「无」「未知」等)的包将自动替换为默认作者。', - ), + description: const Text('新包将预填此值;作者为空或占位(「无」「未知」等)的包将自动替换为默认作者。'), content: SizedBox( width: 320, child: TextBox( @@ -481,6 +412,7 @@ class _SettingState extends State { Widget _buildThemeModeCard() { return SettingsCard( header: const Text('主题模式'), + padding: const .fromLTRB(8, 8, 8, 8), content: _comboBoxField( width: 160, child: ComboBox( @@ -494,10 +426,7 @@ class _SettingState extends State { }, items: >[ for (final ThemeModeSetting mode in ThemeModeSetting.values) - ComboBoxItem( - value: mode, - child: Text(_themeModeLabels[mode]!), - ), + ComboBoxItem(value: mode, child: Text(_themeModeLabels[mode]!)), ], ), ), @@ -506,6 +435,7 @@ class _SettingState extends State { Widget _buildProxyModeCard() { return SettingsCard( + padding: const .fromLTRB(8, 8, 8, 8), header: const Text('代理模式'), description: const Text( '自动检测:使用 Windows 系统代理设置,不可用时自动直连。' @@ -524,10 +454,7 @@ class _SettingState extends State { }, items: >[ for (final ProxyModeSetting mode in ProxyModeSetting.values) - ComboBoxItem( - value: mode, - child: Text(_proxyModeLabels[mode]!), - ), + ComboBoxItem(value: mode, child: Text(_proxyModeLabels[mode]!)), ], ), ), @@ -537,6 +464,7 @@ class _SettingState extends State { Widget _buildProxyManualCard() { final FluentThemeData theme = FluentTheme.of(context); return SettingsCard( + padding: const .fromLTRB(8, 8, 8, 8), header: const Text('手动设置'), description: const Text( '服务器地址支持 host:port 或 http://host:port,端口留空时使用地址中声明的端口' @@ -578,10 +506,7 @@ class _SettingState extends State { padding: const EdgeInsets.only(top: 4), child: Text( _proxyPortError!, - style: TextStyle( - fontSize: 12, - color: AppColors.critical(theme.brightness), - ), + style: TextStyle(fontSize: 12, color: AppColors.critical(theme.brightness)), ), ), ], @@ -594,6 +519,7 @@ class _SettingState extends State { final List priority = widget.settings.compilerPriority; final bool hideRows = _detecting && _detected.isEmpty; return SettingsCard( + padding: const .fromLTRB(8, 8, 8, 8), header: const Text('编译器优先级'), description: const Text('优先使用的编译器(自上而下)'), content: SizedBox( @@ -605,27 +531,20 @@ class _SettingState extends State { Row( children: [ if (_detecting) ...[ - const SizedBox( - width: 16, - height: 16, - child: ProgressRing(strokeWidth: 2), - ), + const SizedBox(width: 16, height: 16, child: ProgressRing(strokeWidth: 2)), const SizedBox(width: 12), const Text('正在检测编译器…'), ], const Spacer(), Button( key: const Key('settingCompilerRefreshButton'), - onPressed: _detecting - ? null - : () => _detectCompilers(showLoading: true), + onPressed: _detecting ? null : () => _detectCompilers(showLoading: true), child: const Text('重新检测'), ), ], ), if (!hideRows) - for (var index = 0; index < priority.length; index++) - _buildCompilerRow(priority[index], index), + for (var index = 0; index < priority.length; index++) _buildCompilerRow(priority[index], index), ], ), ), @@ -646,9 +565,7 @@ class _SettingState extends State { child: Text( compiler?.version ?? '未检测到', overflow: TextOverflow.ellipsis, - style: compiler == null - ? TextStyle(color: theme.resources.textFillColorSecondary) - : null, + style: compiler == null ? TextStyle(color: theme.resources.textFillColorSecondary) : null, ), ), _buildMoveButton( @@ -661,9 +578,7 @@ class _SettingState extends State { key: Key('settingCompilerMoveDown_$id'), icon: FluentIcons.chevron_down, tooltip: '下移', - onPressed: index < widget.settings.compilerPriority.length - 1 - ? () => _moveCompiler(index, 1) - : null, + onPressed: index < widget.settings.compilerPriority.length - 1 ? () => _moveCompiler(index, 1) : null, ), ], ), @@ -699,21 +614,16 @@ class _SettingState extends State { child: SizedBox( width: 24, height: 24, - child: IconButton( - key: key, - icon: Icon(icon, size: 14), - onPressed: onPressed, - ), + child: IconButton(key: key, icon: Icon(icon, size: 14), onPressed: onPressed), ), ); } Widget _buildSkillCard() { return SettingsCard( + padding: const .fromLTRB(8, 8, 8, 8), header: const Text('SKILL.md'), - description: const Text( - '生成 build.py 编写技能文档(SKILL.md),可放入 AI 插件的技能目录使用。', - ), + description: const Text('生成 build.py 编写技能文档(SKILL.md),可放入 AI 插件的技能目录使用。'), content: Button( key: const Key('settingGenerateSkillButton'), onPressed: _generateSkill, @@ -737,11 +647,8 @@ const String _skillFileName = 'SKILL.md'; const String _skillAssetPath = 'assets/build/SKILL.md'; Future _pickSkillSaveLocation(String suggestedName) async { - final FileSaveLocation? location = await getSaveLocation( - suggestedName: suggestedName, - ); + final FileSaveLocation? location = await getSaveLocation(suggestedName: suggestedName); return location?.path; } -Future _loadSkillTemplateAsset() => - rootBundle.loadString(_skillAssetPath); +Future _loadSkillTemplateAsset() => rootBundle.loadString(_skillAssetPath); diff --git a/lib/widgets/floating_toast.dart b/lib/widgets/floating_toast.dart index 9672619..2c7d6d5 100644 --- a/lib/widgets/floating_toast.dart +++ b/lib/widgets/floating_toast.dart @@ -62,7 +62,7 @@ class _FloatingToast extends StatefulWidget { class _FloatingToastState extends State<_FloatingToast> with SingleTickerProviderStateMixin { static const Duration _animationDuration = Duration(milliseconds: 180); - static const Offset _slideFrom = Offset(0.18, 0); + static const Offset _slideFrom = Offset(1, 0); late final AnimationController _controller; late final CurvedAnimation _curve; diff --git a/lib/widgets/library_card.dart b/lib/widgets/library_card.dart index 8ee322b..a808d0c 100644 --- a/lib/widgets/library_card.dart +++ b/lib/widgets/library_card.dart @@ -4,42 +4,25 @@ import 'package:fluent_ui/fluent_ui.dart'; import '../util/svgs.dart'; class LibraryItem extends PaneItem { - LibraryItem({ - super.key, - required Widget? icon, - required String title, - String? version, - Widget? badge, - required Widget body, - }) : super( - icon: icon ?? Svgs.cardboardBox, - title: Row( - mainAxisAlignment: .spaceBetween, - children: [ - Expanded( - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 8), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - version != null - ? Tag( - text: version, - fontSize: 10, - maxWidth: 96, - tooltip: version, - ) - : const SizedBox(width: 8), - if (badge != null) ...[const SizedBox(width: 4), badge], - ], - ), - ], - ), - body: body, - ); + LibraryItem( + {super.key, required Widget? icon, required String title, String? version, Widget? badge, required Widget body}) + : super( + icon: icon ?? Svgs.cardboardBox, + title: Row( + mainAxisAlignment: .spaceBetween, + children: [ + Expanded(child: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis)), + const SizedBox(width: 8), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + version != null ? Tag(text: version, fontSize: 10, maxWidth: 96, tooltip: version) : const SizedBox( + width: 8), + if (badge != null) ...[const SizedBox(width: 4), badge], + ], + ), + ], + ), + body: body, + ); } diff --git a/lib/widgets/settings/settings_card.dart b/lib/widgets/settings/settings_card.dart index 1539fd0..1b85292 100644 --- a/lib/widgets/settings/settings_card.dart +++ b/lib/widgets/settings/settings_card.dart @@ -2,17 +2,14 @@ import 'package:cpp_nuget_pack/util/colors.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/services.dart'; -/// CommunityToolkit(PowerToys)SettingsCard XAML token;组件与测试共用, -/// 避免魔法数在实现与断言间漂移。 abstract final class SettingsCardTokens { - static const double minHeight = 68; - static const double subItemMinHeight = 52; + static const double minHeight = 0; + static const double subItemMinHeight = 0; static const double minWidth = 148; static const double cornerRadius = 4; static const EdgeInsets padding = EdgeInsets.all(16); - /// 子项(SettingsExpander 内部)内边距:左 58 为展开层级缩进。 - static const EdgeInsets subItemPadding = EdgeInsets.fromLTRB(58, 8, 44, 8); + static const EdgeInsets subItemPadding = EdgeInsets.fromLTRB(48, 8, 44, 8); static const double headerIconSize = 20; static const double headerIconSpacing = 20; @@ -25,12 +22,6 @@ abstract final class SettingsCardTokens { static const Duration backgroundTransition = Duration(milliseconds: 83); } -/// 组设置卡:左侧头部(图标 + 标题 + 描述)与右侧内容区。 -/// -/// 严格照 Toolkit token:最小高 68(子项 52)、内边距 16(子项 58,8,44,8)、 -/// 1px 描边 + 圆角 4(子项无圆角、仅顶部 1px 分隔线);窄窗(< 476)内容换行到 -/// 头部下方,更窄(< 286)隐藏头部图标。悬停/按下背景过渡 83ms;仅 -/// [onPressed] 非空(且启用)的卡有 hover/按下态与焦点环。 class SettingsCard extends StatefulWidget { const SettingsCard({ super.key, @@ -62,7 +53,6 @@ class SettingsCard extends StatefulWidget { /// 无边框变体(SettingsExpander 头部:透明底、无描边、无圆角)。 final bool borderless; - /// 内边距覆盖(null 时按 token:16 全边 / 子项 58,8,44,8)。 final EdgeInsetsGeometry? padding; /// 可点击卡的尾部箭头(13px chevron_right,间距 14);[onPressed] 非空时生效。 @@ -99,10 +89,7 @@ class _SettingsCardState extends State { if (widget.borderless) { border = Border.all(color: Colors.transparent, width: 1); } else if (_focused && _clickable) { - border = Border.all( - color: theme.resources.focusStrokeColorOuter, - width: 2, - ); + border = Border.all(color: theme.resources.focusStrokeColorOuter, width: 2); } else if (widget.subItem) { border = Border(top: BorderSide(color: borderColor)); } else { @@ -119,20 +106,10 @@ class _SettingsCardState extends State { duration: SettingsCardTokens.backgroundTransition, constraints: BoxConstraints( minWidth: SettingsCardTokens.minWidth, - minHeight: widget.subItem - ? SettingsCardTokens.subItemMinHeight - : SettingsCardTokens.minHeight, - ), - padding: - widget.padding ?? - (widget.subItem - ? SettingsCardTokens.subItemPadding - : SettingsCardTokens.padding), - decoration: BoxDecoration( - color: background, - border: border, - borderRadius: radius, + minHeight: widget.subItem ? SettingsCardTokens.subItemMinHeight : SettingsCardTokens.minHeight, ), + padding: widget.padding ?? (widget.subItem ? SettingsCardTokens.subItemPadding : SettingsCardTokens.padding), + decoration: BoxDecoration(color: background, border: border, borderRadius: radius), child: card, ); @@ -206,29 +183,19 @@ class _SettingsCardState extends State { return AppColors.stroke(theme); } - Widget _buildBody( - BuildContext context, - BoxConstraints constraints, - FluentThemeData theme, - ) { + Widget _buildBody(BuildContext context, BoxConstraints constraints, FluentThemeData theme) { final bool wrap = constraints.maxWidth < SettingsCardTokens.wrapThreshold; - final bool hideIcon = - constraints.maxWidth < SettingsCardTokens.wrapNoIconThreshold; + final bool hideIcon = constraints.maxWidth < SettingsCardTokens.wrapNoIconThreshold; final Widget? icon = hideIcon ? null : widget.headerIcon; final Widget? iconView = icon == null ? null : Padding( - padding: const EdgeInsets.only( - right: SettingsCardTokens.headerIconSpacing, - ), + padding: const EdgeInsets.only(right: SettingsCardTokens.headerIconSpacing), child: SizedBox( width: SettingsCardTokens.headerIconSize, height: SettingsCardTokens.headerIconSize, child: IconTheme.merge( - data: IconThemeData( - size: SettingsCardTokens.headerIconSize, - color: _foregroundFor(theme), - ), + data: IconThemeData(size: SettingsCardTokens.headerIconSize, color: _foregroundFor(theme)), child: Center(child: icon), ), ), @@ -241,11 +208,7 @@ class _SettingsCardState extends State { children: [ if (widget.header != null) DefaultTextStyle.merge( - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: _foregroundFor(theme), - ), + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w400, color: _foregroundFor(theme)), child: widget.header!, ), if (widget.description != null) @@ -265,9 +228,7 @@ class _SettingsCardState extends State { final Widget? content = widget.content; final Widget? chevron = widget.showChevron && _clickable ? Padding( - padding: const EdgeInsets.only( - left: SettingsCardTokens.actionIconSpacing, - ), + padding: const EdgeInsets.only(left: SettingsCardTokens.actionIconSpacing), child: Icon( FluentIcons.chevron_right, size: SettingsCardTokens.actionIconSize, @@ -284,9 +245,7 @@ class _SettingsCardState extends State { Expanded( child: Padding( padding: EdgeInsets.only( - right: content == null && chevron == null - ? 0 - : SettingsCardTokens.headerContentSpacing, + right: content == null && chevron == null ? 0 : SettingsCardTokens.headerContentSpacing, ), child: headerColumn, ), @@ -307,10 +266,7 @@ class _SettingsCardState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ ?iconView, - if (headerColumn != null) - Expanded(child: headerColumn) - else - const Spacer(), + if (headerColumn != null) Expanded(child: headerColumn) else const Spacer(), ], ), if (content != null || chevron != null) ...[ @@ -319,7 +275,9 @@ class _SettingsCardState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ if (content != null) - Flexible(child: Align(alignment: Alignment.centerLeft, child: content)), + Flexible( + child: Align(alignment: Alignment.centerLeft, child: content), + ), ?chevron, ], ), @@ -366,15 +324,8 @@ class SettingsCardToggle extends StatelessWidget { return SizedBox( height: 36, child: ToggleSwitchTheme( - data: const ToggleSwitchThemeData( - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - ), - child: ToggleSwitch( - checked: checked, - onChanged: enabled ? onChanged : null, - semanticLabel: semanticLabel, - ), + data: const ToggleSwitchThemeData(padding: EdgeInsets.zero, margin: EdgeInsets.zero), + child: ToggleSwitch(checked: checked, onChanged: enabled ? onChanged : null, semanticLabel: semanticLabel), ), ); } diff --git a/lib/widgets/settings/settings_group.dart b/lib/widgets/settings/settings_group.dart index ae68711..34090a9 100644 --- a/lib/widgets/settings/settings_group.dart +++ b/lib/widgets/settings/settings_group.dart @@ -4,7 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart'; /// PowerToys SettingsGroup token:组标题 14 Semibold、上边距 32(首个 8)、 /// 卡容器上距 8、卡间距 2。 abstract final class SettingsGroupTokens { - static const double headerSpacing = 32; + static const double headerSpacing = 8; static const double firstHeaderSpacing = 8; static const double containerSpacing = 8; static const double cardSpacing = 2; diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 8148c1e..441cda1 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -4,13 +4,13 @@ import 'package:fluent_ui/fluent_ui.dart'; /// PowerToys SettingsPageControl token:标题 28 Semibold、模块描述 14 Secondary、 /// 内容最大宽 1000、水平内边距 16、底部滚动留白 48。 abstract final class SettingsPageTokens { - static const double titleFontSize = 28; + static const double titleFontSize = 24; static const double descriptionFontSize = 14; static const double contentMaxWidth = 1000; - static const double horizontalPadding = 16; - static const double bottomPadding = 48; + static const double horizontalPadding = 8; + static const double bottomPadding = 32; static const double descriptionSpacing = 8; - static const double contentSpacing = 24; + static const double contentSpacing = 0; } /// 设置页模板:标题 → 模块描述 → 内容(左对齐、最大宽 1000、可滚动)。 @@ -36,7 +36,6 @@ class SettingsPage extends StatelessWidget { return SizedBox.expand( child: Container( key: const Key('settingsPageSurface'), - decoration: BoxDecoration(color: theme.cardColor), child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB( SettingsPageTokens.horizontalPadding, @@ -52,9 +51,7 @@ class SettingsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.only( - top: SettingsPageTokens.horizontalPadding, - ), + padding: const EdgeInsets.only(top: SettingsPageTokens.horizontalPadding), child: Text( title, style: TextStyle( @@ -66,9 +63,7 @@ class SettingsPage extends StatelessWidget { ), if (description != null) Padding( - padding: const EdgeInsets.only( - top: SettingsPageTokens.descriptionSpacing, - ), + padding: const EdgeInsets.only(top: SettingsPageTokens.descriptionSpacing), child: DefaultTextStyle.merge( style: TextStyle( fontSize: SettingsPageTokens.descriptionFontSize, diff --git a/logo/c++_480px.png b/logo/c++_480px.png new file mode 100644 index 0000000..668263d Binary files /dev/null and b/logo/c++_480px.png differ diff --git a/logo/c_programming_480px.png b/logo/c_programming_480px.png new file mode 100644 index 0000000..b8ac5fe Binary files /dev/null and b/logo/c_programming_480px.png differ diff --git a/logo/dll_480px.png b/logo/dll_480px.png new file mode 100644 index 0000000..93fabd8 Binary files /dev/null and b/logo/dll_480px.png differ diff --git a/logo/empty_box_480px.png b/logo/empty_box_480px.png new file mode 100644 index 0000000..95e4d49 Binary files /dev/null and b/logo/empty_box_480px.png differ diff --git a/logo/logo.png b/logo/logo.png new file mode 100644 index 0000000..bda7d12 Binary files /dev/null and b/logo/logo.png differ diff --git a/logo/logo.psd b/logo/logo.psd new file mode 100644 index 0000000..4684b5f Binary files /dev/null and b/logo/logo.psd differ diff --git a/test/build/library_recipes/openvino/build.py b/test/build/library_recipes/openvino/build.py index 348c319..ab09881 100644 --- a/test/build/library_recipes/openvino/build.py +++ b/test/build/library_recipes/openvino/build.py @@ -1,6 +1,6 @@ # https://github.com/openvinotoolkit/openvino.git # source: none -# option: tbb = off | on +# checkbox: tbb = off | on # # OpenVINO 预构建配方(P3 L6) #