Explorar o código

feat: movie功能、更新功能、过期会员显示

lilu hai 1 mes
pai
achega
6dedd1cb01

BIN=BIN
assets/images/premium_expired.png


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

@@ -61,6 +61,7 @@ class Assets {
   static const String connectionNetworkError = 'assets/images/network.png';
 
   static const String premium = 'assets/images/premium.png';
+  static const String premiumExpired = 'assets/images/premium_expired.png';
   static const String test = 'assets/images/test.png';
   static const String free = 'assets/images/free.png';
 

+ 6 - 0
lib/app/constants/sp_keys.dart

@@ -77,4 +77,10 @@ class SPKeys {
   /// 获取指定位置的 banner 缓存键
   static String bannerCacheKey(String position) =>
       '$bannerCachePrefix$position';
+
+  /// 上次更新提醒时间(毫秒时间戳)
+  static const String lastUpgradeNoticeTime = 'last_upgrade_notice_time';
+
+  /// 上次更新提醒的版本号
+  static const String lastUpgradeNoticeVersion = 'last_upgrade_notice_version';
 }

+ 99 - 2
lib/app/controllers/api_controller.dart

@@ -13,10 +13,12 @@ import 'package:nomo/app/constants/sp_keys.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:play_install_referrer/play_install_referrer.dart';
+import 'package:system_clock/system_clock.dart';
 
 import '../../config/translations/localization_service.dart';
 import '../../config/translations/strings_enum.dart';
 import '../data/models/banner/banner_list.dart';
+import '../data/models/launch/upgrade.dart';
 import 'base_core_api.dart';
 import '../../utils/api_statistics.dart';
 import '../../utils/device_manager.dart';
@@ -99,6 +101,9 @@ class ApiController extends GetxService with WidgetsBindingObserver {
   // 是否在后台
   bool isBackground = false;
 
+  // 切换到后台时的系统时钟时间戳(毫秒,不受系统时间修改影响)
+  int _backgroundElapsedRealtime = 0;
+
   @override
   void onInit() {
     super.onInit();
@@ -116,6 +121,8 @@ class ApiController extends GetxService with WidgetsBindingObserver {
     log(TAG, "App state: $state");
     if (state == AppLifecycleState.paused) {
       isBackground = true;
+      // 记录切换到后台的系统时钟时间戳(不受系统时间修改影响)
+      _backgroundElapsedRealtime = SystemClock.elapsedRealtime().inMilliseconds;
       ApiStatistics.instance.onAppPaused();
       stopRemainTimeCountdown();
     } else if (state == AppLifecycleState.resumed) {
@@ -123,11 +130,40 @@ class ApiController extends GetxService with WidgetsBindingObserver {
         isBackground = false;
         asyncHandleLaunch(isRefreshLaunch: true);
         ApiStatistics.instance.onAppResumed();
-        updateRemainTime(IXSP.getUser()?.remainTime ?? 0);
+        // 计算后台经过的时间,继续倒计时
+        _resumeRemainTimeCountdown();
       }
     }
   }
 
+  /// 恢复倒计时(从后台切换到前台时调用)
+  void _resumeRemainTimeCountdown() {
+    if (_backgroundElapsedRealtime > 0 && _remainTimeSeconds > 0) {
+      // 计算后台经过的秒数(使用系统时钟,不受系统时间修改影响)
+      final currentElapsedRealtime =
+          SystemClock.elapsedRealtime().inMilliseconds;
+      final elapsedSeconds =
+          (currentElapsedRealtime - _backgroundElapsedRealtime) ~/ 1000;
+      // 更新剩余时间
+      final newRemainTime = _remainTimeSeconds - elapsedSeconds;
+      log(
+        TAG,
+        'Resume countdown: elapsed=${elapsedSeconds}s, '
+        'old=$_remainTimeSeconds, new=$newRemainTime',
+      );
+      // 重新启动倒计时
+      if (newRemainTime > 0) {
+        startRemainTimeCountdown(newRemainTime);
+      } else {
+        // 时间已耗尽
+        _remainTimeSeconds = 0;
+        _updateRemainTimeFormatted();
+        _onRemainTimeExpired();
+      }
+    }
+    _backgroundElapsedRealtime = 0;
+  }
+
   Future<Fingerprint> initFingerprint() async {
     // 读取app发布渠道
     if (Platform.isIOS) {
@@ -507,6 +543,11 @@ class ApiController extends GetxService with WidgetsBindingObserver {
   Future<bool> checkUpdate({bool isClickCheck = false}) async {
     try {
       final upgrade = IXSP.getUpgrade();
+      // 如果当前versionCode等于升级的versionCode,则没有更新
+      if (upgrade?.versionCode == fp.appVersionCode) {
+        return false;
+      }
+
       var hasUpdate = false;
       var hasForceUpdate = false;
 
@@ -520,7 +561,47 @@ class ApiController extends GetxService with WidgetsBindingObserver {
       }
 
       if (hasUpdate) {
-        AllDialog.showUpdate(hasForceUpdate: hasForceUpdate);
+        // 强制更新或用户主动点击检查时,直接显示更新弹窗
+        if (hasForceUpdate || isClickCheck) {
+          _showUpgradeDialog(upgrade!, hasForceUpdate);
+          return hasUpdate;
+        }
+
+        // 检查版本是否变化(新版本则重置时间逻辑)
+        final currentVersion = upgrade?.versionCode?.toString() ?? '';
+        final lastNoticeVersion =
+            IXSP.getString(SPKeys.lastUpgradeNoticeVersion) ?? '';
+        final isNewVersion = currentVersion != lastNoticeVersion;
+
+        if (isNewVersion) {
+          // 新版本,直接显示弹窗
+          log(
+            TAG,
+            'New version detected: $currentVersion (last: $lastNoticeVersion)',
+          );
+          _showUpgradeDialog(upgrade!, hasForceUpdate);
+          return hasUpdate;
+        }
+
+        // 检查是否超过 upgradeNoticeTime 分钟
+        final appConfig = IXSP.getAppConfig();
+        final upgradeNoticeMinutes =
+            appConfig?.upgradeNoticeTime ?? 1440; // 默认1天
+        final lastNoticeTimeStr =
+            IXSP.getString(SPKeys.lastUpgradeNoticeTime) ?? '0';
+        final lastNoticeTime = int.tryParse(lastNoticeTimeStr) ?? 0;
+        final now = DateTime.now().millisecondsSinceEpoch;
+        final elapsedMinutes = (now - lastNoticeTime) / (1000 * 60);
+
+        if (elapsedMinutes >= upgradeNoticeMinutes) {
+          _showUpgradeDialog(upgrade!, hasForceUpdate);
+        } else {
+          log(
+            TAG,
+            'Upgrade notice skipped: ${elapsedMinutes.toStringAsFixed(1)}min '
+            'elapsed, need ${upgradeNoticeMinutes}min',
+          );
+        }
       }
       return hasUpdate;
     } catch (e) {
@@ -529,6 +610,21 @@ class ApiController extends GetxService with WidgetsBindingObserver {
     return false;
   }
 
+  /// 显示更新弹窗并记录时间和版本
+  void _showUpgradeDialog(Upgrade upgrade, bool hasForceUpdate) {
+    AllDialog.showUpdate(upgrade, hasForceUpdate: hasForceUpdate);
+    // 记录本次提醒时间
+    IXSP.setString(
+      SPKeys.lastUpgradeNoticeTime,
+      DateTime.now().millisecondsSinceEpoch.toString(),
+    );
+    // 记录本次提醒的版本号
+    IXSP.setString(
+      SPKeys.lastUpgradeNoticeVersion,
+      upgrade.versionCode?.toString() ?? '',
+    );
+  }
+
   Future<Launch> getDispatchInfo(
     int locationId,
     String locationCode, {
@@ -1190,6 +1286,7 @@ class ApiController extends GetxService with WidgetsBindingObserver {
     // - 显示续费提示
     // - 断开VPN连接
     // - 刷新用户信息
+    refreshLaunch();
   }
 
   /// 格式化剩余时间显示

+ 1 - 0
lib/app/data/models/launch/app_config.dart

@@ -50,6 +50,7 @@ class AppConfig with _$AppConfig {
     int? reportActiveInterval,
     int? serverTime,
     int? vipRemainNotice,
+    int? upgradeNoticeTime,
     SmartGeo? smartGeo,
   }) = _AppConfig;
 

+ 24 - 1
lib/app/data/models/launch/app_config.freezed.dart

@@ -63,6 +63,7 @@ mixin _$AppConfig {
   int? get reportActiveInterval => throw _privateConstructorUsedError;
   int? get serverTime => throw _privateConstructorUsedError;
   int? get vipRemainNotice => throw _privateConstructorUsedError;
+  int? get upgradeNoticeTime => throw _privateConstructorUsedError;
   SmartGeo? get smartGeo => throw _privateConstructorUsedError;
 
   /// Serializes this AppConfig to a JSON map.
@@ -121,6 +122,7 @@ abstract class $AppConfigCopyWith<$Res> {
     int? reportActiveInterval,
     int? serverTime,
     int? vipRemainNotice,
+    int? upgradeNoticeTime,
     SmartGeo? smartGeo,
   });
 
@@ -182,6 +184,7 @@ class _$AppConfigCopyWithImpl<$Res, $Val extends AppConfig>
     Object? reportActiveInterval = freezed,
     Object? serverTime = freezed,
     Object? vipRemainNotice = freezed,
+    Object? upgradeNoticeTime = freezed,
     Object? smartGeo = freezed,
   }) {
     return _then(
@@ -346,6 +349,10 @@ class _$AppConfigCopyWithImpl<$Res, $Val extends AppConfig>
                 ? _value.vipRemainNotice
                 : vipRemainNotice // ignore: cast_nullable_to_non_nullable
                       as int?,
+            upgradeNoticeTime: freezed == upgradeNoticeTime
+                ? _value.upgradeNoticeTime
+                : upgradeNoticeTime // ignore: cast_nullable_to_non_nullable
+                      as int?,
             smartGeo: freezed == smartGeo
                 ? _value.smartGeo
                 : smartGeo // ignore: cast_nullable_to_non_nullable
@@ -420,6 +427,7 @@ abstract class _$$AppConfigImplCopyWith<$Res>
     int? reportActiveInterval,
     int? serverTime,
     int? vipRemainNotice,
+    int? upgradeNoticeTime,
     SmartGeo? smartGeo,
   });
 
@@ -481,6 +489,7 @@ class __$$AppConfigImplCopyWithImpl<$Res>
     Object? reportActiveInterval = freezed,
     Object? serverTime = freezed,
     Object? vipRemainNotice = freezed,
+    Object? upgradeNoticeTime = freezed,
     Object? smartGeo = freezed,
   }) {
     return _then(
@@ -645,6 +654,10 @@ class __$$AppConfigImplCopyWithImpl<$Res>
             ? _value.vipRemainNotice
             : vipRemainNotice // ignore: cast_nullable_to_non_nullable
                   as int?,
+        upgradeNoticeTime: freezed == upgradeNoticeTime
+            ? _value.upgradeNoticeTime
+            : upgradeNoticeTime // ignore: cast_nullable_to_non_nullable
+                  as int?,
         smartGeo: freezed == smartGeo
             ? _value.smartGeo
             : smartGeo // ignore: cast_nullable_to_non_nullable
@@ -698,6 +711,7 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
     this.reportActiveInterval,
     this.serverTime,
     this.vipRemainNotice,
+    this.upgradeNoticeTime,
     this.smartGeo,
   }) : _apiIps = apiIps,
        _apiUrls = apiUrls,
@@ -963,11 +977,13 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
   @override
   final int? vipRemainNotice;
   @override
+  final int? upgradeNoticeTime;
+  @override
   final SmartGeo? smartGeo;
 
   @override
   String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
-    return 'AppConfig(apiIps: $apiIps, apiUrls: $apiUrls, routerApiUrls: $routerApiUrls, appStatUrls: $appStatUrls, assetUrls: $assetUrls, logFileUploadUrls: $logFileUploadUrls, realityApiUrls: $realityApiUrls, realityAppStatUrls: $realityAppStatUrls, realityLogFileUploadUrls: $realityLogFileUploadUrls, autoPing: $autoPing, autoPingInterval: $autoPingInterval, backupApiUrls: $backupApiUrls, cacheDataEffectiveDays: $cacheDataEffectiveDays, contacts: $contacts, follows: $follows, providers: $providers, signalThresholds: $signalThresholds, packetLossThresholds: $packetLossThresholds, skipGeo: $skipGeo, speedTestTargetList: $speedTestTargetList, websiteUrl: $websiteUrl, visitorDisabled: $visitorDisabled, privacyAgreement: $privacyAgreement, email: $email, blackPkgs: $blackPkgs, whitePkgs: $whitePkgs, accelerationSampleCount: $accelerationSampleCount, minAccelerationSampleCount: $minAccelerationSampleCount, connectionWarningThreshold: $connectionWarningThreshold, connectiveTestUrl: $connectiveTestUrl, disabledLogModules: $disabledLogModules, realTimeDbPath: $realTimeDbPath, boostTimeInterval: $boostTimeInterval, pingDisplayMode: $pingDisplayMode, peekTimeInterval: $peekTimeInterval, enableAd: $enableAd, adTimeoutDuration: $adTimeoutDuration, reportActiveInterval: $reportActiveInterval, serverTime: $serverTime, vipRemainNotice: $vipRemainNotice, smartGeo: $smartGeo)';
+    return 'AppConfig(apiIps: $apiIps, apiUrls: $apiUrls, routerApiUrls: $routerApiUrls, appStatUrls: $appStatUrls, assetUrls: $assetUrls, logFileUploadUrls: $logFileUploadUrls, realityApiUrls: $realityApiUrls, realityAppStatUrls: $realityAppStatUrls, realityLogFileUploadUrls: $realityLogFileUploadUrls, autoPing: $autoPing, autoPingInterval: $autoPingInterval, backupApiUrls: $backupApiUrls, cacheDataEffectiveDays: $cacheDataEffectiveDays, contacts: $contacts, follows: $follows, providers: $providers, signalThresholds: $signalThresholds, packetLossThresholds: $packetLossThresholds, skipGeo: $skipGeo, speedTestTargetList: $speedTestTargetList, websiteUrl: $websiteUrl, visitorDisabled: $visitorDisabled, privacyAgreement: $privacyAgreement, email: $email, blackPkgs: $blackPkgs, whitePkgs: $whitePkgs, accelerationSampleCount: $accelerationSampleCount, minAccelerationSampleCount: $minAccelerationSampleCount, connectionWarningThreshold: $connectionWarningThreshold, connectiveTestUrl: $connectiveTestUrl, disabledLogModules: $disabledLogModules, realTimeDbPath: $realTimeDbPath, boostTimeInterval: $boostTimeInterval, pingDisplayMode: $pingDisplayMode, peekTimeInterval: $peekTimeInterval, enableAd: $enableAd, adTimeoutDuration: $adTimeoutDuration, reportActiveInterval: $reportActiveInterval, serverTime: $serverTime, vipRemainNotice: $vipRemainNotice, upgradeNoticeTime: $upgradeNoticeTime, smartGeo: $smartGeo)';
   }
 
   @override
@@ -1034,6 +1050,7 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
       ..add(DiagnosticsProperty('reportActiveInterval', reportActiveInterval))
       ..add(DiagnosticsProperty('serverTime', serverTime))
       ..add(DiagnosticsProperty('vipRemainNotice', vipRemainNotice))
+      ..add(DiagnosticsProperty('upgradeNoticeTime', upgradeNoticeTime))
       ..add(DiagnosticsProperty('smartGeo', smartGeo));
   }
 
@@ -1157,6 +1174,8 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
                 other.serverTime == serverTime) &&
             (identical(other.vipRemainNotice, vipRemainNotice) ||
                 other.vipRemainNotice == vipRemainNotice) &&
+            (identical(other.upgradeNoticeTime, upgradeNoticeTime) ||
+                other.upgradeNoticeTime == upgradeNoticeTime) &&
             (identical(other.smartGeo, smartGeo) ||
                 other.smartGeo == smartGeo));
   }
@@ -1205,6 +1224,7 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
     reportActiveInterval,
     serverTime,
     vipRemainNotice,
+    upgradeNoticeTime,
     smartGeo,
   ]);
 
@@ -1264,6 +1284,7 @@ abstract class _AppConfig implements AppConfig {
     final int? reportActiveInterval,
     final int? serverTime,
     final int? vipRemainNotice,
+    final int? upgradeNoticeTime,
     final SmartGeo? smartGeo,
   }) = _$AppConfigImpl;
 
@@ -1351,6 +1372,8 @@ abstract class _AppConfig implements AppConfig {
   @override
   int? get vipRemainNotice;
   @override
+  int? get upgradeNoticeTime;
+  @override
   SmartGeo? get smartGeo;
 
   /// Create a copy of AppConfig

+ 2 - 0
lib/app/data/models/launch/app_config.g.dart

@@ -91,6 +91,7 @@ _$AppConfigImpl _$$AppConfigImplFromJson(
   reportActiveInterval: (json['reportActiveInterval'] as num?)?.toInt(),
   serverTime: (json['serverTime'] as num?)?.toInt(),
   vipRemainNotice: (json['vipRemainNotice'] as num?)?.toInt(),
+  upgradeNoticeTime: (json['upgradeNoticeTime'] as num?)?.toInt(),
   smartGeo: json['smartGeo'] == null
       ? null
       : SmartGeo.fromJson(json['smartGeo'] as Map<String, dynamic>),
@@ -138,6 +139,7 @@ Map<String, dynamic> _$$AppConfigImplToJson(_$AppConfigImpl instance) =>
       'reportActiveInterval': instance.reportActiveInterval,
       'serverTime': instance.serverTime,
       'vipRemainNotice': instance.vipRemainNotice,
+      'upgradeNoticeTime': instance.upgradeNoticeTime,
       'smartGeo': instance.smartGeo,
     };
 

+ 9 - 3
lib/app/dialog/all_dialog.dart

@@ -4,7 +4,9 @@ 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 '../../utils/system_helper.dart';
 import '../constants/iconfont/iconfont.dart';
+import '../data/models/launch/upgrade.dart';
 import 'custom_dialog.dart';
 
 /// 弹窗使用示例
@@ -163,11 +165,10 @@ class AllDialog {
   }
 
   /// 显示更新弹窗
-  static void showUpdate({bool hasForceUpdate = false}) {
+  static void showUpdate(Upgrade upgrade, {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.",
+      message: upgrade.info ?? '',
       buttonText: Strings.upgradeNow.tr,
       cancelText: hasForceUpdate ? null : Strings.cancel.tr,
       onCancel: () {
@@ -176,6 +177,11 @@ class AllDialog {
       svgPath: Assets.update,
       onPressed: () {
         Navigator.of(Get.context!).pop();
+        if (upgrade.appStoreUrl?.isNotEmpty ?? false) {
+          SystemHelper.openGooglePlayUrl(upgrade.appStoreUrl ?? '');
+        } else if (upgrade.websiteUrl?.isNotEmpty ?? false) {
+          SystemHelper.openGooglePlayUrl(upgrade.websiteUrl ?? '');
+        }
       },
     );
   }

+ 6 - 2
lib/app/modules/account/views/account_view.dart

@@ -352,7 +352,9 @@ class AccountView extends BaseView<AccountController> {
                 children: [
                   IXImage(
                     source: controller.apiController.userLevel == 3
-                        ? Assets.premium
+                        ? controller.apiController.remainTimeSeconds > 0
+                              ? Assets.premium
+                              : Assets.premiumExpired
                         : controller.apiController.userLevel == 9999
                         ? Assets.test
                         : Assets.free,
@@ -432,7 +434,9 @@ class AccountView extends BaseView<AccountController> {
                 iconColor: Get.reactiveTheme.shadowColor,
                 title: Strings.validTerm.tr,
                 trailing: Text(
-                  controller.apiController.validTermText,
+                  controller.apiController.remainTimeSeconds > 0
+                      ? controller.apiController.validTermText
+                      : Strings.expired.tr,
                   style: TextStyle(
                     fontSize: 13.sp,
                     color: Get.reactiveTheme.primaryColor,

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

@@ -242,7 +242,7 @@ class HomeController extends BaseController {
 
   void onRefresh() async {
     try {
-      await apiController.launch();
+      await apiController.refreshLaunch();
       getBanner(position: 'nine', isCache: false);
       getBanner(position: 'banner', isCache: false);
       refreshController.refreshCompleted();
@@ -351,7 +351,10 @@ class HomeController extends BaseController {
       case BannerAction.page:
         if (banner?.data != null) {
           final uri = Uri.parse(banner!.data!);
-          Get.toNamed(uri.path, arguments: uri.queryParameters);
+          Get.toNamed(
+            uri.path,
+            arguments: {...uri.queryParameters, 'banner': banner},
+          );
         }
         break;
     }

+ 0 - 1
lib/app/modules/home/views/home_view.dart

@@ -435,7 +435,6 @@ class HomeView extends BaseView<HomeController> {
                 height: 24.w,
                 borderRadius: 4.r,
               ),
-
               10.horizontalSpace,
 
               // 位置名称

+ 178 - 72
lib/app/modules/medialocation/controllers/medialocation_controller.dart

@@ -1,92 +1,198 @@
 import 'package:get/get.dart';
 
-import '../../../../config/translations/strings_enum.dart';
-
-/// 流媒体服务数据模型
-class StreamingService {
-  final String name;
-  final String description;
-  final String logoUrl;
-
-  StreamingService({
-    required this.name,
-    required this.description,
-    required this.logoUrl,
-  });
-}
+import '../../../../utils/log/logger.dart';
+import '../../../../utils/system_helper.dart';
+import '../../../constants/enums.dart';
+import '../../../controllers/api_controller.dart';
+import '../../../controllers/base_core_api.dart';
+import '../../../controllers/core_controller.dart';
+import '../../../data/models/banner/banner_list.dart';
+import '../../../data/models/launch/groups.dart';
+import '../../../data/sp/ix_sp.dart';
+import '../../../dialog/error_dialog.dart';
+import '../../../modules/home/controllers/home_controller.dart';
+import '../../../routes/app_pages.dart';
 
 class MedialocationController extends GetxController {
-  // VPN连接状态
-  final isConnected = false.obs;
-  final isConnecting = false.obs;
-
-  // 流媒体服务列表
-  List<StreamingService> get streamingServices => [
-    StreamingService(
-      name: Strings.netflix.tr,
-      description: Strings.niftyStreaming.tr,
-      logoUrl: 'assets/images/netflix_logo.png',
-    ),
-    StreamingService(
-      name: Strings.youtube.tr,
-      description: Strings.youtubeStreaming.tr,
-      logoUrl: 'assets/images/youtube_logo.png',
-    ),
-    StreamingService(
-      name: Strings.hulu.tr,
-      description: Strings.huluStreaming.tr,
-      logoUrl: 'assets/images/hulu_logo.png',
-    ),
-    StreamingService(
-      name: Strings.amazon.tr,
-      description: Strings.amazonStreaming.tr,
-      logoUrl: 'assets/images/amazon_logo.png',
-    ),
-  ];
+  final TAG = 'MedialocationController';
+
+  final ApiController apiController = Get.find<ApiController>();
+  final CoreController coreController = Get.find<CoreController>();
+  final HomeController homeController = Get.find<HomeController>();
+
+  // VPN连接状态(计算属性,基于 coreController 状态和 bannerLocation)
+  bool get isConnected =>
+      _isSameLocation && coreController.state == ConnectionState.connected;
+  bool get isConnecting =>
+      _isSameLocation && coreController.state == ConnectionState.connecting;
+
+  // 判断当前选中的节点是否是 bannerLocation
+  bool get _isSameLocation {
+    final location = bannerLocation.value;
+    if (location == null) return false;
+    final selectedLocation = homeController.selectedLocation;
+    return selectedLocation != null &&
+        (selectedLocation.id == location.id ||
+            selectedLocation.code == location.code);
+  }
+
+  // Banner 数据
+  final Rxn<Banner> banner = Rxn<Banner>();
+  String get bannerTitle => banner.value?.title ?? 'Movies&TV';
+  String get bannerImg => banner.value?.img ?? '';
+
+  // Banner 列表
+  final _bannerList = <Banner>[].obs;
+  List<Banner> get bannerList => _bannerList;
+  set bannerList(List<Banner> value) => _bannerList.assignAll(value);
+
+  // Banner 背景图信息
+  final Rxn<Banner> bannerInfo = Rxn<Banner>();
+  String get bannerInfoImg => bannerInfo.value?.img ?? '';
+
+  // Banner 关联的节点
+  final Rxn<Location> bannerLocation = Rxn<Location>();
 
   @override
   void onInit() {
     super.onInit();
+    if (Get.arguments != null) {
+      if (Get.arguments['banner'] != null) {
+        banner.value = Get.arguments['banner'] as Banner;
+      }
+      if (Get.arguments['type'] != null) {
+        getBanner(position: Get.arguments['type'] as String);
+      }
+    }
   }
 
-  @override
-  void onReady() {
-    super.onReady();
-  }
+  /// 连接VPN - 连接到 bannerLocation
+  void connect() {
+    final location = bannerLocation.value;
+    if (location == null) {
+      log(TAG, 'Cannot connect: bannerLocation is null');
+      return;
+    }
 
-  @override
-  void onClose() {
-    super.onClose();
-  }
+    if (isConnecting) return;
 
-  /// 打开流媒体服务
-  void openStreamingService(StreamingService service) {
-    Get.snackbar(
-      Strings.opening.tr,
-      '${service.name} ${Strings.willOpenSoon.tr}',
+    // 将 Location 转换为 Locations 并选中
+    final locations = Locations(
+      id: location.id,
+      name: location.name,
+      code: location.code,
+      icon: location.icon,
+      country: location.country,
+      sort: location.sort,
     );
-  }
 
-  /// 连接VPN
-  void connect() {
-    if (isConnecting.value) return;
-    
-    isConnecting.value = true;
-    
-    // 模拟连接过程
-    Future.delayed(const Duration(seconds: 2), () {
-      isConnecting.value = false;
-      isConnected.value = true;
-      Get.snackbar(
-        Strings.success.tr,
-        Strings.connectedSuccessfully.tr,
-      );
-    });
+    // 选中节点并触发连接
+    homeController.selectLocation(locations, locationSelectionType: 'media');
+    coreController.selectLocationConnect();
   }
 
   /// 断开VPN
   void disconnect() {
-    isConnected.value = false;
-    isConnecting.value = false;
+    BaseCoreApi().disconnect();
+  }
+
+  /// 获取 banner 列表
+  /// [position] banner 位置类型,如 "banner"、"media"、"nine" 等
+  Future<void> getBanner({
+    String position = 'media',
+    bool isCache = true,
+  }) async {
+    try {
+      // 先读取缓存数据
+      final cacheBanners = IXSP.getBanner(position);
+      if (cacheBanners != null && isCache) {
+        if (cacheBanners.list != null && cacheBanners.list!.isNotEmpty) {
+          bannerList = cacheBanners.list!;
+        }
+        if (cacheBanners.bannerInfo != null) {
+          bannerInfo.value = cacheBanners.bannerInfo;
+        }
+        if (cacheBanners.location != null) {
+          bannerLocation.value = cacheBanners.location;
+        }
+        log(TAG, 'Loaded banner from cache for position: $position');
+      }
+
+      // 请求最新数据
+      final banners = await apiController.getBanner(position: position);
+      if (banners.list != null && banners.list!.isNotEmpty) {
+        bannerList = banners.list!;
+      }
+      if (banners.bannerInfo != null) {
+        bannerInfo.value = banners.bannerInfo;
+      }
+      if (banners.location != null) {
+        bannerLocation.value = banners.location;
+      }
+      // 保存到缓存
+      await IXSP.saveBanner(position, banners);
+      log(TAG, 'Banner updated and cached for position: $position');
+    } catch (e) {
+      log(TAG, 'Error loading banners for position $position: $e');
+    }
+  }
+
+  //点击banner
+  void onBannerTap(Banner? banner) {
+    if (banner?.action == null) return;
+
+    final action = BannerAction.values.firstWhere(
+      (e) => e.toString().split('.').last == banner?.action,
+      orElse: () => BannerAction.notice, // 默认值
+    );
+
+    switch (action) {
+      case BannerAction.urlOut:
+        if (banner?.data != null) {
+          SystemHelper.openWebPage(banner!.data!);
+        }
+        break;
+      case BannerAction.urlIn:
+        if (banner?.data != null) {
+          Get.toNamed(
+            Routes.WEB,
+            arguments: {
+              'title': banner?.title ?? '',
+              'url': banner?.data ?? '',
+            },
+          );
+        }
+        break;
+      case BannerAction.deepLink:
+        if (banner?.data != null) {
+          // Handle deep link
+          SystemHelper.handleDeepLink(banner!.data!);
+        }
+        break;
+      case BannerAction.openPkg:
+        if (banner?.data != null) {
+          // Open specific package
+          SystemHelper.openPackage(banner!.data!);
+        }
+        break;
+      case BannerAction.notice:
+        if (banner?.data != null) {
+          // Show notice dialog
+          ErrorDialog.show(
+            title: banner?.title ?? '',
+            message: banner?.content,
+          );
+        }
+        break;
+      case BannerAction.page:
+        if (banner?.data != null) {
+          final uri = Uri.parse(banner!.data!);
+          Get.toNamed(
+            uri.path,
+            arguments: {...uri.queryParameters, 'banner': banner},
+          );
+        }
+        break;
+    }
   }
 }

+ 119 - 181
lib/app/modules/medialocation/views/medialocation_view.dart

@@ -1,11 +1,11 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart' hide Banner;
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:nomo/config/theme/dark_theme_colors.dart';
 
 import '../../../../config/translations/strings_enum.dart';
 import '../../../constants/assets.dart';
-import '../../../constants/iconfont/iconfont.dart';
+import '../../../data/models/banner/banner_list.dart';
 import '../../../widgets/click_opacity.dart';
 import '../../../widgets/ix_image.dart';
 import '../../../widgets/submit_btn.dart';
@@ -20,18 +20,24 @@ class MedialocationView extends GetView<MedialocationController> {
       backgroundColor: DarkThemeColors.scaffoldBackgroundColor,
       body: Stack(
         children: [
-          // 视频背景层(只显示顶部214高度)
+          // 背景层(只显示顶部214高度)
           Positioned(
             top: 0,
             left: 0,
             right: 0,
             height: 214.w,
             child: ClipRect(
-              child: IXImage(
-                source: Assets.mediaBg,
-                width: 375.w,
-                height: 214.w,
-                sourceType: ImageSourceType.asset,
+              child: Obx(
+                () => IXImage(
+                  source: controller.bannerInfoImg.isNotEmpty
+                      ? controller.bannerInfoImg
+                      : Assets.mediaBg,
+                  width: 375.w,
+                  height: 214.w,
+                  sourceType: controller.bannerInfoImg.isNotEmpty
+                      ? ImageSourceType.network
+                      : ImageSourceType.asset,
+                ),
               ),
             ),
           ),
@@ -62,26 +68,18 @@ class MedialocationView extends GetView<MedialocationController> {
 
                 // 可滚动内容区域
                 Expanded(
-                  child: SingleChildScrollView(
-                    padding: EdgeInsets.symmetric(horizontal: 16.w),
+                  child: Padding(
+                    padding: EdgeInsets.symmetric(horizontal: 14.w),
                     child: Column(
                       children: [
                         20.verticalSpace,
-
                         // 流媒体服务卡片
                         _buildStreamingServicesCard(),
-
-                        10.verticalSpace,
-
-                        // 功能说明列表
-                        _buildFeaturesList(),
-
-                        32.verticalSpace,
                       ],
                     ),
                   ),
                 ),
-
+                20.verticalSpace,
                 // 底部连接按钮
                 _buildConnectButton(),
               ],
@@ -123,67 +121,76 @@ class MedialocationView extends GetView<MedialocationController> {
       padding: EdgeInsets.symmetric(vertical: 20.w),
       child: Column(
         children: [
-          // Movies&TV 图标和标题
-          Row(
-            mainAxisAlignment: MainAxisAlignment.center,
-            children: [
-              Icon(
-                IconFont.icon19,
-                color: DarkThemeColors.errorColor,
-                size: 32.w,
-              ),
-              4.horizontalSpace,
-              Text(
-                'Movies&TV',
-                style: TextStyle(
-                  fontSize: 22.sp,
-                  fontWeight: FontWeight.w500,
-                  height: 1.3,
-                  color: Colors.white,
+          // 图标和标题
+          Obx(
+            () => Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                IXImage(
+                  source: controller.bannerImg,
+                  width: 32.w,
+                  height: 32.w,
+                  sourceType: ImageSourceType.network,
                 ),
-              ),
-            ],
+                8.horizontalSpace,
+                Text(
+                  controller.bannerTitle,
+                  style: TextStyle(
+                    fontSize: 22.sp,
+                    fontWeight: FontWeight.w500,
+                    height: 1.3,
+                    color: Colors.white,
+                  ),
+                ),
+              ],
+            ),
           ),
 
           8.verticalSpace,
 
           // 连接状态
           Obx(() {
-            final text = controller.isConnected.value
+            // 访问 stateStream 以触发 Obx 响应
+            final _ = controller.coreController.stateStream.value;
+            final text = controller.isConnected
                 ? Strings.connected.tr
-                : controller.isConnecting.value
+                : controller.isConnecting
                 ? Strings.connecting.tr
                 : Strings.disconnected.tr;
-            final textColor = controller.isConnected.value
+            final textColor = controller.isConnected
                 ? DarkThemeColors.text1
-                : controller.isConnecting.value
+                : controller.isConnecting
                 ? DarkThemeColors.text1
                 : DarkThemeColors.text1;
-            final statusImgPath = controller.isConnected.value
+            final statusImgPath = controller.isConnected
                 ? Assets.connected
-                : controller.isConnecting.value
+                : controller.isConnecting
                 ? Assets.connecting
                 : Assets.disconnected;
 
-            return Row(
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: [
-                IXImage(
-                  source: statusImgPath,
-                  sourceType: ImageSourceType.asset,
-                  width: 14.w,
-                  height: 14.w,
-                ),
-                4.horizontalSpace,
-                Text(
-                  text,
-                  style: TextStyle(
-                    fontSize: 14.sp,
-                    fontWeight: FontWeight.w500,
-                    color: textColor,
+            return SizedBox(
+              height: 20.w,
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  IXImage(
+                    source: statusImgPath,
+                    sourceType: ImageSourceType.asset,
+                    width: 14.w,
+                    height: 14.w,
                   ),
-                ),
-              ],
+                  4.horizontalSpace,
+                  Text(
+                    text,
+                    style: TextStyle(
+                      fontSize: 14.sp,
+                      height: 1.4,
+                      fontWeight: FontWeight.w500,
+                      color: textColor,
+                    ),
+                  ),
+                ],
+              ),
             );
           }),
         ],
@@ -193,54 +200,50 @@ class MedialocationView extends GetView<MedialocationController> {
 
   /// 构建流媒体服务卡片
   Widget _buildStreamingServicesCard() {
-    return Container(
-      decoration: BoxDecoration(
-        color: DarkThemeColors.bg2,
-        borderRadius: BorderRadius.circular(12.r),
-      ),
-      child: ListView.separated(
-        shrinkWrap: true,
-        physics: const NeverScrollableScrollPhysics(),
-        padding: EdgeInsets.zero,
-        itemCount: controller.streamingServices.length,
-        separatorBuilder: (context, index) => Divider(
-          height: 1.w,
-          thickness: 1.w,
-          color: DarkThemeColors.strokes1,
+    return Obx(() {
+      if (controller.bannerList.isEmpty) {
+        return const SizedBox.shrink();
+      }
+      return Container(
+        decoration: BoxDecoration(
+          color: DarkThemeColors.bg2,
+          borderRadius: BorderRadius.circular(12.r),
         ),
-        itemBuilder: (context, index) {
-          final service = controller.streamingServices[index];
-          return _buildStreamingServiceItem(service);
-        },
-      ),
-    );
+        child: ListView.separated(
+          shrinkWrap: true,
+          physics: const NeverScrollableScrollPhysics(),
+          padding: EdgeInsets.zero,
+          itemCount: controller.bannerList.length,
+          separatorBuilder: (context, index) => Divider(
+            height: 1.w,
+            thickness: 1.w,
+            color: DarkThemeColors.strokes1,
+          ),
+          itemBuilder: (context, index) {
+            final banner = controller.bannerList[index];
+            return _buildStreamingServiceItem(banner);
+          },
+        ),
+      );
+    });
   }
 
   /// 构建单个流媒体服务项
-  Widget _buildStreamingServiceItem(StreamingService service) {
+  Widget _buildStreamingServiceItem(Banner banner) {
     return ClickOpacity(
-      onTap: () => controller.openStreamingService(service),
+      onTap: () => controller.onBannerTap(banner),
       child: Container(
         padding: EdgeInsets.all(16.w),
         child: Row(
           children: [
             // Logo
-            Container(
-              width: 40.w,
-              height: 40.w,
-              decoration: BoxDecoration(
-                color: _getServiceColor(service.name),
-                borderRadius: BorderRadius.circular(12.r),
-              ),
-              child: Center(
-                child: Text(
-                  service.name[0].toUpperCase(),
-                  style: TextStyle(
-                    fontSize: 24.sp,
-                    fontWeight: FontWeight.bold,
-                    color: DarkThemeColors.text1,
-                  ),
-                ),
+            ClipRRect(
+              borderRadius: BorderRadius.circular(12.r),
+              child: IXImage(
+                source: banner.img ?? '',
+                width: 40.w,
+                height: 40.w,
+                sourceType: ImageSourceType.network,
               ),
             ),
 
@@ -252,7 +255,7 @@ class MedialocationView extends GetView<MedialocationController> {
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
                   Text(
-                    service.name,
+                    banner.title ?? '',
                     style: TextStyle(
                       fontSize: 14.sp,
                       height: 1.4,
@@ -260,14 +263,15 @@ class MedialocationView extends GetView<MedialocationController> {
                       color: DarkThemeColors.text1,
                     ),
                   ),
-                  Text(
-                    service.description,
-                    style: TextStyle(
-                      fontSize: 13.sp,
-                      height: 1.4,
-                      color: DarkThemeColors.text2,
+                  if (banner.content != null && banner.content!.isNotEmpty)
+                    Text(
+                      banner.content!,
+                      style: TextStyle(
+                        fontSize: 13.sp,
+                        height: 1.4,
+                        color: DarkThemeColors.text2,
+                      ),
                     ),
-                  ),
                 ],
               ),
             ),
@@ -292,94 +296,28 @@ class MedialocationView extends GetView<MedialocationController> {
     );
   }
 
-  /// 构建功能说明列表
-  Widget _buildFeaturesList() {
-    return Column(
-      children: [
-        _buildFeatureItem(
-          icon: Icons.favorite_outline,
-          iconColor: Colors.red,
-          text: 'Access your favorite content',
-        ),
-        _buildFeatureItem(
-          icon: Icons.public,
-          iconColor: Colors.blue,
-          text: 'Connection from anywhere',
-        ),
-        _buildFeatureItem(
-          icon: Icons.speed,
-          iconColor: Colors.purple,
-          text: 'Ultra fast servers',
-        ),
-      ],
-    );
-  }
-
-  /// 构建单个功能说明项
-  Widget _buildFeatureItem({
-    required IconData icon,
-    required Color iconColor,
-    required String text,
-  }) {
-    return Container(
-      decoration: BoxDecoration(
-        color: DarkThemeColors.bg3,
-        borderRadius: BorderRadius.circular(12.r),
-      ),
-      height: 40.w,
-      margin: EdgeInsets.only(bottom: 10.w),
-      padding: EdgeInsets.symmetric(horizontal: 14.w),
-      child: Row(
-        children: [
-          Icon(icon, color: iconColor, size: 20.w),
-          10.horizontalSpace,
-          Expanded(
-            child: Text(
-              text,
-              style: TextStyle(fontSize: 13.sp, color: DarkThemeColors.text2),
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-
   /// 构建底部连接按钮
   Widget _buildConnectButton() {
     return Padding(
       padding: EdgeInsets.all(16.w),
       child: Obx(() {
+        // 访问 stateStream 以触发 Obx 响应
+        final _ = controller.coreController.stateStream.value;
         return SubmitButton(
-          text: controller.isConnected.value
+          text: controller.isConnected
               ? Strings.disconnect.tr
-              : controller.isConnecting.value
+              : controller.isConnecting
               ? Strings.connecting.tr
               : Strings.connect.tr,
-          onPressed: controller.isConnected.value
+          onPressed: controller.isConnected
               ? controller.disconnect
               : controller.connect,
-          isLoading: controller.isConnecting.value,
-          bgColor: controller.isConnected.value
+          isLoading: controller.isConnecting,
+          bgColor: controller.isConnected
               ? Colors.red
               : DarkThemeColors.primaryColor,
         );
       }),
     );
   }
-
-  /// 获取服务颜色
-  Color _getServiceColor(String serviceName) {
-    switch (serviceName.toLowerCase()) {
-      case 'netflix':
-        return const Color(0xFFE50914);
-      case 'youtube':
-        return const Color(0xFFFF0000);
-      case 'hulu':
-        return const Color(0xFF1CE783);
-      case 'amazon':
-        return const Color(0xFF00A8E1);
-      default:
-        return DarkThemeColors.primaryColor;
-    }
-  }
 }

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

@@ -1,8 +1,10 @@
 import 'package:get/get.dart';
+import 'package:package_info_plus/package_info_plus.dart';
 
 import '../../../../config/translations/strings_enum.dart';
 import '../../../../utils/awesome_notifications_helper.dart';
 import '../../../controllers/api_controller.dart';
+import '../../../data/sp/ix_sp.dart';
 import '../../../dialog/loading/loading_dialog.dart';
 
 class SettingController extends GetxController {
@@ -17,10 +19,32 @@ class SettingController extends GetxController {
   bool get pushNotifications => _pushNotifications.value;
   set pushNotifications(bool value) => _pushNotifications.value = value;
 
+  final _version = ''.obs;
+  String get version => _version.value;
+  set version(String value) => _version.value = value;
+
+  final _hasUpdate = false.obs;
+  bool get hasUpdate => _hasUpdate.value;
+  set hasUpdate(bool value) => _hasUpdate.value = value;
+
   @override
   void onInit() {
     super.onInit();
     _initPushNotifications();
+    _getVersion();
+  }
+
+  /// 获取版本信息
+  void _getVersion() async {
+    final packageInfo = await PackageInfo.fromPlatform();
+    version = packageInfo.version;
+    final code = int.tryParse(packageInfo.buildNumber) ?? 0;
+    final upgrade = IXSP.getUpgrade();
+    if (upgrade?.versionCode == code) {
+      hasUpdate = false;
+    } else {
+      hasUpdate = upgrade?.upgradeType == 1;
+    }
   }
 
   Future<void> _initPushNotifications() async {

+ 34 - 9
lib/app/modules/setting/views/setting_view.dart

@@ -1,6 +1,5 @@
 import 'dart:io';
 
-import 'package:date_format/date_format.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -131,7 +130,9 @@ class SettingView extends BaseView<SettingController> {
                   children: [
                     IXImage(
                       source: controller.apiController.userLevel == 3
-                          ? Assets.premium
+                          ? controller.apiController.remainTimeSeconds > 0
+                                ? Assets.premium
+                                : Assets.premiumExpired
                           : controller.apiController.userLevel == 9999
                           ? Assets.test
                           : Assets.free,
@@ -217,10 +218,14 @@ class SettingView extends BaseView<SettingController> {
                   iconColor: Get.reactiveTheme.shadowColor,
                   title: Strings.validTerm.tr,
                   trailing: Text(
-                    controller.apiController.validTermText,
+                    controller.apiController.remainTimeSeconds > 0
+                        ? controller.apiController.validTermText
+                        : Strings.expired.tr,
                     style: TextStyle(
                       fontSize: 13.sp,
-                      color: Get.reactiveTheme.primaryColor,
+                      color: controller.apiController.remainTimeSeconds > 0
+                          ? Get.reactiveTheme.primaryColor
+                          : Colors.red,
                       fontWeight: FontWeight.w500,
                     ),
                   ),
@@ -514,13 +519,33 @@ class SettingView extends BaseView<SettingController> {
                 end: Alignment.bottomCenter,
               ),
               title: Strings.version.tr,
-              trailing: Text(
-                'V1.0.0',
-                style: TextStyle(
-                  fontSize: 13.sp,
-                  color: Get.reactiveTheme.hintColor,
+              trailing: Obx(
+                () => Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Text(
+                      'v${controller.version}',
+                      style: TextStyle(
+                        fontSize: 13.sp,
+                        color: Get.reactiveTheme.hintColor,
+                      ),
+                    ),
+                    if (controller.hasUpdate)
+                      Container(
+                        width: 4.w,
+                        height: 4.w,
+                        decoration: BoxDecoration(
+                          color: Colors.red,
+                          borderRadius: BorderRadius.circular(2.r),
+                        ),
+                      ),
+                  ],
                 ),
               ),
+              onTap: () {
+                controller.apiController.checkUpdate(isClickCheck: true);
+              },
             ),
           ],
         ),

+ 20 - 15
lib/app/modules/subscription/controllers/subscription_controller.dart

@@ -122,6 +122,7 @@ class SubscriptionController extends GetxController {
     _initializeVideoPlayer();
     _initializeInAppPurchase();
     _getChannelPlanList();
+    refreshSubscriptionStatus();
   }
 
   @override
@@ -167,8 +168,9 @@ class SubscriptionController extends GetxController {
           'channelItemId': selectedPlan?.channelItemId,
         });
       },
-      onSuccess: () {
-        // 成功后的操作
+      onSuccess: () async {
+        // 刷新用户信息
+        refreshSubscriptionStatus();
       },
     );
   }
@@ -482,20 +484,13 @@ class SubscriptionController extends GetxController {
 
   // ==================== 当前订阅相关方法 ====================
 
-  /// 是否有当前订阅(userLevel == 3 且 planInfo 存在)
-  bool get hasCurrentSubscription {
-    final user = IXSP.getUser();
-    return user?.userLevel == 3 && user?.planInfo != null;
-  }
+  /// 当前订阅状态(响应式)
+  final _hasCurrentSubscription = false.obs;
+  bool get hasCurrentSubscription => _hasCurrentSubscription.value;
 
-  /// 获取当前订阅套餐信息
-  ChannelPlan? get currentPlanInfo {
-    final user = IXSP.getUser();
-    if (user?.userLevel == 3) {
-      return user?.planInfo;
-    }
-    return null;
-  }
+  /// 当前订阅套餐信息(响应式)
+  final Rxn<ChannelPlan> _currentPlanInfo = Rxn<ChannelPlan>();
+  ChannelPlan? get currentPlanInfo => _currentPlanInfo.value;
 
   /// 获取当前订阅套餐标题
   String get currentPlanTitle {
@@ -508,4 +503,14 @@ class SubscriptionController extends GetxController {
     if (plan == null) return '';
     return '${plan.title}-${plan.subTitle}';
   }
+
+  /// 刷新当前订阅状态
+  void refreshSubscriptionStatus() {
+    final user = IXSP.getUser();
+    _hasCurrentSubscription.value =
+        user?.userLevel == 3 &&
+        user?.planInfo != null &&
+        user?.planInfo?.channelItemId?.isNotEmpty == true;
+    _currentPlanInfo.value = user?.userLevel == 3 ? user?.planInfo : null;
+  }
 }

+ 60 - 58
lib/app/modules/subscription/views/subscription_view.dart

@@ -129,69 +129,71 @@ class SubscriptionView extends GetView<SubscriptionController> {
 
   // 当前订阅信息
   Widget _buildCurrentSubscription() {
-    // 判断是否有订阅
-    if (!controller.hasCurrentSubscription) {
-      // 没有订阅,只显示钻石图标
-      return Center(
-        child: IXImage(
-          source: Assets.subscriptionDiamond,
-          width: 92.w,
-          height: 80.w,
-          sourceType: ImageSourceType.asset,
-        ),
-      );
-    }
+    return Obx(() {
+      // 判断是否有订阅
+      if (!controller.hasCurrentSubscription) {
+        // 没有订阅,只显示钻石图标
+        return Center(
+          child: IXImage(
+            source: Assets.subscriptionDiamond,
+            width: 92.w,
+            height: 80.w,
+            sourceType: ImageSourceType.asset,
+          ),
+        );
+      }
 
-    // 有订阅,显示当前套餐信息
-    return Row(
-      mainAxisAlignment: MainAxisAlignment.center,
-      children: [
-        // 钻石图标
-        IXImage(
-          source: Assets.subscriptionDiamond,
-          width: 92.w,
-          height: 80.w,
-          sourceType: ImageSourceType.asset,
-        ),
-        12.horizontalSpace,
-        Expanded(
-          child: Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              Row(
-                children: [
-                  IXImage(
-                    source: Assets.subscriptionWallet,
-                    width: 20.w,
-                    height: 20.w,
-                    sourceType: ImageSourceType.asset,
-                  ),
-                  4.horizontalSpace,
-                  Text(
-                    Strings.currentSubscription.tr,
-                    style: TextStyle(
-                      fontSize: 14.sp,
-                      height: 1.4,
-                      color: DarkThemeColors.subscriptionColor,
-                      fontWeight: FontWeight.w700,
+      // 有订阅,显示当前套餐信息
+      return Row(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          // 钻石图标
+          IXImage(
+            source: Assets.subscriptionDiamond,
+            width: 92.w,
+            height: 80.w,
+            sourceType: ImageSourceType.asset,
+          ),
+          12.horizontalSpace,
+          Expanded(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Row(
+                  children: [
+                    IXImage(
+                      source: Assets.subscriptionWallet,
+                      width: 20.w,
+                      height: 20.w,
+                      sourceType: ImageSourceType.asset,
                     ),
+                    4.horizontalSpace,
+                    Text(
+                      Strings.currentSubscription.tr,
+                      style: TextStyle(
+                        fontSize: 14.sp,
+                        height: 1.4,
+                        color: DarkThemeColors.subscriptionColor,
+                        fontWeight: FontWeight.w700,
+                      ),
+                    ),
+                  ],
+                ),
+                10.verticalSpaceFromWidth,
+                Text(
+                  controller.currentPlanPriceDisplay,
+                  style: TextStyle(
+                    fontSize: 14.sp,
+                    height: 1.4,
+                    color: Colors.white,
                   ),
-                ],
-              ),
-              10.verticalSpaceFromWidth,
-              Text(
-                controller.currentPlanPriceDisplay,
-                style: TextStyle(
-                  fontSize: 14.sp,
-                  height: 1.4,
-                  color: Colors.white,
                 ),
-              ),
-            ],
+              ],
+            ),
           ),
-        ),
-      ],
-    );
+        ],
+      );
+    });
   }
 
   // 订阅计划选项

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

@@ -453,4 +453,5 @@ class Strings {
 
   static const String remainTime = 'Remain time';
   static const String remainTimeEnded = 'Your available time has ended';
+  static const String expired = 'Expired';
 }

+ 4 - 4
pubspec.lock

@@ -1337,7 +1337,7 @@ packages:
     source: hosted
     version: "2.4.1"
   shelf:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: shelf
       sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
@@ -1345,7 +1345,7 @@ packages:
     source: hosted
     version: "1.4.2"
   shelf_web_socket:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: shelf_web_socket
       sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
@@ -1481,10 +1481,10 @@ packages:
     dependency: "direct main"
     description:
       name: system_clock
-      sha256: "8133d7707b4bed8a5e62f0809f040975f8854e11b1c7ee851e56f929f153cc8a"
+      sha256: "7038993add5a6f06558582e0965752f75951e24695c988eb638e6996399c497b"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.1"
+    version: "3.0.0"
   term_glyph:
     dependency: transitive
     description:

+ 1 - 1
pubspec.yaml

@@ -55,7 +55,7 @@ dependencies:
   app_links: ^6.4.1 # 应用链接
   flutter_custom_tabs: ^2.4.0 # 外链Google浏览器打开
   ntp: ^2.0.0 # 网络时间协议
-  system_clock: ^2.0.1 # 系统时钟
+  system_clock: ^3.0.0 # 系统时钟
   flutter_inappwebview: ^6.1.5 # 内嵌浏览器
   crypto: ^3.0.6 # 加密
   encrypt: ^5.0.3 # 加密

+ 0 - 1
windows/flutter/generated_plugins.cmake

@@ -14,7 +14,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST
-  system_clock
 )
 
 set(PLUGIN_BUNDLED_LIBRARIES)