Explorar o código

fix: 优化部分布局显示效果,优化部分翻译问题

lilu hai 3 meses
pai
achega
6300e67ce7
Modificáronse 36 ficheiros con 1760 adicións e 224 borrados
  1. 455 0
      assets/test_jsbridge.html
  2. 5 0
      assets/vectors/push_notifications.svg
  3. 1 0
      assets/vectors/update.svg
  4. 4 0
      lib/app/constants/assets.dart
  5. 4 21
      lib/app/controllers/api_controller.dart
  6. 3 0
      lib/app/controllers/core_controller.dart
  7. 22 0
      lib/app/dialog/all_dialog.dart
  8. 104 50
      lib/app/dialog/custom_dialog.dart
  9. 2 2
      lib/app/modules/home/controllers/home_controller.dart
  10. 4 0
      lib/app/modules/home/widgets/menu_list.dart
  11. 14 0
      lib/app/modules/setting/controllers/setting_controller.dart
  12. 52 2
      lib/app/modules/setting/views/setting_view.dart
  13. 488 0
      lib/app/modules/web/controllers/web_controller.dart
  14. 107 0
      lib/app/modules/web/utils/js_bridge.dart
  15. 166 132
      lib/app/modules/web/views/web_view.dart
  16. 2 0
      lib/config/theme/dark_theme_colors.dart
  17. 5 1
      lib/config/translations/ar_AR/ar_ar_translation.dart
  18. 5 1
      lib/config/translations/de_DE/de_de_translation.dart
  19. 5 2
      lib/config/translations/en_US/en_us_translation.dart
  20. 5 2
      lib/config/translations/es_ES/es_es_translation.dart
  21. 5 2
      lib/config/translations/fa_IR/fa_ir_translation.dart
  22. 5 2
      lib/config/translations/fr_FR/fr_fr_translation.dart
  23. 5 1
      lib/config/translations/ja_JP/ja_jp_translation.dart
  24. 5 1
      lib/config/translations/ko_KR/ko_kr_translation.dart
  25. 5 2
      lib/config/translations/my_MM/my_mm_translation.dart
  26. 5 2
      lib/config/translations/ru_RU/ru_ru_translation.dart
  27. 4 1
      lib/config/translations/strings_enum.dart
  28. 232 0
      lib/utils/awesome_notifications_helper.dart
  29. 13 0
      lib/utils/system_helper.dart
  30. 4 0
      linux/flutter/generated_plugin_registrant.cc
  31. 1 0
      linux/flutter/generated_plugins.cmake
  32. 2 0
      macos/Flutter/GeneratedPluginRegistrant.swift
  33. 16 0
      pubspec.lock
  34. 1 0
      pubspec.yaml
  35. 3 0
      windows/flutter/generated_plugin_registrant.cc
  36. 1 0
      windows/flutter/generated_plugins.cmake

+ 455 - 0
assets/test_jsbridge.html

@@ -0,0 +1,455 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>NOMO JS Bridge 测试</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+      box-sizing: border-box;
+    }
+    
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      color: #333;
+      /* 使用原生属性设置安全边距 */
+      padding-top: calc(var(--status-bar-height, 0px) + 10px);
+      padding-bottom: calc(var(--bottom-bar-height, 0px) + 10px);
+      min-height: 100vh;
+    }
+    
+    .container {
+      max-width: 600px;
+      margin: 0 auto;
+      padding: 15px;
+    }
+    
+    .header {
+      background: white;
+      border-radius: 15px;
+      padding: 20px;
+      margin-bottom: 15px;
+      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    }
+    
+    .header h1 {
+      font-size: 24px;
+      margin-bottom: 15px;
+      color: #667eea;
+    }
+    
+    .info-grid {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 10px;
+      font-size: 13px;
+    }
+    
+    .info-item {
+      background: #f8f9fa;
+      padding: 10px;
+      border-radius: 8px;
+    }
+    
+    .info-label {
+      color: #666;
+      font-size: 11px;
+      margin-bottom: 5px;
+    }
+    
+    .info-value {
+      color: #333;
+      font-weight: 600;
+      word-break: break-all;
+    }
+    
+    .card {
+      background: white;
+      border-radius: 15px;
+      padding: 20px;
+      margin-bottom: 15px;
+      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    }
+    
+    .card h2 {
+      font-size: 18px;
+      margin-bottom: 15px;
+      color: #667eea;
+    }
+    
+    .vpn-status {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 15px;
+      background: #f8f9fa;
+      border-radius: 10px;
+      margin-bottom: 15px;
+    }
+    
+    .status-indicator {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+    
+    .status-dot {
+      width: 12px;
+      height: 12px;
+      border-radius: 50%;
+      background-color: #ccc;
+      animation: pulse 2s infinite;
+    }
+    
+    .status-dot.connected {
+      background-color: #10b981;
+    }
+    
+    .status-dot.connecting {
+      background-color: #f59e0b;
+    }
+    
+    .status-dot.disconnected {
+      background-color: #ef4444;
+    }
+    
+    @keyframes pulse {
+      0%, 100% { opacity: 1; }
+      50% { opacity: 0.5; }
+    }
+    
+    .btn-grid {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 10px;
+    }
+    
+    .btn {
+      padding: 12px 20px;
+      border: none;
+      border-radius: 10px;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      color: white;
+      font-size: 14px;
+      font-weight: 600;
+      cursor: pointer;
+      transition: transform 0.2s, box-shadow 0.2s;
+    }
+    
+    .btn:active {
+      transform: scale(0.95);
+    }
+    
+    .btn-full {
+      grid-column: 1 / -1;
+    }
+    
+    .btn-danger {
+      background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+    }
+    
+    .btn-success {
+      background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+    }
+    
+    .log {
+      background: #1e293b;
+      color: #94a3b8;
+      border-radius: 10px;
+      padding: 15px;
+      max-height: 300px;
+      overflow-y: auto;
+      font-size: 12px;
+      font-family: 'Courier New', monospace;
+    }
+    
+    .log-item {
+      margin-bottom: 8px;
+      line-height: 1.5;
+    }
+    
+    .log-time {
+      color: #64748b;
+    }
+    
+    .log-success {
+      color: #10b981;
+    }
+    
+    .log-error {
+      color: #ef4444;
+    }
+    
+    .log-info {
+      color: #3b82f6;
+    }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <!-- 设备信息 -->
+    <div class="header">
+      <h1>🚀 NOMO JS Bridge</h1>
+      <div class="info-grid">
+        <div class="info-item">
+          <div class="info-label">状态栏高度</div>
+          <div class="info-value" id="statusBarHeight">-</div>
+        </div>
+        <div class="info-item">
+          <div class="info-label">底部安全区</div>
+          <div class="info-value" id="bottomBarHeight">-</div>
+        </div>
+        <div class="info-item">
+          <div class="info-label">平台</div>
+          <div class="info-value" id="platform">-</div>
+        </div>
+        <div class="info-item">
+          <div class="info-label">版本</div>
+          <div class="info-value" id="version">-</div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- VPN 控制 -->
+    <div class="card">
+      <h2>🔐 VPN 控制</h2>
+      <div class="vpn-status">
+        <div class="status-indicator">
+          <div class="status-dot" id="statusDot"></div>
+          <div>
+            <div style="font-weight: 600;" id="vpnStatus">未知</div>
+            <div style="font-size: 12px; color: #666;" id="vpnTimer">00:00:00</div>
+          </div>
+        </div>
+      </div>
+      <div class="btn-grid">
+        <button class="btn btn-success" onclick="handleConnectVPN()">
+          ⚡ 连接
+        </button>
+        <button class="btn btn-danger" onclick="handleDisconnectVPN()">
+          ⏸️ 断开
+        </button>
+        <button class="btn btn-full" onclick="handleGetVPNStatus()">
+          🔍 查询状态
+        </button>
+      </div>
+    </div>
+    
+    <!-- 其他功能 -->
+    <div class="card">
+      <h2>🛠️ 其他功能</h2>
+      <div class="btn-grid">
+        <button class="btn" onclick="handleSetStatusBarColor()">
+          🎨 切换状态栏
+        </button>
+        <button class="btn" onclick="handleOpenApp()">
+          📱 打开微信
+        </button>
+        <button class="btn btn-danger btn-full" onclick="handleExit()">
+          ❌ 退出 WebView
+        </button>
+      </div>
+    </div>
+    
+    <!-- 日志 -->
+    <div class="card">
+      <h2>📋 日志</h2>
+      <div class="log" id="logContainer"></div>
+    </div>
+  </div>
+  
+  <script>
+    let logCount = 0;
+    const maxLogs = 50;
+    
+    // 日志函数
+    function addLog(message, type = 'info') {
+      const logContainer = document.getElementById('logContainer');
+      const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
+      const logClass = `log-${type}`;
+      
+      const logItem = document.createElement('div');
+      logItem.className = 'log-item';
+      logItem.innerHTML = `<span class="log-time">[${time}]</span> <span class="${logClass}">${message}</span>`;
+      
+      logContainer.insertBefore(logItem, logContainer.firstChild);
+      
+      logCount++;
+      if (logCount > maxLogs) {
+        logContainer.removeChild(logContainer.lastChild);
+        logCount--;
+      }
+    }
+    
+    // 更新 VPN 状态显示
+    function updateVPNStatusUI(status) {
+      const statusDot = document.getElementById('statusDot');
+      const statusText = document.getElementById('vpnStatus');
+      
+      statusDot.className = 'status-dot ' + status;
+      
+      const statusMap = {
+        'connected': '已连接',
+        'connecting': '连接中',
+        'disconnected': '未连接',
+        'unknown': '未知'
+      };
+      
+      statusText.textContent = statusMap[status] || status;
+    }
+    
+    // 页面加载完成后初始化
+    window.onload = function() {
+      addLog('页面加载完成,等待原生注入...', 'info');
+      setTimeout(initNativeAttrs, 200);
+    };
+    
+    // 初始化原生属性
+    function initNativeAttrs() {
+      if (window.nativeAttrs) {
+        document.getElementById('statusBarHeight').textContent = window.nativeAttrs.statusBarHeight + 'px';
+        document.getElementById('bottomBarHeight').textContent = window.nativeAttrs.bottomBarHeight + 'px';
+        document.getElementById('platform').textContent = window.nativeAttrs.platform;
+        document.getElementById('version').textContent = window.nativeAttrs.version;
+        
+        // 设置 CSS 变量
+        document.documentElement.style.setProperty('--status-bar-height', window.nativeAttrs.statusBarHeight + 'px');
+        document.documentElement.style.setProperty('--bottom-bar-height', window.nativeAttrs.bottomBarHeight + 'px');
+        
+        addLog('✅ 原生属性注入成功', 'success');
+        addLog(`📱 平台: ${window.nativeAttrs.platform}, 版本: ${window.nativeAttrs.version}`, 'info');
+        
+        // 初始化 VPN 状态
+        handleGetVPNStatus();
+      } else {
+        addLog('❌ 原生属性注入失败', 'error');
+      }
+    }
+    
+    // 设置 VPN 状态回调
+    window.nativeCallbacks = {
+      onVPNStatusChanged: function(status) {
+        addLog(`📡 VPN 状态变化: ${status}`, 'info');
+        updateVPNStatusUI(status);
+      },
+      
+      onVPNConnected: function() {
+        addLog('✅ VPN 连接成功!', 'success');
+        updateVPNStatusUI('connected');
+      },
+      
+      onVPNConnecting: function() {
+        addLog('⏳ VPN 连接中...', 'info');
+        updateVPNStatusUI('connecting');
+      },
+      
+      onVPNDisconnected: function() {
+        addLog('⏸️ VPN 已断开', 'info');
+        updateVPNStatusUI('disconnected');
+      }
+    };
+    
+    // 功能函数
+    async function handleConnectVPN() {
+      try {
+        addLog('🚀 正在连接 VPN...', 'info');
+        const result = await window.native.connectVPN(26, 'ca');
+        if (result.success) {
+          addLog('✅ VPN 连接请求已发送', 'success');
+        } else {
+          addLog(`❌ 连接失败: ${result.error}`, 'error');
+        }
+      } catch (error) {
+        addLog(`❌ 发生错误: ${error}`, 'error');
+      }
+    }
+    
+    async function handleDisconnectVPN() {
+      try {
+        addLog('⏸️ 正在断开 VPN...', 'info');
+        const result = await window.native.disconnectVPN();
+        if (result.success) {
+          addLog('✅ VPN 断开请求已发送', 'success');
+        } else {
+          addLog(`❌ 断开失败: ${result.error}`, 'error');
+        }
+      } catch (error) {
+        addLog(`❌ 发生错误: ${error}`, 'error');
+      }
+    }
+    
+    async function handleGetVPNStatus() {
+      try {
+        const result = await window.native.getVPNStatus();
+        if (result.success) {
+          const status = result.data.status;
+          const timer = result.data.timer;
+          
+          document.getElementById('vpnTimer').textContent = timer;
+          updateVPNStatusUI(status);
+          
+          addLog(`📊 VPN 状态: ${status}, 时长: ${timer}`, 'info');
+        } else {
+          addLog(`❌ 查询失败: ${result.error}`, 'error');
+        }
+      } catch (error) {
+        addLog(`❌ 发生错误: ${error}`, 'error');
+      }
+    }
+    
+    async function handleSetStatusBarColor() {
+      // 只支持 dark 和 light 两种模式
+      const currentMode = document.body.dataset.statusBarMode || 'light';
+      const newMode = currentMode === 'dark' ? 'light' : 'dark';
+      
+      try {
+        addLog(`🎨 切换状态栏模式: ${currentMode} -> ${newMode}`, 'info');
+        const result = await window.native.setStatusBarColor(newMode);
+        if (result.success) {
+          document.body.dataset.statusBarMode = newMode;
+          addLog(`✅ 状态栏模式切换成功: ${newMode} (${newMode === 'dark' ? '白色图标' : '黑色图标'})`, 'success');
+        } else {
+          addLog(`❌ 设置失败: ${result.error}`, 'error');
+        }
+      } catch (error) {
+        addLog(`❌ 发生错误: ${error}`, 'error');
+      }
+    }
+    
+    async function handleOpenApp() {
+      try {
+        addLog('📱 正在打开微信...', 'info');
+        const result = await window.native.openApp('com.tencent.mm', 'weixin://');
+        if (result.success) {
+          addLog('✅ 应用已打开', 'success');
+        } else {
+          addLog(`❌ 打开失败: ${result.error}`, 'error');
+        }
+      } catch (error) {
+        addLog(`❌ 发生错误: ${error}`, 'error');
+      }
+    }
+    
+    async function handleExit() {
+      try {
+        addLog('👋 正在退出 WebView...', 'info');
+        await window.native.exit();
+      } catch (error) {
+        addLog(`❌ 退出失败: ${error}`, 'error');
+      }
+    }
+    
+    // 监听 VPN 状态变化事件
+    window.addEventListener('vpnStatusChanged', function(event) {
+      console.log('收到 VPN 状态变化事件:', event.detail.status);
+    });
+    
+    // 初始日志
+    addLog('🎉 欢迎使用 NOMO JS Bridge 测试页面', 'success');
+  </script>
+</body>
+</html>
+

+ 5 - 0
assets/vectors/push_notifications.svg

@@ -0,0 +1,5 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.5 3.85684L10.0036 1.66699L17.5 3.85684V8.3477C17.5 13.0679 14.4792 16.8417 10.0011 18.3339C5.52171 16.8418 2.5 13.067 2.5 8.34562V3.85684Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M9.99984 9.58366C11.1504 9.58366 12.0832 8.65092 12.0832 7.50033C12.0832 6.34973 11.1504 5.41699 9.99984 5.41699C8.84924 5.41699 7.9165 6.34973 7.9165 7.50033C7.9165 8.65092 8.84924 9.58366 9.99984 9.58366Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.3332 12.9163C13.3332 11.0754 11.8408 9.58301 9.99984 9.58301C8.15888 9.58301 6.6665 11.0754 6.6665 12.9163" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 0
assets/vectors/update.svg


+ 4 - 0
lib/app/constants/assets.dart

@@ -102,4 +102,8 @@ class Assets {
   static const String refreshCircle = 'assets/vectors/refresh_circle.svg';
   static const String successCircle = 'assets/vectors/success_circle.svg';
   static const String failedCircle = 'assets/vectors/failed_circle.svg';
+
+  static const String pushNotifications =
+      'assets/vectors/push_notifications.svg';
+  static const String update = 'assets/vectors/update.svg';
 }

+ 4 - 21
lib/app/controllers/api_controller.dart

@@ -3,7 +3,7 @@ import 'dart:io';
 
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:dio/dio.dart';
-import 'package:flutter/material.dart';
+
 import 'package:get/get.dart';
 import 'package:nomo/app/api/router/api_router.dart';
 import 'package:nomo/app/data/sp/ix_sp.dart';
@@ -17,7 +17,7 @@ import '../../utils/device_manager.dart';
 import '../../utils/geo_downloader.dart';
 import '../../utils/log/logger.dart';
 import '../../utils/network_helper.dart';
-import '../../utils/system_helper.dart';
+
 import '../api/core/api_core.dart';
 import '../components/country_restricted_overlay.dart';
 import '../components/ix_snackbar.dart';
@@ -31,7 +31,7 @@ import '../data/models/failure.dart';
 import '../data/models/fingerprint.dart';
 import '../data/models/launch/groups.dart';
 import '../data/models/launch/launch.dart';
-import '../dialog/update_dailog.dart';
+import '../dialog/all_dialog.dart';
 
 class ApiController extends GetxService {
   final TAG = 'ApiController';
@@ -369,24 +369,7 @@ class ApiController extends GetxService {
       }
 
       if (hasUpdate) {
-        Get.dialog(
-          WillPopScope(
-            onWillPop: () async => false,
-            child: UpdateDialog(
-              upgrade: upgrade,
-              onUpdate: () =>
-                  SystemHelper.openGooglePlayUrl(upgrade?.appStoreUrl ?? ''),
-              onLater: hasForceUpdate
-                  ? null
-                  : () {
-                      Navigator.of(Get.context!).pop();
-                    },
-            ),
-          ),
-          barrierDismissible: false,
-        ).then((_) {
-          log('UpdateDialog closed');
-        });
+        AllDialog.showUpdate(hasForceUpdate: hasForceUpdate);
       }
       return hasUpdate;
     } catch (e) {

+ 3 - 0
lib/app/controllers/core_controller.dart

@@ -26,6 +26,9 @@ class CoreController extends GetxService {
   ConnectionState get state => _state.value;
   set state(ConnectionState value) => _state.value = value;
 
+  // 公开状态流供外部监听
+  Rx<ConnectionState> get stateStream => _state;
+
   final _timer = "00:00:00".obs;
   String get timer => _timer.value;
   set timer(String value) => _timer.value = value;

+ 22 - 0
lib/app/dialog/all_dialog.dart

@@ -1,7 +1,9 @@
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
+import 'package:nomo/app/constants/assets.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/translations/strings_enum.dart';
+import '../../config/theme/dark_theme_colors.dart';
 import '../constants/iconfont/iconfont.dart';
 import 'custom_dialog.dart';
 
@@ -136,6 +138,8 @@ class AllDialog {
       message: Strings.deleteAccountConfirmMessage.tr,
       buttonText: Strings.deleteAccount.tr,
       cancelText: Strings.cancel.tr,
+      icon: IconFont.icon40,
+      iconColor: DarkThemeColors.deleteAccountIconColor,
       onPressed: () {
         // 处理删除账户逻辑
         Navigator.of(Get.context!).pop();
@@ -148,6 +152,24 @@ class AllDialog {
     );
   }
 
+  /// 显示更新弹窗
+  static void showUpdate({bool hasForceUpdate = false}) {
+    CustomDialog.showUpdateDialog(
+      title: Strings.newVersionAvailable.tr,
+      message:
+          "A newer version of the app is ready.\nThis update improves stability and performance, and fixes known issues.",
+      buttonText: Strings.upgradeNow.tr,
+      cancelText: Strings.cancel.tr,
+      onCancel: () {
+        Navigator.of(Get.context!).pop();
+      },
+      svgPath: Assets.update,
+      onPressed: () {
+        Navigator.of(Get.context!).pop();
+      },
+    );
+  }
+
   /// 显示自定义成功弹窗
   static void showCustomSuccess({
     required String title,

+ 104 - 50
lib/app/dialog/custom_dialog.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_svg/flutter_svg.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
@@ -13,6 +14,7 @@ class CustomDialog {
     String buttonText = 'Got it',
     VoidCallback? onPressed,
     IconData? icon,
+    String? svgPath,
     Color? iconColor,
   }) {
     Get.generalDialog(
@@ -24,6 +26,7 @@ class CustomDialog {
           buttonText: buttonText,
           onPressed: onPressed,
           icon: icon ?? Icons.workspace_premium,
+          svgPath: svgPath,
           iconColor: iconColor ?? const Color(0xFFFF9500),
           animation: animation,
         );
@@ -33,6 +36,41 @@ class CustomDialog {
     );
   }
 
+  static void showUpdateDialog({
+    required String title,
+    required String message,
+    String buttonText = 'Retry',
+    String? cancelText,
+    VoidCallback? onPressed,
+    VoidCallback? onCancel,
+    String? svgPath,
+    Color? iconColor,
+    Color? confirmButtonColor,
+  }) {
+    Get.generalDialog(
+      pageBuilder: (context, animation, secondaryAnimation) {
+        return WillPopScope(
+          onWillPop: () async => false,
+          child: _CustomDialogWidget(
+            type: DialogType.error,
+            title: title,
+            message: message,
+            buttonText: buttonText,
+            cancelText: cancelText,
+            onPressed: onPressed,
+            onCancel: onCancel,
+            svgPath: svgPath,
+            iconColor:
+                iconColor ?? Get.reactiveTheme.textTheme.bodyLarge!.color!,
+            animation: animation,
+          ),
+        );
+      },
+      barrierDismissible: false,
+      transitionDuration: const Duration(milliseconds: 300),
+    );
+  }
+
   /// 显示信息弹窗(如邮件发送成功)
   static void showInfo({
     required String title,
@@ -40,6 +78,7 @@ class CustomDialog {
     String buttonText = 'OK',
     VoidCallback? onPressed,
     IconData? icon,
+    String? svgPath,
     Color? iconColor,
   }) {
     Get.generalDialog(
@@ -51,6 +90,7 @@ class CustomDialog {
           buttonText: buttonText,
           onPressed: onPressed,
           icon: icon ?? Icons.mark_email_read,
+          svgPath: svgPath,
           iconColor: iconColor ?? const Color(0xFF00A8E8),
           animation: animation,
         );
@@ -69,6 +109,7 @@ class CustomDialog {
     VoidCallback? onPressed,
     VoidCallback? onCancel,
     IconData? icon,
+    String? svgPath,
     Color? iconColor,
     Color? confirmButtonColor,
     String? errorCode,
@@ -84,6 +125,7 @@ class CustomDialog {
           onPressed: onPressed,
           onCancel: onCancel,
           icon: icon ?? Icons.wifi_off,
+          svgPath: svgPath,
           iconColor: iconColor ?? const Color(0xFFFF9500),
           confirmButtonColor:
               confirmButtonColor ?? Get.reactiveTheme.primaryColor,
@@ -105,6 +147,7 @@ class CustomDialog {
     required VoidCallback onConfirm,
     VoidCallback? onCancel,
     IconData? icon,
+    String? svgPath,
     Color? iconColor,
     Color? confirmButtonColor,
   }) {
@@ -120,6 +163,7 @@ class CustomDialog {
           onConfirm: onConfirm,
           onCancel: onCancel,
           icon: icon ?? Icons.info_outline,
+          svgPath: svgPath,
           iconColor: iconColor ?? const Color(0xFFFF3B30),
           confirmButtonColor: confirmButtonColor ?? const Color(0xFFFF3B30),
           animation: animation,
@@ -146,7 +190,8 @@ class _CustomDialogWidget extends StatelessWidget {
   final VoidCallback? onPressed;
   final VoidCallback? onConfirm;
   final VoidCallback? onCancel;
-  final IconData icon;
+  final IconData? icon;
+  final String? svgPath;
   final Color iconColor;
   final Color? confirmButtonColor;
   final String? errorCode;
@@ -162,7 +207,8 @@ class _CustomDialogWidget extends StatelessWidget {
     this.onPressed,
     this.onConfirm,
     this.onCancel,
-    required this.icon,
+    this.icon,
+    this.svgPath,
     required this.iconColor,
     this.confirmButtonColor,
     this.errorCode,
@@ -262,7 +308,14 @@ class _CustomDialogWidget extends StatelessWidget {
   Widget _buildIcon() {
     return Container(
       margin: EdgeInsets.only(bottom: 10.w),
-      child: Icon(icon, size: 40.w, color: iconColor),
+      child: svgPath != null
+          ? SvgPicture.asset(
+              svgPath!,
+              width: 40.w,
+              height: 40.w,
+              colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn),
+            )
+          : Icon(icon ?? Icons.info_outline, size: 40.w, color: iconColor),
     );
   }
 
@@ -339,16 +392,42 @@ class _CustomDialogWidget extends StatelessWidget {
 
   /// 构建错误弹窗按钮
   Widget _buildErrorButtons() {
-    return Row(
+    return Column(
       children: [
+        SizedBox(
+          width: double.infinity,
+          height: 42.h,
+          child: ClickOpacity(
+            onTap: () {
+              onPressed?.call();
+            },
+            child: Container(
+              decoration: BoxDecoration(
+                color: confirmButtonColor ?? Get.reactiveTheme.primaryColor,
+                borderRadius: BorderRadius.circular(8.r),
+              ),
+              alignment: Alignment.center,
+              child: Text(
+                buttonText,
+                style: TextStyle(
+                  fontSize: 14.sp,
+                  fontWeight: FontWeight.w600,
+                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                ),
+              ),
+            ),
+          ),
+        ),
         if (cancelText != null) ...[
-          Expanded(
+          SizedBox(height: 12.w),
+          SizedBox(
+            width: double.infinity,
+            height: 42.h,
             child: ClickOpacity(
               onTap: () {
                 onCancel?.call();
               },
               child: Container(
-                height: 42.h,
                 decoration: BoxDecoration(
                   borderRadius: BorderRadius.circular(8.r),
                   border: Border.all(
@@ -368,87 +447,62 @@ class _CustomDialogWidget extends StatelessWidget {
               ),
             ),
           ),
-          SizedBox(width: 12.w),
         ],
-        Expanded(
-          child: ClickOpacity(
-            onTap: () {
-              onPressed?.call();
-            },
-            child: Container(
-              height: 42.h,
-              decoration: BoxDecoration(
-                borderRadius: BorderRadius.circular(8.r),
-                border: Border.all(
-                  color: Get.reactiveTheme.dividerColor,
-                  width: 1.w,
-                ),
-              ),
-              alignment: Alignment.center,
-              child: Text(
-                buttonText,
-                style: TextStyle(
-                  fontSize: 14.sp,
-                  fontWeight: FontWeight.w600,
-                  color: confirmButtonColor,
-                ),
-              ),
-            ),
-          ),
-        ),
       ],
     );
   }
 
   /// 构建确认弹窗按钮
   Widget _buildConfirmButtons() {
-    return Row(
+    return Column(
       children: [
-        Expanded(
+        SizedBox(
+          width: double.infinity,
+          height: 42.w,
           child: ClickOpacity(
             onTap: () {
-              onCancel?.call();
+              onConfirm?.call();
             },
             child: Container(
-              height: 42.w,
               decoration: BoxDecoration(
+                color: confirmButtonColor ?? iconColor,
                 borderRadius: BorderRadius.circular(8.r),
-                border: Border.all(
-                  color: Get.reactiveTheme.dividerColor,
-                  width: 1.w,
-                ),
               ),
               alignment: Alignment.center,
               child: Text(
-                cancelText ?? 'Cancel',
+                confirmText ?? 'Confirm',
                 style: TextStyle(
                   fontSize: 14.sp,
                   fontWeight: FontWeight.w600,
-                  color: Get.reactiveTheme.hintColor,
+                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 ),
               ),
             ),
           ),
         ),
-        8.horizontalSpace,
-        Expanded(
+        8.verticalSpace,
+        SizedBox(
+          width: double.infinity,
+          height: 42.w,
           child: ClickOpacity(
             onTap: () {
-              onConfirm?.call();
+              onCancel?.call();
             },
             child: Container(
-              height: 42.w,
               decoration: BoxDecoration(
-                color: confirmButtonColor ?? iconColor,
                 borderRadius: BorderRadius.circular(8.r),
+                border: Border.all(
+                  color: Get.reactiveTheme.dividerColor,
+                  width: 1.w,
+                ),
               ),
               alignment: Alignment.center,
               child: Text(
-                confirmText ?? 'Confirm',
+                cancelText ?? 'Cancel',
                 style: TextStyle(
                   fontSize: 14.sp,
                   fontWeight: FontWeight.w600,
-                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                  color: Get.reactiveTheme.hintColor,
                 ),
               ),
             ),

+ 2 - 2
lib/app/modules/home/controllers/home_controller.dart

@@ -1,7 +1,7 @@
 import 'package:get/get.dart';
 import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
+import '../../../../utils/awesome_notifications_helper.dart';
 import '../../../../utils/log/logger.dart';
-import '../../../../utils/permission_manager.dart';
 import '../../../base/base_controller.dart';
 import '../../../controllers/core_controller.dart';
 import '../../../data/models/launch/groups.dart';
@@ -62,7 +62,7 @@ class HomeController extends BaseController {
   void onInit() {
     super.onInit();
     _initializeLocations();
-    PermissionManager.requestNotificationPermission();
+    AwesomeNotificationsHelper.init();
   }
 
   /// 初始化位置数据

+ 4 - 0
lib/app/modules/home/widgets/menu_list.dart

@@ -4,7 +4,9 @@ import 'package:get/get.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 import '../../../../config/translations/strings_enum.dart';
+import '../../../../utils/system_helper.dart';
 import '../../../constants/iconfont/iconfont.dart';
+import '../../../dialog/all_dialog.dart';
 import '../../../routes/app_pages.dart';
 
 /// 菜单项数据模型
@@ -45,6 +47,7 @@ class MenuList extends StatelessWidget {
         iconColor: const Color(0xFF007AFF),
         onTap: () {
           print('Social tapped');
+          SystemHelper.openTestPage();
         },
       ),
       MenuItem(
@@ -53,6 +56,7 @@ class MenuList extends StatelessWidget {
         iconColor: const Color(0xFF34C759),
         onTap: () {
           print('Support tapped');
+          AllDialog.showUpdate();
         },
       ),
       MenuItem(

+ 14 - 0
lib/app/modules/setting/controllers/setting_controller.dart

@@ -1,6 +1,7 @@
 import 'package:get/get.dart';
 
 import '../../../../config/translations/strings_enum.dart';
+import '../../../../utils/awesome_notifications_helper.dart';
 import '../../../controllers/api_controller.dart';
 import '../../../dialog/loading/loading_dialog.dart';
 
@@ -10,11 +11,24 @@ class SettingController extends GetxController {
   // 自动重连开关
   final autoReconnect = true.obs;
 
+  final pushNotifications = false.obs;
+
   final isPremium = true.obs;
 
   @override
   void onInit() {
     super.onInit();
+    initPushNotifications();
+  }
+
+  Future<void> initPushNotifications() async {
+    pushNotifications.value =
+        await AwesomeNotificationsHelper.checkNotificationPermission();
+  }
+
+  Future<void> showNotificationConfigPage() async {
+    await AwesomeNotificationsHelper.showNotificationConfigPage();
+    initPushNotifications();
   }
 
   // 处理退出登录

+ 52 - 2
lib/app/modules/setting/views/setting_view.dart

@@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_svg/flutter_svg.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/constants/assets.dart';
@@ -419,6 +420,35 @@ class SettingView extends BaseView<SettingController> {
               },
             ),
             _buildDivider(),
+            _buildSettingItem(
+              svgPath: Assets.pushNotifications,
+              iconColor: DarkThemeColors.settingAppLinearGradientStartColor,
+              iconGradient: LinearGradient(
+                colors: [
+                  DarkThemeColors.settingAppLinearGradientStartColor,
+                  DarkThemeColors.settingAppLinearGradientEndColor,
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
+              title: Strings.pushNotifications.tr,
+              trailing: Obx(
+                () => CupertinoSwitch(
+                  value: controller.pushNotifications.value,
+                  onChanged: (value) {
+                    controller.showNotificationConfigPage();
+                  },
+                  activeTrackColor: Get.reactiveTheme.shadowColor,
+                  thumbColor: Colors.white,
+                  inactiveThumbColor: Colors.white,
+                  inactiveTrackColor: Colors.grey,
+                ),
+              ),
+              onTap: () {
+                controller.showNotificationConfigPage();
+              },
+            ),
+            _buildDivider(),
             _buildSettingItem(
               icon: IconFont.icon39,
               iconColor: DarkThemeColors.settingAppLinearGradientStartColor,
@@ -525,7 +555,8 @@ class SettingView extends BaseView<SettingController> {
 
   /// 构建设置项
   Widget _buildSettingItem({
-    required IconData icon,
+    IconData? icon,
+    String? svgPath,
     required Color iconColor,
     Gradient? iconGradient,
     required String title,
@@ -535,6 +566,12 @@ class SettingView extends BaseView<SettingController> {
     VoidCallback? onTap,
     VoidCallback? onInfoTap,
   }) {
+    // 确保至少提供了 icon 或 svgPath 之一
+    assert(
+      icon != null || svgPath != null,
+      'Must provide either icon or svgPath',
+    );
+
     return ClickOpacity(
       onTap: onTap,
       child: Container(
@@ -551,7 +588,20 @@ class SettingView extends BaseView<SettingController> {
                 color: iconGradient == null ? iconColor : null,
                 borderRadius: BorderRadius.circular(8.r),
               ),
-              child: Icon(icon, size: 20.w, color: Colors.white),
+              child: svgPath != null
+                  ? Padding(
+                      padding: EdgeInsets.all(5.w),
+                      child: SvgPicture.asset(
+                        svgPath,
+                        width: 20.w,
+                        height: 20.w,
+                        colorFilter: const ColorFilter.mode(
+                          Colors.white,
+                          BlendMode.srcIn,
+                        ),
+                      ),
+                    )
+                  : Icon(icon!, size: 20.w, color: Colors.white),
             ),
             10.horizontalSpace,
             // 标题

+ 488 - 0
lib/app/modules/web/controllers/web_controller.dart

@@ -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');
+    }
+  }
 }

+ 107 - 0
lib/app/modules/web/utils/js_bridge.dart

@@ -0,0 +1,107 @@
+import 'dart:convert';
+
+/// JS Bridge 常量定义
+class JSBridgeConstants {
+  // JS 对象名称
+  static const String nativeAttrs = 'nativeAttrs';
+
+  // JS 方法名称
+  static const String setStatusBarColor = 'setStatusBarColor';
+  static const String exitWebView = 'exitWebView';
+  static const String connectVPN = 'connectVPN';
+  static const String disconnectVPN = 'disconnectVPN';
+  static const String openApp = 'openApp';
+  static const String getVPNStatus = 'getVPNStatus';
+
+  // Flutter 调用 JS 的方法名称
+  static const String onVPNStatusChanged = 'onVPNStatusChanged';
+  static const String onVPNConnected = 'onVPNConnected';
+  static const String onVPNDisconnected = 'onVPNDisconnected';
+  static const String onVPNConnecting = 'onVPNConnecting';
+}
+
+/// JS Bridge 消息类型
+enum JSBridgeMessageType {
+  setStatusBarColor,
+  exitWebView,
+  connectVPN,
+  disconnectVPN,
+  openApp,
+  getVPNStatus,
+}
+
+/// JS Bridge 消息
+class JSBridgeMessage {
+  final JSBridgeMessageType type;
+  final Map<String, dynamic> data;
+
+  JSBridgeMessage({required this.type, required this.data});
+
+  factory JSBridgeMessage.fromJson(Map<String, dynamic> json) {
+    final typeStr = json['type'] as String;
+    final type = JSBridgeMessageType.values.firstWhere(
+      (e) => e.name == typeStr,
+      orElse: () => JSBridgeMessageType.getVPNStatus,
+    );
+
+    return JSBridgeMessage(
+      type: type,
+      data: json['data'] as Map<String, dynamic>? ?? {},
+    );
+  }
+
+  Map<String, dynamic> toJson() {
+    return {'type': type.name, 'data': data};
+  }
+}
+
+/// JS Bridge 响应
+class JSBridgeResponse {
+  final bool success;
+  final dynamic data;
+  final String? error;
+
+  JSBridgeResponse({required this.success, this.data, this.error});
+
+  Map<String, dynamic> toJson() {
+    return {'success': success, 'data': data, 'error': error};
+  }
+
+  String toJsonString() {
+    return jsonEncode(toJson());
+  }
+}
+
+/// VPN 连接参数
+class VPNConnectParams {
+  final int? id;
+  final String? code;
+
+  VPNConnectParams({this.id, this.code});
+
+  factory VPNConnectParams.fromJson(Map<String, dynamic> json) {
+    return VPNConnectParams(
+      id: json['id'] as int?,
+      code: json['code'] as String?,
+    );
+  }
+
+  bool get isValid => id != null || code != null;
+}
+
+/// 打开 App 参数
+class OpenAppParams {
+  final String packageName;
+  final String? scheme;
+  final Map<String, dynamic>? extras;
+
+  OpenAppParams({required this.packageName, this.scheme, this.extras});
+
+  factory OpenAppParams.fromJson(Map<String, dynamic> json) {
+    return OpenAppParams(
+      packageName: json['packageName'] as String,
+      scheme: json['scheme'] as String?,
+      extras: json['extras'] as Map<String, dynamic>?,
+    );
+  }
+}

+ 166 - 132
lib/app/modules/web/views/web_view.dart

@@ -1,4 +1,5 @@
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_inappwebview/flutter_inappwebview.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 
@@ -15,35 +16,43 @@ class WebView extends BaseView<WebController> {
   const WebView({super.key});
 
   @override
-  PreferredSizeWidget? get appBar => IXAppBar(
-    title: controller.title,
-    // 不需要传递颜色参数,会自动使用响应式主题
-    onBackPressed: () async {
-      if (await controller.checkCanGoBack()) {
-        controller.goBack();
-      } else {
-        Get.back();
-      }
-    },
-    actions: [
-      IconButton(
-        onPressed: () {
-          controller.reload();
-        },
-        icon: const Icon(Icons.refresh_rounded),
-      ),
-      Obx(
-        () => controller.canGoBack.value
-            ? IconButton(
+  PreferredSizeWidget? get appBar {
+    // 如果是全面屏模式,不显示AppBar
+    return controller.isFullScreen.value
+        ? null
+        : IXAppBar(
+            title: controller.title,
+            // 不需要传递颜色参数,会自动使用响应式主题
+            onBackPressed: () async {
+              if (await controller.checkCanGoBack()) {
+                controller.goBack();
+              } else {
+                Get.back();
+              }
+            },
+            actions: [
+              IconButton(
                 onPressed: () {
-                  Get.back();
+                  controller.reload();
                 },
-                icon: const Icon(Icons.close_rounded),
-              )
-            : const SizedBox.shrink(),
-      ),
-    ],
-  );
+                icon: const Icon(Icons.refresh_rounded),
+              ),
+              Obx(
+                () => controller.canGoBack.value
+                    ? IconButton(
+                        onPressed: () {
+                          Get.back();
+                        },
+                        icon: const Icon(Icons.close_rounded),
+                      )
+                    : const SizedBox.shrink(),
+              ),
+            ],
+          );
+  }
+
+  @override
+  bool get extendBodyBehindAppBar => controller.isFullScreen.value;
 
   @override
   Widget buildContent(BuildContext context) {
@@ -51,117 +60,142 @@ class WebView extends BaseView<WebController> {
   }
 
   Widget _buildWebContent() {
-    return Stack(
-      children: [
-        InAppWebView(
-          initialUrlRequest: URLRequest(url: WebUri(controller.url)),
-          initialSettings: InAppWebViewSettings(
-            userAgent: controller.userAgent,
-            javaScriptEnabled: true,
-            useShouldOverrideUrlLoading: true,
-            useOnLoadResource: true,
-            mediaPlaybackRequiresUserGesture: false,
-            allowsInlineMediaPlayback: true,
-            iframeAllow: "camera; microphone",
-            iframeAllowFullscreen: true,
-          ),
-          onWebViewCreated: (InAppWebViewController webViewController) {
-            controller.setWebViewController(webViewController);
-          },
-          onProgressChanged:
-              (InAppWebViewController webViewController, int progress) {
-                controller.loadingProgress.value = progress / 100;
-                controller.isLoading.value = progress < 100;
-              },
-          onLoadStart: (InAppWebViewController webViewController, Uri? url) {
-            controller.checkCanGoBack();
-          },
-          onLoadStop: (InAppWebViewController webViewController, Uri? url) {
-            controller.isLoading.value = false;
-            controller.loadingProgress.value = 0.0;
-            controller.checkCanGoBack();
-          },
-          onReceivedError:
-              (
-                InAppWebViewController webViewController,
-                WebResourceRequest request,
-                WebResourceError error,
-              ) {
-                controller.isLoading.value = false;
-                controller.loadingProgress.value = 0.0;
-              },
-          shouldOverrideUrlLoading:
-              (
-                InAppWebViewController webViewController,
-                NavigationAction navigationAction,
-              ) async {
-                final url = navigationAction.request.url?.toString() ?? '';
-
-                try {
-                  final uri = Uri.parse(url);
+    return Obx(() {
+      // 使用响应式状态栏样式
+      final statusBarStyle = controller.currentStatusBarStyle.value;
 
-                  // 处理 intent:// 链接
-                  if (url.startsWith("intent://")) {
-                    final parsedUrl = controller.parseIntentUrl(url);
-                    final fallbackUrl = controller.parseFallbackUrl(url);
+      return AnnotatedRegion<SystemUiOverlayStyle>(
+        value: statusBarStyle,
+        child: Stack(
+          children: [
+            InAppWebView(
+              initialUrlRequest: URLRequest(url: WebUri(controller.url)),
 
-                    if (parsedUrl != null &&
-                        await canLaunchUrl(Uri.parse(parsedUrl))) {
-                      await launchUrl(
-                        Uri.parse(parsedUrl),
-                        mode: LaunchMode.externalApplication,
-                      );
-                    } else if (fallbackUrl != null &&
-                        await canLaunchUrl(Uri.parse(fallbackUrl))) {
-                      await launchUrl(
-                        Uri.parse(fallbackUrl),
-                        mode: LaunchMode.externalApplication,
-                      );
+              initialSettings: InAppWebViewSettings(
+                userAgent: controller.userAgent,
+                javaScriptEnabled: true,
+                useShouldOverrideUrlLoading: true,
+                useOnLoadResource: true,
+                mediaPlaybackRequiresUserGesture: false,
+                allowsInlineMediaPlayback: true,
+                iframeAllow: "camera; microphone",
+                iframeAllowFullscreen: true,
+                allowFileAccessFromFileURLs: true,
+                allowUniversalAccessFromFileURLs: true,
+              ),
+              onWebViewCreated:
+                  (InAppWebViewController webViewController) async {
+                    controller.setWebViewController(webViewController);
+                    controller.setupJavaScriptHandlers(webViewController);
+                    // 如果 URL 是 assets 路径,则使用 loadData 加载
+                    if (controller.url.startsWith('assets/')) {
+                      await controller.loadAssetHtml(controller.url);
                     }
-                    return NavigationActionPolicy.CANCEL;
-                  }
-                  // 处理下载链接
-                  else if (uri.path.endsWith(".apk") ||
-                      uri.path.endsWith(".pdf") ||
-                      uri.path.contains("download")) {
-                    if (await canLaunchUrl(uri)) {
-                      await launchUrl(
-                        uri,
-                        mode: LaunchMode.externalApplication,
-                      );
-                    }
-                    return NavigationActionPolicy.CANCEL;
-                  }
+                  },
+              onProgressChanged:
+                  (InAppWebViewController webViewController, int progress) {
+                    controller.loadingProgress.value = progress / 100;
+                    controller.isLoading.value = progress < 100;
+                  },
+              onLoadStart:
+                  (InAppWebViewController webViewController, Uri? url) {
+                    controller.checkCanGoBack();
+                  },
+              onLoadStop:
+                  (InAppWebViewController webViewController, Uri? url) async {
+                    controller.isLoading.value = false;
+                    controller.loadingProgress.value = 0.0;
+                    controller.checkCanGoBack();
+                    // 页面加载完成后再次注入,确保万无一失
+                    await controller.injectNativeAttrs();
+                  },
+              onReceivedError:
+                  (
+                    InAppWebViewController webViewController,
+                    WebResourceRequest request,
+                    WebResourceError error,
+                  ) {
+                    controller.isLoading.value = false;
+                    controller.loadingProgress.value = 0.0;
+                  },
+              shouldOverrideUrlLoading:
+                  (
+                    InAppWebViewController webViewController,
+                    NavigationAction navigationAction,
+                  ) async {
+                    final url = navigationAction.request.url?.toString() ?? '';
 
-                  // 处理 http/https 链接
-                  if (uri.scheme == 'http' || uri.scheme == 'https') {
-                    return NavigationActionPolicy.ALLOW;
-                  }
+                    try {
+                      final uri = Uri.parse(url);
 
-                  // 处理其他 scheme
-                  if (await canLaunchUrl(uri)) {
-                    await launchUrl(uri, mode: LaunchMode.externalApplication);
-                  }
-                } catch (e) {
-                  log("Error handling shouldOverrideUrlLoading: $e");
-                }
+                      // 处理 intent:// 链接
+                      if (url.startsWith("intent://")) {
+                        final parsedUrl = controller.parseIntentUrl(url);
+                        final fallbackUrl = controller.parseFallbackUrl(url);
 
-                return NavigationActionPolicy.CANCEL;
-              },
-        ),
-        Obx(
-          () => controller.isLoading.value
-              ? LinearProgressIndicator(
-                  value: controller.loadingProgress.value,
-                  backgroundColor: Get.reactiveTheme.scaffoldBackgroundColor,
-                  minHeight: 2.w,
-                  valueColor: AlwaysStoppedAnimation<Color>(
-                    Get.reactiveTheme.primaryColor,
-                  ),
-                )
-              : const SizedBox.shrink(),
+                        if (parsedUrl != null &&
+                            await canLaunchUrl(Uri.parse(parsedUrl))) {
+                          await launchUrl(
+                            Uri.parse(parsedUrl),
+                            mode: LaunchMode.externalApplication,
+                          );
+                        } else if (fallbackUrl != null &&
+                            await canLaunchUrl(Uri.parse(fallbackUrl))) {
+                          await launchUrl(
+                            Uri.parse(fallbackUrl),
+                            mode: LaunchMode.externalApplication,
+                          );
+                        }
+                        return NavigationActionPolicy.CANCEL;
+                      }
+                      // 处理下载链接
+                      else if (uri.path.endsWith(".apk") ||
+                          uri.path.endsWith(".pdf") ||
+                          uri.path.contains("download")) {
+                        if (await canLaunchUrl(uri)) {
+                          await launchUrl(
+                            uri,
+                            mode: LaunchMode.externalApplication,
+                          );
+                        }
+                        return NavigationActionPolicy.CANCEL;
+                      }
+
+                      // 处理 http/https 链接
+                      if (uri.scheme == 'http' || uri.scheme == 'https') {
+                        return NavigationActionPolicy.ALLOW;
+                      }
+
+                      // 处理其他 scheme
+                      if (await canLaunchUrl(uri)) {
+                        await launchUrl(
+                          uri,
+                          mode: LaunchMode.externalApplication,
+                        );
+                      }
+                    } catch (e) {
+                      log("Error handling shouldOverrideUrlLoading: $e");
+                    }
+
+                    return NavigationActionPolicy.CANCEL;
+                  },
+            ),
+            Obx(
+              () => controller.isLoading.value
+                  ? LinearProgressIndicator(
+                      value: controller.loadingProgress.value,
+                      backgroundColor:
+                          Get.reactiveTheme.scaffoldBackgroundColor,
+                      minHeight: 2.w,
+                      valueColor: AlwaysStoppedAnimation<Color>(
+                        Get.reactiveTheme.primaryColor,
+                      ),
+                    )
+                  : const SizedBox.shrink(),
+            ),
+          ],
         ),
-      ],
-    );
+      );
+    });
   }
 }

+ 2 - 0
lib/config/theme/dark_theme_colors.dart

@@ -95,4 +95,6 @@ class DarkThemeColors {
   );
   static const Color settingSecurityLinearGradientEndColor = Color(0xFFC9433C);
   static const Color validTermColor = Color(0xFFFFCC00);
+
+  static const Color deleteAccountIconColor = Color(0xFFEF0000);
 }

+ 5 - 1
lib/config/translations/ar_AR/ar_ar_translation.dart

@@ -277,7 +277,7 @@ final Map<String, String> arAR = {
   Strings.loginNow: ' تسجيل الدخول الآن',
 
   // Feedback
-  Strings.feedbackPlaceholder: 'نسخة إنجليزية للمرجع: صف مشكلتك أو اقتراحك...',
+  Strings.feedbackPlaceholder: 'صف مشكلتك أو اقتراحك...',
   Strings.emailAddressForReply: '• عنوان بريدك الإلكتروني (لردنا)',
   Strings.send: 'إرسال',
 
@@ -370,4 +370,8 @@ final Map<String, String> arAR = {
   Strings.confirmPasswordMustBeTheSame:
       'كلمتا المرور المُدخلتان غير متطابقتين',
   Strings.yes: 'نعم',
+
+  // Push Notifications
+  Strings.pushNotifications: 'الإشعارات الفورية',
+  Strings.upgradeNow: 'الترقية الآن',
 };

+ 5 - 1
lib/config/translations/de_DE/de_de_translation.dart

@@ -281,7 +281,7 @@ const Map<String, String> deDE = {
 
   // Feedback
   Strings.feedbackPlaceholder:
-      'Eine englische Version als Referenz: Beschreiben Sie Ihr Problem oder Ihren Vorschlag...',
+      'Beschreiben Sie Ihr Problem oder Ihren Vorschlag...',
   Strings.emailAddressForReply: '• Ihre E-Mail-Adresse (für unsere Antwort)',
   Strings.send: 'Senden',
 
@@ -375,4 +375,8 @@ const Map<String, String> deDE = {
   Strings.confirmPasswordMustBeTheSame:
       'Die zweimal eingegebenen Passwörter stimmen nicht überein',
   Strings.yes: 'Ja',
+
+  // Push Notifications
+  Strings.pushNotifications: 'Push-Benachrichtigungen',
+  Strings.upgradeNow: 'Jetzt upgraden',
 };

+ 5 - 2
lib/config/translations/en_US/en_us_translation.dart

@@ -282,8 +282,7 @@ Map<String, String> enUs = {
   Strings.loginNow: ' Login now',
 
   // Feedback
-  Strings.feedbackPlaceholder:
-      'An English version for reference: Describe your issue or suggestion...',
+  Strings.feedbackPlaceholder: 'Describe your issue or suggestion...',
   Strings.emailAddressForReply: '• Your email address (for our reply)',
   Strings.send: 'Send',
 
@@ -376,4 +375,8 @@ Map<String, String> enUs = {
   Strings.confirmPasswordMustBeTheSame:
       'The passwords entered twice are inconsistent',
   Strings.yes: 'Yes',
+
+  // Push Notifications
+  Strings.pushNotifications: 'Push Notifications',
+  Strings.upgradeNow: 'Upgrade Now',
 };

+ 5 - 2
lib/config/translations/es_ES/es_es_translation.dart

@@ -285,8 +285,7 @@ const Map<String, String> esEs = {
   Strings.loginNow: ' Inicia sesión ahora',
 
   // Feedback
-  Strings.feedbackPlaceholder:
-      'Una versión en inglés como referencia: Describe tu problema o sugerencia...',
+  Strings.feedbackPlaceholder: 'Describe tu problema o sugerencia...',
   Strings.emailAddressForReply:
       '• Tu dirección de correo (para nuestra respuesta)',
   Strings.send: 'Enviar',
@@ -381,4 +380,8 @@ const Map<String, String> esEs = {
   Strings.confirmPasswordMustBeTheSame:
       'Las contraseñas ingresadas dos veces no coinciden',
   Strings.yes: 'Sí',
+
+  // Push Notifications
+  Strings.pushNotifications: 'Notificaciones push',
+  Strings.upgradeNow: 'Actualizar ahora',
 };

+ 5 - 2
lib/config/translations/fa_IR/fa_ir_translation.dart

@@ -280,8 +280,7 @@ const Map<String, String> faIR = {
   Strings.loginNow: ' اکنون وارد شوید',
 
   // Feedback
-  Strings.feedbackPlaceholder:
-      'نسخه انگلیسی برای مرجع: مشکل یا پیشنهاد خود را توضیح دهید...',
+  Strings.feedbackPlaceholder: 'مشکل یا پیشنهاد خود را توضیح دهید...',
   Strings.emailAddressForReply: '• آدرس ایمیل شما (برای پاسخ ما)',
   Strings.send: 'ارسال',
 
@@ -377,4 +376,8 @@ const Map<String, String> faIR = {
   Strings.confirmPasswordMustBeTheSame:
       'رمزهای عبور وارد شده دو بار یکسان نیستند',
   Strings.yes: 'بله',
+
+  // Push Notifications
+  Strings.pushNotifications: 'اعلان‌های فوری',
+  Strings.upgradeNow: 'اکنون ارتقا دهید',
 };

+ 5 - 2
lib/config/translations/fr_FR/fr_fr_translation.dart

@@ -286,8 +286,7 @@ const Map<String, String> frFR = {
   Strings.loginNow: ' Connectez-vous maintenant',
 
   // Feedback
-  Strings.feedbackPlaceholder:
-      'Une version anglaise pour référence : Décrivez votre problème ou suggestion...',
+  Strings.feedbackPlaceholder: 'Décrivez votre problème ou suggestion...',
   Strings.emailAddressForReply: '• Votre adresse e-mail (pour notre réponse)',
   Strings.send: 'Envoyer',
 
@@ -382,4 +381,8 @@ const Map<String, String> frFR = {
   Strings.confirmPasswordMustBeTheSame:
       'Les mots de passe saisis deux fois ne correspondent pas',
   Strings.yes: 'Oui',
+
+  // Push Notifications
+  Strings.pushNotifications: 'Notifications push',
+  Strings.upgradeNow: 'Mettre à niveau maintenant',
 };

+ 5 - 1
lib/config/translations/ja_JP/ja_jp_translation.dart

@@ -265,7 +265,7 @@ const Map<String, String> jaJP = {
   Strings.loginNow: ' 今すぐログイン',
 
   // Feedback
-  Strings.feedbackPlaceholder: '参考用の英語版:問題や提案を説明してください...',
+  Strings.feedbackPlaceholder: '問題や提案を説明してください...',
   Strings.emailAddressForReply: '• メールアドレス(返信用)',
   Strings.send: '送信',
 
@@ -355,4 +355,8 @@ const Map<String, String> jaJP = {
   Strings.confirmPasswordMustBeTheSame:
       '2回入力されたパスワードが一致しません',
   Strings.yes: 'はい',
+
+  // Push Notifications
+  Strings.pushNotifications: 'プッシュ通知',
+  Strings.upgradeNow: '今すぐアップグレード',
 };

+ 5 - 1
lib/config/translations/ko_KR/ko_kr_translation.dart

@@ -259,7 +259,7 @@ const Map<String, String> koKR = {
   Strings.loginNow: ' 지금 로그인',
 
   // Feedback
-  Strings.feedbackPlaceholder: '참고용 영어 버전: 문제나 제안을 설명해주세요...',
+  Strings.feedbackPlaceholder: '문제나 제안을 설명해주세요...',
   Strings.emailAddressForReply: '• 이메일 주소 (답변용)',
   Strings.send: '보내기',
 
@@ -348,4 +348,8 @@ const Map<String, String> koKR = {
   Strings.confirmPasswordMustBeTheSame:
       '두 번 입력한 비밀번호가 일치하지 않습니다',
   Strings.yes: '예',
+
+  // Push Notifications
+  Strings.pushNotifications: '푸시 알림',
+  Strings.upgradeNow: '지금 업그레이드',
 };

+ 5 - 2
lib/config/translations/my_MM/my_mm_translation.dart

@@ -288,8 +288,7 @@ const Map<String, String> myMM = {
   Strings.loginNow: ' ယခုအကောင့်ဝင်ပါ',
 
   // Feedback
-  Strings.feedbackPlaceholder:
-      'ရည်ညွှန်းအတွက် အင်္ဂလိပ်ဗားရှင်း: သင့်ပြဿနာ သို့မဟုတ် အကြံပြုချက်ကို ဖော်ပြပါ...',
+  Strings.feedbackPlaceholder: 'သင့်ပြဿနာ သို့မဟုတ် အကြံပြုချက်ကို ဖော်ပြပါ...',
   Strings.emailAddressForReply:
       '• သင့်အီးမေးလ်လိပ်စာ (ကျွန်ုပ်တို့၏ပြန်လည်ဖြေကြားမှုအတွက်)',
   Strings.send: 'ပို့ပါ',
@@ -385,4 +384,8 @@ const Map<String, String> myMM = {
   Strings.confirmPasswordMustBeTheSame:
       'နှစ်ကြိမ် ထည့်သွင်းသော စကားဝှက်များ မတူညီပါ',
   Strings.yes: 'ဟုတ်ကဲ့',
+
+  // Push Notifications
+  Strings.pushNotifications: 'Push အကြောင်းကြားချက်များ',
+  Strings.upgradeNow: 'ယခုပင် အဆင့်မြှင့်ပါ',
 };

+ 5 - 2
lib/config/translations/ru_RU/ru_ru_translation.dart

@@ -283,8 +283,7 @@ const Map<String, String> ruRU = {
   Strings.loginNow: ' Войдите сейчас',
 
   // Feedback
-  Strings.feedbackPlaceholder:
-      'Английская версия для справки: Опишите вашу проблему или предложение...',
+  Strings.feedbackPlaceholder: 'Опишите вашу проблему или предложение...',
   Strings.emailAddressForReply: '• Ваш адрес email (для нашего ответа)',
   Strings.send: 'Отправить',
 
@@ -381,4 +380,8 @@ const Map<String, String> ruRU = {
   Strings.confirmPasswordMustBeTheSame:
       'Введённые дважды пароли не совпадают',
   Strings.yes: 'Да',
+
+  // Push Notifications
+  Strings.pushNotifications: 'Push-уведомления',
+  Strings.upgradeNow: 'Обновить сейчас',
 };

+ 4 - 1
lib/config/translations/strings_enum.dart

@@ -304,7 +304,7 @@ class Strings {
 
   // Feedback
   static const String feedbackPlaceholder =
-      'An English version for reference: Describe your issue or suggestion...';
+      'Describe your issue or suggestion...';
   static const String emailAddressForReply =
       '• Your email address (for our reply)';
   static const String send = 'Send';
@@ -420,4 +420,7 @@ class Strings {
   static const String deleteAccountConfirmMessage =
       'Deleting your account will permanently remove your data and membership information. This action cannot be undone.';
   static const String deleteAccountConfirmButton = 'Delete';
+
+  static const String pushNotifications = 'Push Notifications';
+  static const String upgradeNow = 'Upgrade Now';
 }

+ 232 - 0
lib/utils/awesome_notifications_helper.dart

@@ -0,0 +1,232 @@
+import 'package:awesome_notifications/awesome_notifications.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+import '../app/routes/app_pages.dart';
+import 'log/logger.dart';
+
+class AwesomeNotificationsHelper {
+  // prevent making instance
+  AwesomeNotificationsHelper._();
+
+  static const String TAG = 'AwesomeNotificationsHelper';
+
+  // Notification lib
+  static AwesomeNotifications awesomeNotifications = AwesomeNotifications();
+
+  // 标记是否已经请求过权限
+  static bool _hasRequestedPermission = false;
+
+  /// initialize local notifications service, create channels and groups
+  /// setup notifications button actions handlers
+  static init() async {
+    try {
+      // 初始化通知渠道和组
+      await _initNotification();
+
+      // 设置监听
+      listenToActionButtons();
+
+      // 检查权限但不立即请求
+      await checkNotificationPermission();
+    } catch (e) {
+      log(TAG, 'Notification initialization failed: $e');
+    }
+  }
+
+  /// 检查通知权限状态
+  static Future<bool> checkNotificationPermission() async {
+    return await awesomeNotifications.isNotificationAllowed();
+  }
+
+  /// 打开通知权限设置
+  static Future<void> showNotificationConfigPage() async {
+    return await awesomeNotifications.showNotificationConfigPage();
+  }
+
+  /// 请求通知权限(可以被Firebase或其他组件调用)
+  static Future<bool> requestNotificationPermission() async {
+    // 如果已经请求过权限,先检查当前状态
+    if (_hasRequestedPermission) {
+      final isAllowed = await checkNotificationPermission();
+      if (isAllowed) return true;
+    }
+
+    // 请求权限
+    final isAllowed = await awesomeNotifications
+        .requestPermissionToSendNotifications();
+    // 标记已请求过权限
+    _hasRequestedPermission = true;
+
+    return isAllowed;
+  }
+
+  /// when user click on notification or click on button on the notification
+  static listenToActionButtons() {
+    // Only after at least the action method is set, the notification events are delivered
+    awesomeNotifications.setListeners(
+      onActionReceivedMethod: NotificationController.onActionReceivedMethod,
+      onNotificationCreatedMethod:
+          NotificationController.onNotificationCreatedMethod,
+      onNotificationDisplayedMethod:
+          NotificationController.onNotificationDisplayedMethod,
+      onDismissActionReceivedMethod:
+          NotificationController.onDismissActionReceivedMethod,
+    );
+  }
+
+  ///init notifications channels
+  static _initNotification() async {
+    await awesomeNotifications.initialize(
+      'resource://mipmap/launcher_icon', // 添加小图标资源路径,
+      [
+        NotificationChannel(
+          channelGroupKey: NotificationChannels.generalChannelGroupKey,
+          channelKey: NotificationChannels.generalChannelKey,
+          channelName: NotificationChannels.generalChannelName,
+          groupKey: NotificationChannels.generalGroupKey,
+          channelDescription: NotificationChannels.generalChannelDescription,
+          defaultColor: Colors.green,
+          ledColor: Colors.white,
+          channelShowBadge: true,
+          playSound: true,
+          importance: NotificationImportance.Max,
+        ),
+        NotificationChannel(
+          channelGroupKey: NotificationChannels.chatChannelGroupKey,
+          channelKey: NotificationChannels.chatChannelKey,
+          channelName: NotificationChannels.chatChannelName,
+          groupKey: NotificationChannels.chatGroupKey,
+          channelDescription: NotificationChannels.chatChannelDescription,
+          defaultColor: Colors.green,
+          ledColor: Colors.white,
+          channelShowBadge: true,
+          playSound: true,
+          importance: NotificationImportance.Max,
+        ),
+      ],
+      channelGroups: [
+        NotificationChannelGroup(
+          channelGroupKey: NotificationChannels.generalChannelGroupKey,
+          channelGroupName: NotificationChannels.generalChannelGroupName,
+        ),
+        NotificationChannelGroup(
+          channelGroupKey: NotificationChannels.chatChannelGroupKey,
+          channelGroupName: NotificationChannels.chatChannelGroupName,
+        ),
+      ],
+    );
+  }
+
+  //display notification for user with sound
+  static Future<bool> showNotification({
+    required String title,
+    required String body,
+    required int id,
+    String? channelKey,
+    String? groupKey,
+    NotificationLayout? notificationLayout,
+    String? summary,
+    List<NotificationActionButton>? actionButtons,
+    Map<String, String>? payload,
+    String? largeIcon,
+  }) async {
+    try {
+      final isAllowed = await checkNotificationPermission();
+      if (!isAllowed) {
+        final requested = await requestNotificationPermission();
+        if (!requested) return false;
+      }
+
+      await awesomeNotifications.createNotification(
+        content: NotificationContent(
+          id: id,
+          title: title,
+          body: body,
+          groupKey: groupKey ?? NotificationChannels.generalGroupKey,
+          channelKey: channelKey ?? NotificationChannels.generalChannelKey,
+          showWhen: true,
+          payload: payload,
+          notificationLayout: notificationLayout ?? NotificationLayout.Default,
+          autoDismissible: true,
+          summary: summary,
+          largeIcon: largeIcon,
+        ),
+        actionButtons: actionButtons,
+      );
+      return true;
+    } catch (e) {
+      log(TAG, 'Sending notification failed: $e');
+      return false;
+    }
+  }
+}
+
+class NotificationController {
+  /// Use this method to detect when a new notification or a schedule is created
+  @pragma("vm:entry-point")
+  static Future<void> onNotificationCreatedMethod(
+    ReceivedNotification receivedNotification,
+  ) async {
+    // Your code goes here
+  }
+
+  /// Use this method to detect every time that a new notification is displayed
+  @pragma("vm:entry-point")
+  static Future<void> onNotificationDisplayedMethod(
+    ReceivedNotification receivedNotification,
+  ) async {
+    // Your code goes here
+  }
+
+  /// Use this method to detect if the user dismissed a notification
+  @pragma("vm:entry-point")
+  static Future<void> onDismissActionReceivedMethod(
+    ReceivedAction receivedAction,
+  ) async {
+    // Your code goes here
+  }
+
+  /// Use this method to detect when the user taps on a notification or action button
+  @pragma("vm:entry-point")
+  static Future<void> onActionReceivedMethod(
+    ReceivedAction receivedAction,
+  ) async {
+    try {
+      final payload = receivedAction.payload;
+      if (payload != null) {
+        final route = payload['route'];
+        if (route != null) {
+          Get.key.currentState?.pushNamed(route);
+        } else {
+          Get.key.currentState?.pushNamed(Routes.HOME);
+        }
+      }
+    } catch (e) {
+      log(
+        "onActionReceivedMethod",
+        'Handling notification click event failed: $e',
+      );
+    }
+  }
+}
+
+class NotificationChannels {
+  // chat channel (for messages only)
+  static String get chatChannelKey => "chat_channel";
+  static String get chatChannelName => "Chat Notifications";
+  static String get chatGroupKey => "chat_group_key";
+  static String get chatChannelGroupKey => "chat_channel_group";
+  static String get chatChannelGroupName => "Chat Notifications";
+  static String get chatChannelDescription =>
+      "Receive chat and message notifications";
+
+  // general channel (for all other notifications)
+  static String get generalChannelKey => "general_channel";
+  static String get generalGroupKey => "general_group_key";
+  static String get generalChannelGroupKey => "general_channel_group";
+  static String get generalChannelGroupName => "General Notifications";
+  static String get generalChannelName => "General Notifications";
+  static String get generalChannelDescription =>
+      "Receive general app notifications";
+}

+ 13 - 0
lib/utils/system_helper.dart

@@ -171,4 +171,17 @@ class SystemHelper {
       );
     }
   }
+
+  // 打开 JS Bridge 测试页面
+  static void openTestPage() {
+    Get.toNamed(
+      Routes.WEB,
+      arguments: {
+        // 使用 assets/ 前缀,WebView 会自动使用 loadData 加载
+        'url': 'assets/test_jsbridge.html',
+        'title': 'JS Bridge 测试',
+        'fullScreen': true,
+      },
+    );
+  }
 }

+ 4 - 0
linux/flutter/generated_plugin_registrant.cc

@@ -6,11 +6,15 @@
 
 #include "generated_plugin_registrant.h"
 
+#include <awesome_notifications/awesome_notifications_plugin.h>
 #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
 #include <gtk/gtk_plugin.h>
 #include <url_launcher_linux/url_launcher_plugin.h>
 
 void fl_register_plugins(FlPluginRegistry* registry) {
+  g_autoptr(FlPluginRegistrar) awesome_notifications_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "AwesomeNotificationsPlugin");
+  awesome_notifications_plugin_register_with_registrar(awesome_notifications_registrar);
   g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
       fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
   flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);

+ 1 - 0
linux/flutter/generated_plugins.cmake

@@ -3,6 +3,7 @@
 #
 
 list(APPEND FLUTTER_PLUGIN_LIST
+  awesome_notifications
   flutter_secure_storage_linux
   gtk
   url_launcher_linux

+ 2 - 0
macos/Flutter/GeneratedPluginRegistrant.swift

@@ -6,6 +6,7 @@ import FlutterMacOS
 import Foundation
 
 import app_links
+import awesome_notifications
 import connectivity_plus
 import device_info_plus
 import flutter_inappwebview_macos
@@ -22,6 +23,7 @@ import video_player_avfoundation
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
+  AwesomeNotificationsPlugin.register(with: registry.registrar(forPlugin: "AwesomeNotificationsPlugin"))
   ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
   DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
   InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))

+ 16 - 0
pubspec.lock

@@ -113,6 +113,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.13.0"
+  awesome_notifications:
+    dependency: "direct main"
+    description:
+      name: awesome_notifications
+      sha256: "0d5fa4457f2ba4e536adc3ef6af709cdcecf4a05a1f3035981e9afa2f899b2a8"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.10.1"
   boolean_selector:
     dependency: transitive
     description:
@@ -816,6 +824,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.4.6+1"
+  intl:
+    dependency: transitive
+    description:
+      name: intl
+      sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.20.2"
   io:
     dependency: transitive
     description:

+ 1 - 0
pubspec.yaml

@@ -83,6 +83,7 @@ dependencies:
   in_app_purchase: ^3.2.3 # 内购
   carousel_slider: ^5.1.1 # 轮播图
   pull_to_refresh_flutter3: ^2.0.2 # 下拉刷新
+  awesome_notifications: ^0.10.1 # 通知
 
 dev_dependencies:
   flutter_test:

+ 3 - 0
windows/flutter/generated_plugin_registrant.cc

@@ -7,6 +7,7 @@
 #include "generated_plugin_registrant.h"
 
 #include <app_links/app_links_plugin_c_api.h>
+#include <awesome_notifications/awesome_notifications_plugin_c_api.h>
 #include <connectivity_plus/connectivity_plus_windows_plugin.h>
 #include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
 #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
@@ -17,6 +18,8 @@
 void RegisterPlugins(flutter::PluginRegistry* registry) {
   AppLinksPluginCApiRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
+  AwesomeNotificationsPluginCApiRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("AwesomeNotificationsPluginCApi"));
   ConnectivityPlusWindowsPluginRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
   FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(

+ 1 - 0
windows/flutter/generated_plugins.cmake

@@ -4,6 +4,7 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   app_links
+  awesome_notifications
   connectivity_plus
   flutter_inappwebview_windows
   flutter_secure_storage_windows

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio