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 打包工具
+
+

-把 C++ 头文件、源码与库可视化为原生 NuGet 包的 Windows 桌面工具。通过图形界面完成包信息编辑、目录映射、依赖与编译设置,并一键导出 Visual Studio C++ 工程可直接引用的 `.nupkg`,或供 CMake `find_package` 消费的配置包。
+# CCPPP — C/C++ 包管理工具
-- 平台:仅 Windows 桌面
-- 界面:全中文、Win11 风格(fluent_ui)、Fluent 默认配色(浅色/深色两族,跟随系统)
-- 数据:配置保存在程序目录下的 `config/`,便携、无需安装
+[](LICENSE)
+
-## 功能特性
+[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':