import 'dart:io'; import 'package:yaml/yaml.dart'; Future main(List args) async { if (args.isEmpty) { print('\x1BM[31mUsage: dart run tool/generate_app.dart \x1B[0m'); print('\x1BM[33mExample: dart run tool/generate_app.dart quanxue\x1B[0m'); exit(1); } final String brand = args.first; final File configFile = File('flavors/$brand.yaml'); if (!configFile.existsSync()) { print('\x1BM[31m[Error] Configuration file not found: ${configFile.path}\x1B[0m'); exit(1); } print('\x1BM[34m[Info] Generating app for brand: $brand...\x1B[0m'); // 1. Parse YAML Configuration 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✔ Configuration loaded successfully.\x1B[0m'); // 2. Create the Flutter App await _createFlutterApp(brand, appDir, applicationId); // 3. Add Core Dependency await _addCoreDependency(appDir); // 4. Overwrite MainActivity.java to extend CoreShellActivity await _overwriteMainActivity(appDir, applicationId); // 5. Overwrite AndroidManifest.xml for Label await _overwriteManifestLabel(appDir, appName); // 6. Generate lib/main.dart await _generateDartEntrypoint( appDir, appName, appKey, accentColor, bgColor, textColor, mutedTextColor ); // 7. Generate Icons and Splash await _generateBrandingAssets(brand, appDir, config); print('\x1B[32m✔ App $brand generated successfully at $appDir!\x1B[0m'); print('\x1B[34mTo build the app:\x1B[0m'); print(' cd $appDir && flutter build apk'); } Future _createFlutterApp(String brand, String appDir, String applicationId) async { print('\x1BM[34m[Info] Running flutter create...\x1B[0m'); final Directory dir = Directory(appDir); if (dir.existsSync()) { print('\x1BM[33m[Warning] Directory $appDir already exists. Cleaning up...\x1B[0m'); dir.deleteSync(recursive: true); } // Extract org // e.g., 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,ios', appDir, ]); if (result.exitCode != 0) { print('\x1BM[31m[Error] flutter create failed:\n${result.stderr}\x1B[0m'); exit(1); } } Future _addCoreDependency(String appDir) async { print('\x1BM[34m[Info] Adding web_shell_core dependency...\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('\x1BM[31m[Error] Failed to add dependency:\n${result.stderr}\x1B[0m'); exit(1); } } Future _overwriteMainActivity(String appDir, String applicationId) async { print('\x1BM[34m[Info] Injecting CoreShellActivity inheritance...\x1B[0m'); final String mainActivityPath = "$appDir/android/app/src/main/java/${applicationId.replaceAll('.', '/')}/MainActivity.java"; final File mainActivityFile = File(mainActivityPath); if (!mainActivityFile.existsSync()) { print('\x1B[31m[Error] MainActivity.java not found at ${mainActivityFile.path}\x1B[0m'); // Flutter might have generated Kotlin depending on settings. Let's handle both. final String ktPath = "$appDir/android/app/src/main/kotlin/${applicationId.replaceAll('.', '/')}/MainActivity.kt"; final File ktFile = File(ktPath); if (ktFile.existsSync()) { ktFile.deleteSync(); // We just recreate the java one. } else { print('\x1B[31m[Error] Not found Kotlin either.\x1B[0m'); exit(1); } } // Prepare the Java file 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('\x1BM[34m[Info] Updating AndroidManifest.xml label...\x1B[0m'); final File manifestFile = File('$appDir/android/app/src/main/AndroidManifest.xml'); String content = await manifestFile.readAsString(); // A simple regex to replace 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('\x1BM[34m[Info] Generating 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); } Future _generateBrandingAssets(String brand, String appDir, YamlMap config) async { print('\x1BM[34m[Info] Configuring icons and splash screens...\x1B[0m'); if (config['branding'] == null) { print('\x1BM[33m[Warning] No branding section found in config. Skipping asset generation.\x1B[0m'); return; } final YamlMap branding = config['branding'] as YamlMap; // Create flutter_launcher_icons.yaml in appDir final String iconsYaml = ''' flutter_launcher_icons: android: true ios: true image_path: "${branding['icon']}" adaptive_icon_background: "${branding['icon_background']}" adaptive_icon_foreground: "${branding['icon_foreground']}" '''; await File('$appDir/flutter_launcher_icons-config.yaml').writeAsString(iconsYaml); // Create flutter_native_splash.yaml in appDir final String splashYaml = ''' flutter_native_splash: color: "${branding['splash_color']}" image: "${branding['splash']}" android_12: image: "${branding['splash']}" icon_background_color: "${branding['splash_color']}" '''; await File('$appDir/flutter_native_splash-config.yaml').writeAsString(splashYaml); // Copy branding assets print('\x1BM[34m[Info] Copying assets...\x1B[0m'); final Directory brandingDir = Directory('assets/branding/$brand'); if (brandingDir.existsSync()) { final Directory targetDir = Directory('$appDir/assets/branding/$brand'); await targetDir.create(recursive: true); for (final entity in brandingDir.listSync(recursive: true)) { if (entity is File) { final relativePath = entity.path.substring(brandingDir.path.length + 1); final targetFile = File('${targetDir.path}/$relativePath'); await targetFile.parent.create(recursive: true); await entity.copy(targetFile.path); } } } print('\x1BM[34m[Info] Running asset generators...\x1B[0m'); // Run flutter_launcher_icons await Process.run('dart', [ 'run', 'flutter_launcher_icons', '-f', 'flutter_launcher_icons-config.yaml' ], workingDirectory: appDir); // Run flutter_native_splash await Process.run('dart', [ 'run', 'flutter_native_splash:create', '--path=flutter_native_splash-config.yaml' ], workingDirectory: appDir); }