feat: 添加应用品牌信息支持,重构主页 URL 生成逻辑
This commit is contained in:
@@ -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? {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user