|
|
@@ -1,17 +1,37 @@
|
|
|
import 'dart:io';
|
|
|
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'package:flutter/services.dart';
|
|
|
import 'package:get/get.dart';
|
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
|
|
+import 'package:url_launcher/url_launcher.dart';
|
|
|
+
|
|
|
+import '../../../controllers/core_controller.dart';
|
|
|
+import '../../../data/sp/ix_sp.dart';
|
|
|
+import '../../../constants/enums.dart' as enums;
|
|
|
+import '../../../../utils/log/logger.dart';
|
|
|
+import '../utils/js_bridge.dart';
|
|
|
+
|
|
|
+export 'package:flutter/services.dart' show rootBundle;
|
|
|
|
|
|
class WebController extends GetxController {
|
|
|
+ final TAG = 'WebController';
|
|
|
var title = '';
|
|
|
var url = '';
|
|
|
InAppWebViewController? webViewController;
|
|
|
final isLoading = true.obs;
|
|
|
final loadingProgress = 0.0.obs;
|
|
|
final canGoBack = false.obs; // 添加是否可以回退的观察变量
|
|
|
+ final isFullScreen = false.obs; // 全面屏模式
|
|
|
+
|
|
|
+ // 当前的状态栏样式(响应式)
|
|
|
+ final currentStatusBarStyle = const SystemUiOverlayStyle(
|
|
|
+ statusBarColor: Colors.transparent,
|
|
|
+ statusBarIconBrightness: Brightness.light,
|
|
|
+ statusBarBrightness: Brightness.light,
|
|
|
+ ).obs;
|
|
|
|
|
|
// final _externalSchemes = [
|
|
|
// 'mailto:',
|
|
|
@@ -27,11 +47,31 @@ class WebController extends GetxController {
|
|
|
if (Get.arguments != null) {
|
|
|
title = Get.arguments['title'] ?? '';
|
|
|
url = Get.arguments['url'] ?? '';
|
|
|
+ isFullScreen.value = Get.arguments['fullScreen'] ?? false;
|
|
|
}
|
|
|
loadUserAgent();
|
|
|
+ _listenToVPNStatusChanges();
|
|
|
super.onInit();
|
|
|
}
|
|
|
|
|
|
+ /// 加载本地 Assets HTML 文件
|
|
|
+ Future<void> loadAssetHtml(String assetPath) async {
|
|
|
+ try {
|
|
|
+ final htmlContent = await rootBundle.loadString(assetPath);
|
|
|
+ if (webViewController != null) {
|
|
|
+ await webViewController!.loadData(
|
|
|
+ data: htmlContent,
|
|
|
+ baseUrl: WebUri('about:blank'),
|
|
|
+ mimeType: 'text/html',
|
|
|
+ encoding: 'utf-8',
|
|
|
+ );
|
|
|
+ log(TAG, '成功加载本地 HTML: $assetPath');
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '加载本地 HTML 失败: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
Future<void> loadUserAgent() async {
|
|
|
initWebView(await generateUserAgent());
|
|
|
}
|
|
|
@@ -143,4 +183,452 @@ class WebController extends GetxController {
|
|
|
final match = RegExp(r"S\.browser_fallback_url=([^;]+);").firstMatch(url);
|
|
|
return match != null ? Uri.decodeComponent(match.group(1)!) : null;
|
|
|
}
|
|
|
+
|
|
|
+ // ==================== JS Bridge 功能 ====================
|
|
|
+
|
|
|
+ /// 设置 JavaScript Handlers
|
|
|
+ void setupJavaScriptHandlers(InAppWebViewController controller) {
|
|
|
+ // 设置状态栏颜色
|
|
|
+ controller.addJavaScriptHandler(
|
|
|
+ handlerName: JSBridgeConstants.setStatusBarColor,
|
|
|
+ callback: (args) async {
|
|
|
+ return await _handleSetStatusBarColor(args);
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ // 退出 WebView
|
|
|
+ controller.addJavaScriptHandler(
|
|
|
+ handlerName: JSBridgeConstants.exitWebView,
|
|
|
+ callback: (args) async {
|
|
|
+ return await _handleExitWebView(args);
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ // 连接 VPN
|
|
|
+ controller.addJavaScriptHandler(
|
|
|
+ handlerName: JSBridgeConstants.connectVPN,
|
|
|
+ callback: (args) async {
|
|
|
+ return await _handleConnectVPN(args);
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ // 断开 VPN
|
|
|
+ controller.addJavaScriptHandler(
|
|
|
+ handlerName: JSBridgeConstants.disconnectVPN,
|
|
|
+ callback: (args) async {
|
|
|
+ return await _handleDisconnectVPN(args);
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ // 打开指定 App
|
|
|
+ controller.addJavaScriptHandler(
|
|
|
+ handlerName: JSBridgeConstants.openApp,
|
|
|
+ callback: (args) async {
|
|
|
+ return await _handleOpenApp(args);
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ // 获取 VPN 状态
|
|
|
+ controller.addJavaScriptHandler(
|
|
|
+ handlerName: JSBridgeConstants.getVPNStatus,
|
|
|
+ callback: (args) async {
|
|
|
+ return await _handleGetVPNStatus(args);
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ log(TAG, 'JavaScript Handlers 已设置');
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 注入原生属性到 JS
|
|
|
+ Future<void> injectNativeAttrs() async {
|
|
|
+ if (webViewController == null) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 获取状态栏和底部安全区域高度
|
|
|
+ double statusBarHeight = 0;
|
|
|
+ double bottomBarHeight = 0;
|
|
|
+
|
|
|
+ try {
|
|
|
+ statusBarHeight = Get.mediaQuery.padding.top;
|
|
|
+ bottomBarHeight = Get.mediaQuery.padding.bottom;
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '获取 MediaQuery 失败,使用默认值: $e');
|
|
|
+ // 使用默认值
|
|
|
+ statusBarHeight = Platform.isIOS ? 47 : 24;
|
|
|
+ bottomBarHeight = Platform.isIOS ? 34 : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取 token
|
|
|
+ final user = IXSP.getUser();
|
|
|
+ final token = user?.accessToken ?? '';
|
|
|
+
|
|
|
+ // 注入 nativeAttrs 对象
|
|
|
+ final jsCode =
|
|
|
+ '''
|
|
|
+ (function() {
|
|
|
+ // 创建 nativeAttrs 对象
|
|
|
+ window.${JSBridgeConstants.nativeAttrs} = {
|
|
|
+ statusBarHeight: $statusBarHeight,
|
|
|
+ bottomBarHeight: $bottomBarHeight,
|
|
|
+ token: '$token',
|
|
|
+ platform: '${Platform.isIOS ? 'ios' : 'android'}',
|
|
|
+ version: '${await _getAppVersion()}',
|
|
|
+ };
|
|
|
+
|
|
|
+ // 创建原生方法调用接口
|
|
|
+ window.native = {
|
|
|
+ // 设置状态栏模式(只支持 'dark' 或 'light')
|
|
|
+ setStatusBarColor: function(mode) {
|
|
|
+ return window.flutter_inappwebview.callHandler('${JSBridgeConstants.setStatusBarColor}', {color: mode});
|
|
|
+ },
|
|
|
+
|
|
|
+ // 退出 WebView
|
|
|
+ exit: function() {
|
|
|
+ return window.flutter_inappwebview.callHandler('${JSBridgeConstants.exitWebView}');
|
|
|
+ },
|
|
|
+
|
|
|
+ // 连接 VPN
|
|
|
+ connectVPN: function(id, code) {
|
|
|
+ return window.flutter_inappwebview.callHandler('${JSBridgeConstants.connectVPN}', {id: id, code: code});
|
|
|
+ },
|
|
|
+
|
|
|
+ // 断开 VPN
|
|
|
+ disconnectVPN: function() {
|
|
|
+ return window.flutter_inappwebview.callHandler('${JSBridgeConstants.disconnectVPN}');
|
|
|
+ },
|
|
|
+
|
|
|
+ // 打开指定 App
|
|
|
+ openApp: function(packageName, scheme, extras) {
|
|
|
+ return window.flutter_inappwebview.callHandler('${JSBridgeConstants.openApp}', {
|
|
|
+ packageName: packageName,
|
|
|
+ scheme: scheme,
|
|
|
+ extras: extras
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取 VPN 状态
|
|
|
+ getVPNStatus: function() {
|
|
|
+ return window.flutter_inappwebview.callHandler('${JSBridgeConstants.getVPNStatus}');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 创建事件回调接口
|
|
|
+ window.nativeCallbacks = {
|
|
|
+ onVPNStatusChanged: function(status) {
|
|
|
+ console.log('VPN状态变化:', status);
|
|
|
+ },
|
|
|
+ onVPNConnected: function() {
|
|
|
+ console.log('VPN已连接');
|
|
|
+ },
|
|
|
+ onVPNDisconnected: function() {
|
|
|
+ console.log('VPN已断开');
|
|
|
+ },
|
|
|
+ onVPNConnecting: function() {
|
|
|
+ console.log('VPN连接中');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('Native bridge initialized:', window.${JSBridgeConstants.nativeAttrs});
|
|
|
+ })();
|
|
|
+ ''';
|
|
|
+
|
|
|
+ await webViewController!.evaluateJavascript(source: jsCode);
|
|
|
+ log(TAG, 'Native Attrs 注入成功');
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '注入 Native Attrs 失败: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 获取应用版本
|
|
|
+ Future<String> _getAppVersion() async {
|
|
|
+ try {
|
|
|
+ final packageInfo = await PackageInfo.fromPlatform();
|
|
|
+ return packageInfo.version;
|
|
|
+ } catch (e) {
|
|
|
+ return '1.0.0';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== Handler 实现 ====================
|
|
|
+
|
|
|
+ /// 处理设置状态栏模式(只支持 dark/light)
|
|
|
+ Future<Map<String, dynamic>> _handleSetStatusBarColor(
|
|
|
+ List<dynamic> args,
|
|
|
+ ) async {
|
|
|
+ try {
|
|
|
+ if (args.isEmpty) {
|
|
|
+ return JSBridgeResponse(success: false, error: '缺少模式参数').toJson();
|
|
|
+ }
|
|
|
+
|
|
|
+ final params = args[0] as Map<String, dynamic>;
|
|
|
+ final mode = params['color'] as String?;
|
|
|
+
|
|
|
+ if (mode == null || mode.isEmpty) {
|
|
|
+ return JSBridgeResponse(success: false, error: '模式值不能为空').toJson();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只支持 'dark' 和 'light' 两种模式
|
|
|
+ final Brightness iconBrightness;
|
|
|
+ if (mode.toLowerCase() == 'dark') {
|
|
|
+ // dark 模式 = 白色图标(用于深色背景)
|
|
|
+ iconBrightness = Brightness.light;
|
|
|
+ } else if (mode.toLowerCase() == 'light') {
|
|
|
+ // light 模式 = 黑色图标(用于浅色背景)
|
|
|
+ iconBrightness = Brightness.dark;
|
|
|
+ } else {
|
|
|
+ return JSBridgeResponse(
|
|
|
+ success: false,
|
|
|
+ error: '只支持 "dark" 或 "light" 模式',
|
|
|
+ ).toJson();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新的状态栏样式(背景色保持透明)
|
|
|
+ final newStyle = SystemUiOverlayStyle(
|
|
|
+ statusBarColor: Colors.transparent, // 保持透明
|
|
|
+ statusBarIconBrightness: iconBrightness, // 只改变图标颜色
|
|
|
+ statusBarBrightness: iconBrightness,
|
|
|
+ );
|
|
|
+
|
|
|
+ // 更新响应式状态栏样式,触发 AnnotatedRegion 重新渲染
|
|
|
+ currentStatusBarStyle.value = newStyle;
|
|
|
+
|
|
|
+ log(TAG, '设置状态栏模式: $mode (图标亮度: $iconBrightness)');
|
|
|
+
|
|
|
+ return JSBridgeResponse(success: true, data: {'mode': mode}).toJson();
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '设置状态栏模式失败: $e');
|
|
|
+ return JSBridgeResponse(success: false, error: e.toString()).toJson();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理退出 WebView
|
|
|
+ Future<Map<String, dynamic>> _handleExitWebView(List<dynamic> args) async {
|
|
|
+ try {
|
|
|
+ log(TAG, '退出 WebView');
|
|
|
+ Get.back();
|
|
|
+
|
|
|
+ return JSBridgeResponse(success: true).toJson();
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '退出 WebView 失败: $e');
|
|
|
+ return JSBridgeResponse(success: false, error: e.toString()).toJson();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理连接 VPN
|
|
|
+ Future<Map<String, dynamic>> _handleConnectVPN(List<dynamic> args) async {
|
|
|
+ try {
|
|
|
+ final params = args.isNotEmpty
|
|
|
+ ? args[0] as Map<String, dynamic>
|
|
|
+ : <String, dynamic>{};
|
|
|
+ final vpnParams = VPNConnectParams.fromJson(params);
|
|
|
+
|
|
|
+ log(TAG, '连接 VPN: id=${vpnParams.id}, code=${vpnParams.code}');
|
|
|
+
|
|
|
+ // 如果提供了 id 和 code,保存为选中的位置
|
|
|
+ if (vpnParams.id != null && vpnParams.code != null) {
|
|
|
+ await IXSP.saveSelectedLocation({
|
|
|
+ 'id': vpnParams.id,
|
|
|
+ 'code': vpnParams.code,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用 CoreController 连接 VPN
|
|
|
+ final coreController = Get.find<CoreController>();
|
|
|
+ coreController.handleConnection();
|
|
|
+
|
|
|
+ return JSBridgeResponse(
|
|
|
+ success: true,
|
|
|
+ data: {'id': vpnParams.id, 'code': vpnParams.code},
|
|
|
+ ).toJson();
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '连接 VPN 失败: $e');
|
|
|
+ return JSBridgeResponse(success: false, error: e.toString()).toJson();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理断开 VPN
|
|
|
+ Future<Map<String, dynamic>> _handleDisconnectVPN(List<dynamic> args) async {
|
|
|
+ try {
|
|
|
+ log(TAG, '断开 VPN');
|
|
|
+
|
|
|
+ // 调用 CoreController 断开 VPN
|
|
|
+ final coreController = Get.find<CoreController>();
|
|
|
+ if (coreController.state != enums.ConnectionState.disconnected) {
|
|
|
+ coreController.handleConnection();
|
|
|
+ }
|
|
|
+
|
|
|
+ return JSBridgeResponse(success: true).toJson();
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '断开 VPN 失败: $e');
|
|
|
+ return JSBridgeResponse(success: false, error: e.toString()).toJson();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理打开指定 App
|
|
|
+ Future<Map<String, dynamic>> _handleOpenApp(List<dynamic> args) async {
|
|
|
+ try {
|
|
|
+ if (args.isEmpty) {
|
|
|
+ return JSBridgeResponse(success: false, error: '缺少参数').toJson();
|
|
|
+ }
|
|
|
+
|
|
|
+ final params = args[0] as Map<String, dynamic>;
|
|
|
+ final appParams = OpenAppParams.fromJson(params);
|
|
|
+
|
|
|
+ log(TAG, '打开 App: ${appParams.packageName}');
|
|
|
+
|
|
|
+ Uri? uri;
|
|
|
+
|
|
|
+ // 优先使用 scheme
|
|
|
+ if (appParams.scheme != null && appParams.scheme!.isNotEmpty) {
|
|
|
+ uri = Uri.parse(appParams.scheme!);
|
|
|
+ } else if (Platform.isAndroid) {
|
|
|
+ // Android 使用 package name
|
|
|
+ uri = Uri.parse('package:${appParams.packageName}');
|
|
|
+ } else if (Platform.isIOS) {
|
|
|
+ // iOS 需要使用 app 的 URL Scheme
|
|
|
+ return JSBridgeResponse(
|
|
|
+ success: false,
|
|
|
+ error: 'iOS 需要提供 URL Scheme',
|
|
|
+ ).toJson();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uri != null && await canLaunchUrl(uri)) {
|
|
|
+ await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
|
+
|
|
|
+ return JSBridgeResponse(
|
|
|
+ success: true,
|
|
|
+ data: {'packageName': appParams.packageName},
|
|
|
+ ).toJson();
|
|
|
+ } else {
|
|
|
+ return JSBridgeResponse(success: false, error: '无法打开应用').toJson();
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '打开 App 失败: $e');
|
|
|
+ return JSBridgeResponse(success: false, error: e.toString()).toJson();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理获取 VPN 状态
|
|
|
+ Future<Map<String, dynamic>> _handleGetVPNStatus(List<dynamic> args) async {
|
|
|
+ try {
|
|
|
+ final coreController = Get.find<CoreController>();
|
|
|
+ final state = coreController.state;
|
|
|
+ final timer = coreController.timer;
|
|
|
+
|
|
|
+ String statusText;
|
|
|
+ switch (state) {
|
|
|
+ case enums.ConnectionState.connected:
|
|
|
+ statusText = 'connected';
|
|
|
+ break;
|
|
|
+ case enums.ConnectionState.connecting:
|
|
|
+ statusText = 'connecting';
|
|
|
+ break;
|
|
|
+ case enums.ConnectionState.disconnected:
|
|
|
+ statusText = 'disconnected';
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ statusText = 'unknown';
|
|
|
+ }
|
|
|
+
|
|
|
+ log(TAG, '获取 VPN 状态: $statusText');
|
|
|
+
|
|
|
+ return JSBridgeResponse(
|
|
|
+ success: true,
|
|
|
+ data: {'status': statusText, 'timer': timer},
|
|
|
+ ).toJson();
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '获取 VPN 状态失败: $e');
|
|
|
+ return JSBridgeResponse(success: false, error: e.toString()).toJson();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 监听 VPN 状态变化并通知 JS ====================
|
|
|
+
|
|
|
+ // 保存 Worker 引用
|
|
|
+ Worker? _vpnStatusWorker;
|
|
|
+
|
|
|
+ @override
|
|
|
+ void onClose() {
|
|
|
+ // 取消 VPN 状态监听
|
|
|
+ _vpnStatusWorker?.dispose();
|
|
|
+ _vpnStatusWorker = null;
|
|
|
+ super.onClose();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 监听 VPN 状态变化
|
|
|
+ void _listenToVPNStatusChanges() {
|
|
|
+ try {
|
|
|
+ final coreController = Get.find<CoreController>();
|
|
|
+
|
|
|
+ // 使用 ever 监听 CoreController 提供的状态流
|
|
|
+ _vpnStatusWorker = ever(coreController.stateStream, (
|
|
|
+ enums.ConnectionState state,
|
|
|
+ ) {
|
|
|
+ _notifyVPNStatusToJS(state);
|
|
|
+ log(TAG, 'VPN 状态已变化并通知 JS: $state');
|
|
|
+ });
|
|
|
+
|
|
|
+ log(TAG, 'VPN 状态监听已启动');
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '监听 VPN 状态变化失败: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 通知 JS VPN 状态变化
|
|
|
+ void _notifyVPNStatusToJS(enums.ConnectionState state) async {
|
|
|
+ if (webViewController == null) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ String statusText;
|
|
|
+ String callbackName;
|
|
|
+
|
|
|
+ switch (state) {
|
|
|
+ case enums.ConnectionState.connected:
|
|
|
+ statusText = 'connected';
|
|
|
+ callbackName = 'onVPNConnected';
|
|
|
+ break;
|
|
|
+ case enums.ConnectionState.connecting:
|
|
|
+ statusText = 'connecting';
|
|
|
+ callbackName = 'onVPNConnecting';
|
|
|
+ break;
|
|
|
+ case enums.ConnectionState.disconnected:
|
|
|
+ statusText = 'disconnected';
|
|
|
+ callbackName = 'onVPNDisconnected';
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ statusText = 'unknown';
|
|
|
+ callbackName = 'onVPNStatusChanged';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用 JS 回调
|
|
|
+ final jsCode =
|
|
|
+ '''
|
|
|
+ (function() {
|
|
|
+ try {
|
|
|
+ // 调用通用状态变化回调
|
|
|
+ if (window.nativeCallbacks && typeof window.nativeCallbacks.onVPNStatusChanged === 'function') {
|
|
|
+ window.nativeCallbacks.onVPNStatusChanged('$statusText');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用特定状态回调
|
|
|
+ if (window.nativeCallbacks && typeof window.nativeCallbacks.$callbackName === 'function') {
|
|
|
+ window.nativeCallbacks.$callbackName();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 触发自定义事件
|
|
|
+ window.dispatchEvent(new CustomEvent('vpnStatusChanged', {
|
|
|
+ detail: { status: '$statusText' }
|
|
|
+ }));
|
|
|
+ } catch (e) {
|
|
|
+ console.error('VPN status callback error:', e);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ ''';
|
|
|
+
|
|
|
+ await webViewController!.evaluateJavascript(source: jsCode);
|
|
|
+ log(TAG, '通知 JS VPN 状态变化: $statusText');
|
|
|
+ } catch (e) {
|
|
|
+ log(TAG, '通知 JS VPN 状态变化失败: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|