From cbe5bbb6576853d0208a87974058b89df254219f Mon Sep 17 00:00:00 2001 From: Booker Date: Mon, 25 May 2026 11:12:54 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=93=81=E7=89=8C=E4=BF=A1=E6=81=AF=E6=94=AF=E6=8C=81=EF=BC=8C?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=BB=E9=A1=B5=20URL=20=E7=94=9F=E6=88=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/openim/flutter/openim/MainActivity.kt | 54 ++++++++++++++++ lib/config/app_config.dart | 32 +++++++++- lib/main.dart | 61 ++++++++++++++++--- test/widget_test.dart | 5 +- 4 files changed, 142 insertions(+), 10 deletions(-) diff --git a/android/app/src/main/kotlin/io/openim/flutter/openim/MainActivity.kt b/android/app/src/main/kotlin/io/openim/flutter/openim/MainActivity.kt index c7cb89a..21ca782 100644 --- a/android/app/src/main/kotlin/io/openim/flutter/openim/MainActivity.kt +++ b/android/app/src/main/kotlin/io/openim/flutter/openim/MainActivity.kt @@ -3,8 +3,13 @@ package io.openim.flutter.openim import android.content.Intent import android.content.pm.PackageInfo 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.os.Build +import android.util.Base64 import android.util.Log import androidx.core.content.FileProvider import io.flutter.embedding.android.FlutterActivity @@ -12,10 +17,12 @@ import org.conscrypt.Conscrypt import java.security.Security import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import java.io.ByteArrayOutputStream import java.io.File class MainActivity : FlutterActivity() { 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" 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? { diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 6286a1c..4987fe5 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -4,6 +4,8 @@ enum Environment { class AppConfig { static const Environment currentEnvironment = Environment.production; + static const String appName = '本地打包'; + static const String appLogo = ''; static final Map> _environmentHosts = { Environment.production: [ @@ -15,11 +17,15 @@ class AppConfig { return _environmentHosts[currentEnvironment] ?? const []; } - static String get homeUrl { + static String homeUrl({String? appName, String? appLogo}) { final host = environmentHosts.isNotEmpty ? environmentHosts.first : 'h5-im.imharry.work'; - return _normalizeHomeUrl(host); + return _withShellBranding( + _normalizeHomeUrl(host), + appName: appName, + appLogo: appLogo, + ); } static String _normalizeHomeUrl(String host) { @@ -29,4 +35,26 @@ class AppConfig { } return 'https://$value/'; } + + static String _withShellBranding( + String url, { + String? appName, + String? appLogo, + }) { + final uri = Uri.parse(url); + final queryParameters = Map.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(); + } } diff --git a/lib/main.dart b/lib/main.dart index dc936d3..be1eff1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,11 +10,12 @@ import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; import 'config/app_config.dart'; -final _homeUrl = AppConfig.homeUrl; const _shellBackground = Color(0xFFF8FBFF); const _shellAccent = Color(0xFF0089FF); const _shellSubText = Color(0xFF8E9AB0); const _resumeCoverDuration = Duration(milliseconds: 700); +const _shellBrandingChannel = + MethodChannel('io.openim.flutter.im_webview_app/shell_branding'); const _stopWebMediaScript = r''' (() => { try { @@ -29,7 +30,7 @@ const _stopWebMediaScript = r''' })(); '''; -void main() { +Future main() async { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( @@ -39,29 +40,70 @@ void main() { 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 load() async { + try { + final data = await _shellBrandingChannel + .invokeMapMethod('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 { - const ImWebViewApp({super.key}); + const ImWebViewApp({ + super.key, + this.shellBranding = ShellBranding.fallback, + }); + + final ShellBranding shellBranding; @override Widget build(BuildContext context) { return MaterialApp( - title: '集中营', + title: shellBranding.appName, debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FEB)), scaffoldBackgroundColor: _shellBackground, useMaterial3: true, ), - home: const H5ShellPage(), + home: H5ShellPage(shellBranding: shellBranding), ); } } class H5ShellPage extends StatefulWidget { - const H5ShellPage({super.key}); + const H5ShellPage({super.key, required this.shellBranding}); + + final ShellBranding shellBranding; @override State createState() => _H5ShellPageState(); @@ -69,6 +111,7 @@ class H5ShellPage extends StatefulWidget { class _H5ShellPageState extends State with WidgetsBindingObserver { late final WebViewController _controller; + late final String _homeUrl; int _progress = 0; String? _loadError; @@ -79,6 +122,10 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + _homeUrl = AppConfig.homeUrl( + appName: widget.shellBranding.appName, + appLogo: widget.shellBranding.appLogo, + ); _controller = _buildController()..loadRequest(Uri.parse(_homeUrl)); } diff --git a/test/widget_test.dart b/test/widget_test.dart index ebf76f5..5c00c39 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -4,6 +4,9 @@ import 'package:im_webview_app/main.dart'; void main() { test('creates the WebView shell app widget', () { - expect(const ImWebViewApp(), isA()); + expect( + const ImWebViewApp(shellBranding: ShellBranding.fallback), + isA(), + ); }); }