diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 390548a..15c27d1 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -807,5 +807,8 @@ "serviceNameMatrix": "Matrix Software and Game Marketplace", "serviceNamePaperclip": "Attachments, Images and Files", "serviceNameWallet": "Source Points Wallet", - "serviceNamePassport": "Authorization and Authentication" + "serviceNamePassport": "Authorization and Authentication", + "accountActionEvent": "Action Events", + "accountActionEventDescription": "View your action event logs.", + "eventMetadata": "Metadata" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 394311a..3e0c867 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -805,5 +805,8 @@ "serviceNameMatrix": "矩阵市场", "serviceNamePaperclip": "附件", "serviceNameWallet": "源点钱包", - "serviceNamePassport": "身份验证与授权" + "serviceNamePassport": "身份验证与授权", + "accountActionEvent": "操作日志", + "accountActionEventDescription": "查看你的操作日志。", + "eventMetadata": "元数据" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3db39ca..1ac558f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -126,8 +126,6 @@ PODS: - gal (1.0.0): - Flutter - FlutterMacOS - - geolocator_apple (1.2.0): - - Flutter - GoogleAppMeasurement (11.8.0): - GoogleAppMeasurement/AdIdSupport (= 11.8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) @@ -278,7 +276,6 @@ DEPENDENCIES: - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - gal (from `.symlinks/plugins/gal/darwin`) - - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) @@ -362,8 +359,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_webrtc/ios" gal: :path: ".symlinks/plugins/gal/darwin" - geolocator_apple: - :path: ".symlinks/plugins/geolocator_apple/ios" home_widget: :path: ".symlinks/plugins/home_widget/ios" image_picker_ios: @@ -436,7 +431,6 @@ SPEC CHECKSUMS: flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1 gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 - geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1 GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d diff --git a/lib/router.dart b/lib/router.dart index f041d1a..3ec55ea 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:surface/screens/abuse_report.dart'; import 'package:surface/screens/account.dart'; import 'package:surface/screens/account/account_settings.dart'; +import 'package:surface/screens/account/action_events.dart'; import 'package:surface/screens/account/badges.dart'; import 'package:surface/screens/account/factor_settings.dart'; import 'package:surface/screens/account/keypairs.dart'; @@ -124,6 +125,11 @@ final _appRoutes = [ name: 'account', builder: (context, state) => const AccountScreen(), routes: [ + GoRoute( + path: '/events', + name: 'accountActionEvents', + builder: (context, state) => const ActionEventScreen(), + ), GoRoute( path: '/badges', name: 'accountBadges', @@ -172,7 +178,7 @@ final _appRoutes = [ ), ), GoRoute( - path: '/:name', + path: '/profile/:name', name: 'accountProfilePage', pageBuilder: (context, state) => NoTransitionPage( child: UserScreen(name: state.pathParameters['name']!), diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 1b1b466..7f5542c 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -207,6 +207,16 @@ class _AuthorizedAccountScreen extends StatelessWidget { GoRouter.of(context).pushNamed('accountKeyPairs'); }, ), + ListTile( + title: Text('accountActionEvent').tr(), + subtitle: Text('accountActionEventDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.history), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed('accountActionEvents'); + }, + ), ListTile( title: Text('accountSettings').tr(), subtitle: Text('accountSettingsSubtitle').tr(), diff --git a/lib/screens/account/action_events.dart b/lib/screens/account/action_events.dart new file mode 100644 index 0000000..8236e89 --- /dev/null +++ b/lib/screens/account/action_events.dart @@ -0,0 +1,167 @@ +import 'dart:convert'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/account.dart'; +import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; +import 'package:timelines_plus/timelines_plus.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; + +class ActionEventScreen extends StatefulWidget { + const ActionEventScreen({super.key}); + + @override + State createState() => _ActionEventScreenState(); +} + +class _ActionEventScreenState extends State { + bool _isBusy = false; + int? _totalCount; + final List _actionEvents = List.empty(growable: true); + + Future _fetchActionEvents() async { + setState(() => _isBusy = true); + try { + final sn = context.read(); + final resp = await sn.client.get( + '/cgi/id/users/me/events', + queryParameters: { + 'take': 10, + 'offset': _actionEvents.length, + }, + ); + _totalCount = resp.data['count']; + _actionEvents.addAll( + (resp.data['data'] as List) + .map((e) => SnActionEvent.fromJson(e)), + ); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + _fetchActionEvents(); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text('accountActionEvent').tr(), + ), + body: Column( + children: [ + LoadingIndicator(isActive: _isBusy), + Expanded( + child: RefreshIndicator( + onRefresh: () { + _totalCount = null; + _actionEvents.clear(); + return _fetchActionEvents(); + }, + child: InfiniteList( + padding: EdgeInsets.only(left: 20, right: 8), + itemCount: _actionEvents.length, + hasReachedMax: + _totalCount != null && _actionEvents.length >= _totalCount!, + onFetchData: _fetchActionEvents, + itemBuilder: (context, idx) { + final event = _actionEvents[idx]; + return TimelineTile( + nodeAlign: TimelineNodeAlign.start, + contents: Card( + margin: EdgeInsets.symmetric(horizontal: 8, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + event.type, + maxLines: 1, + style: GoogleFonts.robotoMono(), + ), + if (event.ipAddress.isNotEmpty) + Text( + event.ipAddress, + style: GoogleFonts.robotoMono(fontSize: 12), + ), + if (event.location?.isNotEmpty ?? false) + Text(event.location!) + ], + ), + ), + if (event.location?.isNotEmpty ?? false) + SizedBox( + height: 180, + child: FlutterMap( + options: MapOptions( + initialCenter: LatLng( + event.coordinateX!, + event.coordinateY!, + ), + ), + children: [ + TileLayer( + urlTemplate: + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.solsynth.solian', + ), + ], + ), + ).padding(bottom: 6), + if (event.metadata != null) + ExpansionTile( + minTileHeight: 40, + tilePadding: EdgeInsets.symmetric(horizontal: 16), + title: Text('eventMetadata').tr(), + expandedAlignment: Alignment.topLeft, + expandedCrossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + JsonEncoder.withIndent('\t') + .convert(event.metadata), + style: GoogleFonts.robotoMono(), + ).padding(vertical: 8, horizontal: 16), + ], + ).padding(bottom: 6), + ], + ), + ), + node: TimelineNode( + indicator: DotIndicator(), + startConnector: SolidLineConnector(), + endConnector: SolidLineConnector(), + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/types/account.dart b/lib/types/account.dart index 5ce6980..1d90874 100644 --- a/lib/types/account.dart +++ b/lib/types/account.dart @@ -162,3 +162,25 @@ abstract class SnAbuseReport with _$SnAbuseReport { factory SnAbuseReport.fromJson(Map json) => _$SnAbuseReportFromJson(json); } + +@freezed +abstract class SnActionEvent with _$SnActionEvent { + const factory SnActionEvent({ + required int id, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + required String type, + required Map? metadata, + required String? location, + required double? coordinateX, + required double? coordinateY, + required String ipAddress, + required String userAgent, + required SnAccount account, + required int accountId, + }) = _SnActionEvent; + + factory SnActionEvent.fromJson(Map json) => + _$SnActionEventFromJson(json); +} diff --git a/lib/types/account.freezed.dart b/lib/types/account.freezed.dart index 6e3130d..a3f02a3 100644 --- a/lib/types/account.freezed.dart +++ b/lib/types/account.freezed.dart @@ -3027,4 +3027,447 @@ class __$SnAbuseReportCopyWithImpl<$Res> } } +/// @nodoc +mixin _$SnActionEvent { + int get id; + DateTime get createdAt; + DateTime get updatedAt; + DateTime? get deletedAt; + String get type; + Map? get metadata; + String? get location; + double? get coordinateX; + double? get coordinateY; + String get ipAddress; + String get userAgent; + SnAccount get account; + int get accountId; + + /// Create a copy of SnActionEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $SnActionEventCopyWith get copyWith => + _$SnActionEventCopyWithImpl( + this as SnActionEvent, _$identity); + + /// Serializes this SnActionEvent to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is SnActionEvent && + (identical(other.id, id) || other.id == id) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.deletedAt, deletedAt) || + other.deletedAt == deletedAt) && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality().equals(other.metadata, metadata) && + (identical(other.location, location) || + other.location == location) && + (identical(other.coordinateX, coordinateX) || + other.coordinateX == coordinateX) && + (identical(other.coordinateY, coordinateY) || + other.coordinateY == coordinateY) && + (identical(other.ipAddress, ipAddress) || + other.ipAddress == ipAddress) && + (identical(other.userAgent, userAgent) || + other.userAgent == userAgent) && + (identical(other.account, account) || other.account == account) && + (identical(other.accountId, accountId) || + other.accountId == accountId)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + createdAt, + updatedAt, + deletedAt, + type, + const DeepCollectionEquality().hash(metadata), + location, + coordinateX, + coordinateY, + ipAddress, + userAgent, + account, + accountId); + + @override + String toString() { + return 'SnActionEvent(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, metadata: $metadata, location: $location, coordinateX: $coordinateX, coordinateY: $coordinateY, ipAddress: $ipAddress, userAgent: $userAgent, account: $account, accountId: $accountId)'; + } +} + +/// @nodoc +abstract mixin class $SnActionEventCopyWith<$Res> { + factory $SnActionEventCopyWith( + SnActionEvent value, $Res Function(SnActionEvent) _then) = + _$SnActionEventCopyWithImpl; + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + String type, + Map? metadata, + String? location, + double? coordinateX, + double? coordinateY, + String ipAddress, + String userAgent, + SnAccount account, + int accountId}); + + $SnAccountCopyWith<$Res> get account; +} + +/// @nodoc +class _$SnActionEventCopyWithImpl<$Res> + implements $SnActionEventCopyWith<$Res> { + _$SnActionEventCopyWithImpl(this._self, this._then); + + final SnActionEvent _self; + final $Res Function(SnActionEvent) _then; + + /// Create a copy of SnActionEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? type = null, + Object? metadata = freezed, + Object? location = freezed, + Object? coordinateX = freezed, + Object? coordinateY = freezed, + Object? ipAddress = null, + Object? userAgent = null, + Object? account = null, + Object? accountId = null, + }) { + return _then(_self.copyWith( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _self.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _self.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _self.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + type: null == type + ? _self.type + : type // ignore: cast_nullable_to_non_nullable + as String, + metadata: freezed == metadata + ? _self.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map?, + location: freezed == location + ? _self.location + : location // ignore: cast_nullable_to_non_nullable + as String?, + coordinateX: freezed == coordinateX + ? _self.coordinateX + : coordinateX // ignore: cast_nullable_to_non_nullable + as double?, + coordinateY: freezed == coordinateY + ? _self.coordinateY + : coordinateY // ignore: cast_nullable_to_non_nullable + as double?, + ipAddress: null == ipAddress + ? _self.ipAddress + : ipAddress // ignore: cast_nullable_to_non_nullable + as String, + userAgent: null == userAgent + ? _self.userAgent + : userAgent // ignore: cast_nullable_to_non_nullable + as String, + account: null == account + ? _self.account + : account // ignore: cast_nullable_to_non_nullable + as SnAccount, + accountId: null == accountId + ? _self.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int, + )); + } + + /// Create a copy of SnActionEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnAccountCopyWith<$Res> get account { + return $SnAccountCopyWith<$Res>(_self.account, (value) { + return _then(_self.copyWith(account: value)); + }); + } +} + +/// @nodoc +@JsonSerializable() +class _SnActionEvent implements SnActionEvent { + const _SnActionEvent( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.type, + required final Map? metadata, + required this.location, + required this.coordinateX, + required this.coordinateY, + required this.ipAddress, + required this.userAgent, + required this.account, + required this.accountId}) + : _metadata = metadata; + factory _SnActionEvent.fromJson(Map json) => + _$SnActionEventFromJson(json); + + @override + final int id; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + @override + final DateTime? deletedAt; + @override + final String type; + final Map? _metadata; + @override + Map? get metadata { + final value = _metadata; + if (value == null) return null; + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + final String? location; + @override + final double? coordinateX; + @override + final double? coordinateY; + @override + final String ipAddress; + @override + final String userAgent; + @override + final SnAccount account; + @override + final int accountId; + + /// Create a copy of SnActionEvent + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$SnActionEventCopyWith<_SnActionEvent> get copyWith => + __$SnActionEventCopyWithImpl<_SnActionEvent>(this, _$identity); + + @override + Map toJson() { + return _$SnActionEventToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _SnActionEvent && + (identical(other.id, id) || other.id == id) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.deletedAt, deletedAt) || + other.deletedAt == deletedAt) && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality().equals(other._metadata, _metadata) && + (identical(other.location, location) || + other.location == location) && + (identical(other.coordinateX, coordinateX) || + other.coordinateX == coordinateX) && + (identical(other.coordinateY, coordinateY) || + other.coordinateY == coordinateY) && + (identical(other.ipAddress, ipAddress) || + other.ipAddress == ipAddress) && + (identical(other.userAgent, userAgent) || + other.userAgent == userAgent) && + (identical(other.account, account) || other.account == account) && + (identical(other.accountId, accountId) || + other.accountId == accountId)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + createdAt, + updatedAt, + deletedAt, + type, + const DeepCollectionEquality().hash(_metadata), + location, + coordinateX, + coordinateY, + ipAddress, + userAgent, + account, + accountId); + + @override + String toString() { + return 'SnActionEvent(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, metadata: $metadata, location: $location, coordinateX: $coordinateX, coordinateY: $coordinateY, ipAddress: $ipAddress, userAgent: $userAgent, account: $account, accountId: $accountId)'; + } +} + +/// @nodoc +abstract mixin class _$SnActionEventCopyWith<$Res> + implements $SnActionEventCopyWith<$Res> { + factory _$SnActionEventCopyWith( + _SnActionEvent value, $Res Function(_SnActionEvent) _then) = + __$SnActionEventCopyWithImpl; + @override + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + String type, + Map? metadata, + String? location, + double? coordinateX, + double? coordinateY, + String ipAddress, + String userAgent, + SnAccount account, + int accountId}); + + @override + $SnAccountCopyWith<$Res> get account; +} + +/// @nodoc +class __$SnActionEventCopyWithImpl<$Res> + implements _$SnActionEventCopyWith<$Res> { + __$SnActionEventCopyWithImpl(this._self, this._then); + + final _SnActionEvent _self; + final $Res Function(_SnActionEvent) _then; + + /// Create a copy of SnActionEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? type = null, + Object? metadata = freezed, + Object? location = freezed, + Object? coordinateX = freezed, + Object? coordinateY = freezed, + Object? ipAddress = null, + Object? userAgent = null, + Object? account = null, + Object? accountId = null, + }) { + return _then(_SnActionEvent( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _self.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _self.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _self.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + type: null == type + ? _self.type + : type // ignore: cast_nullable_to_non_nullable + as String, + metadata: freezed == metadata + ? _self._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map?, + location: freezed == location + ? _self.location + : location // ignore: cast_nullable_to_non_nullable + as String?, + coordinateX: freezed == coordinateX + ? _self.coordinateX + : coordinateX // ignore: cast_nullable_to_non_nullable + as double?, + coordinateY: freezed == coordinateY + ? _self.coordinateY + : coordinateY // ignore: cast_nullable_to_non_nullable + as double?, + ipAddress: null == ipAddress + ? _self.ipAddress + : ipAddress // ignore: cast_nullable_to_non_nullable + as String, + userAgent: null == userAgent + ? _self.userAgent + : userAgent // ignore: cast_nullable_to_non_nullable + as String, + account: null == account + ? _self.account + : account // ignore: cast_nullable_to_non_nullable + as SnAccount, + accountId: null == accountId + ? _self.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int, + )); + } + + /// Create a copy of SnActionEvent + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnAccountCopyWith<$Res> get account { + return $SnAccountCopyWith<$Res>(_self.account, (value) { + return _then(_self.copyWith(account: value)); + }); + } +} + // dart format on diff --git a/lib/types/account.g.dart b/lib/types/account.g.dart index 10ed4c8..54aaefd 100644 --- a/lib/types/account.g.dart +++ b/lib/types/account.g.dart @@ -283,3 +283,39 @@ Map _$SnAbuseReportToJson(_SnAbuseReport instance) => 'status': instance.status, 'account_id': instance.accountId, }; + +_SnActionEvent _$SnActionEventFromJson(Map json) => + _SnActionEvent( + id: (json['id'] as num).toInt(), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + type: json['type'] as String, + metadata: json['metadata'] as Map?, + location: json['location'] as String?, + coordinateX: (json['coordinate_x'] as num?)?.toDouble(), + coordinateY: (json['coordinate_y'] as num?)?.toDouble(), + ipAddress: json['ip_address'] as String, + userAgent: json['user_agent'] as String, + account: SnAccount.fromJson(json['account'] as Map), + accountId: (json['account_id'] as num).toInt(), + ); + +Map _$SnActionEventToJson(_SnActionEvent instance) => + { + 'id': instance.id, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + 'type': instance.type, + 'metadata': instance.metadata, + 'location': instance.location, + 'coordinate_x': instance.coordinateX, + 'coordinate_y': instance.coordinateY, + 'ip_address': instance.ipAddress, + 'user_agent': instance.userAgent, + 'account': instance.account.toJson(), + 'account_id': instance.accountId, + }; diff --git a/pubspec.lock b/pubspec.lock index a56ecba..59f8ba8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1262,7 +1262,7 @@ packages: source: hosted version: "6.9.4" latlong2: - dependency: transitive + dependency: "direct main" description: name: latlong2 sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" diff --git a/pubspec.yaml b/pubspec.yaml index d4832a2..27976d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -141,6 +141,7 @@ dependencies: html2md: ^1.3.2 flutter_blurhash: ^0.8.2 timelines_plus: ^1.0.6 + latlong2: ^0.9.1 dev_dependencies: flutter_test: