Bladeren bron

[new]增加好友显示、增加隐私弹窗、调整启动流程代码

zk 9 maanden geleden
bovenliggende
commit
5eb8046c07
41 gewijzigde bestanden met toevoegingen van 1222 en 227 verwijderingen
  1. BIN
      assets/images/icon_agreement_close.webp
  2. BIN
      assets/images/icon_default_friend_avatar.webp
  3. BIN
      assets/images/icon_default_mine_avatar.webp
  4. 10 1
      assets/string/base/string.xml
  5. 7 1
      lib/data/api/atmob_api.dart
  6. 47 6
      lib/data/api/atmob_api.g.dart
  7. 24 0
      lib/data/api/request/friends_list_request.dart
  8. 71 0
      lib/data/api/request/friends_list_request.g.dart
  9. 24 0
      lib/data/api/response/friends_list_response.dart
  10. 22 0
      lib/data/api/response/friends_list_response.g.dart
  11. 4 0
      lib/data/bean/user_info.dart
  12. 37 0
      lib/data/consts/channel_util.dart
  13. 0 10
      lib/data/consts/constants.dart
  14. 18 0
      lib/data/consts/web_url.dart
  15. 92 8
      lib/data/repositories/account_repository.dart
  16. 50 0
      lib/data/repositories/friends_repository.dart
  17. 7 2
      lib/di/get_it.config.dart
  18. 236 0
      lib/dialog/agreement_dialog.dart
  19. 20 44
      lib/main.dart
  20. 1 1
      lib/module/add_friend/add_friend_view.dart
  21. 4 6
      lib/module/login/login_page.dart
  22. 21 1
      lib/module/main/main_controller.dart
  23. 178 0
      lib/module/main/main_friend_item.dart
  24. 50 14
      lib/module/main/main_page.dart
  25. 2 2
      lib/module/mine/mine_page.dart
  26. 41 1
      lib/module/splash/splash_controller.dart
  27. 6 7
      lib/module/splash/splash_page.dart
  28. 15 0
      lib/resource/assets.gen.dart
  29. 15 6
      lib/resource/string.gen.dart
  30. 0 1
      lib/router/app_pages.dart
  31. 6 0
      lib/utils/expand.dart
  32. 9 0
      lib/utils/common_style.dart
  33. 61 0
      lib/utils/common_util.dart
  34. 6 0
      lib/utils/http_handler.dart
  35. 40 0
      lib/utils/privacy_compliance.dart
  36. 0 0
      lib/utils/project_expand.dart
  37. 1 1
      lib/widget/map_view.dart
  38. 64 0
      lib/widget/marquee_text.dart
  39. 25 0
      pubspec.lock
  40. 8 0
      pubspec.yaml
  41. 0 115
      string_resource_builder.dart

BIN
assets/images/icon_agreement_close.webp


BIN
assets/images/icon_default_friend_avatar.webp


BIN
assets/images/icon_default_mine_avatar.webp


+ 10 - 1
assets/string/base/string.xml

@@ -36,7 +36,7 @@
     <string name="mine_open_vip">开通VIP可体验更多服务</string>
     <string name="mine_vip">您好,尊贵的VIP用户</string>
 
-    <string name="mine_member_permanent">您已是尊贵的永久会员</string>
+    <string name="mine_member_permanent">永久会员</string>
 
     <string name="member_level_0">未开通</string>
     <string name="member_level_100">日卡会员</string>
@@ -84,4 +84,13 @@
     <string name="login_success">登录成功</string>
     <string name="login_too_often_toast">登录过于频繁,请稍后再试</string>
     <string name="login_failed_toast">登录失败</string>
+
+    <string name="privacy_title">隐私权政策与服务条款</string>
+    <string name="privacy_disagree">不同意</string>
+    <string name="privacy_disagree_and_exit">不同意并退出</string>
+    <string name="privacy_agree">同意</string>
+
+    <string name="location_mine">我</string>
+    <string name="location_trace">轨迹</string>
+
 </resources>

+ 7 - 1
lib/data/api/atmob_api.dart

@@ -1,8 +1,10 @@
 import 'package:dio/dio.dart';
 import 'package:location/base/app_base_request.dart';
 import 'package:location/base/base_response.dart';
+import 'package:location/data/api/request/friends_list_request.dart';
 import 'package:location/data/api/request/login_request.dart';
 import 'package:location/data/api/request/send_code_request.dart';
+import 'package:location/data/api/response/friends_list_response.dart';
 import 'package:location/data/api/response/login_response.dart';
 import 'package:location/data/api/response/member_status_response.dart';
 import 'package:retrofit/error_logger.dart';
@@ -23,6 +25,10 @@ abstract class AtmobApi {
       @Body() LoginRequest request);
 
   @POST("/s/v1/user/member")
-  Future<BaseResponse<MemberStatusResponse>> getMemberStatus(
+  Future<BaseResponse<MemberStatusResponse?>> getMemberStatus(
       @Body() AppBaseRequest request);
+
+  @POST("/s/v1/friend/list")
+  Future<BaseResponse<FriendsListResponse>> friendList(
+      @Body() FriendsListRequest request);
 }

+ 47 - 6
lib/data/api/atmob_api.g.dart

@@ -97,21 +97,62 @@ class _AtmobApi implements AtmobApi {
   }
 
   @override
-  Future<BaseResponse<MemberStatusResponse>> getMemberStatus(
+  Future<BaseResponse<MemberStatusResponse?>> getMemberStatus(
       AppBaseRequest request) async {
     final _extra = <String, dynamic>{};
     final queryParameters = <String, dynamic>{};
     final _headers = <String, dynamic>{};
     final _data = <String, dynamic>{};
     _data.addAll(request.toJson());
-    final _options = _setStreamType<BaseResponse<MemberStatusResponse>>(Options(
+    final _options =
+        _setStreamType<BaseResponse<MemberStatusResponse?>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+            .compose(
+              _dio.options,
+              '/s/v1/user/member',
+              queryParameters: queryParameters,
+              data: _data,
+            )
+            .copyWith(
+                baseUrl: _combineBaseUrls(
+              _dio.options.baseUrl,
+              baseUrl,
+            )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<MemberStatusResponse?> _value;
+    try {
+      _value = BaseResponse<MemberStatusResponse?>.fromJson(
+        _result.data!,
+        (json) => json == null
+            ? null
+            : MemberStatusResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<FriendsListResponse>> friendList(
+      FriendsListRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<FriendsListResponse>>(Options(
       method: 'POST',
       headers: _headers,
       extra: _extra,
     )
         .compose(
           _dio.options,
-          '/s/v1/user/member',
+          '/s/v1/friend/list',
           queryParameters: queryParameters,
           data: _data,
         )
@@ -121,11 +162,11 @@ class _AtmobApi implements AtmobApi {
           baseUrl,
         )));
     final _result = await _dio.fetch<Map<String, dynamic>>(_options);
-    late BaseResponse<MemberStatusResponse> _value;
+    late BaseResponse<FriendsListResponse> _value;
     try {
-      _value = BaseResponse<MemberStatusResponse>.fromJson(
+      _value = BaseResponse<FriendsListResponse>.fromJson(
         _result.data!,
-        (json) => MemberStatusResponse.fromJson(json as Map<String, dynamic>),
+        (json) => FriendsListResponse.fromJson(json as Map<String, dynamic>),
       );
     } on Object catch (e, s) {
       errorLogger?.logError(e, s, _options);

+ 24 - 0
lib/data/api/request/friends_list_request.dart

@@ -0,0 +1,24 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:location/base/app_base_request.dart';
+
+part 'friends_list_request.g.dart';
+
+@JsonSerializable()
+class FriendsListRequest extends AppBaseRequest {
+  @JsonKey(name: "offset")
+  int offset;
+
+  @JsonKey(name: "limit")
+  int limit;
+
+  FriendsListRequest({
+    required this.offset,
+    required this.limit,
+  });
+
+  factory FriendsListRequest.fromJson(Map<String, dynamic> json) =>
+      _$FriendsListRequestFromJson(json);
+
+  @override
+  Map<String, dynamic> toJson() => _$FriendsListRequestToJson(this);
+}

+ 71 - 0
lib/data/api/request/friends_list_request.g.dart

@@ -0,0 +1,71 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'friends_list_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+FriendsListRequest _$FriendsListRequestFromJson(Map<String, dynamic> json) =>
+    FriendsListRequest(
+      offset: (json['offset'] as num).toInt(),
+      limit: (json['limit'] as num).toInt(),
+    )
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$FriendsListRequestToJson(FriendsListRequest instance) =>
+    <String, dynamic>{
+      'appPlatform': instance.appPlatform,
+      'os': instance.os,
+      'osVersion': instance.osVersion,
+      'packageName': instance.packageName,
+      'appVersionName': instance.appVersionName,
+      'appVersionCode': instance.appVersionCode,
+      'channelName': instance.channelName,
+      'appId': instance.appId,
+      'tgPlatform': instance.tgPlatform,
+      'oaid': instance.oaid,
+      'aaid': instance.aaid,
+      'androidId': instance.androidId,
+      'imei': instance.imei,
+      'simImei0': instance.simImei0,
+      'simImei1': instance.simImei1,
+      'mac': instance.mac,
+      'idfa': instance.idfa,
+      'idfv': instance.idfv,
+      'machineId': instance.machineId,
+      'brand': instance.brand,
+      'model': instance.model,
+      'wifiName': instance.wifiName,
+      'region': instance.region,
+      'locLng': instance.locLng,
+      'locLat': instance.locLat,
+      'authToken': instance.authToken,
+      'offset': instance.offset,
+      'limit': instance.limit,
+    };

+ 24 - 0
lib/data/api/response/friends_list_response.dart

@@ -0,0 +1,24 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/user_info.dart';
+
+part 'friends_list_response.g.dart';
+
+@JsonSerializable()
+class FriendsListResponse {
+  @JsonKey(name: "list")
+  List<UserInfo> friends;
+
+  @JsonKey(name: "count")
+  int count;
+
+  FriendsListResponse({
+    required this.friends,
+    required this.count,
+  });
+
+  factory FriendsListResponse.fromJson(Map<String, dynamic> json) =>
+      _$FriendsListResponseFromJson(json);
+
+  Map<String, dynamic> toJson() => _$FriendsListResponseToJson(this);
+}

+ 22 - 0
lib/data/api/response/friends_list_response.g.dart

@@ -0,0 +1,22 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'friends_list_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+FriendsListResponse _$FriendsListResponseFromJson(Map<String, dynamic> json) =>
+    FriendsListResponse(
+      friends: (json['list'] as List<dynamic>)
+          .map((e) => UserInfo.fromJson(e as Map<String, dynamic>))
+          .toList(),
+      count: (json['count'] as num).toInt(),
+    );
+
+Map<String, dynamic> _$FriendsListResponseToJson(
+        FriendsListResponse instance) =>
+    <String, dynamic>{
+      'list': instance.friends,
+      'count': instance.count,
+    };

+ 4 - 0
lib/data/bean/user_info.dart

@@ -48,4 +48,8 @@ class UserInfo {
       _$UserInfoFromJson(json);
 
   Map<String, dynamic> toJson() => _$UserInfoToJson(this);
+
+  String getUserNickName() {
+    return remark ?? phoneNumber;
+  }
 }

+ 37 - 0
lib/data/consts/channel_util.dart

@@ -0,0 +1,37 @@
+import 'package:atmob_channel_reader/atmob_channel_reader.dart';
+
+import '../../device/atmob_platform_info.dart';
+import '../../utils/mmkv_util.dart';
+import 'constants.dart';
+
+class ChannelUtil {
+  ChannelUtil._();
+
+  static Future<void> initChannel() async {
+    await AtmobChannelReader.default4Test(Constants.appDefaultChannel,
+        Constants.appDefaultAppId, Constants.appDefaultTgPlatformId);
+
+    String? channel = KVUtil.getString(
+        Constants.appChanelName, await AtmobChannelReader.getChannel());
+    KVUtil.putString(Constants.appChanelName, channel);
+
+    int? channelId = KVUtil.getInt(Constants.appChannelId, -1);
+    if (channelId == -1) {
+      channelId = await AtmobChannelReader.getAppId();
+    }
+    if (channelId != null) {
+      KVUtil.putInt(Constants.appChannelId, channelId);
+    }
+
+    int? appTgPlatformId = KVUtil.getInt(Constants.appTgPlatformId, -1);
+    if (appTgPlatformId == -1) {
+      appTgPlatformId = await AtmobChannelReader.getTgPlatformId();
+    }
+    if (appTgPlatformId != null) {
+      KVUtil.putInt(Constants.appTgPlatformId, appTgPlatformId);
+    }
+    atmobPlatformInfo.setChannelName(channel);
+    atmobPlatformInfo.setAppId(channelId);
+    atmobPlatformInfo.setTgPlatform(appTgPlatformId);
+  }
+}

+ 0 - 10
lib/data/consts/constants.dart

@@ -17,16 +17,9 @@ class Constants {
 
   static const String _prodBaseUrl = "http://loc-api.v8dashen.com";
 
-  static const String privacyPolicy =
-      "https://doc.v8dashen.com/doc/298eb75d38dc2c4a";
-
-  static const String userAgreement =
-      "https://doc.v8dashen.com/doc/417838a4f155ec74";
 
   static String baseUrl = getBaseUrl();
 
-  static const String isPolicyGranted = 'isPolicyGranted';
-
   static bool isProdEnv() {
     return Constants.env == Constants.envProd;
   }
@@ -52,6 +45,3 @@ String getBaseUrl() {
   }
 }
 
-bool isAgreePrivacyPolicy() {
-  return KVUtil.getBool(Constants.isPolicyGranted, false);
-}

+ 18 - 0
lib/data/consts/web_url.dart

@@ -0,0 +1,18 @@
+class WebUrl {
+  WebUrl._();
+
+  static const String _privacyPolicy =
+      "https://doc.v8dashen.com/doc/298eb75d38dc2c4a";
+
+  static const String _privacyPolicyIos =
+      "https://cdn.v8dashen.com/static/xt-xm-i-privacy.html";
+
+  static const String _userAgreement =
+      "https://doc.v8dashen.com/doc/417838a4f155ec74";
+
+  static String get privacyPolicy => _privacyPolicy;
+
+  static String get privacyPolicyIos => _privacyPolicyIos;
+
+  static String get userAgreement => _userAgreement;
+}

+ 92 - 8
lib/data/repositories/account_repository.dart

@@ -1,22 +1,30 @@
+import 'dart:async';
+
 import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
+import 'package:location/base/app_base_request.dart';
 import 'package:location/data/api/atmob_api.dart';
 import 'package:location/data/api/request/login_request.dart';
 import 'package:location/data/api/request/send_code_request.dart';
 import 'package:location/data/bean/member_status_info.dart';
+import 'package:location/data/bean/user_info.dart';
 import 'package:location/data/consts/error_code.dart';
+import 'package:location/data/repositories/friends_repository.dart';
+import 'package:location/di/get_it.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/async_util.dart';
 import 'package:location/utils/atmob_log.dart';
 import 'package:location/utils/http_handler.dart';
 import 'package:location/utils/mmkv_util.dart';
-
 import '../api/response/login_response.dart';
 
 @lazySingleton
 class AccountRepository {
   final AtmobApi atmobApi;
-
-  final String keyAccountLoginPhoneNum = 'key_account_login_phone_num';
-  final String keyAccountLoginToken = 'key_account_login_token';
+  final String tag = "AccountRepository";
+  static final String keyAccountLoginPhoneNum = 'key_account_login_phone_num';
+  static final String keyAccountLoginToken = 'key_account_login_token';
+  static final String keyAccountLoginUserId = 'key_account_login_user_id';
 
   RxnString loginPhoneNum = RxnString();
   RxBool isLogin = RxBool(false);
@@ -25,16 +33,29 @@ class AccountRepository {
   int? _lastRequestCodeTime;
   int _errorCodeTimes = 0;
 
-  static String? token;
+  Timer? refreshMemberHandler;
+  CancelableFuture? memberStatusFuture;
+
+  static String? token = KVUtil.getString(keyAccountLoginToken, null);
+
+  late final FriendsRepository friendsRepository;
+
+  final Rx<UserInfo> locationUserInfo = Rx<UserInfo>(
+      UserInfo(id: "-1", phoneNumber: StringName.locationMine, isMine: true));
 
   AccountRepository(this.atmobApi) {
-    token = KVUtil.getString(keyAccountLoginToken, null);
+    AtmobLog.d(tag, '$tag....init');
+
     isLogin.bindStream(
       loginPhoneNum.map((value) {
         return value?.isNotEmpty == true;
       }),
     );
     loginPhoneNum.value = KVUtil.getString(keyAccountLoginPhoneNum, null);
+
+    friendsRepository = FriendsRepository.getInstance();
+
+    refreshMemberStatus();
   }
 
   Future<void> loginSendCode(String phoneNum) {
@@ -75,16 +96,79 @@ class AccountRepository {
   }
 
   void onLoginSuccess(String phoneNum, String authToken) {
-    AccountRepository.token = token;
+    AccountRepository.token = authToken;
     loginPhoneNum.value = phoneNum;
 
     KVUtil.putString(keyAccountLoginPhoneNum, phoneNum);
     KVUtil.putString(keyAccountLoginToken, authToken);
 
     refreshMemberStatus();
+
+    friendsRepository.refreshFriends();
+  }
+
+  void logout() {
+    token = null;
+
+    refreshMemberHandler?.cancel();
+
+    KVUtil.putString(keyAccountLoginPhoneNum, null);
+    KVUtil.putString(keyAccountLoginToken, null);
+    KVUtil.putString(keyAccountLoginUserId, null);
+    loginPhoneNum.value = null;
+    memberStatusInfo.value = null;
   }
 
-  void refreshMemberStatus() {}
+  void refreshMemberStatus() {
+    memberStatusFuture?.cancel();
+    memberStatusFuture = AsyncUtil.retryWithExponentialBackoff(
+        () => getMemberStatus(), 10, predicate: (error) {
+      if (error is ServerErrorException) {
+        return error.code != ErrorCode.noLoginError;
+      }
+      return true;
+    });
+    memberStatusFuture?.then((data) {
+      AtmobLog.d(tag, "getMemberStatus success: ${memberStatusInfo.value}");
+    }).catchError((error) {
+      AtmobLog.e(tag, "getMemberStatus error: $error");
+    });
+  }
+
+  Future<MemberStatusInfo?> getMemberStatus() {
+    return atmobApi
+        .getMemberStatus(AppBaseRequest())
+        .then(HttpHandler.handle(true))
+        .then((response) {
+      refreshMemberHandler?.cancel();
+      if (response != null) {
+        KVUtil.putString(keyAccountLoginUserId, response.userId);
+        if (!response.permanent && !response.expired) {
+          refreshMemberHandler = Timer(
+              Duration(
+                  milliseconds:
+                      response.endTimestamp - response.serverTimestamp),
+              () => refreshMemberStatus());
+        }
+      }
+      return response;
+    }).then((response) {
+      if (response == null) {
+        return null;
+      }
+      MemberStatusInfo memberStatusInfo = MemberStatusInfo(
+          level: response.level,
+          endTimestamp: response.endTimestamp,
+          expired: response.expired,
+          permanent: response.permanent);
+      this.memberStatusInfo.value = memberStatusInfo;
+      return memberStatusInfo;
+    });
+  }
+
+  static AccountRepository getInstance() {
+    return getIt.get<AccountRepository>();
+  }
 }
 
 class RequestCodeTooOftenException implements Exception {

+ 50 - 0
lib/data/repositories/friends_repository.dart

@@ -0,0 +1,50 @@
+import 'package:get/get.dart';
+import 'package:injectable/injectable.dart';
+import 'package:location/data/api/atmob_api.dart';
+import 'package:location/data/api/request/friends_list_request.dart';
+import 'package:location/di/get_it.dart';
+import 'package:location/utils/http_handler.dart';
+
+import '../../utils/atmob_log.dart';
+import '../api/response/friends_list_response.dart';
+import '../bean/user_info.dart';
+
+@lazySingleton
+class FriendsRepository {
+  final String tag = 'FriendsRepository';
+  final AtmobApi atmobApi;
+  bool refreshFriendsFlag = false;
+  final RxList<UserInfo> friendsList = RxList();
+
+  FriendsRepository(this.atmobApi) {
+    AtmobLog.d(tag, '$tag....init');
+
+    refreshFriends();
+  }
+
+  void clearFriends() {
+    friendsList.clear();
+  }
+
+  void refreshFriends() {
+    if (refreshFriendsFlag) {
+      return;
+    }
+    refreshFriendsFlag = true;
+    friendList(0, 300).then((value) {
+      friendsList.value = value.friends;
+    }).whenComplete(() {
+      refreshFriendsFlag = false;
+    });
+  }
+
+  Future<FriendsListResponse> friendList(int offset, int limit) {
+    return atmobApi
+        .friendList(FriendsListRequest(offset: offset, limit: limit))
+        .then(HttpHandler.handle(true));
+  }
+
+  static FriendsRepository getInstance() {
+    return getIt.get<FriendsRepository>();
+  }
+}

+ 7 - 2
lib/di/get_it.config.dart

@@ -14,6 +14,7 @@ import 'package:injectable/injectable.dart' as _i526;
 
 import '../data/api/atmob_api.dart' as _i243;
 import '../data/repositories/account_repository.dart' as _i20;
+import '../data/repositories/friends_repository.dart' as _i1053;
 import '../module/add_friend/add_friend_dialog_controller.dart' as _i897;
 import '../module/browser/browser_controller.dart' as _i923;
 import '../module/login/login_controller.dart' as _i1008;
@@ -43,10 +44,14 @@ extension GetItInjectableX on _i174.GetIt {
         () => networkModule.provideAtmobApi(gh<_i361.Dio>()));
     gh.lazySingleton<_i20.AccountRepository>(
         () => _i20.AccountRepository(gh<_i243.AtmobApi>()));
+    gh.lazySingleton<_i1053.FriendsRepository>(
+        () => _i1053.FriendsRepository(gh<_i243.AtmobApi>()));
+    gh.factory<_i731.MainController>(() => _i731.MainController(
+          gh<_i1053.FriendsRepository>(),
+          gh<_i20.AccountRepository>(),
+        ));
     gh.factory<_i1008.LoginController>(
         () => _i1008.LoginController(gh<_i20.AccountRepository>()));
-    gh.factory<_i731.MainController>(
-        () => _i731.MainController(gh<_i20.AccountRepository>()));
     gh.factory<_i732.MineController>(
         () => _i732.MineController(gh<_i20.AccountRepository>()));
     return this;

+ 236 - 0
lib/dialog/agreement_dialog.dart

@@ -0,0 +1,236 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:location/module/browser/browser_view.dart';
+import 'package:location/resource/assets.gen.dart';
+import 'package:location/resource/colors.gen.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/common_expand.dart';
+import '../data/consts/web_url.dart';
+
+class AgreementDialog {
+  static final tag = "AgreementDialog";
+
+  static void show(
+      {required VoidCallback cancelClick, required VoidCallback sureClick}) {
+    SmartDialog.show(
+      tag: tag,
+      backDismiss: false,
+      builder: (_) {
+        return _AgreementDialog(cancelClick, sureClick);
+      },
+      clickMaskDismiss: false,
+    );
+  }
+
+  static void dismiss() {
+    SmartDialog.dismiss(tag: tag);
+  }
+}
+
+class _AgreementDialog extends Dialog {
+  final RxBool isFirstStep = true.obs;
+
+  final VoidCallback cancelClick;
+  final VoidCallback sureClick;
+
+  _AgreementDialog(this.cancelClick, this.sureClick);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: 300.w,
+      decoration: BoxDecoration(
+        color: Colors.white,
+        borderRadius: BorderRadius.circular(8.w),
+      ),
+      child: IntrinsicHeight(
+        child: Stack(
+          children: [
+            Column(children: [
+              SizedBox(height: 24.w),
+              Text(StringName.privacyTitle,
+                  style: TextStyle(
+                      fontSize: 17.sp,
+                      color: '#202020'.color,
+                      fontWeight: FontWeight.bold)),
+              SizedBox(height: 16.w),
+              Container(
+                  margin: EdgeInsets.symmetric(horizontal: 24.w),
+                  child: Obx(() {
+                    if (isFirstStep.value) {
+                      return oneStepContentRichText();
+                    } else {
+                      return twoStepContentRichText();
+                    }
+                  })),
+              SizedBox(height: 28.w),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  Obx(() {
+                    return cancelText(
+                        isFirstStep.value
+                            ? StringName.privacyDisagree
+                            : StringName.privacyDisagreeAndExit,
+                        () => cancel());
+                  }),
+                  SizedBox(width: 16.w),
+                  sureText(StringName.privacyAgree, () {
+                    sureClick();
+                  })
+                ],
+              ),
+              SizedBox(height: 24.w)
+            ]),
+            Positioned(
+                top: 12.w,
+                right: 12.w,
+                child: GestureDetector(
+                  onTap: () => cancel(),
+                  child: Assets.images.iconAgreementClose
+                      .image(width: 14.w, height: 14.w),
+                )),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget twoStepContentRichText() {
+    return RichText(
+      text: TextSpan(
+          style: TextStyle(fontSize: 14.sp, color: '#404040'.color),
+          children: [
+            const TextSpan(
+              text: "需要同意",
+            ),
+            TextSpan(
+              text: StringName.privacyPolicy,
+              style: TextStyle(color: '#2F79FF'.color),
+              recognizer: TapGestureRecognizer()
+                ..onTap = () {
+                  if (GetPlatform.isIOS) {
+                    BrowserPage.start(WebUrl.privacyPolicyIos);
+                  } else {
+                    BrowserPage.start(WebUrl.privacyPolicy);
+                  }
+                },
+            ),
+            const TextSpan(
+              text: "和",
+            ),
+            TextSpan(
+              text: StringName.termOfService,
+              style: TextStyle(color: '#2F79FF'.color),
+              recognizer: TapGestureRecognizer()
+                ..onTap = () {
+                  BrowserPage.start(WebUrl.userAgreement);
+                },
+            ),
+            const TextSpan(
+              text: "才能继续为您提供服务,如您选择不同意很遗憾我们难以继续为您继续服务。",
+            ),
+          ]),
+    );
+  }
+
+  Widget oneStepContentRichText() {
+    return RichText(
+      text: TextSpan(
+          style: TextStyle(fontSize: 14.sp, color: '#404040'.color),
+          children: [
+            const TextSpan(
+              text:
+                  "亲爱的用户,感谢您使用我们产品,为了更好的为您服务,我们可能向系统申请以下必要权限:定位信息权限、存储空间、电话权限等,用于应用的基本服务和功能),你有权拒绝或者撤回权限。\n本软件非常重视您的隐私和个人信息,在您使用之前请仔细阅读",
+            ),
+            TextSpan(
+              text: StringName.privacyPolicy,
+              style: TextStyle(color: '#2F79FF'.color),
+              recognizer: TapGestureRecognizer()
+                ..onTap = () {
+                  if (GetPlatform.isIOS) {
+                    BrowserPage.start(WebUrl.privacyPolicyIos);
+                  } else {
+                    BrowserPage.start(WebUrl.privacyPolicy);
+                  }
+                },
+            ),
+            const TextSpan(
+              text: "和",
+            ),
+            TextSpan(
+              text: StringName.termOfService,
+              style: TextStyle(color: '#2F79FF'.color),
+              recognizer: TapGestureRecognizer()
+                ..onTap = () {
+                  BrowserPage.start(WebUrl.userAgreement);
+                },
+            ),
+            const TextSpan(
+              text: "全文,如您同意,请点击点击下方的“同意”按钮。",
+            ),
+          ]),
+    );
+  }
+
+  void privacyClick() {
+    BrowserPage.start(WebUrl.privacyPolicy);
+  }
+
+  void userAgreementClick() {
+    BrowserPage.start(WebUrl.userAgreement);
+  }
+
+  void cancel() {
+    if (isFirstStep.value) {
+      isFirstStep.value = !isFirstStep.value;
+    } else {
+      cancelClick();
+    }
+  }
+
+  Widget cancelText(String txt, VoidCallback onTap) {
+    return GestureDetector(
+      onTap: onTap,
+      child: Container(
+        decoration: BoxDecoration(
+          color: '#F8F8F8'.color,
+          borderRadius: BorderRadius.circular(26.w),
+        ),
+        width: 118.w,
+        height: 40.w,
+        child: Center(
+          child: Text(txt,
+              style: TextStyle(
+                  fontSize: 16.sp,
+                  color: '#A7A7A7'.color,
+                  fontWeight: FontWeight.bold)),
+        ),
+      ),
+    );
+  }
+
+  Widget sureText(String txt, VoidCallback onTap) {
+    return GestureDetector(
+      onTap: onTap,
+      child: Container(
+        decoration: BoxDecoration(
+          color: ColorName.colorPrimary,
+          borderRadius: BorderRadius.circular(26.w),
+        ),
+        width: 118.w,
+        height: 40.w,
+        child: Center(
+          child: Text(txt,
+              style: TextStyle(
+                  fontSize: 16.sp,
+                  color: Colors.white,
+                  fontWeight: FontWeight.bold)),
+        ),
+      ),
+    );
+  }
+}

+ 20 - 44
lib/main.dart

@@ -1,6 +1,5 @@
 import 'dart:io';
 
-import 'package:atmob_channel_reader/atmob_channel_reader.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -13,10 +12,11 @@ import 'package:location/resource/string_source.dart';
 import 'package:location/router/app_pages.dart';
 import 'package:location/utils/app_info_util.dart';
 import 'package:location/utils/mmkv_util.dart';
+import 'package:location/utils/privacy_compliance.dart';
 import 'package:location/utils/toast_util.dart';
 import 'package:pull_to_refresh/pull_to_refresh.dart';
 
-import 'device/atmob_platform_info.dart';
+import 'data/consts/channel_util.dart';
 import 'di/get_it.dart';
 import 'data/consts/build_config.dart';
 import 'data/consts/constants.dart';
@@ -25,60 +25,46 @@ import 'device/device_info_util.dart';
 void main() async {
   WidgetsFlutterBinding.ensureInitialized();
 
-  configureDependencies();
-
+  //必要初始化
   await initRequired();
 
   //非隐私相关
   initCommon();
 
-  //系统参数&第三方sdk初始化
-  await initAfterGrant();
+  //隐私相关:系统参数&第三方sdk初始化
+  await PrivacyCompliance.ensurePolicyGranted(AppInitTask());
+
+  runApp(const MyApp());
 
   //檢查地址
   checkEnv();
-
-  runApp(const MyApp());
 }
 
 Future<void> initRequired() async {
   //存储
   await KVUtil.init();
+
+  //getit
+  configureDependencies();
 }
 
 void initCommon() {
   //全局配置smartDialog
   smartConfig();
   //渠道(仅Android)
-  _initChannel();
+  ChannelUtil.initChannel();
 }
 
-_initChannel() async {
-  await AtmobChannelReader.default4Test(Constants.appDefaultChannel,
-      Constants.appDefaultAppId, Constants.appDefaultTgPlatformId);
 
-  String? channel = KVUtil.getString(
-      Constants.appChanelName, await AtmobChannelReader.getChannel());
-  KVUtil.putString(Constants.appChanelName, channel);
-
-  int? channelId = KVUtil.getInt(Constants.appChannelId, -1);
-  if (channelId == -1) {
-    channelId = await AtmobChannelReader.getAppId();
-  }
-  if (channelId != null) {
-    KVUtil.putInt(Constants.appChannelId, channelId);
-  }
-
-  int? appTgPlatformId = KVUtil.getInt(Constants.appTgPlatformId, -1);
-  if (appTgPlatformId == -1) {
-    appTgPlatformId = await AtmobChannelReader.getTgPlatformId();
-  }
-  if (appTgPlatformId != null) {
-    KVUtil.putInt(Constants.appTgPlatformId, appTgPlatformId);
+/// 隐私相关初始化
+class AppInitTask implements EnsurePolicyGrant {
+  @override
+  Future<void> onPolicyGrant() async {
+    //初始化基础信息
+    await appInfoUtil.init();
+    await deviceInfoUtil.init();
+    //初始化其他sdk
   }
-  atmobPlatformInfo.setChannelName(channel);
-  atmobPlatformInfo.setAppId(channelId);
-  atmobPlatformInfo.setTgPlatform(appTgPlatformId);
 }
 
 void smartConfig() {
@@ -86,16 +72,6 @@ void smartConfig() {
       SmartConfigCustom(animationType: SmartAnimationType.fade);
 }
 
-Future<void> initAfterGrant() async {
-  // if (!isAgreePrivacyPolicy()) {
-  //   return;
-  // }
-  //获取包信息
-  await appInfoUtil.init();
-  //获取设备信息
-  await deviceInfoUtil.init();
-}
-
 void checkEnv() {
   if (!Constants.isProdEnv() && !BuildConfig.isDebug) {
     ToastUtil.show('不是正式环境!!!', addPostFrame: true);
@@ -115,7 +91,7 @@ class MyApp extends StatelessWidget {
     );
   }
 
-  buildApp() {
+  Widget buildApp() {
     return RefreshConfiguration(
       headerBuilder: () => Platform.isAndroid
           ? const MaterialClassicHeader(color: ColorName.colorPrimary)

+ 1 - 1
lib/module/add_friend/add_friend_view.dart

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get_core/src/get_main.dart';
-import 'package:location/utils/expand.dart';
+import 'package:location/utils/common_expand.dart';
 import '../../../base/base_view.dart';
 import '../../../resource/assets.gen.dart';
 import '../../../resource/colors.gen.dart';

+ 4 - 6
lib/module/login/login_page.dart

@@ -6,12 +6,12 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get_core/src/get_main.dart';
 import 'package:location/base/base_page.dart';
+import 'package:location/data/consts/web_url.dart';
 import 'package:location/resource/assets.gen.dart';
 import 'package:location/resource/colors.gen.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/router/app_pages.dart';
-import 'package:location/utils/expand.dart';
-import '../../data/consts/constants.dart';
+import 'package:location/utils/common_expand.dart';
 import '../browser/browser_view.dart';
 import 'login_controller.dart';
 
@@ -95,11 +95,9 @@ class LoginPage extends BasePage<LoginController> {
                         decoration: TextDecoration.none),
                     children: [
                   TextSpan(text: StringName.loginEtPrivacyRead),
-                  buildLinkText(
-                      StringName.privacyPolicy, Constants.privacyPolicy),
+                  buildLinkText(StringName.privacyPolicy, WebUrl.privacyPolicy),
                   TextSpan(text: StringName.loginEtPrivacyAnd),
-                  buildLinkText(
-                      StringName.termOfService, Constants.userAgreement),
+                  buildLinkText(StringName.termOfService, WebUrl.userAgreement),
                 ]))
           ],
         ),

+ 21 - 1
lib/module/main/main_controller.dart

@@ -1,13 +1,28 @@
+import 'package:get/get_rx/src/rx_types/rx_types.dart';
 import 'package:injectable/injectable.dart';
 import 'package:location/base/base_controller.dart';
+import 'package:location/data/bean/user_info.dart';
 import 'package:location/data/repositories/account_repository.dart';
+import 'package:location/data/repositories/friends_repository.dart';
+import 'package:location/utils/atmob_log.dart';
 
 import '../add_friend/add_friend_view.dart';
 import '../mine/mine_page.dart';
 
 @injectable
 class MainController extends BaseController {
-  MainController(AccountRepository accountRepository);
+  final FriendsRepository friendsRepository;
+  final AccountRepository accountRepository;
+
+  RxList<UserInfo> get friendsList => friendsRepository.friendsList;
+
+  UserInfo get mineLocation => accountRepository.locationUserInfo.value;
+
+  MainController(this.friendsRepository, this.accountRepository);
+
+  final Rxn<UserInfo> _selectedFriend = Rxn<UserInfo>();
+
+  UserInfo? get selectedFriend => _selectedFriend.value;
 
   void onAddFriendClick() {
     AddFriendView.show();
@@ -16,4 +31,9 @@ class MainController extends BaseController {
   void onMineClick() {
     MinePage.start();
   }
+
+  void onSelectClick(UserInfo mineLocation) {
+    AtmobLog.d("zkzk", "onSelectClick: ${mineLocation.phoneNumber}");
+    _selectedFriend.value = mineLocation;
+  }
 }

+ 178 - 0
lib/module/main/main_friend_item.dart

@@ -0,0 +1,178 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:location/data/bean/user_info.dart';
+import 'package:location/resource/assets.gen.dart';
+import 'package:location/resource/colors.gen.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/common_expand.dart';
+import 'package:location/utils/project_expand.dart';
+import 'package:marquee/marquee.dart';
+import '../../utils/common_style.dart';
+import '../../utils/common_util.dart';
+import '../../widget/marquee_text.dart';
+
+Widget mainFriendItem(UserInfo userInfo, bool isSelected,
+    {required VoidCallback onTap}) {
+  return GestureDetector(
+    onTap: onTap,
+    child: Container(
+      margin: EdgeInsets.only(right: 12.w),
+      child: Stack(
+        children: [
+          Container(
+              decoration: BoxDecoration(
+                  color: ColorName.white,
+                  shape: BoxShape.circle,
+                  border: isSelected
+                      ? Border.all(color: ColorName.colorPrimary, width: 2.w)
+                      : null),
+              width: 52.w,
+              height: 52.w,
+              child: Center(
+                  child: Image.asset(
+                      userInfo.isMine == true
+                          ? Assets.images.iconDefaultMineAvatar.path
+                          : Assets.images.iconDefaultFriendAvatar.path,
+                      width: 44.w,
+                      height: 44.w))),
+          Positioned(
+            left: 3.w,
+            bottom: 0,
+            right: 3.w,
+            child: Container(
+              height: 16.w,
+              padding: EdgeInsets.symmetric(horizontal: 2.w),
+              decoration: BoxDecoration(
+                color: ColorName.white,
+                borderRadius: BorderRadius.circular(10.w),
+                border: Border.all(color: '#E8E8E8'.color, width: 1.w),
+              ),
+              child: Builder(builder: (context) {
+                String nickName = userNickName(
+                    userInfo.remark, userInfo.phoneNumber, !isSelected);
+                TextStyle style =
+                    TextStyle(fontSize: 10.sp, color: ColorName.black80);
+                return Center(
+                  child: isSelected
+                      ? MarqueeText.marquee(
+                          text: nickName,
+                          containerWidth: 45.w,
+                          textStyle: TextStyle(
+                              fontSize: 10.sp, color: ColorName.black80))
+                      : Text(nickName, style: style),
+                );
+              }),
+            ),
+          )
+        ],
+      ),
+    ),
+  );
+}
+
+Widget mainSelectedFriendItem(UserInfo userInfo) {
+  return Container(
+      width: 336.w,
+      height: 86.w,
+      decoration: BoxDecoration(
+          color: ColorName.white,
+          borderRadius: BorderRadius.all(Radius.circular(20.w))),
+      child: Row(
+        children: [
+          SizedBox(width: 7.w),
+          Image(
+              image: userInfo.isMine == true
+                  ? Assets.images.iconDefaultMineAvatar.provider()
+                  : Assets.images.iconDefaultFriendAvatar.provider(),
+              width: 50.w,
+              height: 50.w),
+          SizedBox(width: 5.w),
+          Expanded(
+            child: Container(
+              margin: EdgeInsets.symmetric(vertical: 15.w),
+              child: Column(
+                mainAxisAlignment: MainAxisAlignment.center,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Row(
+                    children: [
+                      Text(
+                        userInfo.getUserNickName(),
+                        style: TextStyle(
+                            fontWeight: FontWeight.bold,
+                            fontSize: 16.sp,
+                            color: '#202020'.color),
+                      ),
+                      SizedBox(width: 7.w),
+                      Text(time2TimeDesc(userInfo.timestamp),
+                          style: TextStyle(
+                              fontSize: 12.sp, color: '#A7A7A7'.color)),
+                      Spacer(),
+                      Container(
+                          margin: EdgeInsets.only(right: 16.w),
+                          decoration: getPrimaryBtnDecoration(32.w),
+                          padding: EdgeInsets.symmetric(
+                              horizontal: 21.w, vertical: 5.w),
+                          child: Text(StringName.locationTrace,
+                              style: TextStyle(
+                                  fontSize: 15.sp, color: Colors.white)))
+                    ],
+                  ),
+                  Expanded(
+                    child: Container(
+                      margin: EdgeInsets.only(right: 17.w),
+                      child: MarqueeText.marquee(
+                          text: addressCheck(userInfo.lastLocation?.address),
+                          textStyle: TextStyle(
+                              fontSize: 13.sp, color: ColorName.black50),
+                          containerWidth: 244.w),
+                    ),
+                  )
+                  // Text('广东省广州市天河区XX街街XX街区XX村XX')
+                ],
+              ),
+            ),
+          )
+          // Column(
+          //   children: [
+          //     Row(
+          //       children: [
+          //         Text(
+          //           userInfo.getUserNickName(),
+          //           style: TextStyle(
+          //               fontWeight: FontWeight.bold,
+          //               fontSize: 16.sp,
+          //               color: '#202020'.color),
+          //         ),
+          //         SizedBox(width: 7.w),
+          //         Text('1分钟前',
+          //             style:
+          //                 TextStyle(fontSize: 12.sp, color: '#A7A7A7'.color)),
+          //         // Spacer(),
+          //         Container(
+          //             margin: EdgeInsets.only(right: 17.w),
+          //             decoration: getPrimaryBtnDecoration(32.w),
+          //             padding: EdgeInsets.symmetric(
+          //                 horizontal: 21.w, vertical: 5.w),
+          //             child: Text(StringName.locationTrace,
+          //                 style: TextStyle(
+          //                     fontSize: 15.sp, color: Colors.white)))
+          //       ],
+          //     ),
+          //     SizedBox(height: 10.w),
+          //
+          //     Container(
+          //       width: 250.w,
+          //       height: 50.w,
+          //       child: MarqueeText.marquee(
+          //           text: '广东省广州市天河区XX街街XX街区XX村XXasdasdadadasdadadad',
+          //           containerWidth: 250.w,
+          //           textStyle:
+          //           TextStyle(fontSize: 13.sp, color: ColorName.black50)),
+          //     )
+          //   ],
+          // )
+        ],
+      ));
+}

+ 50 - 14
lib/module/main/main_page.dart

@@ -5,23 +5,24 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get_core/src/get_main.dart';
 import 'package:location/base/base_page.dart';
+import 'package:location/data/bean/user_info.dart';
 import 'package:location/module/main/main_controller.dart';
-import 'package:location/module/mine/mine_page.dart';
 import 'package:location/resource/assets.gen.dart';
 import 'package:location/resource/colors.gen.dart';
 import 'package:location/resource/string.gen.dart';
-import 'package:location/utils/expand.dart';
+import 'package:location/utils/common_expand.dart';
 import 'package:location/widget/map_view.dart';
 import '../../router/app_pages.dart';
+import 'main_friend_item.dart';
 
 class MainPage extends BasePage<MainController> {
   const MainPage({super.key});
 
-  static start({bool? isNotClear}) {
+  static start({bool? isNotClear, Map<String, dynamic>? arguments}) {
     if (isNotClear == null || !isNotClear) {
-      Get.offAllNamed(RoutePath.mainTab);
+      Get.offAllNamed(RoutePath.mainTab, arguments: arguments);
     } else {
-      Get.toNamed(RoutePath.mainTab);
+      Get.toNamed(RoutePath.mainTab, arguments: arguments);
     }
   }
 
@@ -91,7 +92,11 @@ class MainPage extends BasePage<MainController> {
                       ),
                       SizedBox(height: 12.w),
                       buildSelectFriendInfoView(),
-                      SizedBox(height: 13.w),
+                      Obx(() {
+                        return Visibility(
+                            visible: controller.selectedFriend != null,
+                            child: SizedBox(height: 13.w));
+                      }),
                       buildTabContainer()
                     ],
                   ),
@@ -105,7 +110,7 @@ class MainPage extends BasePage<MainController> {
             margin: EdgeInsets.only(top: 26.w),
             child: Row(
               children: [
-                Expanded(child: Text('好友列表')),
+                Expanded(child: buildMainFriendList()),
                 GestureDetector(
                   onTap: () {
                     controller.onAddFriendClick();
@@ -192,12 +197,43 @@ class MainPage extends BasePage<MainController> {
   }
 
   Widget buildSelectFriendInfoView() {
-    return Container(
-        decoration: BoxDecoration(
-            color: ColorName.white,
-            borderRadius: BorderRadius.all(Radius.circular(20.w))),
-        child: Row(
-          children: [],
-        ));
+    return Obx(() {
+      UserInfo? userInfo = controller.selectedFriend;
+      if (userInfo == null) {
+        return SizedBox.shrink();
+      }
+      return mainSelectedFriendItem(userInfo);
+    });
+  }
+
+  Widget buildMainFriendList() {
+    return SizedBox(
+      height: 58.w,
+      child: Obx(() {
+        return ListView(
+          physics: const BouncingScrollPhysics(
+              parent: AlwaysScrollableScrollPhysics()),
+          padding: EdgeInsets.only(left: 12.w),
+          scrollDirection: Axis.horizontal,
+          children: [
+            Obx(() {
+              return mainFriendItem(controller.mineLocation,
+                  controller.selectedFriend?.id == controller.mineLocation.id,
+                  onTap: () {
+                controller.onSelectClick(controller.mineLocation);
+              });
+            }),
+            for (UserInfo userInfo in controller.friendsList)
+              Obx(() {
+                return mainFriendItem(
+                    userInfo, controller.selectedFriend?.id == userInfo.id,
+                    onTap: () {
+                  controller.onSelectClick(userInfo);
+                });
+              })
+          ],
+        );
+      }),
+    );
   }
 }

+ 2 - 2
lib/module/mine/mine_page.dart

@@ -8,7 +8,7 @@ import 'package:location/base/base_page.dart';
 import 'package:location/resource/assets.gen.dart';
 import 'package:location/resource/colors.gen.dart';
 import 'package:location/resource/string.gen.dart';
-import 'package:location/utils/expand.dart';
+import 'package:location/utils/common_expand.dart';
 import '../../router/app_pages.dart';
 import '../../utils/date_util.dart';
 import '../../widget/common_view.dart';
@@ -295,7 +295,7 @@ class MinePage extends BasePage<MineController> {
         margin: EdgeInsets.only(right: 20.w),
         decoration: BoxDecoration(
             color: ColorName.white, borderRadius: BorderRadius.circular(26.w)),
-        padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w),
+        padding: EdgeInsets.symmetric(horizontal: 13.w, vertical: 6.w),
         child: Text(txt,
             style: TextStyle(
                 fontSize: 12.sp,

+ 41 - 1
lib/module/splash/splash_controller.dart

@@ -1,8 +1,48 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/services.dart';
+import 'package:get/get_utils/src/platform/platform.dart';
 import 'package:injectable/injectable.dart';
 import 'package:location/base/base_controller.dart';
+import 'package:location/dialog/agreement_dialog.dart';
+import 'package:location/module/main/main_page.dart';
+import '../../utils/privacy_compliance.dart';
 
 @injectable
 class SplashController extends BaseController {
+  final splashDelayedTime = 2;
+
   @override
-  void onReady() {}
+  void onReady() {
+    final isAgreePrivacy = PrivacyCompliance.isAgreePrivacyPolicy();
+    if (isAgreePrivacy) {
+      isAgreePrivacyNextStep();
+    } else {
+      AgreementDialog.show(cancelClick: () {
+        exitApp();
+      }, sureClick: () {
+        PrivacyCompliance.setPrivacyPolicy(true);
+        isAgreePrivacyNextStep();
+      });
+    }
+  }
+
+  void isAgreePrivacyNextStep() {
+    _goMain(Duration(seconds: splashDelayedTime));
+  }
+
+  void _goMain(Duration delayTime, {Map<String, dynamic>? arguments}) {
+    Timer(delayTime, () {
+      MainPage.start(arguments: arguments);
+    });
+  }
+
+  void exitApp() {
+    if (GetPlatform.isAndroid) {
+      SystemNavigator.pop();
+    } else {
+      exit(0);
+    }
+  }
 }

+ 6 - 7
lib/module/splash/splash_page.dart

@@ -1,14 +1,18 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/src/widgets/framework.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
 import 'package:location/base/base_page.dart';
 import 'package:location/module/splash/splash_controller.dart';
 import 'package:location/resource/assets.gen.dart';
 
-import '../main/main_page.dart';
 
 class SplashPage extends BasePage<SplashController> {
-  const SplashPage({super.key});
+  SplashPage({super.key});
+
+  @override
+  final SplashController controller = Get.put(SplashController());
 
   @override
   bool immersive() {
@@ -17,11 +21,6 @@ class SplashPage extends BasePage<SplashController> {
 
   @override
   Widget buildBody(BuildContext context) {
-    Future.delayed(Duration(seconds: 1), () {
-      MainPage.start();
-    });
-    //测试代码
-
     return Stack(
       children: [
         Assets.images.bgPageBackground.image(width: double.infinity),

+ 15 - 0
lib/resource/assets.gen.dart

@@ -28,6 +28,10 @@ class $AssetsImagesGen {
   AssetGenImage get bgPageBackground =>
       const AssetGenImage('assets/images/bg_page_background.webp');
 
+  /// File path: assets/images/icon_agreement_close.webp
+  AssetGenImage get iconAgreementClose =>
+      const AssetGenImage('assets/images/icon_agreement_close.webp');
+
   /// File path: assets/images/icon_black_back.webp
   AssetGenImage get iconBlackBack =>
       const AssetGenImage('assets/images/icon_black_back.webp');
@@ -40,6 +44,14 @@ class $AssetsImagesGen {
   AssetGenImage get iconCheckboxUnSelect =>
       const AssetGenImage('assets/images/icon_checkbox_un_select.webp');
 
+  /// File path: assets/images/icon_default_friend_avatar.webp
+  AssetGenImage get iconDefaultFriendAvatar =>
+      const AssetGenImage('assets/images/icon_default_friend_avatar.webp');
+
+  /// File path: assets/images/icon_default_mine_avatar.webp
+  AssetGenImage get iconDefaultMineAvatar =>
+      const AssetGenImage('assets/images/icon_default_mine_avatar.webp');
+
   /// File path: assets/images/icon_experiment.webp
   AssetGenImage get iconExperiment =>
       const AssetGenImage('assets/images/icon_experiment.webp');
@@ -170,9 +182,12 @@ class $AssetsImagesGen {
         bgLoginHeadContainer,
         bgMineMemberCard,
         bgPageBackground,
+        iconAgreementClose,
         iconBlackBack,
         iconCheckboxSelected,
         iconCheckboxUnSelect,
+        iconDefaultFriendAvatar,
+        iconDefaultMineAvatar,
         iconExperiment,
         iconLoginAddressBook,
         iconLoginClose,

+ 15 - 6
lib/resource/string.gen.dart

@@ -2,7 +2,6 @@ import 'package:get/get.dart';
 
 class StringName {
   StringName._();
-
   static final String appName = 'app_name'.tr; // 手机定位追迹
   static final String mainTabHome = 'main_tab_home'.tr; // 首页
   static final String mainTabCamera = 'main_tab_camera'.tr; // 扫一扫
@@ -33,8 +32,7 @@ class StringName {
   static final String mineNotLoginDesc = 'mine_not_login_desc'.tr; // 登录后可体验更多服务
   static final String mineOpenVip = 'mine_open_vip'.tr; // 开通VIP可体验更多服务
   static final String mineVip = 'mine_vip'.tr; // 您好,尊贵的VIP用户
-  static final String mineMemberPermanent =
-      'mine_member_permanent'.tr; // 您已是尊贵的永久会员
+  static final String mineMemberPermanent = 'mine_member_permanent'.tr; // 永久会员
   static final String memberLevel0 = 'member_level_0'.tr; // 未开通
   static final String memberLevel100 = 'member_level_100'.tr; // 日卡会员
   static final String memberLevel700 = 'member_level_700'.tr; // 周卡会员
@@ -95,8 +93,14 @@ class StringName {
   static final String loginTooOftenToast =
       'login_too_often_toast'.tr; // 登录过于频繁,请稍后再试
   static final String loginFailedToast = 'login_failed_toast'.tr; // 登录失败
+  static final String privacyTitle = 'privacy_title'.tr; // 隐私权政策与服务条款
+  static final String privacyDisagree = 'privacy_disagree'.tr; // 不同意
+  static final String privacyDisagreeAndExit =
+      'privacy_disagree_and_exit'.tr; // 不同意并退出
+  static final String privacyAgree = 'privacy_agree'.tr; // 同意
+  static final String locationMine = 'location_mine'.tr; // 我
+  static final String locationTrace = 'location_trace'.tr; // 轨迹
 }
-
 class StringMultiSource {
   StringMultiSource._();
   static const Map<String, Map<String, String>> values = {
@@ -128,7 +132,7 @@ class StringMultiSource {
       'mine_not_login_desc': '登录后可体验更多服务',
       'mine_open_vip': '开通VIP可体验更多服务',
       'mine_vip': '您好,尊贵的VIP用户',
-      'mine_member_permanent': '您已是尊贵的永久会员',
+      'mine_member_permanent': '永久会员',
       'member_level_0': '未开通',
       'member_level_100': '日卡会员',
       'member_level_700': '周卡会员',
@@ -170,7 +174,12 @@ class StringMultiSource {
       'login_success': '登录成功',
       'login_too_often_toast': '登录过于频繁,请稍后再试',
       'login_failed_toast': '登录失败',
+      'privacy_title': '隐私权政策与服务条款',
+      'privacy_disagree': '不同意',
+      'privacy_disagree_and_exit': '不同意并退出',
+      'privacy_agree': '同意',
+      'location_mine': '我',
+      'location_trace': '轨迹',
     },
   };
 }
-

+ 0 - 1
lib/router/app_pages.dart

@@ -28,7 +28,6 @@ abstract class RoutePath {
 class AppBinding extends Bindings {
   @override
   void dependencies() {
-    lazyPut(() => getIt.get<SplashController>());
     lazyPut(() => getIt.get<MainController>());
     lazyPut(() => getIt.get<AddFriendDialogController>());
     lazyPut(() => getIt.get<LoginController>());

+ 6 - 0
lib/utils/expand.dart

@@ -25,3 +25,9 @@ extension DoubleExtension on double {
     return double.parse(toStringAsFixed(fractionDigits));
   }
 }
+
+extension FutureMap<T> on Future<T> {
+  Future<R> map<R>(R Function(T) transform) {
+    return then(transform);
+  }
+}

+ 9 - 0
lib/utils/common_style.dart

@@ -0,0 +1,9 @@
+import 'package:flutter/cupertino.dart';
+import 'package:location/resource/colors.gen.dart';
+
+Decoration getPrimaryBtnDecoration(double radius) {
+  return BoxDecoration(
+    color: ColorName.colorPrimary,
+    borderRadius: BorderRadius.circular(radius),
+  );
+}

+ 61 - 0
lib/utils/common_util.dart

@@ -1 +1,62 @@
+/// 截取后几位
+String stringExpand(String phone) {
+  if (phone.length < 4) {
+    return phone;
+  }
+  return phone.substring(phone.length - 4);
+}
 
+///截取前几位
+String stringShort(String phone) {
+  if (phone.length < 4) {
+    return phone;
+  }
+  return phone.substring(0, 4);
+}
+
+String userNickName(String? remark, String phoneNumber, bool isIntercept) {
+  if (remark != null && remark.isNotEmpty) {
+    return isIntercept ? stringShort(remark) : remark;
+  } else {
+    return isIntercept ? stringExpand(phoneNumber) : phoneNumber;
+  }
+}
+
+String time2TimeDesc(int? timeMillis) {
+  if (timeMillis == 0 || timeMillis == null) {
+    return "未知";
+  }
+  int currentTimeMillis = DateTime.now().millisecondsSinceEpoch;
+  int timeDiff = currentTimeMillis - timeMillis;
+  if (timeDiff < 0) {
+    return "刚刚";
+  }
+  int minute = timeDiff ~/ 60 ~/ 1000;
+  if (minute < 60) {
+    if (minute == 0) {
+      return "刚刚";
+    }
+    return "$minute分钟前";
+  }
+  int hour = minute ~/ 60;
+  if (hour < 24) {
+    return "$hour小时前";
+  }
+  int day = hour ~/ 24;
+  if (day < 30) {
+    return "$day天前";
+  }
+  int month = day ~/ 30;
+  if (month < 12) {
+    return "$month月前";
+  }
+  int year = month ~/ 12;
+  return "$year年前";
+}
+
+String addressCheck(String? address) {
+  if (address == null || address.isEmpty) {
+    return "未开启定位";
+  }
+  return address;
+}

+ 6 - 0
lib/utils/http_handler.dart

@@ -1,6 +1,9 @@
 import 'dart:async';
 
+import 'package:location/data/repositories/account_repository.dart';
+
 import '../base/base_response.dart';
+import '../data/consts/error_code.dart';
 
 class HttpHandler {
   HttpHandler._();
@@ -15,6 +18,9 @@ class HttpHandler {
           throw Exception('data is null');
         }
       } else {
+        if (response.code == ErrorCode.noLoginError) {
+          AccountRepository.getInstance().logout();
+        }
         throw ServerErrorException(response.code, response.message);
       }
     };

+ 40 - 0
lib/utils/privacy_compliance.dart

@@ -0,0 +1,40 @@
+import 'mmkv_util.dart';
+
+class PrivacyCompliance {
+  PrivacyCompliance._();
+
+  static const String isPolicyGranted = 'isPolicyGranted';
+  static final List<EnsurePolicyGrant> _pendingTasks = [];
+  static bool _isPolicyGranted = false;
+
+  static Future<void> ensurePolicyGranted(EnsurePolicyGrant ensure) async {
+    if (isAgreePrivacyPolicy()) {
+      await ensure.onPolicyGrant();
+    } else {
+      _pendingTasks.add(ensure);
+    }
+  }
+
+  static Future<void> setPrivacyPolicy(bool isAgree) async {
+    final bool oldValue = isAgreePrivacyPolicy();
+    if (isAgree == oldValue) return;
+
+    KVUtil.putBool(isPolicyGranted, isAgree);
+    _isPolicyGranted = isAgree;
+
+    if (isAgree) {
+      for (final task in _pendingTasks) {
+        await task.onPolicyGrant();
+      }
+      _pendingTasks.clear();
+    }
+  }
+
+  static bool isAgreePrivacyPolicy() {
+    return _isPolicyGranted || KVUtil.getBool(isPolicyGranted, false);
+  }
+}
+
+abstract class EnsurePolicyGrant {
+  Future<void> onPolicyGrant();
+}

+ 0 - 0
lib/utils/project_expand.dart


+ 1 - 1
lib/widget/map_view.dart

@@ -1,6 +1,6 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:location/utils/expand.dart';
+import 'package:location/utils/common_expand.dart';
 
 class MapView extends StatelessWidget {
   const MapView({super.key});

+ 64 - 0
lib/widget/marquee_text.dart

@@ -0,0 +1,64 @@
+import 'package:flutter/material.dart';
+import 'package:marquee/marquee.dart';
+
+typedef MarqueeBuilder = Marquee Function(
+    BuildContext context, String text, TextStyle textStyle);
+typedef TextBuilder = Text Function(
+    BuildContext context, String text, TextStyle textStyle);
+
+class MarqueeText extends StatelessWidget {
+  final String text;
+  final TextStyle textStyle;
+  final double containerWidth;
+  final TextBuilder textBuilder;
+  final MarqueeBuilder marqueeBuilder;
+
+  const MarqueeText({
+    super.key,
+    required this.marqueeBuilder,
+    required this.textBuilder,
+    required this.text,
+    required this.textStyle,
+    required this.containerWidth,
+  });
+
+  static MarqueeText marquee(
+      {required String text,
+      required TextStyle textStyle,
+      required double containerWidth,
+      double velocity = 18.0,
+      double blankSpace = 20}) {
+    return MarqueeText(
+      text: text,
+      textStyle: textStyle,
+      containerWidth: containerWidth,
+      textBuilder: (context, text, textStyle) {
+        return Text(text, style: textStyle);
+      },
+      marqueeBuilder: (context, text, textStyle) {
+        return Marquee(
+            text: text,
+            style: textStyle,
+            blankSpace: blankSpace,
+            velocity: velocity);
+      },
+    );
+  }
+
+  Size calculateTextSize(String text, TextStyle style) {
+    final TextPainter textPainter = TextPainter(
+        text: TextSpan(text: text, style: style),
+        maxLines: 1,
+        textDirection: TextDirection.ltr)
+      ..layout(minWidth: 0, maxWidth: double.infinity);
+    return textPainter.size;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final textWidth = calculateTextSize(text, textStyle).width;
+    return textWidth < containerWidth
+        ? textBuilder(context, text, textStyle)
+        : marqueeBuilder(context, text, textStyle);
+  }
+}

+ 25 - 0
pubspec.lock

@@ -262,6 +262,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.0.0"
+  fading_edge_scrollview:
+    dependency: transitive
+    description:
+      name: fading_edge_scrollview
+      sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.1"
   fake_async:
     dependency: transitive
     description:
@@ -538,6 +546,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.1.2-main.4"
+  marquee:
+    dependency: "direct main"
+    description:
+      name: marquee
+      sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.0"
   matcher:
     dependency: transitive
     description:
@@ -928,6 +944,15 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
+  string_get_runner:
+    dependency: "direct dev"
+    description:
+      path: "."
+      ref: "v0.0.3"
+      resolved-ref: eb3371f025bab7df1270ce1511523648045d853a
+      url: "http://git.atmob.com:28999/Atmob-Flutter/string_get_runner.git"
+    source: git
+    version: "0.0.3"
   string_scanner:
     dependency: transitive
     description:

+ 8 - 0
pubspec.yaml

@@ -78,6 +78,9 @@ dependencies:
   #网页跳转
   webview_flutter: 4.10.0
 
+  #跑马灯
+  marquee: 2.3.0
+
   #日志打印
   atmob_logging:
     version: ^0.0.5
@@ -114,6 +117,11 @@ dev_dependencies:
 
   injectable_generator: 2.6.2
 
+  string_get_runner:
+    git:
+      url: http://git.atmob.com:28999/Atmob-Flutter/string_get_runner.git
+      ref: v0.0.3
+
 #----------gen配置---------------
 flutter_gen:
   output: lib/resource/

+ 0 - 115
string_resource_builder.dart

@@ -1,115 +0,0 @@
-import 'dart:async';
-import 'dart:io';
-import 'package:build/build.dart';
-import 'package:xml/xml.dart';
-
-const baseType = 'zh_CN';
-
-Builder builds(BuilderOptions options) => StringResourceBuilder();
-
-class StringResourceBuilder implements Builder {
-  @override
-  Future<void> build(BuildStep buildStep) async {
-    generateStringResources();
-  }
-
-  @override
-  Map<String, List<String>> get buildExtensions => {
-    '.xml': ['.s.dart'],
-  };
-}
-
-void main() {
-  generateStringResources();
-}
-
-void generateStringResources() {
-  print('generateStringResources...start');
-  final directory = Directory('assets/string/');
-
-  final buffer = StringBuffer();
-  final multiBuffer = StringBuffer();
-  buffer.writeln('import \'package:get/get.dart\';');
-  buffer.writeln();
-  buffer.writeln('class StringName {');
-  buffer.writeln('StringName._();');
-
-  multiBuffer.writeln('class StringMultiSource {');
-  multiBuffer.writeln('StringMultiSource._();');
-  multiBuffer.writeln(
-    '  static const Map<String, Map<String, String>> values = {',
-  );
-  directory.listSync(recursive: true).forEach((element) {
-    if (element is Directory) {
-      final files = element.listSync().where(
-        (file) => file.path.endsWith('.xml'),
-      );
-      bool isBase = element.path.endsWith('base');
-      String node = isBase ? baseType : element.path.split('/').last;
-      for (var file in files) {
-        final content = File(file.path).readAsStringSync();
-        final document = XmlDocument.parse(content);
-        final strings = document.findAllElements('string');
-        multiBuffer.writeln('    \'$node\': {');
-        for (var string in strings) {
-          final name = string.getAttribute('name');
-          String value = processXmlText(string.text);
-          if (isBase) {
-            final camelCaseName = toCamelCase(name);
-            buffer.writeln(
-              '  static final String $camelCaseName = \'$name\'.tr;// $value',
-            );
-          }
-          multiBuffer.writeln('      \'$name\': \'$value\',');
-        }
-        multiBuffer.writeln('    },');
-      }
-    }
-  });
-
-  multiBuffer.writeln('  };');
-  multiBuffer.writeln('}');
-
-  buffer.writeln('}');
-  buffer.writeln();
-  buffer.writeln(multiBuffer.toString());
-
-  createDirectory('lib/resource');
-  final outputFile = File('lib/resource/string.gen.dart');
-  outputFile.writeAsStringSync(buffer.toString());
-
-  print('Strings file generated successfully!');
-}
-
-String processXmlText(String original) {
-  return original
-      .replaceAll('\r\n', ' ')
-      .replaceAll('\n', ' ')
-      .replaceAll(RegExp(r'\s+'), ' ')
-      .trim()
-      .replaceAll("'", "\\'");
-}
-
-void createDirectory(String path) {
-  final directory = Directory(path);
-  if (!directory.existsSync()) {
-    directory.createSync(recursive: true);
-    print('Directory created: $path');
-  }
-}
-
-String toCamelCase(String? snakeCase) {
-  if (snakeCase == null) {
-    return '';
-  }
-  return snakeCase
-      .split('_')
-      .map((word) {
-        if (word == snakeCase.split('_').first) {
-          return word;
-        } else {
-          return word[0].toUpperCase() + word.substring(1);
-        }
-      })
-      .join('');
-}