web_shell_flutter/tool/generate_app.dart

244 lines
8.0 KiB
Dart

import 'dart:io';
import 'package:yaml/yaml.dart';
Future<void> main(List<String> args) async {
if (args.isEmpty) {
print('\x1BM[31mUsage: dart run tool/generate_app.dart <brand_name>\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<void> _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<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,ios',
appDir,
]);
if (result.exitCode != 0) {
print('\x1BM[31m[Error] flutter create failed:\n${result.stderr}\x1B[0m');
exit(1);
}
}
Future<void> _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<void> _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<void> _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<void> _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<void> _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);
}