From 14ee6845ed5f8d887e95fc07903132d263c0710f Mon Sep 17 00:00:00 2001
From: LittleSheep <littlesheep.code@hotmail.com>
Date: Sat, 15 Mar 2025 19:28:37 +0800
Subject: [PATCH] :sparkles: Action events

---
 assets/translations/en-US.json         |   5 +-
 assets/translations/zh-CN.json         |   5 +-
 ios/Podfile.lock                       |   6 -
 lib/router.dart                        |   8 +-
 lib/screens/account.dart               |  10 +
 lib/screens/account/action_events.dart | 167 ++++++++++
 lib/types/account.dart                 |  22 ++
 lib/types/account.freezed.dart         | 443 +++++++++++++++++++++++++
 lib/types/account.g.dart               |  36 ++
 pubspec.lock                           |   2 +-
 pubspec.yaml                           |   1 +
 11 files changed, 695 insertions(+), 10 deletions(-)
 create mode 100644 lib/screens/account/action_events.dart

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<ActionEventScreen> createState() => _ActionEventScreenState();
+}
+
+class _ActionEventScreenState extends State<ActionEventScreen> {
+  bool _isBusy = false;
+  int? _totalCount;
+  final List<SnActionEvent> _actionEvents = List.empty(growable: true);
+
+  Future<void> _fetchActionEvents() async {
+    setState(() => _isBusy = true);
+    try {
+      final sn = context.read<SnNetworkProvider>();
+      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<dynamic>)
+            .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<String, Object?> 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<String, dynamic>? 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<String, Object?> 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<String, dynamic>? 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<SnActionEvent> get copyWith =>
+      _$SnActionEventCopyWithImpl<SnActionEvent>(
+          this as SnActionEvent, _$identity);
+
+  /// Serializes this SnActionEvent to a JSON map.
+  Map<String, dynamic> 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<String, dynamic>? 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<String, dynamic>?,
+      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<String, dynamic>? 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<String, dynamic> 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<String, dynamic>? _metadata;
+  @override
+  Map<String, dynamic>? 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<String, dynamic> 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<String, dynamic>? 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<String, dynamic>?,
+      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<String, dynamic> _$SnAbuseReportToJson(_SnAbuseReport instance) =>
       'status': instance.status,
       'account_id': instance.accountId,
     };
+
+_SnActionEvent _$SnActionEventFromJson(Map<String, dynamic> 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<String, dynamic>?,
+      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<String, dynamic>),
+      accountId: (json['account_id'] as num).toInt(),
+    );
+
+Map<String, dynamic> _$SnActionEventToJson(_SnActionEvent instance) =>
+    <String, dynamic>{
+      '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: