Browse Source

fix: 增加首页banner接口

lilu 3 tháng trước cách đây
mục cha
commit
747da7b35b

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

@@ -0,0 +1,80 @@
+/// SharedPreferences 键名常量统一管理
+class SPKeys {
+  SPKeys._();
+
+  /// 设备ID
+  static const String deviceId = 'omon_device_id';
+
+  /// FCM Token
+  static const String fcmToken = 'fcm_token';
+
+  /// 当前语言
+  static const String currentLocal = 'current_local';
+
+  /// 主题是否为浅色
+  static const String lightTheme = 'is_theme_light';
+
+  /// 是否启动游戏
+  static const String launchGame = 'is_launch_game';
+
+  /// 忽略的版本号
+  static const String ignoreVersion = 'ignore_version';
+
+  /// 是否新安装
+  static const String isNewInstall = 'is_new_install';
+
+  /// Launch 数据
+  static const String launchData = 'launch_data';
+
+  /// 是否地区禁用
+  static const String isRegionDisabled = 'is_region_disabled';
+
+  /// 是否用户禁用
+  static const String isUserDisabled = 'is_user_disabled';
+
+  /// 是否设备禁用
+  static const String isDeviceDisabled = 'is_device_disabled';
+
+  /// 最后一次上传的性能日志 boostSessionId
+  static const String lastMetricsLog = 'last_metrics_log';
+
+  /// 最后一次上传的 ES 日志 boostSessionId
+  static const String lastESLog = 'last_es_log';
+
+  /// 是否开启 debug log
+  static const String enableDebugLog = 'enable_debug_log';
+
+  /// 是否开启 ping(0默认 1开启 2关闭)
+  static const String enablePingMode = 'enable_ping_mode';
+
+  /// 当前选中的节点
+  static const String selectedLocation = 'selected_location';
+
+  /// 最近选择的节点列表
+  static const String recentLocations = 'recent_locations';
+
+  /// 路由模式选择
+  static const String routingModeSelected = 'routing_mode_selected';
+
+  /// 分流隧道选中的模式
+  static const String splittunnelingSelectedMode =
+      'splittunneling_selected_mode';
+
+  /// 分流隧道缓存应用
+  static const String splittunnelingCachedApps = 'splittunneling_cached_apps';
+
+  /// 分流隧道排除模式选中的应用
+  static const String splittunnelingExcludeSelectedApps =
+      'splittunneling_exclude_selected_apps';
+
+  /// 分流隧道包含模式选中的应用
+  static const String splittunnelingIncludeSelectedApps =
+      'splittunneling_include_selected_apps';
+
+  /// Banner 缓存键前缀(完整键为 banner_cache_{position})
+  static const String bannerCachePrefix = 'banner_cache_';
+
+  /// 获取指定位置的 banner 缓存键
+  static String bannerCacheKey(String position) =>
+      '$bannerCachePrefix$position';
+}

+ 57 - 10
lib/app/controllers/api_controller.dart

@@ -9,12 +9,14 @@ 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';
+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 '../../config/translations/localization_service.dart';
 import '../../config/translations/strings_enum.dart';
+import '../data/models/banner/banner_list.dart';
 import 'base_core_api.dart';
 import '../../utils/api_statistics.dart';
 import '../../utils/device_manager.dart';
@@ -80,12 +82,16 @@ class ApiController extends GetxService with WidgetsBindingObserver {
   Fingerprint fp = Fingerprint.empty();
 
   // 全局剩余时间倒计时(秒)
-  final _remainTimeSeconds = 0.obs;
-  int get remainTimeSeconds => _remainTimeSeconds.value;
-  set remainTimeSeconds(int value) => _remainTimeSeconds.value = value;
+  int _remainTimeSeconds = 0;
+  int get remainTimeSeconds => _remainTimeSeconds;
 
-  // 格式化后的剩余时间字符串
-  String get remainTimeFormatted => _formatRemainTime(_remainTimeSeconds.value);
+  // 格式化后的剩余时间字符串(响应式,只有文案变化时才更新 UI)
+  final _remainTimeFormatted = ''.obs;
+  String get remainTimeFormatted => _remainTimeFormatted.value;
+
+  // 是否应该显示倒计时(响应式,只有状态变化时才更新 UI)
+  final _shouldShowCountdown = false.obs;
+  bool get shouldShowCountdown => _shouldShowCountdown.value;
 
   // 倒计时定时器
   Timer? _remainTimeTimer;
@@ -535,7 +541,8 @@ class ApiController extends GetxService with WidgetsBindingObserver {
         request['locationId'] = locationId;
         request['locationCode'] = locationCode;
         // 获取选中的路由模式
-        final routingMode = IXSP.getString("routing_mode_selected") ?? "smart";
+        final routingMode =
+            IXSP.getString(SPKeys.routingModeSelected) ?? "smart";
         request['routingMode'] = routingMode;
         final result = await ApiRouter().getDispatchInfo(
           request,
@@ -905,6 +912,24 @@ class ApiController extends GetxService with WidgetsBindingObserver {
     }
   }
 
+  Future<BannerList> getBanner({String position = "banner"}) async {
+    try {
+      final request = fp.toJson();
+      request["position"] = position;
+      final result = await ApiCore().getBanner(request);
+      if (!result.success) {
+        throw Failure(
+          code: result.errorCode ?? '',
+          message: result.errorMessage ?? '',
+        );
+      }
+      final bannerList = BannerList.fromJson(result.data);
+      return bannerList;
+    } catch (e) {
+      rethrow;
+    }
+  }
+
   Future<String> uploadLogs(List<dynamic> items, {bool isCache = false}) async {
     await updateFingerprintData();
     Map<String, dynamic> request = fp.toJson();
@@ -1105,12 +1130,16 @@ class ApiController extends GetxService with WidgetsBindingObserver {
     _remainTimeTimer?.cancel();
 
     // 设置初始剩余时间
-    _remainTimeSeconds.value = seconds;
+    _remainTimeSeconds = seconds;
+    // 立即更新格式化文案
+    _updateRemainTimeFormatted();
 
     // 启动每秒倒计时
     _remainTimeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
-      if (_remainTimeSeconds.value > 0) {
-        _remainTimeSeconds.value--;
+      if (_remainTimeSeconds > 0) {
+        _remainTimeSeconds--;
+        // 只有当格式化文案变化时才更新 UI
+        _updateRemainTimeFormatted();
       } else {
         // 倒计时结束
         timer.cancel();
@@ -1120,6 +1149,23 @@ class ApiController extends GetxService with WidgetsBindingObserver {
     });
   }
 
+  /// 更新格式化后的剩余时间(只有文案变化时才触发 UI 更新)
+  void _updateRemainTimeFormatted() {
+    final newFormatted = _formatRemainTime(_remainTimeSeconds);
+    if (_remainTimeFormatted.value != newFormatted) {
+      _remainTimeFormatted.value = newFormatted;
+    }
+
+    // 更新是否显示倒计时的状态
+    final vipRemainNoticeSeconds =
+        (IXSP.getAppConfig()?.vipRemainNotice ?? 600) * 60;
+    final newShouldShow =
+        _remainTimeSeconds > 0 && _remainTimeSeconds < vipRemainNoticeSeconds;
+    if (_shouldShowCountdown.value != newShouldShow) {
+      _shouldShowCountdown.value = newShouldShow;
+    }
+  }
+
   /// 停止剩余时间倒计时
   void stopRemainTimeCountdown() {
     _remainTimeTimer?.cancel();
@@ -1132,7 +1178,8 @@ class ApiController extends GetxService with WidgetsBindingObserver {
     if (seconds > 0) {
       startRemainTimeCountdown(seconds);
     } else {
-      _remainTimeSeconds.value = 0;
+      _remainTimeSeconds = 0;
+      _updateRemainTimeFormatted();
     }
   }
 

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

@@ -23,6 +23,7 @@ import '../data/models/api_exception.dart';
 import '../data/models/failure.dart';
 import '../data/models/vpn_message.dart';
 import '../data/sp/ix_sp.dart';
+import '../constants/sp_keys.dart';
 import '../dialog/error_dialog.dart';
 import '../dialog/feedback_bottom_sheet.dart';
 import 'api_controller.dart';
@@ -208,7 +209,7 @@ class CoreController extends GetxService {
 
     try {
       // 读取选中的模式
-      final modeString = IXSP.getString('splittunneling_selected_mode');
+      final modeString = IXSP.getString(SPKeys.splittunnelingSelectedMode);
       if (modeString == null) {
         log(TAG, '分流隧道未设置模式');
         return {
@@ -231,8 +232,8 @@ class CoreController extends GetxService {
 
       // 根据模式获取对应的应用列表
       final key = isExcludeMode
-          ? 'splittunneling_exclude_selected_apps'
-          : 'splittunneling_include_selected_apps';
+          ? SPKeys.splittunnelingExcludeSelectedApps
+          : SPKeys.splittunnelingIncludeSelectedApps;
 
       final selectedAppsJson = IXSP.getString(key);
       if (selectedAppsJson != null) {

+ 56 - 0
lib/app/data/models/banner/banner_list.dart

@@ -0,0 +1,56 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter/foundation.dart';
+part 'banner_list.freezed.dart';
+part 'banner_list.g.dart';
+
+@freezed
+abstract class BannerList with _$BannerList {
+  const factory BannerList({
+    Location? location,
+    Banner? bannerInfo,
+    List<Banner>? list,
+  }) = _BannerList;
+
+  factory BannerList.fromJson(Map<String, Object?> json) =>
+      _$BannerListFromJson(json);
+}
+
+@freezed
+abstract class Location with _$Location {
+  const factory Location({
+    int? id,
+    String? name,
+    String? code,
+    String? icon,
+    String? country,
+    int? sort,
+    Coordinates? coordinates,
+    int? userLevel,
+    bool? isTrial,
+    bool? showGDPR,
+  }) = _Location;
+
+  factory Location.fromJson(Map<String, Object?> json) =>
+      _$LocationFromJson(json);
+}
+
+@freezed
+abstract class Coordinates with _$Coordinates {
+  const factory Coordinates({int? lat, int? lng}) = _Coordinates;
+
+  factory Coordinates.fromJson(Map<String, Object?> json) =>
+      _$CoordinatesFromJson(json);
+}
+
+@freezed
+abstract class Banner with _$Banner {
+  const factory Banner({
+    String? action,
+    String? img,
+    String? title,
+    String? content,
+    String? data,
+  }) = _Banner;
+
+  factory Banner.fromJson(Map<String, Object?> json) => _$BannerFromJson(json);
+}

+ 1089 - 0
lib/app/data/models/banner/banner_list.freezed.dart

@@ -0,0 +1,1089 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'banner_list.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity<T>(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+  'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+BannerList _$BannerListFromJson(Map<String, dynamic> json) {
+  return _BannerList.fromJson(json);
+}
+
+/// @nodoc
+mixin _$BannerList {
+  Location? get location => throw _privateConstructorUsedError;
+  Banner? get bannerInfo => throw _privateConstructorUsedError;
+  List<Banner>? get list => throw _privateConstructorUsedError;
+
+  /// Serializes this BannerList to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of BannerList
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $BannerListCopyWith<BannerList> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $BannerListCopyWith<$Res> {
+  factory $BannerListCopyWith(
+    BannerList value,
+    $Res Function(BannerList) then,
+  ) = _$BannerListCopyWithImpl<$Res, BannerList>;
+  @useResult
+  $Res call({Location? location, Banner? bannerInfo, List<Banner>? list});
+
+  $LocationCopyWith<$Res>? get location;
+  $BannerCopyWith<$Res>? get bannerInfo;
+}
+
+/// @nodoc
+class _$BannerListCopyWithImpl<$Res, $Val extends BannerList>
+    implements $BannerListCopyWith<$Res> {
+  _$BannerListCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of BannerList
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? location = freezed,
+    Object? bannerInfo = freezed,
+    Object? list = freezed,
+  }) {
+    return _then(
+      _value.copyWith(
+            location: freezed == location
+                ? _value.location
+                : location // ignore: cast_nullable_to_non_nullable
+                      as Location?,
+            bannerInfo: freezed == bannerInfo
+                ? _value.bannerInfo
+                : bannerInfo // ignore: cast_nullable_to_non_nullable
+                      as Banner?,
+            list: freezed == list
+                ? _value.list
+                : list // ignore: cast_nullable_to_non_nullable
+                      as List<Banner>?,
+          )
+          as $Val,
+    );
+  }
+
+  /// Create a copy of BannerList
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $LocationCopyWith<$Res>? get location {
+    if (_value.location == null) {
+      return null;
+    }
+
+    return $LocationCopyWith<$Res>(_value.location!, (value) {
+      return _then(_value.copyWith(location: value) as $Val);
+    });
+  }
+
+  /// Create a copy of BannerList
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $BannerCopyWith<$Res>? get bannerInfo {
+    if (_value.bannerInfo == null) {
+      return null;
+    }
+
+    return $BannerCopyWith<$Res>(_value.bannerInfo!, (value) {
+      return _then(_value.copyWith(bannerInfo: value) as $Val);
+    });
+  }
+}
+
+/// @nodoc
+abstract class _$$BannerListImplCopyWith<$Res>
+    implements $BannerListCopyWith<$Res> {
+  factory _$$BannerListImplCopyWith(
+    _$BannerListImpl value,
+    $Res Function(_$BannerListImpl) then,
+  ) = __$$BannerListImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({Location? location, Banner? bannerInfo, List<Banner>? list});
+
+  @override
+  $LocationCopyWith<$Res>? get location;
+  @override
+  $BannerCopyWith<$Res>? get bannerInfo;
+}
+
+/// @nodoc
+class __$$BannerListImplCopyWithImpl<$Res>
+    extends _$BannerListCopyWithImpl<$Res, _$BannerListImpl>
+    implements _$$BannerListImplCopyWith<$Res> {
+  __$$BannerListImplCopyWithImpl(
+    _$BannerListImpl _value,
+    $Res Function(_$BannerListImpl) _then,
+  ) : super(_value, _then);
+
+  /// Create a copy of BannerList
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? location = freezed,
+    Object? bannerInfo = freezed,
+    Object? list = freezed,
+  }) {
+    return _then(
+      _$BannerListImpl(
+        location: freezed == location
+            ? _value.location
+            : location // ignore: cast_nullable_to_non_nullable
+                  as Location?,
+        bannerInfo: freezed == bannerInfo
+            ? _value.bannerInfo
+            : bannerInfo // ignore: cast_nullable_to_non_nullable
+                  as Banner?,
+        list: freezed == list
+            ? _value._list
+            : list // ignore: cast_nullable_to_non_nullable
+                  as List<Banner>?,
+      ),
+    );
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$BannerListImpl with DiagnosticableTreeMixin implements _BannerList {
+  const _$BannerListImpl({
+    this.location,
+    this.bannerInfo,
+    final List<Banner>? list,
+  }) : _list = list;
+
+  factory _$BannerListImpl.fromJson(Map<String, dynamic> json) =>
+      _$$BannerListImplFromJson(json);
+
+  @override
+  final Location? location;
+  @override
+  final Banner? bannerInfo;
+  final List<Banner>? _list;
+  @override
+  List<Banner>? get list {
+    final value = _list;
+    if (value == null) return null;
+    if (_list is EqualUnmodifiableListView) return _list;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableListView(value);
+  }
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'BannerList(location: $location, bannerInfo: $bannerInfo, list: $list)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'BannerList'))
+      ..add(DiagnosticsProperty('location', location))
+      ..add(DiagnosticsProperty('bannerInfo', bannerInfo))
+      ..add(DiagnosticsProperty('list', list));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$BannerListImpl &&
+            (identical(other.location, location) ||
+                other.location == location) &&
+            (identical(other.bannerInfo, bannerInfo) ||
+                other.bannerInfo == bannerInfo) &&
+            const DeepCollectionEquality().equals(other._list, _list));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(
+    runtimeType,
+    location,
+    bannerInfo,
+    const DeepCollectionEquality().hash(_list),
+  );
+
+  /// Create a copy of BannerList
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$BannerListImplCopyWith<_$BannerListImpl> get copyWith =>
+      __$$BannerListImplCopyWithImpl<_$BannerListImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$BannerListImplToJson(this);
+  }
+}
+
+abstract class _BannerList implements BannerList {
+  const factory _BannerList({
+    final Location? location,
+    final Banner? bannerInfo,
+    final List<Banner>? list,
+  }) = _$BannerListImpl;
+
+  factory _BannerList.fromJson(Map<String, dynamic> json) =
+      _$BannerListImpl.fromJson;
+
+  @override
+  Location? get location;
+  @override
+  Banner? get bannerInfo;
+  @override
+  List<Banner>? get list;
+
+  /// Create a copy of BannerList
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$BannerListImplCopyWith<_$BannerListImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+Location _$LocationFromJson(Map<String, dynamic> json) {
+  return _Location.fromJson(json);
+}
+
+/// @nodoc
+mixin _$Location {
+  int? get id => throw _privateConstructorUsedError;
+  String? get name => throw _privateConstructorUsedError;
+  String? get code => throw _privateConstructorUsedError;
+  String? get icon => throw _privateConstructorUsedError;
+  String? get country => throw _privateConstructorUsedError;
+  int? get sort => throw _privateConstructorUsedError;
+  Coordinates? get coordinates => throw _privateConstructorUsedError;
+  int? get userLevel => throw _privateConstructorUsedError;
+  bool? get isTrial => throw _privateConstructorUsedError;
+  bool? get showGDPR => throw _privateConstructorUsedError;
+
+  /// Serializes this Location to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of Location
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $LocationCopyWith<Location> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $LocationCopyWith<$Res> {
+  factory $LocationCopyWith(Location value, $Res Function(Location) then) =
+      _$LocationCopyWithImpl<$Res, Location>;
+  @useResult
+  $Res call({
+    int? id,
+    String? name,
+    String? code,
+    String? icon,
+    String? country,
+    int? sort,
+    Coordinates? coordinates,
+    int? userLevel,
+    bool? isTrial,
+    bool? showGDPR,
+  });
+
+  $CoordinatesCopyWith<$Res>? get coordinates;
+}
+
+/// @nodoc
+class _$LocationCopyWithImpl<$Res, $Val extends Location>
+    implements $LocationCopyWith<$Res> {
+  _$LocationCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of Location
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = freezed,
+    Object? name = freezed,
+    Object? code = freezed,
+    Object? icon = freezed,
+    Object? country = freezed,
+    Object? sort = freezed,
+    Object? coordinates = freezed,
+    Object? userLevel = freezed,
+    Object? isTrial = freezed,
+    Object? showGDPR = freezed,
+  }) {
+    return _then(
+      _value.copyWith(
+            id: freezed == id
+                ? _value.id
+                : id // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            name: freezed == name
+                ? _value.name
+                : name // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            code: freezed == code
+                ? _value.code
+                : code // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            icon: freezed == icon
+                ? _value.icon
+                : icon // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            country: freezed == country
+                ? _value.country
+                : country // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            sort: freezed == sort
+                ? _value.sort
+                : sort // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            coordinates: freezed == coordinates
+                ? _value.coordinates
+                : coordinates // ignore: cast_nullable_to_non_nullable
+                      as Coordinates?,
+            userLevel: freezed == userLevel
+                ? _value.userLevel
+                : userLevel // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            isTrial: freezed == isTrial
+                ? _value.isTrial
+                : isTrial // ignore: cast_nullable_to_non_nullable
+                      as bool?,
+            showGDPR: freezed == showGDPR
+                ? _value.showGDPR
+                : showGDPR // ignore: cast_nullable_to_non_nullable
+                      as bool?,
+          )
+          as $Val,
+    );
+  }
+
+  /// Create a copy of Location
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $CoordinatesCopyWith<$Res>? get coordinates {
+    if (_value.coordinates == null) {
+      return null;
+    }
+
+    return $CoordinatesCopyWith<$Res>(_value.coordinates!, (value) {
+      return _then(_value.copyWith(coordinates: value) as $Val);
+    });
+  }
+}
+
+/// @nodoc
+abstract class _$$LocationImplCopyWith<$Res>
+    implements $LocationCopyWith<$Res> {
+  factory _$$LocationImplCopyWith(
+    _$LocationImpl value,
+    $Res Function(_$LocationImpl) then,
+  ) = __$$LocationImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({
+    int? id,
+    String? name,
+    String? code,
+    String? icon,
+    String? country,
+    int? sort,
+    Coordinates? coordinates,
+    int? userLevel,
+    bool? isTrial,
+    bool? showGDPR,
+  });
+
+  @override
+  $CoordinatesCopyWith<$Res>? get coordinates;
+}
+
+/// @nodoc
+class __$$LocationImplCopyWithImpl<$Res>
+    extends _$LocationCopyWithImpl<$Res, _$LocationImpl>
+    implements _$$LocationImplCopyWith<$Res> {
+  __$$LocationImplCopyWithImpl(
+    _$LocationImpl _value,
+    $Res Function(_$LocationImpl) _then,
+  ) : super(_value, _then);
+
+  /// Create a copy of Location
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = freezed,
+    Object? name = freezed,
+    Object? code = freezed,
+    Object? icon = freezed,
+    Object? country = freezed,
+    Object? sort = freezed,
+    Object? coordinates = freezed,
+    Object? userLevel = freezed,
+    Object? isTrial = freezed,
+    Object? showGDPR = freezed,
+  }) {
+    return _then(
+      _$LocationImpl(
+        id: freezed == id
+            ? _value.id
+            : id // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        name: freezed == name
+            ? _value.name
+            : name // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        code: freezed == code
+            ? _value.code
+            : code // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        icon: freezed == icon
+            ? _value.icon
+            : icon // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        country: freezed == country
+            ? _value.country
+            : country // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        sort: freezed == sort
+            ? _value.sort
+            : sort // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        coordinates: freezed == coordinates
+            ? _value.coordinates
+            : coordinates // ignore: cast_nullable_to_non_nullable
+                  as Coordinates?,
+        userLevel: freezed == userLevel
+            ? _value.userLevel
+            : userLevel // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        isTrial: freezed == isTrial
+            ? _value.isTrial
+            : isTrial // ignore: cast_nullable_to_non_nullable
+                  as bool?,
+        showGDPR: freezed == showGDPR
+            ? _value.showGDPR
+            : showGDPR // ignore: cast_nullable_to_non_nullable
+                  as bool?,
+      ),
+    );
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$LocationImpl with DiagnosticableTreeMixin implements _Location {
+  const _$LocationImpl({
+    this.id,
+    this.name,
+    this.code,
+    this.icon,
+    this.country,
+    this.sort,
+    this.coordinates,
+    this.userLevel,
+    this.isTrial,
+    this.showGDPR,
+  });
+
+  factory _$LocationImpl.fromJson(Map<String, dynamic> json) =>
+      _$$LocationImplFromJson(json);
+
+  @override
+  final int? id;
+  @override
+  final String? name;
+  @override
+  final String? code;
+  @override
+  final String? icon;
+  @override
+  final String? country;
+  @override
+  final int? sort;
+  @override
+  final Coordinates? coordinates;
+  @override
+  final int? userLevel;
+  @override
+  final bool? isTrial;
+  @override
+  final bool? showGDPR;
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'Location(id: $id, name: $name, code: $code, icon: $icon, country: $country, sort: $sort, coordinates: $coordinates, userLevel: $userLevel, isTrial: $isTrial, showGDPR: $showGDPR)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'Location'))
+      ..add(DiagnosticsProperty('id', id))
+      ..add(DiagnosticsProperty('name', name))
+      ..add(DiagnosticsProperty('code', code))
+      ..add(DiagnosticsProperty('icon', icon))
+      ..add(DiagnosticsProperty('country', country))
+      ..add(DiagnosticsProperty('sort', sort))
+      ..add(DiagnosticsProperty('coordinates', coordinates))
+      ..add(DiagnosticsProperty('userLevel', userLevel))
+      ..add(DiagnosticsProperty('isTrial', isTrial))
+      ..add(DiagnosticsProperty('showGDPR', showGDPR));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$LocationImpl &&
+            (identical(other.id, id) || other.id == id) &&
+            (identical(other.name, name) || other.name == name) &&
+            (identical(other.code, code) || other.code == code) &&
+            (identical(other.icon, icon) || other.icon == icon) &&
+            (identical(other.country, country) || other.country == country) &&
+            (identical(other.sort, sort) || other.sort == sort) &&
+            (identical(other.coordinates, coordinates) ||
+                other.coordinates == coordinates) &&
+            (identical(other.userLevel, userLevel) ||
+                other.userLevel == userLevel) &&
+            (identical(other.isTrial, isTrial) || other.isTrial == isTrial) &&
+            (identical(other.showGDPR, showGDPR) ||
+                other.showGDPR == showGDPR));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(
+    runtimeType,
+    id,
+    name,
+    code,
+    icon,
+    country,
+    sort,
+    coordinates,
+    userLevel,
+    isTrial,
+    showGDPR,
+  );
+
+  /// Create a copy of Location
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$LocationImplCopyWith<_$LocationImpl> get copyWith =>
+      __$$LocationImplCopyWithImpl<_$LocationImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$LocationImplToJson(this);
+  }
+}
+
+abstract class _Location implements Location {
+  const factory _Location({
+    final int? id,
+    final String? name,
+    final String? code,
+    final String? icon,
+    final String? country,
+    final int? sort,
+    final Coordinates? coordinates,
+    final int? userLevel,
+    final bool? isTrial,
+    final bool? showGDPR,
+  }) = _$LocationImpl;
+
+  factory _Location.fromJson(Map<String, dynamic> json) =
+      _$LocationImpl.fromJson;
+
+  @override
+  int? get id;
+  @override
+  String? get name;
+  @override
+  String? get code;
+  @override
+  String? get icon;
+  @override
+  String? get country;
+  @override
+  int? get sort;
+  @override
+  Coordinates? get coordinates;
+  @override
+  int? get userLevel;
+  @override
+  bool? get isTrial;
+  @override
+  bool? get showGDPR;
+
+  /// Create a copy of Location
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$LocationImplCopyWith<_$LocationImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+Coordinates _$CoordinatesFromJson(Map<String, dynamic> json) {
+  return _Coordinates.fromJson(json);
+}
+
+/// @nodoc
+mixin _$Coordinates {
+  int? get lat => throw _privateConstructorUsedError;
+  int? get lng => throw _privateConstructorUsedError;
+
+  /// Serializes this Coordinates to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of Coordinates
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $CoordinatesCopyWith<Coordinates> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $CoordinatesCopyWith<$Res> {
+  factory $CoordinatesCopyWith(
+    Coordinates value,
+    $Res Function(Coordinates) then,
+  ) = _$CoordinatesCopyWithImpl<$Res, Coordinates>;
+  @useResult
+  $Res call({int? lat, int? lng});
+}
+
+/// @nodoc
+class _$CoordinatesCopyWithImpl<$Res, $Val extends Coordinates>
+    implements $CoordinatesCopyWith<$Res> {
+  _$CoordinatesCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of Coordinates
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({Object? lat = freezed, Object? lng = freezed}) {
+    return _then(
+      _value.copyWith(
+            lat: freezed == lat
+                ? _value.lat
+                : lat // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            lng: freezed == lng
+                ? _value.lng
+                : lng // ignore: cast_nullable_to_non_nullable
+                      as int?,
+          )
+          as $Val,
+    );
+  }
+}
+
+/// @nodoc
+abstract class _$$CoordinatesImplCopyWith<$Res>
+    implements $CoordinatesCopyWith<$Res> {
+  factory _$$CoordinatesImplCopyWith(
+    _$CoordinatesImpl value,
+    $Res Function(_$CoordinatesImpl) then,
+  ) = __$$CoordinatesImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({int? lat, int? lng});
+}
+
+/// @nodoc
+class __$$CoordinatesImplCopyWithImpl<$Res>
+    extends _$CoordinatesCopyWithImpl<$Res, _$CoordinatesImpl>
+    implements _$$CoordinatesImplCopyWith<$Res> {
+  __$$CoordinatesImplCopyWithImpl(
+    _$CoordinatesImpl _value,
+    $Res Function(_$CoordinatesImpl) _then,
+  ) : super(_value, _then);
+
+  /// Create a copy of Coordinates
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({Object? lat = freezed, Object? lng = freezed}) {
+    return _then(
+      _$CoordinatesImpl(
+        lat: freezed == lat
+            ? _value.lat
+            : lat // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        lng: freezed == lng
+            ? _value.lng
+            : lng // ignore: cast_nullable_to_non_nullable
+                  as int?,
+      ),
+    );
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$CoordinatesImpl with DiagnosticableTreeMixin implements _Coordinates {
+  const _$CoordinatesImpl({this.lat, this.lng});
+
+  factory _$CoordinatesImpl.fromJson(Map<String, dynamic> json) =>
+      _$$CoordinatesImplFromJson(json);
+
+  @override
+  final int? lat;
+  @override
+  final int? lng;
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'Coordinates(lat: $lat, lng: $lng)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'Coordinates'))
+      ..add(DiagnosticsProperty('lat', lat))
+      ..add(DiagnosticsProperty('lng', lng));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$CoordinatesImpl &&
+            (identical(other.lat, lat) || other.lat == lat) &&
+            (identical(other.lng, lng) || other.lng == lng));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(runtimeType, lat, lng);
+
+  /// Create a copy of Coordinates
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$CoordinatesImplCopyWith<_$CoordinatesImpl> get copyWith =>
+      __$$CoordinatesImplCopyWithImpl<_$CoordinatesImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$CoordinatesImplToJson(this);
+  }
+}
+
+abstract class _Coordinates implements Coordinates {
+  const factory _Coordinates({final int? lat, final int? lng}) =
+      _$CoordinatesImpl;
+
+  factory _Coordinates.fromJson(Map<String, dynamic> json) =
+      _$CoordinatesImpl.fromJson;
+
+  @override
+  int? get lat;
+  @override
+  int? get lng;
+
+  /// Create a copy of Coordinates
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$CoordinatesImplCopyWith<_$CoordinatesImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+Banner _$BannerFromJson(Map<String, dynamic> json) {
+  return _Banner.fromJson(json);
+}
+
+/// @nodoc
+mixin _$Banner {
+  String? get action => throw _privateConstructorUsedError;
+  String? get img => throw _privateConstructorUsedError;
+  String? get title => throw _privateConstructorUsedError;
+  String? get content => throw _privateConstructorUsedError;
+  String? get data => throw _privateConstructorUsedError;
+
+  /// Serializes this Banner to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of Banner
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $BannerCopyWith<Banner> get copyWith => throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $BannerCopyWith<$Res> {
+  factory $BannerCopyWith(Banner value, $Res Function(Banner) then) =
+      _$BannerCopyWithImpl<$Res, Banner>;
+  @useResult
+  $Res call({
+    String? action,
+    String? img,
+    String? title,
+    String? content,
+    String? data,
+  });
+}
+
+/// @nodoc
+class _$BannerCopyWithImpl<$Res, $Val extends Banner>
+    implements $BannerCopyWith<$Res> {
+  _$BannerCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of Banner
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? action = freezed,
+    Object? img = freezed,
+    Object? title = freezed,
+    Object? content = freezed,
+    Object? data = freezed,
+  }) {
+    return _then(
+      _value.copyWith(
+            action: freezed == action
+                ? _value.action
+                : action // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            img: freezed == img
+                ? _value.img
+                : img // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            title: freezed == title
+                ? _value.title
+                : title // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            content: freezed == content
+                ? _value.content
+                : content // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            data: freezed == data
+                ? _value.data
+                : data // ignore: cast_nullable_to_non_nullable
+                      as String?,
+          )
+          as $Val,
+    );
+  }
+}
+
+/// @nodoc
+abstract class _$$BannerImplCopyWith<$Res> implements $BannerCopyWith<$Res> {
+  factory _$$BannerImplCopyWith(
+    _$BannerImpl value,
+    $Res Function(_$BannerImpl) then,
+  ) = __$$BannerImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({
+    String? action,
+    String? img,
+    String? title,
+    String? content,
+    String? data,
+  });
+}
+
+/// @nodoc
+class __$$BannerImplCopyWithImpl<$Res>
+    extends _$BannerCopyWithImpl<$Res, _$BannerImpl>
+    implements _$$BannerImplCopyWith<$Res> {
+  __$$BannerImplCopyWithImpl(
+    _$BannerImpl _value,
+    $Res Function(_$BannerImpl) _then,
+  ) : super(_value, _then);
+
+  /// Create a copy of Banner
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? action = freezed,
+    Object? img = freezed,
+    Object? title = freezed,
+    Object? content = freezed,
+    Object? data = freezed,
+  }) {
+    return _then(
+      _$BannerImpl(
+        action: freezed == action
+            ? _value.action
+            : action // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        img: freezed == img
+            ? _value.img
+            : img // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        title: freezed == title
+            ? _value.title
+            : title // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        content: freezed == content
+            ? _value.content
+            : content // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        data: freezed == data
+            ? _value.data
+            : data // ignore: cast_nullable_to_non_nullable
+                  as String?,
+      ),
+    );
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$BannerImpl with DiagnosticableTreeMixin implements _Banner {
+  const _$BannerImpl({
+    this.action,
+    this.img,
+    this.title,
+    this.content,
+    this.data,
+  });
+
+  factory _$BannerImpl.fromJson(Map<String, dynamic> json) =>
+      _$$BannerImplFromJson(json);
+
+  @override
+  final String? action;
+  @override
+  final String? img;
+  @override
+  final String? title;
+  @override
+  final String? content;
+  @override
+  final String? data;
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'Banner(action: $action, img: $img, title: $title, content: $content, data: $data)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'Banner'))
+      ..add(DiagnosticsProperty('action', action))
+      ..add(DiagnosticsProperty('img', img))
+      ..add(DiagnosticsProperty('title', title))
+      ..add(DiagnosticsProperty('content', content))
+      ..add(DiagnosticsProperty('data', data));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$BannerImpl &&
+            (identical(other.action, action) || other.action == action) &&
+            (identical(other.img, img) || other.img == img) &&
+            (identical(other.title, title) || other.title == title) &&
+            (identical(other.content, content) || other.content == content) &&
+            (identical(other.data, data) || other.data == data));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode =>
+      Object.hash(runtimeType, action, img, title, content, data);
+
+  /// Create a copy of Banner
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$BannerImplCopyWith<_$BannerImpl> get copyWith =>
+      __$$BannerImplCopyWithImpl<_$BannerImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$BannerImplToJson(this);
+  }
+}
+
+abstract class _Banner implements Banner {
+  const factory _Banner({
+    final String? action,
+    final String? img,
+    final String? title,
+    final String? content,
+    final String? data,
+  }) = _$BannerImpl;
+
+  factory _Banner.fromJson(Map<String, dynamic> json) = _$BannerImpl.fromJson;
+
+  @override
+  String? get action;
+  @override
+  String? get img;
+  @override
+  String? get title;
+  @override
+  String? get content;
+  @override
+  String? get data;
+
+  /// Create a copy of Banner
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$BannerImplCopyWith<_$BannerImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}

+ 83 - 0
lib/app/data/models/banner/banner_list.g.dart

@@ -0,0 +1,83 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'banner_list.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$BannerListImpl _$$BannerListImplFromJson(Map<String, dynamic> json) =>
+    _$BannerListImpl(
+      location: json['location'] == null
+          ? null
+          : Location.fromJson(json['location'] as Map<String, dynamic>),
+      bannerInfo: json['bannerInfo'] == null
+          ? null
+          : Banner.fromJson(json['bannerInfo'] as Map<String, dynamic>),
+      list: (json['list'] as List<dynamic>?)
+          ?.map((e) => Banner.fromJson(e as Map<String, dynamic>))
+          .toList(),
+    );
+
+Map<String, dynamic> _$$BannerListImplToJson(_$BannerListImpl instance) =>
+    <String, dynamic>{
+      'location': instance.location,
+      'bannerInfo': instance.bannerInfo,
+      'list': instance.list,
+    };
+
+_$LocationImpl _$$LocationImplFromJson(Map<String, dynamic> json) =>
+    _$LocationImpl(
+      id: (json['id'] as num?)?.toInt(),
+      name: json['name'] as String?,
+      code: json['code'] as String?,
+      icon: json['icon'] as String?,
+      country: json['country'] as String?,
+      sort: (json['sort'] as num?)?.toInt(),
+      coordinates: json['coordinates'] == null
+          ? null
+          : Coordinates.fromJson(json['coordinates'] as Map<String, dynamic>),
+      userLevel: (json['userLevel'] as num?)?.toInt(),
+      isTrial: json['isTrial'] as bool?,
+      showGDPR: json['showGDPR'] as bool?,
+    );
+
+Map<String, dynamic> _$$LocationImplToJson(_$LocationImpl instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'name': instance.name,
+      'code': instance.code,
+      'icon': instance.icon,
+      'country': instance.country,
+      'sort': instance.sort,
+      'coordinates': instance.coordinates,
+      'userLevel': instance.userLevel,
+      'isTrial': instance.isTrial,
+      'showGDPR': instance.showGDPR,
+    };
+
+_$CoordinatesImpl _$$CoordinatesImplFromJson(Map<String, dynamic> json) =>
+    _$CoordinatesImpl(
+      lat: (json['lat'] as num?)?.toInt(),
+      lng: (json['lng'] as num?)?.toInt(),
+    );
+
+Map<String, dynamic> _$$CoordinatesImplToJson(_$CoordinatesImpl instance) =>
+    <String, dynamic>{'lat': instance.lat, 'lng': instance.lng};
+
+_$BannerImpl _$$BannerImplFromJson(Map<String, dynamic> json) => _$BannerImpl(
+  action: json['action'] as String?,
+  img: json['img'] as String?,
+  title: json['title'] as String?,
+  content: json['content'] as String?,
+  data: json['data'] as String?,
+);
+
+Map<String, dynamic> _$$BannerImplToJson(_$BannerImpl instance) =>
+    <String, dynamic>{
+      'action': instance.action,
+      'img': instance.img,
+      'title': instance.title,
+      'content': instance.content,
+      'data': instance.data,
+    };

+ 82 - 65
lib/app/data/sp/ix_sp.dart

@@ -7,6 +7,8 @@ import '../../../config/translations/localization_service.dart';
 import '../../../utils/crypto.dart';
 import '../../../utils/log/logger.dart';
 import '../../constants/keys.dart';
+import '../../constants/sp_keys.dart';
+import '../models/banner/banner_list.dart';
 import '../models/launch/app_config.dart';
 import '../models/launch/groups.dart';
 import '../models/launch/launch.dart';
@@ -22,31 +24,6 @@ class IXSP {
   // get storage
   static late SharedPreferences _sharedPreferences;
 
-  // STORING KEYS
-  /// Key for storing the device ID
-  static const String _deviceIdKey = 'omon_device_id';
-  static const String _fcmTokenKey = 'fcm_token';
-  static const String _currentLocalKey = 'current_local';
-  static const String _lightThemeKey = 'is_theme_light';
-  static const String _launchGame = 'is_launch_game';
-  static const String _ignoreVersionKey = 'ignore_version';
-  static const String _isNewInstallKey = 'is_new_install';
-  static const String _launchDataKey = 'launch_data';
-  //记录最后一次是否是地区禁用
-  static const String _isRegionDisabledKey = 'is_region_disabled';
-  //记录最后一次是否是用户禁用
-  static const String _isUserDisabledKey = 'is_user_disabled';
-  //记录最后一次是否是设备禁用
-  static const String _isDeviceDisabledKey = 'is_device_disabled';
-  //记录最后一次上传的性能日志boostSessionId
-  static const String _lastMetricsLogKey = 'last_metrics_log';
-  //记录最后一次上传的es日志boostSessionId
-  static const String _lastESLogKey = 'last_es_log';
-  //记录是否开启debug log
-  static const String _enableDebugLogKey = 'enable_debug_log';
-  //记录是否开启ping 0默认 1开启 2关闭
-  static const String _enablePingModeKey = 'enable_ping_mode';
-
   /// init get storage services
   static Future<void> init() async {
     _sharedPreferences = await SharedPreferences.getInstance();
@@ -58,20 +35,20 @@ class IXSP {
 
   /// set theme current type as light theme
   static Future<void> setThemeIsLight(bool lightTheme) =>
-      _sharedPreferences.setBool(_lightThemeKey, lightTheme);
+      _sharedPreferences.setBool(SPKeys.lightTheme, lightTheme);
 
   /// get if the current theme type is light
   static bool getThemeIsLight() =>
-      _sharedPreferences.getBool(_lightThemeKey) ??
+      _sharedPreferences.getBool(SPKeys.lightTheme) ??
       false; // todo set the default theme (true for light, false for dark)
 
   /// save current locale
   static Future<void> setCurrentLanguage(String languageCode) =>
-      _sharedPreferences.setString(_currentLocalKey, languageCode);
+      _sharedPreferences.setString(SPKeys.currentLocal, languageCode);
 
   /// get current locale
   static Locale getCurrentLocal() {
-    String? langCode = _sharedPreferences.getString(_currentLocalKey);
+    String? langCode = _sharedPreferences.getString(SPKeys.currentLocal);
     // default language is english
     if (langCode == null) {
       return LocalizationService.defaultLanguage;
@@ -81,116 +58,111 @@ class IXSP {
 
   /// save generated fcm token
   static Future<void> setFcmToken(String token) =>
-      _sharedPreferences.setString(_fcmTokenKey, token);
+      _sharedPreferences.setString(SPKeys.fcmToken, token);
 
   /// get authorization token
   static String getFcmToken() =>
-      _sharedPreferences.getString(_fcmTokenKey) ?? '';
+      _sharedPreferences.getString(SPKeys.fcmToken) ?? '';
 
   /// set launch game
   static Future<void> setLaunchGame(bool isLaunch) =>
-      _sharedPreferences.setBool(_launchGame, isLaunch);
+      _sharedPreferences.setBool(SPKeys.launchGame, isLaunch);
 
   /// get launch game
   static bool getLaunchGame() =>
-      _sharedPreferences.getBool(_launchGame) ?? false;
+      _sharedPreferences.getBool(SPKeys.launchGame) ?? false;
 
   /// clear all data from shared pref
   static Future<void> clear() async => await _sharedPreferences.clear();
 
   /// Save unique ID to shared preferences
   static Future<void> setDeviceId(String deviceId) =>
-      _sharedPreferences.setString(_deviceIdKey, deviceId);
+      _sharedPreferences.setString(SPKeys.deviceId, deviceId);
 
   /// Get unique ID from shared preferences
-  static String? getDeviceId() => _sharedPreferences.getString(_deviceIdKey);
+  static String? getDeviceId() => _sharedPreferences.getString(SPKeys.deviceId);
 
   /// Save ignore version to shared preferences
   static Future<void> setIgnoreVersion(int version) =>
-      _sharedPreferences.setInt(_ignoreVersionKey, version);
+      _sharedPreferences.setInt(SPKeys.ignoreVersion, version);
 
   /// Get ignore version from shared preferences
   static int getIgnoreVersion() =>
-      _sharedPreferences.getInt(_ignoreVersionKey) ?? 0;
+      _sharedPreferences.getInt(SPKeys.ignoreVersion) ?? 0;
 
   /// Save is new install to shared preferences
   static Future<void> setIsNewInstall(bool isNewInstall) =>
-      _sharedPreferences.setBool(_isNewInstallKey, isNewInstall);
+      _sharedPreferences.setBool(SPKeys.isNewInstall, isNewInstall);
 
   /// Get is new install from shared preferences
   static bool getIsNewInstall() =>
-      _sharedPreferences.getBool(_isNewInstallKey) ?? true;
+      _sharedPreferences.getBool(SPKeys.isNewInstall) ?? true;
 
   // 记录最后一次是否是用户禁用
   static Future<void> setLastIsUserDisabled(bool isUserDisabled) async {
-    await _sharedPreferences.setBool(_isUserDisabledKey, isUserDisabled);
+    await _sharedPreferences.setBool(SPKeys.isUserDisabled, isUserDisabled);
   }
 
   // 获取最后一次是否是用户禁用
   static bool getLastIsUserDisabled() {
-    return _sharedPreferences.getBool(_isUserDisabledKey) ?? false;
+    return _sharedPreferences.getBool(SPKeys.isUserDisabled) ?? false;
   }
 
   // 记录最后一次是否是地区禁用
   static Future<void> setLastIsRegionDisabled(bool isRegionDisabled) async {
-    await _sharedPreferences.setBool(_isRegionDisabledKey, isRegionDisabled);
+    await _sharedPreferences.setBool(SPKeys.isRegionDisabled, isRegionDisabled);
   }
 
   // 获取最后一次是否是地区禁用
   static bool getLastIsRegionDisabled() {
-    return _sharedPreferences.getBool(_isRegionDisabledKey) ?? false;
+    return _sharedPreferences.getBool(SPKeys.isRegionDisabled) ?? false;
   }
 
   // 记录最后一次是否是设备禁用
   static Future<void> setLastIsDeviceDisabled(bool isDeviceDisabled) async {
-    await _sharedPreferences.setBool(_isDeviceDisabledKey, isDeviceDisabled);
+    await _sharedPreferences.setBool(SPKeys.isDeviceDisabled, isDeviceDisabled);
   }
 
   // 获取最后一次是否是设备禁用
   static bool getLastIsDeviceDisabled() {
-    return _sharedPreferences.getBool(_isDeviceDisabledKey) ?? false;
+    return _sharedPreferences.getBool(SPKeys.isDeviceDisabled) ?? false;
   }
 
   /// Save last metrics log
   static Future<void> setLastMetricsLog(String log) async {
-    await _sharedPreferences.setString(_lastMetricsLogKey, log);
+    await _sharedPreferences.setString(SPKeys.lastMetricsLog, log);
   }
 
   /// Get last metrics log
   static String getLastMetricsLog() =>
-      _sharedPreferences.getString(_lastMetricsLogKey) ?? '';
+      _sharedPreferences.getString(SPKeys.lastMetricsLog) ?? '';
 
   /// Save last es log
   static Future<void> setLastESLog(String log) async {
-    await _sharedPreferences.setString(_lastESLogKey, log);
+    await _sharedPreferences.setString(SPKeys.lastESLog, log);
   }
 
   /// Get last es log
   static String getLastESLog() =>
-      _sharedPreferences.getString(_lastESLogKey) ?? '';
+      _sharedPreferences.getString(SPKeys.lastESLog) ?? '';
 
   /// Save enable debug log
   static Future<void> setEnableDebugLog(bool enable) async {
-    await _sharedPreferences.setBool(_enableDebugLogKey, enable);
+    await _sharedPreferences.setBool(SPKeys.enableDebugLog, enable);
   }
 
   /// Get enable debug log
   static bool getEnableDebugLog() =>
-      _sharedPreferences.getBool(_enableDebugLogKey) ?? false;
+      _sharedPreferences.getBool(SPKeys.enableDebugLog) ?? false;
 
   /// Save enable ping
   static Future<void> setEnablePingMode(int model) async {
-    await _sharedPreferences.setInt(_enablePingModeKey, model);
+    await _sharedPreferences.setInt(SPKeys.enablePingMode, model);
   }
 
   /// Get enable ping
   static int getEnablePingMode() =>
-      _sharedPreferences.getInt(_enablePingModeKey) ?? 0;
-
-  //记录当前选中的节点
-  static const String _selectedLocationKey = 'selected_location';
-  //记录最近选择的节点列表(最多3个)
-  static const String _recentLocationsKey = 'recent_locations';
+      _sharedPreferences.getInt(SPKeys.enablePingMode) ?? 0;
 
   /// 保存当前选中的节点
   static Future<void> saveSelectedLocation(
@@ -198,7 +170,7 @@ class IXSP {
   ) async {
     try {
       await _sharedPreferences.setString(
-        _selectedLocationKey,
+        SPKeys.selectedLocation,
         jsonEncode(location),
       );
       log('IXSP', 'Selected location saved successfully');
@@ -210,7 +182,7 @@ class IXSP {
   /// 获取当前选中的节点
   static Map<String, dynamic>? getSelectedLocation() {
     try {
-      final jsonData = _sharedPreferences.getString(_selectedLocationKey);
+      final jsonData = _sharedPreferences.getString(SPKeys.selectedLocation);
       if (jsonData == null) {
         return null;
       }
@@ -227,7 +199,7 @@ class IXSP {
   ) async {
     try {
       await _sharedPreferences.setString(
-        _recentLocationsKey,
+        SPKeys.recentLocations,
         jsonEncode(locations),
       );
       log('IXSP', 'Recent locations saved successfully');
@@ -239,7 +211,7 @@ class IXSP {
   /// 获取最近选择的节点列表
   static List<Map<String, dynamic>> getRecentLocations() {
     try {
-      final jsonData = _sharedPreferences.getString(_recentLocationsKey);
+      final jsonData = _sharedPreferences.getString(SPKeys.recentLocations);
       if (jsonData == null || jsonData.isEmpty) {
         return [];
       }
@@ -263,7 +235,7 @@ class IXSP {
 
       // 保存数据
       await _sharedPreferences.setString(
-        _launchDataKey,
+        SPKeys.launchData,
         Crypto.encrypt(jsonData, Keys.aesKey, Keys.aesIv),
       );
 
@@ -320,7 +292,7 @@ class IXSP {
   /// 获取 Launch 数据
   static Launch? getLaunch() {
     try {
-      final jsonData = _sharedPreferences.getString(_launchDataKey);
+      final jsonData = _sharedPreferences.getString(SPKeys.launchData);
 
       if (jsonData == null) {
         return null;
@@ -378,7 +350,7 @@ class IXSP {
   /// 清除 Launch 数据
   static Future<bool> clearLaunchData() async {
     try {
-      await _sharedPreferences.remove(_launchDataKey);
+      await _sharedPreferences.remove(SPKeys.launchData);
       log('IXSP', 'Launch data cleared');
       return true;
     } catch (e) {
@@ -401,4 +373,49 @@ class IXSP {
       return false;
     }
   }
+
+  /// 保存 Banner 缓存
+  /// [position] banner 位置类型,如 "boost"、"home" 等
+  static Future<bool> saveBanner(String position, BannerList bannerList) async {
+    try {
+      final key = SPKeys.bannerCacheKey(position);
+      final jsonData = jsonEncode(bannerList.toJson());
+      await _sharedPreferences.setString(key, jsonData);
+      log('IXSP', 'Banner cache saved for position: $position');
+      return true;
+    } catch (e) {
+      log('IXSP', 'Error saving banner cache for position $position: $e');
+      return false;
+    }
+  }
+
+  /// 获取 Banner 缓存
+  /// [position] banner 位置类型,如 "boost"、"home" 等
+  static BannerList? getBanner(String position) {
+    try {
+      final key = SPKeys.bannerCacheKey(position);
+      final jsonData = _sharedPreferences.getString(key);
+      if (jsonData == null) {
+        return null;
+      }
+      final Map<String, dynamic> map = jsonDecode(jsonData);
+      return BannerList.fromJson(map);
+    } catch (e) {
+      log('IXSP', 'Error getting banner cache for position $position: $e');
+      return null;
+    }
+  }
+
+  /// 清除指定位置的 Banner 缓存
+  static Future<bool> clearBanner(String position) async {
+    try {
+      final key = SPKeys.bannerCacheKey(position);
+      await _sharedPreferences.remove(key);
+      log('IXSP', 'Banner cache cleared for position: $position');
+      return true;
+    } catch (e) {
+      log('IXSP', 'Error clearing banner cache for position $position: $e');
+      return false;
+    }
+  }
 }

+ 32 - 10
lib/app/modules/account/views/account_view.dart

@@ -12,6 +12,8 @@ import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import '../../../../config/theme/dark_theme_colors.dart';
 import '../../../../config/translations/strings_enum.dart';
 import '../../../../utils/device_manager.dart';
+import '../../../constants/enums.dart';
+import '../../../data/sp/ix_sp.dart';
 import '../../../components/ix_snackbar.dart';
 import '../../../constants/assets.dart';
 import '../../../constants/iconfont/iconfont.dart';
@@ -342,16 +344,25 @@ class AccountView extends BaseView<AccountController> {
             _buildSettingItem(
               icon: IconFont.icon29,
               iconColor: Get.reactiveTheme.shadowColor,
-              title: Strings.account.tr,
-              trailing: IXImage(
-                source: controller.apiController.userLevel == 3
-                    ? Assets.premium
-                    : controller.apiController.userLevel == 9999
-                    ? Assets.test
-                    : Assets.free,
-                width: controller.apiController.userLevel == 3 ? 92.w : 64.w,
-                height: 28.w,
-                sourceType: ImageSourceType.asset,
+              title: _getUserAccount().isNotEmpty
+                  ? _getUserAccount()
+                  : Strings.account.tr,
+              trailing: Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  IXImage(
+                    source: controller.apiController.userLevel == 3
+                        ? Assets.premium
+                        : controller.apiController.userLevel == 9999
+                        ? Assets.test
+                        : Assets.free,
+                    width: controller.apiController.userLevel == 3
+                        ? 92.w
+                        : 64.w,
+                    height: 28.w,
+                    sourceType: ImageSourceType.asset,
+                  ),
+                ],
               ),
               onTap: () {
                 Get.toNamed(Routes.ACCOUNT);
@@ -638,4 +649,15 @@ class AccountView extends BaseView<AccountController> {
       ),
     );
   }
+
+  /// 获取用户账号显示文本
+  String _getUserAccount() {
+    final user = IXSP.getUser();
+    if (user == null) return '';
+
+    if (user.memberLevel == MemberLevel.normal.level) {
+      return user.account?.username ?? '';
+    }
+    return '';
+  }
 }

+ 114 - 11
lib/app/modules/home/controllers/home_controller.dart

@@ -1,14 +1,18 @@
 import 'package:get/get.dart';
 import 'package:nomo/app/controllers/api_controller.dart';
 import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
+import '../../../../utils/system_helper.dart';
 import '../../../controllers/base_core_api.dart';
 import '../../../../utils/awesome_notifications_helper.dart';
 import '../../../../utils/log/logger.dart';
 import '../../../base/base_controller.dart';
 import '../../../constants/enums.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 '../../../routes/app_pages.dart';
 
 /// 主页控制器
 class HomeController extends BaseController {
@@ -19,17 +23,9 @@ class HomeController extends BaseController {
   final _refreshController = RefreshController(initialRefresh: false);
   RefreshController get refreshController => _refreshController;
 
-  final _isOn = false.obs;
-  bool get isOn => _isOn.value;
-  set isOn(bool value) => _isOn.value = value;
-
-  final _currentIndex = 0.obs;
-  int get currentIndex => _currentIndex.value;
-  set currentIndex(int value) => _currentIndex.value = value;
-
-  final _lastIndex = 0.obs;
-  int get lastIndex => _lastIndex.value;
-  set lastIndex(int value) => _lastIndex.value = value;
+  final _currentBannerIndex = 0.obs;
+  int get currentBannerIndex => _currentBannerIndex.value;
+  set currentBannerIndex(int value) => _currentBannerIndex.value = value;
 
   // 最近位置是否展开
   final _isRecentLocationsExpanded = false.obs;
@@ -64,11 +60,23 @@ class HomeController extends BaseController {
   List<Locations> get recentLocations =>
       _recentLocations.where((loc) => loc.id != selectedLocation?.id).toList();
 
+  // Banner 列表
+  final _bannerList = <Banner>[].obs;
+  List<Banner> get bannerList => _bannerList;
+  set bannerList(List<Banner> value) => _bannerList.assignAll(value);
+
+  // Banner 列表
+  final _nineBannerList = <Banner>[].obs;
+  List<Banner> get nineBannerList => _nineBannerList;
+  set nineBannerList(List<Banner> value) => _nineBannerList.assignAll(value);
+
   @override
   void onInit() {
     super.onInit();
     _initializeLocations();
     AwesomeNotificationsHelper.init();
+    getBanner(position: 'nine');
+    getBanner(position: 'banner');
   }
 
   /// 初始化位置数据
@@ -235,6 +243,8 @@ class HomeController extends BaseController {
   void onRefresh() async {
     try {
       await apiController.launch();
+      getBanner(position: 'nine', isCache: false);
+      getBanner(position: 'banner', isCache: false);
       refreshController.refreshCompleted();
     } catch (e) {
       refreshController.refreshFailed();
@@ -252,4 +262,97 @@ class HomeController extends BaseController {
     coreController.locationSelectionType = 'auto';
     coreController.handleConnection();
   }
+
+  /// 获取 banner 列表
+  /// [position] banner 位置类型,如 "banner"、"media"、"nine" 等
+  Future<void> getBanner({
+    String position = 'banner',
+    bool isCache = true,
+  }) async {
+    try {
+      // 先读取缓存数据
+      final cacheBanners = IXSP.getBanner(position);
+      if (cacheBanners != null &&
+          cacheBanners.list != null &&
+          cacheBanners.list!.isNotEmpty &&
+          isCache) {
+        if (position == 'banner') {
+          bannerList = cacheBanners.list!;
+        } else if (position == 'nine') {
+          nineBannerList = cacheBanners.list!;
+        }
+        log(TAG, 'Loaded banner from cache for position: $position');
+      }
+
+      // 请求最新数据
+      final banners = await apiController.getBanner(position: position);
+      if (banners.list != null && banners.list!.isNotEmpty) {
+        if (position == 'banner') {
+          bannerList = banners.list!;
+        } else if (position == 'nine') {
+          nineBannerList = banners.list!;
+        }
+        // 保存到缓存
+        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
+        }
+        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);
+        }
+        break;
+    }
+  }
 }

+ 106 - 25
lib/app/modules/home/views/home_view.dart

@@ -16,7 +16,6 @@ import '../../../widgets/click_opacity.dart';
 import '../../../widgets/country_icon.dart';
 import '../../../widgets/ix_image.dart';
 import '../controllers/home_controller.dart';
-import '../../../data/sp/ix_sp.dart';
 
 import '../widgets/connection_round_button.dart';
 import '../widgets/menu_list.dart';
@@ -96,25 +95,108 @@ class HomeView extends BaseView<HomeController> {
                                     padding: EdgeInsets.symmetric(
                                       vertical: 14.w,
                                     ),
-                                    child: CarouselSlider(
-                                      options: CarouselOptions(
-                                        height: 80.w,
-                                        viewportFraction: 1.0,
-                                      ),
-                                      items: [1, 2, 3, 4, 5].map((i) {
-                                        return Builder(
-                                          builder: (BuildContext context) {
-                                            return IXImage(
-                                              source: Assets.bannerTest,
-                                              width: double.infinity,
+                                    child: Obx(() {
+                                      if (controller.bannerList.isEmpty) {
+                                        return SizedBox.shrink();
+                                      }
+                                      return Stack(
+                                        children: [
+                                          CarouselSlider(
+                                            options: CarouselOptions(
                                               height: 80.w,
-                                              sourceType: ImageSourceType.asset,
-                                              borderRadius: 14.r,
-                                            );
-                                          },
-                                        );
-                                      }).toList(),
-                                    ),
+                                              viewportFraction: 1.0,
+                                              autoPlay:
+                                                  controller.bannerList.length >
+                                                  1,
+                                              autoPlayInterval: const Duration(
+                                                seconds: 5,
+                                              ),
+                                              onPageChanged: (index, reason) {
+                                                controller.currentBannerIndex =
+                                                    index;
+                                              },
+                                            ),
+                                            items: controller.bannerList.map((
+                                              banner,
+                                            ) {
+                                              return Builder(
+                                                builder:
+                                                    (BuildContext context) {
+                                                      return GestureDetector(
+                                                        onTap: () => controller
+                                                            .onBannerTap(
+                                                              banner,
+                                                            ),
+                                                        child: IXImage(
+                                                          source:
+                                                              banner.img ?? '',
+                                                          width:
+                                                              double.infinity,
+                                                          height: 80.w,
+                                                          sourceType:
+                                                              ImageSourceType
+                                                                  .network,
+                                                          borderRadius: 14.r,
+                                                        ),
+                                                      );
+                                                    },
+                                              );
+                                            }).toList(),
+                                          ),
+                                          if (controller.bannerList.length > 1)
+                                            Positioned(
+                                              bottom: 0,
+                                              left: 0,
+                                              right: 0,
+                                              child: Row(
+                                                mainAxisAlignment:
+                                                    MainAxisAlignment.center,
+                                                children: controller.bannerList
+                                                    .asMap()
+                                                    .entries
+                                                    .map((entry) {
+                                                      return AnimatedContainer(
+                                                        duration:
+                                                            const Duration(
+                                                              milliseconds: 300,
+                                                            ),
+                                                        curve: Curves.easeInOut,
+                                                        width:
+                                                            controller
+                                                                    .currentBannerIndex ==
+                                                                entry.key
+                                                            ? 16
+                                                            : 6,
+                                                        height: 6,
+                                                        margin:
+                                                            const EdgeInsets.symmetric(
+                                                              vertical: 8.0,
+                                                              horizontal: 2.0,
+                                                            ),
+                                                        decoration: BoxDecoration(
+                                                          borderRadius:
+                                                              BorderRadius.circular(
+                                                                6,
+                                                              ),
+                                                          color: Colors.white
+                                                              .withValues(
+                                                                alpha:
+                                                                    controller
+                                                                            .currentBannerIndex ==
+                                                                        entry
+                                                                            .key
+                                                                    ? 0.9
+                                                                    : 0.4,
+                                                              ),
+                                                        ),
+                                                      );
+                                                    })
+                                                    .toList(),
+                                              ),
+                                            ),
+                                        ],
+                                      );
+                                    }),
                                   ),
                                   MenuList(),
                                 ],
@@ -274,12 +356,11 @@ class HomeView extends BaseView<HomeController> {
     // );
 
     return Obx(() {
-      final vipRemainNoticeSeconds =
-          (IXSP.getAppConfig()?.vipRemainNotice ?? 600) * 60;
-      final showCountdown =
-          controller.apiController.remainTimeSeconds > 0 &&
-          controller.apiController.remainTimeSeconds < vipRemainNoticeSeconds;
-      if (!showCountdown) return const SizedBox.shrink();
+      // 只监听 shouldShowCountdown 和 remainTimeFormatted
+      // 只有文案或显示状态变化时才更新 UI
+      if (!controller.apiController.shouldShowCountdown) {
+        return const SizedBox.shrink();
+      }
       return Text(
         '${controller.apiController.remainTimeFormatted} ',
         style: TextStyle(

+ 48 - 109
lib/app/modules/home/widgets/menu_list.dart

@@ -1,32 +1,16 @@
-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/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';
-
-/// 菜单项数据模型
-class MenuItem {
-  final IconData icon;
-  final String title;
-  final Color iconColor;
-  final VoidCallback? onTap;
-
-  MenuItem({
-    required this.icon,
-    required this.title,
-    required this.iconColor,
-    this.onTap,
-  });
-}
+import '../../../data/models/banner/banner_list.dart';
+import '../../../widgets/ix_image.dart';
+import '../controllers/home_controller.dart';
 
 /// 菜单列表组件
 /// 支持放在 SliverFillRemaining 中
 /// 排列规则:
+/// - 9个:3排,每排3个
 /// - 6个:2排,每排3个
 /// - 5个:2排,第一排3个,第二排2个
 /// - 4个:2排,每排2个
@@ -36,120 +20,70 @@ class MenuItem {
 class MenuList extends StatelessWidget {
   const MenuList({super.key});
 
-  // 获取菜单项列表(最多6个)
-  List<MenuItem> _getMenuItems() {
-    return [
-      MenuItem(
-        icon: IconFont.icon19,
-        title: Strings.moviesAndTV.tr,
-        iconColor: const Color(0xFFFF3B30),
-        onTap: () {
-          print('Movies&TV tapped');
-          Get.toNamed(Routes.MEDIALOCATION);
-        },
-      ),
-      MenuItem(
-        icon: IconFont.icon26,
-        title: Strings.social.tr,
-        iconColor: const Color(0xFF007AFF),
-        onTap: () {
-          print('Social tapped');
-          SystemHelper.openTestPage();
-        },
-      ),
-      MenuItem(
-        icon: IconFont.icon28,
-        title: Strings.support.tr,
-        iconColor: const Color(0xFF34C759),
-        onTap: () {
-          print('Support tapped');
-          AllDialog.showUpdate(hasForceUpdate: true);
-        },
-      ),
-      MenuItem(
-        icon: IconFont.icon41,
-        title: Strings.sport.tr,
-        iconColor: const Color(0xFFFF9500),
-        onTap: () {
-          print('Sport tapped');
-        },
-      ),
-      MenuItem(
-        icon: IconFont.icon52,
-        title: Strings.music.tr,
-        iconColor: const Color(0xFF00C7BE),
-        onTap: () {
-          print('Music tapped');
-        },
-      ),
-      MenuItem(
-        icon: IconFont.icon53,
-        title: Strings.game.tr,
-        iconColor: const Color(0xFFAF52DE),
-        onTap: () {
-          print('Game tapped');
-        },
-      ),
-    ];
-  }
-
   /// 根据数量计算每排的布局
-  /// 返回一个二维列表,每个子列表表示一排的菜单项
-  List<List<MenuItem>> _calculateLayout(List<MenuItem> items) {
-    final count = items.length.clamp(0, 6);
+  /// 返回一个二维列表,每个子列表表示一排的 Banner
+  List<List<Banner>> _calculateLayout(List<Banner> items) {
+    final count = items.length.clamp(0, 9);
     if (count == 0) return [];
 
     switch (count) {
       case 1:
-        // 1个:1排占满
         return [items.sublist(0, 1)];
       case 2:
-        // 2个:1排,每排2个
         return [items.sublist(0, 2)];
       case 3:
-        // 3个:1排,每排3个
         return [items.sublist(0, 3)];
       case 4:
-        // 4个:2排,每排2个
         return [items.sublist(0, 2), items.sublist(2, 4)];
       case 5:
-        // 5个:2排,第一排3个,第二排2个
         return [items.sublist(0, 3), items.sublist(3, 5)];
       case 6:
-      default:
-        // 6个:2排,每排3个
         return [items.sublist(0, 3), items.sublist(3, 6)];
+      case 7:
+        return [items.sublist(0, 3), items.sublist(3, 6), items.sublist(6, 7)];
+      case 8:
+        return [items.sublist(0, 3), items.sublist(3, 6), items.sublist(6, 8)];
+      case 9:
+      default:
+        return [items.sublist(0, 3), items.sublist(3, 6), items.sublist(6, 9)];
     }
   }
 
   @override
   Widget build(BuildContext context) {
-    final menuItems = _getMenuItems();
-    final layout = _calculateLayout(menuItems);
+    final controller = Get.find<HomeController>();
 
-    return Column(
-      mainAxisSize: MainAxisSize.min,
-      children: layout.asMap().entries.map((entry) {
-        final rowIndex = entry.key;
-        final rowItems = entry.value;
-        return Padding(
-          padding: EdgeInsets.only(top: rowIndex > 0 ? 8.w : 0),
-          child: _buildRow(rowItems),
-        );
-      }).toList(),
-    );
+    return Obx(() {
+      if (controller.nineBannerList.isEmpty) {
+        return const SizedBox.shrink();
+      }
+
+      final layout = _calculateLayout(controller.nineBannerList);
+
+      return Column(
+        mainAxisSize: MainAxisSize.min,
+        children: layout.asMap().entries.map((entry) {
+          final rowIndex = entry.key;
+          final rowItems = entry.value;
+          return Padding(
+            padding: EdgeInsets.only(top: rowIndex > 0 ? 8.w : 0),
+            child: _buildRow(rowItems, controller),
+          );
+        }).toList(),
+      );
+    });
   }
 
   /// 构建一排菜单项
-  Widget _buildRow(List<MenuItem> items) {
+  Widget _buildRow(List<Banner> items, HomeController controller) {
     return Row(
       children: items.asMap().entries.map((entry) {
         final index = entry.key;
-        final item = entry.value;
+        final banner = entry.value;
         return Expanded(
           child: Padding(
             padding: EdgeInsets.only(left: index > 0 ? 8.w : 0),
-            child: _buildMenuItem(item),
+            child: _buildMenuItem(banner, controller),
           ),
         );
       }).toList(),
@@ -157,9 +91,9 @@ class MenuList extends StatelessWidget {
   }
 
   /// 构建单个菜单项
-  Widget _buildMenuItem(MenuItem item) {
+  Widget _buildMenuItem(Banner banner, HomeController controller) {
     return GestureDetector(
-      onTap: item.onTap,
+      onTap: () => controller.onBannerTap(banner),
       child: Container(
         height: 56.w,
         decoration: BoxDecoration(
@@ -169,12 +103,17 @@ class MenuList extends StatelessWidget {
         child: Column(
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
-            // 图标
-            Icon(item.icon, size: 20.w, color: item.iconColor),
+            // 图标(网络图片)
+            IXImage(
+              source: banner.img ?? '',
+              width: 20.w,
+              height: 20.w,
+              sourceType: ImageSourceType.network,
+            ),
             4.verticalSpaceFromWidth,
             // 标题
             Text(
-              item.title,
+              banner.title ?? '',
               textAlign: TextAlign.center,
               maxLines: 1,
               overflow: TextOverflow.ellipsis,

+ 3 - 5
lib/app/modules/routingmode/controllers/routingmode_controller.dart

@@ -1,12 +1,10 @@
 import 'package:get/get.dart';
 import 'package:nomo/app/data/sp/ix_sp.dart';
+import 'package:nomo/app/constants/sp_keys.dart';
 
 enum RoutingMode { smart, global }
 
 class RoutingmodeController extends GetxController {
-  // 缓存键
-  static const String _selectedModeKey = 'routing_mode_selected';
-
   // 当前选中的路由模式,默认为Smart
   final selectedMode = RoutingMode.smart.obs;
 
@@ -18,7 +16,7 @@ class RoutingmodeController extends GetxController {
 
   /// 加载保存的模式
   void _loadSelectedMode() {
-    final modeString = IXSP.getString(_selectedModeKey);
+    final modeString = IXSP.getString(SPKeys.routingModeSelected);
     if (modeString != null) {
       selectedMode.value = modeString == 'global'
           ? RoutingMode.global
@@ -31,7 +29,7 @@ class RoutingmodeController extends GetxController {
     final modeString = selectedMode.value == RoutingMode.global
         ? 'global'
         : 'smart';
-    IXSP.setString(_selectedModeKey, modeString);
+    IXSP.setString(SPKeys.routingModeSelected, modeString);
   }
 
   /// 选择路由模式

+ 17 - 1
lib/app/modules/setting/views/setting_view.dart

@@ -1,5 +1,6 @@
 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';
@@ -14,6 +15,8 @@ import 'package:nomo/app/widgets/ix_image.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/utils/device_manager.dart';
 
+import '../../../constants/enums.dart';
+import '../../../data/sp/ix_sp.dart';
 import '../../../../config/theme/dark_theme_colors.dart';
 import '../../../../config/translations/localization_service.dart';
 import '../../../../config/translations/strings_enum.dart';
@@ -121,7 +124,9 @@ class SettingView extends BaseView<SettingController> {
               _buildSettingItem(
                 icon: IconFont.icon29,
                 iconColor: Get.reactiveTheme.shadowColor,
-                title: Strings.account.tr,
+                title: _getUserAccount().isNotEmpty
+                    ? _getUserAccount()
+                    : Strings.account.tr,
                 trailing: Row(
                   children: [
                     IXImage(
@@ -698,4 +703,15 @@ class SettingView extends BaseView<SettingController> {
       ),
     );
   }
+
+  /// 获取用户账号显示文本
+  String _getUserAccount() {
+    final user = IXSP.getUser();
+    if (user == null) return '';
+
+    if (user.memberLevel == MemberLevel.normal.level) {
+      return user.account?.username ?? '';
+    }
+    return '';
+  }
 }

+ 9 - 8
lib/app/modules/splittunneling/controllers/splittunneling_controller.dart

@@ -5,6 +5,7 @@ import 'package:get/get.dart';
 import '../../../../utils/event_bus.dart';
 import '../../../../utils/log/logger.dart';
 import 'package:nomo/app/data/sp/ix_sp.dart';
+import 'package:nomo/app/constants/sp_keys.dart';
 import '../../../routes/app_pages.dart';
 import '../selectapp/controllers/splittunneling_selectapp_controller.dart';
 
@@ -14,9 +15,6 @@ class SplittunnelingController extends GetxController with EventBusMixin {
   // 当前选中的分流隧道模式
   final selectedMode = SplitTunnelingMode.none.obs;
 
-  // 缓存键
-  static const String _selectedModeKey = 'splittunneling_selected_mode';
-
   @override
   void onInit() {
     super.onInit();
@@ -46,7 +44,7 @@ class SplittunnelingController extends GetxController with EventBusMixin {
 
   /// 加载选中的模式
   void _loadSelectedMode() {
-    final modeString = IXSP.getString(_selectedModeKey);
+    final modeString = IXSP.getString(SPKeys.splittunnelingSelectedMode);
     if (modeString != null) {
       try {
         selectedMode.value = SplitTunnelingMode.values.firstWhere(
@@ -61,7 +59,10 @@ class SplittunnelingController extends GetxController with EventBusMixin {
 
   /// 保存选中的模式
   void _saveSelectedMode() {
-    IXSP.setString(_selectedModeKey, selectedMode.value.toString());
+    IXSP.setString(
+      SPKeys.splittunnelingSelectedMode,
+      selectedMode.value.toString(),
+    );
   }
 
   /// 选择分流隧道模式
@@ -98,8 +99,8 @@ class SplittunnelingController extends GetxController with EventBusMixin {
 
     try {
       final key = mode == SplitTunnelingMode.exclude
-          ? 'splittunneling_exclude_selected_apps'
-          : 'splittunneling_include_selected_apps';
+          ? SPKeys.splittunnelingExcludeSelectedApps
+          : SPKeys.splittunnelingIncludeSelectedApps;
 
       final selectedAppsJson = IXSP.getString(key);
       if (selectedAppsJson != null) {
@@ -107,7 +108,7 @@ class SplittunnelingController extends GetxController with EventBusMixin {
             jsonDecode(selectedAppsJson) as List<dynamic>;
 
         // 获取缓存的应用数据
-        final cachedAppsJson = IXSP.getString('splittunneling_cached_apps');
+        final cachedAppsJson = IXSP.getString(SPKeys.splittunnelingCachedApps);
         if (cachedAppsJson != null) {
           final cachedApps = jsonDecode(cachedAppsJson) as List;
 

+ 7 - 13
lib/app/modules/splittunneling/selectapp/controllers/splittunneling_selectapp_controller.dart

@@ -3,6 +3,7 @@ import 'dart:convert';
 import 'package:get/get.dart';
 import 'package:nomo/app/base/base_controller.dart';
 import 'package:nomo/app/data/sp/ix_sp.dart';
+import 'package:nomo/app/constants/sp_keys.dart';
 import 'package:nomo/app/controllers/base_core_api.dart';
 import 'package:nomo/utils/log/logger.dart';
 
@@ -37,13 +38,6 @@ class SplittunnelingSelectappController extends BaseController {
 
   static const String TAG = 'SplittunnelingSelectappController';
 
-  // 缓存键
-  static const String _cachedAppsKey = 'splittunneling_cached_apps';
-  static const String _excludeSelectedAppsKey =
-      'splittunneling_exclude_selected_apps';
-  static const String _includeSelectedAppsKey =
-      'splittunneling_include_selected_apps';
-
   @override
   void onInit() {
     super.onInit();
@@ -54,7 +48,7 @@ class SplittunnelingSelectappController extends BaseController {
   /// 加载缓存数据
   void _loadCachedData() {
     // 加载缓存的应用数据
-    final cachedAppsJson = IXSP.getString(_cachedAppsKey);
+    final cachedAppsJson = IXSP.getString(SPKeys.splittunnelingCachedApps);
     if (cachedAppsJson != null) {
       try {
         final cachedApps = jsonDecode(cachedAppsJson) as List;
@@ -135,7 +129,7 @@ class SplittunnelingSelectappController extends BaseController {
             )
             .toList(),
       );
-      IXSP.setString(_cachedAppsKey, appsJson);
+      IXSP.setString(SPKeys.splittunnelingCachedApps, appsJson);
     } catch (e) {
       log(TAG, '缓存应用数据失败: $e');
     }
@@ -144,8 +138,8 @@ class SplittunnelingSelectappController extends BaseController {
   /// 加载选中的应用
   void _loadSelectedApps(SplitTunnelingMode mode) {
     final key = mode == SplitTunnelingMode.exclude
-        ? _excludeSelectedAppsKey
-        : _includeSelectedAppsKey;
+        ? SPKeys.splittunnelingExcludeSelectedApps
+        : SPKeys.splittunnelingIncludeSelectedApps;
 
     final selectedAppsJson = IXSP.getString(key);
     if (selectedAppsJson != null) {
@@ -193,8 +187,8 @@ class SplittunnelingSelectappController extends BaseController {
       final selectedAppsJson = jsonEncode(selectedPackageNames);
 
       final key = mode == SplitTunnelingMode.exclude
-          ? _excludeSelectedAppsKey
-          : _includeSelectedAppsKey;
+          ? SPKeys.splittunnelingExcludeSelectedApps
+          : SPKeys.splittunnelingIncludeSelectedApps;
 
       IXSP.setString(key, selectedAppsJson);
     } catch (e) {