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.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? {

View File

@@ -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<Environment, List<String>> _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<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';
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<void> 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<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 {
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<H5ShellPage> createState() => _H5ShellPageState();
@@ -69,6 +111,7 @@ class H5ShellPage extends StatefulWidget {
class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
late final WebViewController _controller;
late final String _homeUrl;
int _progress = 0;
String? _loadError;
@@ -79,6 +122,10 @@ class _H5ShellPageState extends State<H5ShellPage> 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));
}

View File

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