feat: 添加应用品牌信息支持,重构主页 URL 生成逻辑

This commit is contained in:
Booker
2026-05-25 11:12:54 +07:00
parent abfd3717dc
commit cbe5bbb657
4 changed files with 142 additions and 10 deletions

View File

@@ -3,8 +3,13 @@ package io.openim.flutter.openim
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Base64
import android.util.Log import android.util.Log
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
@@ -12,10 +17,12 @@ import org.conscrypt.Conscrypt
import java.security.Security import java.security.Security
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
private val CHANNEL = "io.openim.flutter.openim/apk_info" private val CHANNEL = "io.openim.flutter.openim/apk_info"
private val SHELL_BRANDING_CHANNEL = "io.openim.flutter.im_webview_app/shell_branding"
private val TAG = "MainActivity" private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: android.os.Bundle?) { override fun onCreate(savedInstanceState: android.os.Bundle?) {
@@ -80,6 +87,53 @@ class MainActivity : FlutterActivity() {
} }
} }
} }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SHELL_BRANDING_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getShellBranding" -> {
try {
result.success(
mapOf(
"appName" to getCurrentAppName(),
"appLogo" to getCurrentAppIconDataUrl()
)
)
} catch (e: Exception) {
result.error("ERROR", "无法获取当前应用品牌信息: ${e.message}", null)
}
}
else -> {
result.notImplemented()
}
}
}
}
private fun getCurrentAppName(): String {
val label = packageManager.getApplicationLabel(applicationInfo)
return label?.toString() ?: ""
}
private fun getCurrentAppIconDataUrl(): String {
val icon = packageManager.getApplicationIcon(packageName)
val bitmap = drawableToBitmap(icon)
val output = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, output)
val base64 = Base64.encodeToString(output.toByteArray(), Base64.NO_WRAP)
return "data:image/png;base64,$base64"
}
private fun drawableToBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable && drawable.bitmap != null) {
return drawable.bitmap
}
val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else 192
val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else 192
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
} }
private fun getApkPackageName(apkPath: String): String? { private fun getApkPackageName(apkPath: String): String? {

View File

@@ -4,6 +4,8 @@ enum Environment {
class AppConfig { class AppConfig {
static const Environment currentEnvironment = Environment.production; static const Environment currentEnvironment = Environment.production;
static const String appName = '本地打包';
static const String appLogo = '';
static final Map<Environment, List<String>> _environmentHosts = { static final Map<Environment, List<String>> _environmentHosts = {
Environment.production: [ Environment.production: [
@@ -15,11 +17,15 @@ class AppConfig {
return _environmentHosts[currentEnvironment] ?? const []; return _environmentHosts[currentEnvironment] ?? const [];
} }
static String get homeUrl { static String homeUrl({String? appName, String? appLogo}) {
final host = environmentHosts.isNotEmpty final host = environmentHosts.isNotEmpty
? environmentHosts.first ? environmentHosts.first
: 'h5-im.imharry.work'; : 'h5-im.imharry.work';
return _normalizeHomeUrl(host); return _withShellBranding(
_normalizeHomeUrl(host),
appName: appName,
appLogo: appLogo,
);
} }
static String _normalizeHomeUrl(String host) { static String _normalizeHomeUrl(String host) {
@@ -29,4 +35,26 @@ class AppConfig {
} }
return 'https://$value/'; return 'https://$value/';
} }
static String _withShellBranding(
String url, {
String? appName,
String? appLogo,
}) {
final uri = Uri.parse(url);
final queryParameters = Map<String, String>.from(uri.queryParameters)
..['flutter_shell'] = '1';
final trimmedName = (appName ?? AppConfig.appName).trim();
if (trimmedName.isNotEmpty) {
queryParameters['shell_app_name'] = trimmedName;
}
final trimmedLogo = (appLogo ?? AppConfig.appLogo).trim();
if (trimmedLogo.isNotEmpty) {
queryParameters['shell_app_logo'] = trimmedLogo;
}
return uri.replace(queryParameters: queryParameters).toString();
}
} }

View File

@@ -10,11 +10,12 @@ import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
import 'config/app_config.dart'; import 'config/app_config.dart';
final _homeUrl = AppConfig.homeUrl;
const _shellBackground = Color(0xFFF8FBFF); const _shellBackground = Color(0xFFF8FBFF);
const _shellAccent = Color(0xFF0089FF); const _shellAccent = Color(0xFF0089FF);
const _shellSubText = Color(0xFF8E9AB0); const _shellSubText = Color(0xFF8E9AB0);
const _resumeCoverDuration = Duration(milliseconds: 700); const _resumeCoverDuration = Duration(milliseconds: 700);
const _shellBrandingChannel =
MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
const _stopWebMediaScript = r''' const _stopWebMediaScript = r'''
(() => { (() => {
try { try {
@@ -29,7 +30,7 @@ const _stopWebMediaScript = r'''
})(); })();
'''; ''';
void main() { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle( const SystemUiOverlayStyle(
@@ -39,29 +40,70 @@ void main() {
systemNavigationBarIconBrightness: Brightness.dark, systemNavigationBarIconBrightness: Brightness.dark,
), ),
); );
runApp(const ImWebViewApp()); final shellBranding = await ShellBranding.load();
runApp(ImWebViewApp(shellBranding: shellBranding));
}
class ShellBranding {
const ShellBranding({
required this.appName,
required this.appLogo,
});
static const fallback = ShellBranding(
appName: AppConfig.appName,
appLogo: AppConfig.appLogo,
);
final String appName;
final String appLogo;
static Future<ShellBranding> load() async {
try {
final data = await _shellBrandingChannel
.invokeMapMethod<String, String>('getShellBranding');
final appName = _trim(data?['appName']);
final appLogo = _trim(data?['appLogo']);
return ShellBranding(
appName: appName.isNotEmpty ? appName : fallback.appName,
appLogo: appLogo.isNotEmpty ? appLogo : fallback.appLogo,
);
} catch (_) {
return fallback;
}
}
static String _trim(String? value) => value?.trim() ?? '';
} }
class ImWebViewApp extends StatelessWidget { class ImWebViewApp extends StatelessWidget {
const ImWebViewApp({super.key}); const ImWebViewApp({
super.key,
this.shellBranding = ShellBranding.fallback,
});
final ShellBranding shellBranding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: '集中营', title: shellBranding.appName,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FEB)), colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FEB)),
scaffoldBackgroundColor: _shellBackground, scaffoldBackgroundColor: _shellBackground,
useMaterial3: true, useMaterial3: true,
), ),
home: const H5ShellPage(), home: H5ShellPage(shellBranding: shellBranding),
); );
} }
} }
class H5ShellPage extends StatefulWidget { class H5ShellPage extends StatefulWidget {
const H5ShellPage({super.key}); const H5ShellPage({super.key, required this.shellBranding});
final ShellBranding shellBranding;
@override @override
State<H5ShellPage> createState() => _H5ShellPageState(); State<H5ShellPage> createState() => _H5ShellPageState();
@@ -69,6 +111,7 @@ class H5ShellPage extends StatefulWidget {
class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver { class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
late final WebViewController _controller; late final WebViewController _controller;
late final String _homeUrl;
int _progress = 0; int _progress = 0;
String? _loadError; String? _loadError;
@@ -79,6 +122,10 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_homeUrl = AppConfig.homeUrl(
appName: widget.shellBranding.appName,
appLogo: widget.shellBranding.appLogo,
);
_controller = _buildController()..loadRequest(Uri.parse(_homeUrl)); _controller = _buildController()..loadRequest(Uri.parse(_homeUrl));
} }

View File

@@ -4,6 +4,9 @@ import 'package:im_webview_app/main.dart';
void main() { void main() {
test('creates the WebView shell app widget', () { test('creates the WebView shell app widget', () {
expect(const ImWebViewApp(), isA<Widget>()); expect(
const ImWebViewApp(shellBranding: ShellBranding.fallback),
isA<Widget>(),
);
}); });
} }