web_shell_flutter/tool/generate_app.dart

359 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'package:yaml/yaml.dart';
/// 品牌名允许的字符:小写字母、数字、下划线。
final RegExp _validBrandName = RegExp(r'^[a-z][a-z0-9_]*$');
Future<void> main(List<String> 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);
final String? defaultUrl = config['default_url'] as String?;
// 6. 生成 lib/main.dart
await _generateDartEntrypoint(
appDir,
appName,
appKey,
accentColor,
bgColor,
textColor,
mutedTextColor,
defaultUrl,
);
// 7. 生成图标与启动页配置
await _generateBrandingAssets(brand, appDir, config);
// 8. 在 pubspec.yaml 中注册 Flutter assets
await _registerFlutterAssets(appDir);
print('\x1B[32m✔ 应用 $brand 已生成到 $appDir\x1B[0m');
print('\x1B[34m构建应用请执行\x1B[0m');
print(' cd $appDir && flutter build apk');
}
Future<void> _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.yuanxuan.quanxue -> org: com.yuanxuan
final List<String> 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<void> _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<void> _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<void> _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<void> _generateDartEntrypoint(
String appDir,
String appName,
String appKey,
String accentColor,
String bgColor,
String textColor,
String mutedTextColor,
String? defaultUrl,
) async {
print('\x1B[34m[信息] 正在生成 lib/main.dart...\x1B[0m');
final File mainFile = File('$appDir/lib/main.dart');
final String urlParam = defaultUrl != null ? "initialUrl: '$defaultUrl'," : "";
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),
splashImage: const AssetImage('assets/branding/splash.png'),
$urlParam
),
);
}
''';
await mainFile.writeAsString(dartContent);
// 清理 flutter create 默认生成的测试文件(因为它依赖已删除的 MyApp 类)
final File defaultTestFile = File('$appDir/test/widget_test.dart');
if (defaultTestFile.existsSync()) {
await defaultTestFile.delete();
}
}
Future<void> _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. 确保依赖已解析 ──
await Process.run('flutter', ['pub', 'get'], workingDirectory: appDir);
// ── 4. 生成 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');
}
Future<void> _registerFlutterAssets(String appDir) async {
print('\x1B[34m[信息] 正在注册 Flutter assets...\x1B[0m');
final File pubspecFile = File('$appDir/pubspec.yaml');
String content = await pubspecFile.readAsString();
// 检查是否已有未注释的 assets: 声明
final bool hasAssets = RegExp(r'^\s+assets:', multiLine: true).hasMatch(content);
if (!hasAssets) {
// 必须匹配行首的顶层 flutter: 键,忽略缩进的 flutter: (SDK 依赖)
content = content.replaceFirst(
RegExp(r'^flutter:\s*\n', multiLine: true),
'flutter:\n assets:\n - assets/branding/\n',
);
await pubspecFile.writeAsString(content);
}
print('\x1B[32m✔ Flutter assets 已注册。\x1B[0m');
}