import 'dart:io'; import 'package:yaml/yaml.dart'; /// 品牌名允许的字符:小写字母、数字、下划线。 final RegExp _validBrandName = RegExp(r'^[a-z][a-z0-9_]*$'); Future main(List args) async { if (args.isEmpty) { print( '\x1B[31m用法:dart run tool/generate_app.dart <品牌名>\x1B[0m', ); print('\x1B[33m示例:dart run tool/generate_app.dart quanxue\x1B[0m'); exit(1); } final String brand = args.first; if (!_validBrandName.hasMatch(brand)) { print( '\x1B[31m[错误] 品牌名 "$brand" 格式无效,' '仅允许小写字母、数字和下划线(且必须以字母开头)。\x1B[0m', ); exit(1); } final File configFile = File('flavors/$brand.yaml'); if (!configFile.existsSync()) { print( '\x1B[31m[错误] 未找到配置文件:${configFile.path}\x1B[0m', ); exit(1); } print('\x1B[34m[信息] 正在为品牌生成应用:$brand...\x1B[0m'); // 1. 解析 YAML 配置 final String yamlString = await configFile.readAsString(); final YamlMap config = loadYaml(yamlString) as YamlMap; final String appName = config['app_name'] as String; final String applicationId = config['application_id'] as String; final String appKey = config['app_key'] as String; final YamlMap theme = config['theme'] as YamlMap; final String accentColor = theme['accent_color'] as String; final String bgColor = theme['bg_color'] as String; final String textColor = theme['text_color'] as String; final String mutedTextColor = theme['muted_text_color'] as String; final String appDir = 'apps/$brand'; print('\x1B[32m✔ 配置加载完成。\x1B[0m'); // 2. 创建 Flutter 应用 await _createFlutterApp(brand, appDir, applicationId); // 3. 添加核心依赖 await _addCoreDependency(appDir); // 4. 覆盖 MainActivity.java 以继承 CoreShellActivity await _overwriteMainActivity(appDir, applicationId); // 5. 覆盖 AndroidManifest.xml 中的应用名称 await _overwriteManifestLabel(appDir, appName); // 6. 生成 lib/main.dart await _generateDartEntrypoint( appDir, appName, appKey, accentColor, bgColor, textColor, mutedTextColor, ); // 7. 生成图标与启动页配置 await _generateBrandingAssets(brand, appDir, config); print('\x1B[32m✔ 应用 $brand 已生成到 $appDir!\x1B[0m'); print('\x1B[34m构建应用请执行:\x1B[0m'); print(' cd $appDir && flutter build apk'); } Future _createFlutterApp( String brand, String appDir, String applicationId, ) async { print('\x1B[34m[信息] 正在执行 flutter create...\x1B[0m'); final Directory dir = Directory(appDir); if (dir.existsSync()) { print( '\x1B[33m[警告] 目录 $appDir 已存在,正在清理...\x1B[0m', ); dir.deleteSync(recursive: true); } // 提取组织名 // 例如:com.wanmake.quanxue -> org: com.wanmake final List segments = applicationId.split('.'); final String org = segments.sublist(0, segments.length - 1).join('.'); final ProcessResult result = await Process.run('flutter', [ 'create', '--org', org, '--project-name', brand.replaceAll('-', '_'), '--platforms', 'android', '--android-language', 'java', appDir, ]); if (result.exitCode != 0) { print('\x1B[31m[错误] flutter create 执行失败:\n${result.stderr}\x1B[0m'); exit(1); } } Future _addCoreDependency(String appDir) async { print('\x1B[34m[信息] 正在添加 web_shell_core 依赖...\x1B[0m'); final ProcessResult result = await Process.run('flutter', [ 'pub', 'add', 'web_shell_core', '--path', '../../packages/web_shell_core', ], workingDirectory: appDir); if (result.exitCode != 0) { print( '\x1B[31m[错误] 添加依赖失败:\n${result.stderr}\x1B[0m', ); exit(1); } } Future _overwriteMainActivity(String appDir, String applicationId) async { print('\x1B[34m[信息] 正在注入 CoreShellActivity 继承关系...\x1B[0m'); final String mainActivityPath = "$appDir/android/app/src/main/java/${applicationId.replaceAll('.', '/')}/MainActivity.java"; // 准备 Java 文件内容 final String javaContent = ''' package $applicationId; import com.yuanxuan.webshell.core.web_shell_core.CoreShellActivity; public class MainActivity extends CoreShellActivity { } '''; await File(mainActivityPath).create(recursive: true); await File(mainActivityPath).writeAsString(javaContent); } Future _overwriteManifestLabel(String appDir, String appName) async { print('\x1B[34m[信息] 正在更新 AndroidManifest.xml 应用名...\x1B[0m'); final File manifestFile = File( '$appDir/android/app/src/main/AndroidManifest.xml', ); String content = await manifestFile.readAsString(); // 使用简单正则替换 android:label="..." content = content.replaceAll( RegExp(r'android:label="[^"]*"'), 'android:label="$appName"', ); await manifestFile.writeAsString(content); } Future _generateDartEntrypoint( String appDir, String appName, String appKey, String accentColor, String bgColor, String textColor, String mutedTextColor, ) async { print('\x1B[34m[信息] 正在生成 lib/main.dart...\x1B[0m'); final File mainFile = File('$appDir/lib/main.dart'); final String dartContent = ''' import 'package:flutter/material.dart'; import 'package:web_shell_core/web_shell_core.dart'; void main() { runShellApp( ShellEnvironment( appName: '$appName', appKey: '$appKey', accentColor: const Color($accentColor), backgroundColor: const Color($bgColor), textColor: const Color($textColor), mutedTextColor: const Color($mutedTextColor), ), ); } '''; await mainFile.writeAsString(dartContent); // 清理 flutter create 默认生成的测试文件(因为它依赖已删除的 MyApp 类) final File defaultTestFile = File('$appDir/test/widget_test.dart'); if (defaultTestFile.existsSync()) { await defaultTestFile.delete(); } } Future _generateBrandingAssets( String brand, String appDir, YamlMap config, ) async { print('\x1B[34m[信息] 正在配置图标与启动页...\x1B[0m'); if (config['branding'] == null) { print( '\x1B[33m[警告] 配置中未找到 branding 段,跳过资源生成。\x1B[0m', ); return; } final YamlMap branding = config['branding'] as YamlMap; // ── 1. 复制品牌资源到生成的应用目录 ── final Directory brandSourceDir = Directory('flavors/$brand'); final Directory brandTargetDir = Directory('$appDir/assets/branding'); if (!brandSourceDir.existsSync()) { print( '\x1B[31m[错误] 品牌资源目录不存在:${brandSourceDir.path}\x1B[0m', ); print( '\x1B[33m请在 flavors/$brand/ 目录下放置 icon.png、' 'icon_foreground.png、splash.png 等资源文件。\x1B[0m', ); exit(1); } await brandTargetDir.create(recursive: true); for (final entity in brandSourceDir.listSync(recursive: true)) { if (entity is File) { final relativePath = entity.path.substring( brandSourceDir.path.length + 1, ); final targetFile = File('${brandTargetDir.path}/$relativePath'); await targetFile.parent.create(recursive: true); await entity.copy(targetFile.path); } } print('\x1B[32m✔ 品牌资源已复制到 ${brandTargetDir.path}\x1B[0m'); // ── 2. 添加资源生成器的 dev 依赖 ── print('\x1B[34m[信息] 正在添加资源生成器依赖...\x1B[0m'); final ProcessResult addDevDepsResult = await Process.run('flutter', [ 'pub', 'add', '--dev', 'flutter_launcher_icons', 'flutter_native_splash', ], workingDirectory: appDir); if (addDevDepsResult.exitCode != 0) { print( '\x1B[31m[错误] 添加资源生成器依赖失败:' '\n${addDevDepsResult.stderr}\x1B[0m', ); exit(1); } // ── 3. 生成 flutter_launcher_icons 配置 ── // 资源路径指向复制后的位置(相对于 appDir) final String iconPath = 'assets/branding/${branding['icon']}'; final String iconForeground = 'assets/branding/${branding['icon_foreground']}'; final String iconBackground = branding['icon_background'] as String; final String iconsYaml = ''' flutter_launcher_icons: android: true image_path: "$iconPath" adaptive_icon_background: "$iconBackground" adaptive_icon_foreground: "$iconForeground" '''; await File('$appDir/flutter_launcher_icons.yaml').writeAsString(iconsYaml); // ── 4. 生成 flutter_native_splash 配置 ── final String splashPath = 'assets/branding/${branding['splash']}'; final String splashColor = branding['splash_color'] as String; final String splashYaml = ''' flutter_native_splash: color: "$splashColor" image: "$splashPath" android_12: image: "$splashPath" icon_background_color: "$splashColor" '''; await File('$appDir/flutter_native_splash.yaml').writeAsString(splashYaml); // ── 5. 执行资源生成器 ── print('\x1B[34m[信息] 正在生成应用图标...\x1B[0m'); final ProcessResult iconsResult = await Process.run('dart', [ 'run', 'flutter_launcher_icons', '-f', 'flutter_launcher_icons.yaml', ], workingDirectory: appDir); if (iconsResult.exitCode != 0) { print( '\x1B[31m[错误] 图标生成失败:\n${iconsResult.stderr}\x1B[0m', ); print('\x1B[33mstdout:\n${iconsResult.stdout}\x1B[0m'); exit(1); } print('\x1B[32m✔ 应用图标已生成。\x1B[0m'); print('\x1B[34m[信息] 正在生成启动页...\x1B[0m'); final ProcessResult splashResult = await Process.run('dart', [ 'run', 'flutter_native_splash:create', '--path=flutter_native_splash.yaml', ], workingDirectory: appDir); if (splashResult.exitCode != 0) { print( '\x1B[31m[错误] 启动页生成失败:\n${splashResult.stderr}\x1B[0m', ); print('\x1B[33mstdout:\n${splashResult.stdout}\x1B[0m'); exit(1); } print('\x1B[32m✔ 启动页已生成。\x1B[0m'); }