Sfoglia il codice sorgente

feat(mobile): Facial recognition (#2507)

* Add API service

* Added service, provider

* merge main

* update pubspec

* styling

* dev: add person search result page

* dev: display person asset on page

* dev: add rename form

* style form

* dev: mechanism to add name to faces

* styling

* fix bad merge

* update api

* test

* revert

* Add header widget

* change name

* show all people page

* fix test

* pr feedback

* Add name to app bar

* feedback

* styling
Alex 2 anni fa
parent
commit
0d0866d5d9

+ 4 - 2
mobile/assets/i18n/en-US.json

@@ -217,6 +217,7 @@
   "search_page_selfies": "Selfies",
   "search_page_things": "Things",
   "search_page_videos": "Videos",
+  "search_page_people": "People",
   "search_page_view_all_button": "View all",
   "search_page_your_activity": "Your activity",
   "search_result_page_new_search_hint": "New Search",
@@ -285,5 +286,6 @@
   "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
   "version_announcement_overlay_text_2": "please take your time to visit the ",
   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
-  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
-}
+  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
+  "all_people_page_title": "People"
+}

+ 6 - 6
mobile/ios/Podfile.lock

@@ -39,7 +39,7 @@ PODS:
   - shared_preferences_foundation (0.0.1):
     - Flutter
     - FlutterMacOS
-  - sqflite (0.0.2):
+  - sqflite (0.0.3):
     - Flutter
     - FMDB (>= 2.7.5)
   - Toast (4.0.0)
@@ -128,21 +128,21 @@ SPEC CHECKSUMS:
   flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
   fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
   FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
-  image_picker_ios: 58b9c4269cb176f89acea5e5d043c9358f2d25f8
+  image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
   integration_test: 13825b8a9334a850581300559b8839134b124670
   isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
   package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
-  path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
+  path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
   path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
   permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
   photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
   share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
-  shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472
-  sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
+  shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
+  sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
   Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
   url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
-  video_player_avfoundation: 6d971a232d72e6ee25368378d48a079dea01f1cf
+  video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126
   wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
 
 PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382

+ 3 - 0
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart

@@ -28,6 +28,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
   final bool showMultiSelectIndicator;
   final void Function(ItemPosition start, ItemPosition end)?
       visibleItemsListener;
+  final Widget? topWidget;
 
   const ImmichAssetGrid({
     super.key,
@@ -44,6 +45,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
     this.dynamicLayout,
     this.showMultiSelectIndicator = true,
     this.visibleItemsListener,
+    this.topWidget,
   });
 
   @override
@@ -125,6 +127,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
                   settings.getSetting(AppSettingsEnum.dynamicLayout),
               showMultiSelectIndicator: showMultiSelectIndicator,
               visibleItemsListener: visibleItemsListener,
+              topWidget: topWidget,
             ),
           ),
         ),

+ 4 - 0
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart

@@ -206,6 +206,8 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
           key: ValueKey(section.offset),
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
+            if (section.offset == 0 && widget.topWidget != null)
+              widget.topWidget!,
             if (section.type == RenderAssetGridElementType.monthTitle)
               _buildMonthTitle(context, section.date),
             if (section.type == RenderAssetGridElementType.groupDividerTitle ||
@@ -401,6 +403,7 @@ class ImmichAssetGridView extends StatefulWidget {
   final bool showMultiSelectIndicator;
   final void Function(ItemPosition start, ItemPosition end)?
       visibleItemsListener;
+  final Widget? topWidget;
 
   const ImmichAssetGridView({
     super.key,
@@ -416,6 +419,7 @@ class ImmichAssetGridView extends StatefulWidget {
     this.dynamicLayout = true,
     this.showMultiSelectIndicator = true,
     this.visibleItemsListener,
+    this.topWidget,
   });
 
   @override

+ 44 - 0
mobile/lib/modules/search/providers/people.provider.dart

@@ -0,0 +1,44 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
+import 'package:immich_mobile/modules/search/services/person.service.dart';
+import 'package:openapi/api.dart';
+
+final personAssetsProvider = FutureProvider.family
+    .autoDispose<RenderList, String>((ref, personId) async {
+  final PersonService personService = ref.watch(personServiceProvider);
+
+  final assets = await personService.getPersonAssets(personId);
+
+  if (assets == null) {
+    return RenderList.empty();
+  }
+
+  return RenderList.fromAssets(assets, GroupAssetsBy.auto);
+});
+
+final getCuratedPeopleProvider =
+    FutureProvider.autoDispose<List<PersonResponseDto>>((ref) async {
+  final PersonService personService = ref.watch(personServiceProvider);
+
+  final curatedPeople = await personService.getCuratedPeople();
+
+  return curatedPeople ?? [];
+});
+
+class UpdatePersonName {
+  final String id;
+  final String name;
+
+  UpdatePersonName(this.id, this.name);
+}
+
+final updatePersonNameProvider =
+    StateProvider.family<void, UpdatePersonName>((ref, dto) async {
+  final PersonService personService = ref.watch(personServiceProvider);
+
+  final person = await personService.updateName(dto.id, dto.name);
+
+  if (person != null && person.name == dto.name) {
+    ref.invalidate(getCuratedPeopleProvider);
+  }
+});

+ 56 - 0
mobile/lib/modules/search/services/person.service.dart

@@ -0,0 +1,56 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/shared/providers/api.provider.dart';
+import 'package:immich_mobile/shared/services/api.service.dart';
+import 'package:openapi/api.dart';
+
+final personServiceProvider = Provider(
+  (ref) => PersonService(
+    ref.watch(apiServiceProvider),
+  ),
+);
+
+class PersonService {
+  final ApiService _apiService;
+
+  PersonService(this._apiService);
+
+  Future<List<PersonResponseDto>?> getCuratedPeople() async {
+    try {
+      return await _apiService.personApi.getAllPeople();
+    } catch (e) {
+      debugPrint("Error [getCuratedPeople] ${e.toString()}");
+      return null;
+    }
+  }
+
+  Future<List<Asset>?> getPersonAssets(String id) async {
+    try {
+      final assets = await _apiService.personApi.getPersonAssets(id);
+
+      if (assets == null) {
+        return null;
+      }
+
+      return assets.map((e) => Asset.remote(e)).toList();
+    } catch (e) {
+      debugPrint("Error [getPersonAssets] ${e.toString()}");
+      return null;
+    }
+  }
+
+  Future<PersonResponseDto?> updateName(String id, String name) async {
+    try {
+      return await _apiService.personApi.updatePerson(
+        id,
+        PersonUpdateDto(
+          name: name,
+        ),
+      );
+    } catch (e) {
+      debugPrint("Error [updateName] ${e.toString()}");
+      return null;
+    }
+  }
+}

+ 114 - 0
mobile/lib/modules/search/ui/curated_people_row.dart

@@ -0,0 +1,114 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/modules/search/models/curated_content.dart';
+import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
+import 'package:immich_mobile/shared/models/store.dart';
+import 'package:immich_mobile/utils/image_url_builder.dart';
+
+class CuratedPeopleRow extends StatelessWidget {
+  final List<CuratedContent> content;
+
+  /// Callback with the content and the index when tapped
+  final Function(CuratedContent, int)? onTap;
+  final Function(CuratedContent, int)? onNameTap;
+
+  const CuratedPeopleRow({
+    super.key,
+    required this.content,
+    this.onTap,
+    required this.onNameTap,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    const imageSize = 85.0;
+
+    // Guard empty [content]
+    if (content.isEmpty) {
+      // Return empty thumbnail
+      return Align(
+        alignment: Alignment.centerLeft,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 16.0),
+          child: SizedBox(
+            width: imageSize,
+            height: imageSize,
+            child: ThumbnailWithInfo(
+              textInfo: '',
+              onTap: () {},
+            ),
+          ),
+        ),
+      );
+    }
+
+    return ListView.builder(
+      scrollDirection: Axis.horizontal,
+      padding: const EdgeInsets.only(
+        left: 16,
+        top: 8,
+      ),
+      itemBuilder: (context, index) {
+        final person = content[index];
+        final headers = {
+          "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"
+        };
+        return Padding(
+          padding: const EdgeInsets.only(right: 18.0),
+          child: SizedBox(
+            width: imageSize,
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                GestureDetector(
+                  onTap: () => onTap?.call(person, index),
+                  child: SizedBox(
+                    height: imageSize,
+                    child: Material(
+                      shape: const CircleBorder(side: BorderSide.none),
+                      elevation: 3,
+                      child: CircleAvatar(
+                        maxRadius: imageSize / 2,
+                        backgroundImage: NetworkImage(
+                          getFaceThumbnailUrl(person.id),
+                          headers: headers,
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+                if (person.label == "")
+                  GestureDetector(
+                    onTap: () => onNameTap?.call(person, index),
+                    child: Padding(
+                      padding: const EdgeInsets.only(top: 8.0),
+                      child: Text(
+                        "Add name",
+                        style: TextStyle(
+                          fontWeight: FontWeight.bold,
+                          color: Theme.of(context).primaryColor,
+                        ),
+                      ),
+                    ),
+                  )
+                else
+                  Padding(
+                    padding: const EdgeInsets.only(top: 8.0),
+                    child: Text(
+                      person.label,
+                      textAlign: TextAlign.center,
+                      overflow: TextOverflow.ellipsis,
+                      style: const TextStyle(
+                        fontWeight: FontWeight.bold,
+                        fontSize: 13.0,
+                      ),
+                    ),
+                  )
+              ],
+            ),
+          ),
+        );
+      },
+      itemCount: content.length,
+    );
+  }
+}

+ 18 - 5
mobile/lib/modules/search/ui/explore_grid.dart

@@ -4,12 +4,16 @@ import 'package:immich_mobile/modules/search/models/curated_content.dart';
 import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/store.dart';
+import 'package:immich_mobile/utils/image_url_builder.dart';
 
 class ExploreGrid extends StatelessWidget {
   final List<CuratedContent> curatedContent;
+  final bool isPeople;
+
   const ExploreGrid({
     super.key,
     required this.curatedContent,
+    this.isPeople = false,
   });
 
   @override
@@ -36,16 +40,25 @@ class ExploreGrid extends StatelessWidget {
       ),
       itemBuilder: (context, index) {
         final content = curatedContent[index];
-        final thumbnailRequestUrl =
-            '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${content.id}';
+        final thumbnailRequestUrl = isPeople
+            ? getFaceThumbnailUrl(content.id)
+            : '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${content.id}';
+
         return ThumbnailWithInfo(
           imageUrl: thumbnailRequestUrl,
           textInfo: content.label,
           borderRadius: 0,
           onTap: () {
-            AutoRouter.of(context).push(
-              SearchResultRoute(searchTerm: 'm:${content.label}'),
-            );
+            isPeople
+                ? AutoRouter.of(context).push(
+                    PersonResultRoute(
+                      personId: content.id,
+                      personName: content.label,
+                    ),
+                  )
+                : AutoRouter.of(context).push(
+                    SearchResultRoute(searchTerm: 'm:${content.label}'),
+                  );
           },
         );
       },

+ 82 - 0
mobile/lib/modules/search/ui/person_name_edit_form.dart

@@ -0,0 +1,82 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/search/providers/people.provider.dart';
+
+class PersonNameEditFormResult {
+  final bool success;
+  final String updatedName;
+
+  PersonNameEditFormResult(this.success, this.updatedName);
+}
+
+class PersonNameEditForm extends HookConsumerWidget {
+  final String personId;
+  final String personName;
+
+  const PersonNameEditForm({
+    super.key,
+    required this.personId,
+    required this.personName,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final controller = useTextEditingController(text: personName);
+
+    return AlertDialog(
+      title: const Text(
+        "Add a name",
+        style: TextStyle(fontWeight: FontWeight.bold),
+      ),
+      content: SingleChildScrollView(
+        child: TextFormField(
+          controller: controller,
+          autofocus: true,
+          decoration: const InputDecoration(
+            hintText: 'Name',
+          ),
+        ),
+      ),
+      actions: [
+        TextButton(
+          style: TextButton.styleFrom(),
+          onPressed: () {
+            Navigator.of(context, rootNavigator: true)
+                .pop<PersonNameEditFormResult>(
+              PersonNameEditFormResult(false, ''),
+            );
+          },
+          child: Text(
+            "Cancel",
+            style: TextStyle(
+              color: Colors.red[300],
+              fontWeight: FontWeight.bold,
+            ),
+          ),
+        ),
+        TextButton(
+          onPressed: () {
+            ref.read(
+              updatePersonNameProvider(
+                UpdatePersonName(personId, controller.text),
+              ),
+            );
+
+            Navigator.of(context, rootNavigator: true)
+                .pop<PersonNameEditFormResult>(
+              PersonNameEditFormResult(true, controller.text),
+            );
+          },
+          child: Text(
+            "Save",
+            style: TextStyle(
+              color: Theme.of(context).primaryColor,
+              fontWeight: FontWeight.bold,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 46 - 0
mobile/lib/modules/search/ui/search_row_title.dart

@@ -0,0 +1,46 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+
+class SearchRowTitle extends StatelessWidget {
+  final Function() onViewAllPressed;
+  final String title;
+  final double top;
+
+  const SearchRowTitle({
+    super.key,
+    required this.onViewAllPressed,
+    required this.title,
+    this.top = 12,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: EdgeInsets.only(
+        left: 16.0,
+        right: 16.0,
+        top: top,
+      ),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Text(
+            title,
+            style: Theme.of(context).textTheme.titleSmall,
+          ),
+          TextButton(
+            onPressed: onViewAllPressed,
+            child: Text(
+              'search_page_view_all_button',
+              style: TextStyle(
+                color: Theme.of(context).primaryColor,
+                fontWeight: FontWeight.bold,
+                fontSize: 14.0,
+              ),
+            ).tr(),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 51 - 0
mobile/lib/modules/search/views/all_people_page.dart

@@ -0,0 +1,51 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/search/models/curated_content.dart';
+import 'package:immich_mobile/modules/search/providers/people.provider.dart';
+import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
+import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+
+class AllPeoplePage extends HookConsumerWidget {
+  const AllPeoplePage({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final curatedPeople = ref.watch(getCuratedPeopleProvider);
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(
+          'all_people_page_title',
+          style: TextStyle(
+            color: Theme.of(context).primaryColor,
+            fontWeight: FontWeight.bold,
+            fontSize: 16.0,
+          ),
+        ).tr(),
+        leading: IconButton(
+          onPressed: () => AutoRouter.of(context).pop(),
+          icon: const Icon(Icons.arrow_back_ios_rounded),
+        ),
+      ),
+      body: curatedPeople.when(
+        loading: () => const Center(child: ImmichLoadingIndicator()),
+        error: (err, stack) => Center(
+          child: Text('Error: $err'),
+        ),
+        data: (people) => ExploreGrid(
+          isPeople: true,
+          curatedContent: people
+              .map(
+                (person) => CuratedContent(
+                  label: person.name,
+                  id: person.id,
+                ),
+              )
+              .toList(),
+        ),
+      ),
+    );
+  }
+}

+ 152 - 0
mobile/lib/modules/search/views/person_result_page.dart

@@ -0,0 +1,152 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
+import 'package:immich_mobile/modules/search/providers/people.provider.dart';
+import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
+import 'package:immich_mobile/shared/models/store.dart' as isar_store;
+import 'package:immich_mobile/utils/image_url_builder.dart';
+
+class PersonResultPage extends HookConsumerWidget {
+  final String personId;
+  final String personName;
+
+  const PersonResultPage({
+    super.key,
+    required this.personId,
+    required this.personName,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final name = useState(personName);
+
+    showEditNameDialog() {
+      showDialog<PersonNameEditFormResult>(
+        context: context,
+        builder: (BuildContext context) {
+          return PersonNameEditForm(
+            personId: personId,
+            personName: personName,
+          );
+        },
+      ).then((result) {
+        if (result != null && result.success) {
+          name.value = result.updatedName;
+        }
+      });
+    }
+
+    void buildBottomSheet() {
+      showModalBottomSheet(
+        backgroundColor: Theme.of(context).scaffoldBackgroundColor,
+        isScrollControlled: false,
+        context: context,
+        useSafeArea: true,
+        builder: (context) {
+          return SafeArea(
+            child: Column(
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                ListTile(
+                  leading: const Icon(Icons.edit_outlined),
+                  title: const Text(
+                    'Edit name',
+                    style: TextStyle(fontWeight: FontWeight.bold),
+                  ),
+                  onTap: showEditNameDialog,
+                )
+              ],
+            ),
+          );
+        },
+      );
+    }
+
+    buildTitleBlock() {
+      if (name.value == "") {
+        return GestureDetector(
+          onTap: showEditNameDialog,
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(
+                'Add a name',
+                style: Theme.of(context).textTheme.titleSmall?.copyWith(
+                      color: Theme.of(context).colorScheme.secondary,
+                    ),
+              ),
+              Text(
+                'Find them fast by name with search',
+                style: Theme.of(context).textTheme.labelSmall,
+              ),
+            ],
+          ),
+        );
+      }
+
+      return Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(
+            name.value,
+            style: Theme.of(context).textTheme.titleLarge,
+          ),
+        ],
+      );
+    }
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(name.value),
+        leading: IconButton(
+          onPressed: () => AutoRouter.of(context).pop(),
+          icon: const Icon(Icons.arrow_back_ios_rounded),
+        ),
+        actions: [
+          IconButton(
+            onPressed: buildBottomSheet,
+            icon: const Icon(Icons.more_vert_rounded),
+          ),
+        ],
+      ),
+      body: ref.watch(personAssetsProvider(personId)).when(
+            loading: () => const Center(child: CircularProgressIndicator()),
+            error: (error, stackTrace) => Center(
+              child: Text(
+                error.toString(),
+              ),
+            ),
+            data: (data) => data.isEmpty
+                ? const Center(
+                    child: Text('Opps'),
+                  )
+                : ImmichAssetGrid(
+                    renderList: data,
+                    topWidget: Padding(
+                      padding: const EdgeInsets.only(left: 8.0, top: 24),
+                      child: Row(
+                        children: [
+                          CircleAvatar(
+                            radius: 36,
+                            backgroundImage: NetworkImage(
+                              getFaceThumbnailUrl(personId),
+                              headers: {
+                                "Authorization":
+                                    "Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}"
+                              },
+                            ),
+                          ),
+                          Padding(
+                            padding: const EdgeInsets.only(left: 16.0),
+                            child: buildTitleBlock(),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ),
+          ),
+    );
+  }
+}

+ 67 - 59
mobile/lib/modules/search/views/search_page.dart

@@ -4,13 +4,16 @@ import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/search/models/curated_content.dart';
+import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
+import 'package:immich_mobile/modules/search/ui/curated_people_row.dart';
 import 'package:immich_mobile/modules/search/ui/curated_row.dart';
 import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart';
+import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
+import 'package:immich_mobile/modules/search/ui/search_row_title.dart';
 import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
-import 'package:openapi/api.dart';
 
 // ignore: must_be_immutable
 class SearchPage extends HookConsumerWidget {
@@ -21,10 +24,9 @@ class SearchPage extends HookConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
-    AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation =
-        ref.watch(getCuratedLocationProvider);
-    AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects =
-        ref.watch(getCuratedObjectProvider);
+    final curatedLocation = ref.watch(getCuratedLocationProvider);
+    final curatedObjects = ref.watch(getCuratedObjectProvider);
+    final curatedPeople = ref.watch(getCuratedPeopleProvider);
     var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
     double imageSize = MediaQuery.of(context).size.width / 3;
 
@@ -54,6 +56,50 @@ class SearchPage extends HookConsumerWidget {
       );
     }
 
+    showNameEditModel(
+      String personId,
+      String personName,
+    ) {
+      return showDialog(
+        context: context,
+        builder: (BuildContext context) {
+          return PersonNameEditForm(personId: personId, personName: personName);
+        },
+      );
+    }
+
+    buildPeople() {
+      return SizedBox(
+        height: MediaQuery.of(context).size.width / 3,
+        child: curatedPeople.when(
+          loading: () => const Center(child: ImmichLoadingIndicator()),
+          error: (err, stack) => Center(child: Text('Error: $err')),
+          data: (people) => CuratedPeopleRow(
+            content: people
+                .map(
+                  (person) => CuratedContent(
+                    id: person.id,
+                    label: person.name,
+                  ),
+                )
+                .take(12)
+                .toList(),
+            onTap: (content, index) {
+              AutoRouter.of(context).push(
+                PersonResultRoute(
+                  personId: content.id,
+                  personName: content.label,
+                ),
+              );
+            },
+            onNameTap: (person, index) => {
+              showNameEditModel(person.id, person.label),
+            },
+          ),
+        ),
+      );
+    }
+
     buildPlaces() {
       return SizedBox(
         height: imageSize,
@@ -130,63 +176,25 @@ class SearchPage extends HookConsumerWidget {
           children: [
             ListView(
               children: [
-                Padding(
-                  padding: const EdgeInsets.symmetric(
-                    horizontal: 16.0,
-                    vertical: 4.0,
+                SearchRowTitle(
+                  title: "search_page_people".tr(),
+                  onViewAllPressed: () => AutoRouter.of(context).push(
+                    const AllPeopleRoute(),
                   ),
-                  child: Row(
-                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                    children: [
-                      Text(
-                        "search_page_places",
-                        style: Theme.of(context).textTheme.titleSmall,
-                      ).tr(),
-                      TextButton(
-                        child: Text(
-                          'search_page_view_all_button',
-                          style: TextStyle(
-                            color: Theme.of(context).primaryColor,
-                            fontWeight: FontWeight.bold,
-                            fontSize: 14.0,
-                          ),
-                        ).tr(),
-                        onPressed: () => AutoRouter.of(context).push(
-                          const CuratedLocationRoute(),
-                        ),
-                      ),
-                    ],
+                ),
+                buildPeople(),
+                SearchRowTitle(
+                  title: "search_page_places".tr(),
+                  onViewAllPressed: () => AutoRouter.of(context).push(
+                    const CuratedLocationRoute(),
                   ),
+                  top: 0,
                 ),
                 buildPlaces(),
-                Padding(
-                  padding: const EdgeInsets.only(
-                    top: 24.0,
-                    bottom: 4.0,
-                    left: 16.0,
-                    right: 16.0,
-                  ),
-                  child: Row(
-                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                    children: [
-                      Text(
-                        "search_page_things",
-                        style: Theme.of(context).textTheme.titleSmall,
-                      ).tr(),
-                      TextButton(
-                        child: Text(
-                          'search_page_view_all_button',
-                          style: TextStyle(
-                            color: Theme.of(context).primaryColor,
-                            fontWeight: FontWeight.bold,
-                            fontSize: 14.0,
-                          ),
-                        ).tr(),
-                        onPressed: () => AutoRouter.of(context).push(
-                          const CuratedObjectRoute(),
-                        ),
-                      ),
-                    ],
+                SearchRowTitle(
+                  title: "search_page_things".tr(),
+                  onViewAllPressed: () => AutoRouter.of(context).push(
+                    const CuratedObjectRoute(),
                   ),
                 ),
                 buildThings(),
@@ -200,7 +208,7 @@ class SearchPage extends HookConsumerWidget {
                 ),
                 ListTile(
                   leading: Icon(
-                    Icons.favorite_border,
+                    Icons.star_outline,
                     color: categoryIconColor,
                   ),
                   title:

+ 12 - 2
mobile/lib/routing/router.dart

@@ -25,9 +25,11 @@ import 'package:immich_mobile/modules/login/views/login_page.dart';
 import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart';
 import 'package:immich_mobile/modules/search/views/all_motion_videos_page.dart';
+import 'package:immich_mobile/modules/search/views/all_people_page.dart';
 import 'package:immich_mobile/modules/search/views/all_videos_page.dart';
 import 'package:immich_mobile/modules/search/views/curated_location_page.dart';
 import 'package:immich_mobile/modules/search/views/curated_object_page.dart';
+import 'package:immich_mobile/modules/search/views/person_result_page.dart';
 import 'package:immich_mobile/modules/search/views/recently_added_page.dart';
 import 'package:immich_mobile/modules/search/views/search_page.dart';
 import 'package:immich_mobile/modules/search/views/search_result_page.dart';
@@ -37,8 +39,8 @@ import 'package:immich_mobile/routing/duplicate_guard.dart';
 import 'package:immich_mobile/routing/gallery_permission_guard.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/album.dart';
-import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/models/logger_message.model.dart';
+import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/providers/api.provider.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
 import 'package:immich_mobile/shared/views/app_log_detail_page.dart';
@@ -140,7 +142,15 @@ part 'router.gr.dart';
       ],
     ),
     AutoRoute(page: PartnerPage, guards: [AuthGuard, DuplicateGuard]),
-    AutoRoute(page: PartnerDetailPage, guards: [AuthGuard, DuplicateGuard])
+    AutoRoute(page: PartnerDetailPage, guards: [AuthGuard, DuplicateGuard]),
+    AutoRoute(
+      page: PersonResultPage,
+      guards: [
+        AuthGuard,
+        DuplicateGuard,
+      ],
+    ),
+    AutoRoute(page: AllPeoplePage, guards: [AuthGuard, DuplicateGuard]),
   ],
 )
 class AppRouter extends _$AppRouter {

+ 97 - 7
mobile/lib/routing/router.gr.dart

@@ -84,6 +84,7 @@ class _$AppRouter extends RootStackRouter {
           onVideoEnded: args.onVideoEnded,
           onPlaying: args.onPlaying,
           onPaused: args.onPaused,
+          placeholder: args.placeholder,
         ),
       );
     },
@@ -272,6 +273,23 @@ class _$AppRouter extends RootStackRouter {
         ),
       );
     },
+    PersonResultRoute.name: (routeData) {
+      final args = routeData.argsAs<PersonResultRouteArgs>();
+      return MaterialPageX<dynamic>(
+        routeData: routeData,
+        child: PersonResultPage(
+          key: args.key,
+          personId: args.personId,
+          personName: args.personName,
+        ),
+      );
+    },
+    AllPeopleRoute.name: (routeData) {
+      return MaterialPageX<dynamic>(
+        routeData: routeData,
+        child: const AllPeoplePage(),
+      );
+    },
     HomeRoute.name: (routeData) {
       return MaterialPageX<dynamic>(
         routeData: routeData,
@@ -555,6 +573,22 @@ class _$AppRouter extends RootStackRouter {
             duplicateGuard,
           ],
         ),
+        RouteConfig(
+          PersonResultRoute.name,
+          path: '/person-result-page',
+          guards: [
+            authGuard,
+            duplicateGuard,
+          ],
+        ),
+        RouteConfig(
+          AllPeopleRoute.name,
+          path: '/all-people-page',
+          guards: [
+            authGuard,
+            duplicateGuard,
+          ],
+        ),
       ];
 }
 
@@ -670,9 +704,10 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
     Key? key,
     required Asset asset,
     required bool isMotionVideo,
-    required dynamic onVideoEnded,
-    dynamic onPlaying,
-    dynamic onPaused,
+    required void Function() onVideoEnded,
+    void Function()? onPlaying,
+    void Function()? onPaused,
+    Widget? placeholder,
   }) : super(
           VideoViewerRoute.name,
           path: '/video-viewer-page',
@@ -683,6 +718,7 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
             onVideoEnded: onVideoEnded,
             onPlaying: onPlaying,
             onPaused: onPaused,
+            placeholder: placeholder,
           ),
         );
 
@@ -697,6 +733,7 @@ class VideoViewerRouteArgs {
     required this.onVideoEnded,
     this.onPlaying,
     this.onPaused,
+    this.placeholder,
   });
 
   final Key? key;
@@ -705,15 +742,17 @@ class VideoViewerRouteArgs {
 
   final bool isMotionVideo;
 
-  final dynamic onVideoEnded;
+  final void Function() onVideoEnded;
 
-  final dynamic onPlaying;
+  final void Function()? onPlaying;
 
-  final dynamic onPaused;
+  final void Function()? onPaused;
+
+  final Widget? placeholder;
 
   @override
   String toString() {
-    return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded, onPlaying: $onPlaying, onPaused: $onPaused}';
+    return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded, onPlaying: $onPlaying, onPaused: $onPaused, placeholder: $placeholder}';
   }
 }
 
@@ -1191,6 +1230,57 @@ class PartnerDetailRouteArgs {
   }
 }
 
+/// generated route for
+/// [PersonResultPage]
+class PersonResultRoute extends PageRouteInfo<PersonResultRouteArgs> {
+  PersonResultRoute({
+    Key? key,
+    required String personId,
+    required String personName,
+  }) : super(
+          PersonResultRoute.name,
+          path: '/person-result-page',
+          args: PersonResultRouteArgs(
+            key: key,
+            personId: personId,
+            personName: personName,
+          ),
+        );
+
+  static const String name = 'PersonResultRoute';
+}
+
+class PersonResultRouteArgs {
+  const PersonResultRouteArgs({
+    this.key,
+    required this.personId,
+    required this.personName,
+  });
+
+  final Key? key;
+
+  final String personId;
+
+  final String personName;
+
+  @override
+  String toString() {
+    return 'PersonResultRouteArgs{key: $key, personId: $personId, personName: $personName}';
+  }
+}
+
+/// generated route for
+/// [AllPeoplePage]
+class AllPeopleRoute extends PageRouteInfo<void> {
+  const AllPeopleRoute()
+      : super(
+          AllPeopleRoute.name,
+          path: '/all-people-page',
+        );
+
+  static const String name = 'AllPeopleRoute';
+}
+
 /// generated route for
 /// [HomePage]
 class HomeRoute extends PageRouteInfo<void> {

+ 2 - 0
mobile/lib/routing/tab_navigation_observer.dart

@@ -1,6 +1,7 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
+import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 
 import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@@ -32,6 +33,7 @@ class TabNavigationObserver extends AutoRouterObserver {
       // Refresh Location State
       ref.invalidate(getCuratedLocationProvider);
       ref.invalidate(getCuratedObjectProvider);
+      ref.invalidate(getCuratedPeopleProvider);
     }
 
     if (route.name == 'SharingRoute') {

+ 2 - 0
mobile/lib/shared/services/api.service.dart

@@ -17,6 +17,7 @@ class ApiService {
   late SearchApi searchApi;
   late ServerInfoApi serverInfoApi;
   late PartnerApi partnerApi;
+  late PersonApi personApi;
 
   ApiService() {
     final endpoint = Store.tryGet(StoreKey.serverEndpoint);
@@ -39,6 +40,7 @@ class ApiService {
     serverInfoApi = ServerInfoApi(_apiClient);
     searchApi = SearchApi(_apiClient);
     partnerApi = PartnerApi(_apiClient);
+    personApi = PersonApi(_apiClient);
   }
 
   Future<String> resolveAndSetEndpoint(String serverUrl) async {

+ 4 - 0
mobile/lib/utils/image_url_builder.dart

@@ -59,3 +59,7 @@ String _getThumbnailUrl(
 }) {
   return '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$id?format=${type.value}';
 }
+
+String getFaceThumbnailUrl(final String personId) {
+  return '${Store.get(StoreKey.serverEndpoint)}/person/$personId/thumbnail';
+}

+ 42 - 0
mobile/lib/utils/immich_app_theme.dart

@@ -32,6 +32,8 @@ ThemeData immichLightTheme = ThemeData(
   primarySwatch: Colors.indigo,
   primaryColor: Colors.indigo,
   hintColor: Colors.indigo,
+  focusColor: Colors.indigo,
+  splashColor: Colors.indigo.withOpacity(0.15),
   fontFamily: 'WorkSans',
   scaffoldBackgroundColor: immichBackgroundColor,
   snackBarTheme: const SnackBarThemeData(
@@ -119,6 +121,26 @@ ThemeData immichLightTheme = ThemeData(
       ),
     ),
   ),
+  dialogTheme: const DialogTheme(
+    surfaceTintColor: Colors.transparent,
+  ),
+  inputDecorationTheme: const InputDecorationTheme(
+    focusedBorder: OutlineInputBorder(
+      borderSide: BorderSide(
+        color: Colors.indigo,
+      ),
+    ),
+    labelStyle: TextStyle(
+      color: Colors.indigo,
+    ),
+    hintStyle: TextStyle(
+      fontSize: 14.0,
+      fontWeight: FontWeight.normal,
+    ),
+  ),
+  textSelectionTheme: const TextSelectionThemeData(
+    cursorColor: Colors.indigo,
+  ),
 );
 
 ThemeData immichDarkTheme = ThemeData(
@@ -217,4 +239,24 @@ ThemeData immichDarkTheme = ThemeData(
       ),
     ),
   ),
+  dialogTheme: const DialogTheme(
+    surfaceTintColor: Colors.transparent,
+  ),
+  inputDecorationTheme: InputDecorationTheme(
+    focusedBorder: OutlineInputBorder(
+      borderSide: BorderSide(
+        color: immichDarkThemePrimaryColor,
+      ),
+    ),
+    labelStyle: TextStyle(
+      color: immichDarkThemePrimaryColor,
+    ),
+    hintStyle: const TextStyle(
+      fontSize: 14.0,
+      fontWeight: FontWeight.normal,
+    ),
+  ),
+  textSelectionTheme: TextSelectionThemeData(
+    cursorColor: immichDarkThemePrimaryColor,
+  ),
 );

+ 157 - 149
mobile/pubspec.lock

@@ -21,18 +21,18 @@ packages:
     dependency: transitive
     description:
       name: archive
-      sha256: "80e5141fafcb3361653ce308776cfd7d45e6e9fbb429e14eec571382c0c5fecb"
+      sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
       url: "https://pub.dev"
     source: hosted
-    version: "3.3.2"
+    version: "3.3.7"
   args:
     dependency: transitive
     description:
       name: args
-      sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
+      sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.0"
+    version: "2.4.1"
   async:
     dependency: transitive
     description:
@@ -77,10 +77,10 @@ packages:
     dependency: transitive
     description:
       name: build
-      sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777"
+      sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.1"
+    version: "2.4.0"
   build_config:
     dependency: transitive
     description:
@@ -93,34 +93,34 @@ packages:
     dependency: transitive
     description:
       name: build_daemon
-      sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf"
+      sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.0"
+    version: "4.0.0"
   build_resolvers:
     dependency: transitive
     description:
       name: build_resolvers
-      sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6"
+      sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.10"
+    version: "2.2.0"
   build_runner:
     dependency: "direct dev"
     description:
       name: build_runner
-      sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727
+      sha256: "220ae4553e50d7c21a17c051afc7b183d28a24a420502e842f303f8e4e6edced"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.3"
+    version: "2.4.4"
   build_runner_core:
     dependency: transitive
     description:
       name: build_runner_core
-      sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292"
+      sha256: "30859c90e9ddaccc484f56303931f477b1f1ba2bab74aa32ed5d6ce15870f8cf"
       url: "https://pub.dev"
     source: hosted
-    version: "7.2.7"
+    version: "7.2.8"
   built_collection:
     dependency: transitive
     description:
@@ -133,10 +133,10 @@ packages:
     dependency: transitive
     description:
       name: built_value
-      sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725"
+      sha256: "2f17434bd5d52a26762043d6b43bb53b3acd029b4d9071a329f46d67ef297e6d"
       url: "https://pub.dev"
     source: hosted
-    version: "8.4.3"
+    version: "8.5.0"
   cached_network_image:
     dependency: "direct main"
     description:
@@ -165,18 +165,18 @@ packages:
     dependency: transitive
     description:
       name: cancellation_token
-      sha256: "44891ef71d605bc59ef7974c403630d8e8506fcd897a29c3e38466ef69e5c4eb"
+      sha256: "7bacc556338b9f84e4db991805fdfa37fa1eda3689b94185bdc7459099455d71"
       url: "https://pub.dev"
     source: hosted
-    version: "1.6.1"
+    version: "2.0.0"
   cancellation_token_http:
     dependency: "direct main"
     description:
       name: cancellation_token_http
-      sha256: e0396730db74d96522cb7162cb390c73b3e30aa24450001a24374cd09f8484ea
+      sha256: bb91655e2e47d6274b681261ee6a687b7aa9023f49cfc28f42d095b2f86febc3
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.0"
+    version: "1.3.0"
   characters:
     dependency: transitive
     description:
@@ -189,18 +189,18 @@ packages:
     dependency: transitive
     description:
       name: checked_yaml
-      sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311"
+      sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.2"
+    version: "2.0.3"
   chewie:
     dependency: "direct main"
     description:
       name: chewie
-      sha256: e9da4898ee4859825404f507969f57113c04ca0060e152b95c9afd73934126ad
+      sha256: "745e81e84c6d7f3835f89f85bb49771c0a66099e4caf8f8e9e9a372bc66fb2c1"
       url: "https://pub.dev"
     source: hosted
-    version: "1.4.0"
+    version: "1.5.0"
   clock:
     dependency: transitive
     description:
@@ -269,10 +269,10 @@ packages:
     dependency: transitive
     description:
       name: dart_style
-      sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4"
+      sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.4"
+    version: "2.3.1"
   dartx:
     dependency: transitive
     description:
@@ -285,10 +285,10 @@ packages:
     dependency: "direct main"
     description:
       name: device_info_plus
-      sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95"
+      sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903
       url: "https://pub.dev"
     source: hosted
-    version: "8.1.0"
+    version: "8.2.2"
   device_info_plus_platform_interface:
     dependency: transitive
     description:
@@ -309,10 +309,10 @@ packages:
     dependency: "direct main"
     description:
       name: easy_localization
-      sha256: f30e9b20ed4d1b890171c30241d9b9c43efe21fee55dee7bd68f94daf269ea75
+      sha256: "30ebf25448ffe169e0bd9bc4b5da94faa8398967a2ad2ca09f438be8b6953645"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.2-dev.2"
+    version: "3.0.2"
   easy_logger:
     dependency: transitive
     description:
@@ -436,18 +436,18 @@ packages:
     dependency: transitive
     description:
       name: flutter_plugin_android_lifecycle
-      sha256: "4bef634684b2c7f3468c77c766c831229af829a0cd2d4ee6c1b99558bd14e5d2"
+      sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.8"
+    version: "2.0.15"
   flutter_riverpod:
     dependency: transitive
     description:
       name: flutter_riverpod
-      sha256: "46a27b7a11dc13738054093076f2dc65692ddcd463979b15092accf5681aea20"
+      sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.0"
+    version: "2.3.6"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -507,26 +507,26 @@ packages:
     dependency: transitive
     description:
       name: graphs
-      sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2
+      sha256: "772db3d53d23361d4ffcf5a9bb091cf3ee9b22f2be52cd107cd7a2683a89ba0e"
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.0"
+    version: "2.3.0"
   hooks_riverpod:
     dependency: "direct main"
     description:
       name: hooks_riverpod
-      sha256: a596bcb1eaf48eae6da1ce8b9e60ec9538ef7d15725e941c3626f29dfcc01d96
+      sha256: be68cf7653fcab798500f9047ac58c3f109287a1595012412f4a0d654a9bb9c5
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.0"
+    version: "2.3.6"
   html:
     dependency: transitive
     description:
       name: html
-      sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269
+      sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8"
       url: "https://pub.dev"
     source: hosted
-    version: "0.15.1"
+    version: "0.15.3"
   http:
     dependency: "direct main"
     description:
@@ -563,34 +563,34 @@ packages:
     dependency: "direct main"
     description:
       name: image_picker
-      sha256: "22207768556b82d55ec70166824350fee32298732d5efa4d6e756f848f51f66a"
+      sha256: "9978d3510af4e6a902e545ce19229b926e6de6a1828d6134d3aab2e129a4d270"
       url: "https://pub.dev"
     source: hosted
-    version: "0.8.6+3"
+    version: "0.8.7+5"
   image_picker_android:
     dependency: transitive
     description:
       name: image_picker_android
-      sha256: "68d067baf7f6e401b1124ee83dd6967e67847314250fd68012aab34a69beb344"
+      sha256: "364967c8d581f5d75fc05f6c79fcf1115e3c05db3d3eee1aaca52e0da3f7501c"
       url: "https://pub.dev"
     source: hosted
-    version: "0.8.5+7"
+    version: "0.8.6+15"
   image_picker_for_web:
     dependency: transitive
     description:
       name: image_picker_for_web
-      sha256: "66fc6e3877bbde82c33d122f3588777c3784ac5bd7d1cdd79213ef7aecb85b34"
+      sha256: "98f50d6b9f294c8ba35e25cc0d13b04bfddd25dbc8d32fa9d566a6572f2c081c"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.11"
+    version: "2.1.12"
   image_picker_ios:
     dependency: transitive
     description:
       name: image_picker_ios
-      sha256: "39aa70b5f1e5e7c94585b9738632d5fdb764a5655e40cd9e7b95fbd2fc50c519"
+      sha256: d779210bda268a03b57e923fb1e410f32f5c5e708ad256348bcbf1f44f558fd0
       url: "https://pub.dev"
     source: hosted
-    version: "0.8.6+9"
+    version: "0.8.7+4"
   image_picker_platform_interface:
     dependency: transitive
     description:
@@ -656,10 +656,10 @@ packages:
     dependency: transitive
     description:
       name: json_annotation
-      sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
+      sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
       url: "https://pub.dev"
     source: hosted
-    version: "4.8.0"
+    version: "4.8.1"
   latlong2:
     dependency: "direct main"
     description:
@@ -672,10 +672,10 @@ packages:
     dependency: transitive
     description:
       name: lints
-      sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
+      sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.1"
+    version: "2.1.0"
   lists:
     dependency: transitive
     description:
@@ -799,26 +799,26 @@ packages:
     dependency: "direct main"
     description:
       name: path_provider
-      sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9"
+      sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.13"
+    version: "2.0.15"
   path_provider_android:
     dependency: transitive
     description:
       name: path_provider_android
-      sha256: "7623b7d4be0f0f7d9a8b5ee6879fc13e4522d4c875ab86801dee4af32b54b83e"
+      sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.23"
+    version: "2.0.27"
   path_provider_foundation:
     dependency: transitive
     description:
       name: path_provider_foundation
-      sha256: eec003594f19fe2456ea965ae36b3fc967bc5005f508890aafe31fa75e41d972
+      sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.2"
+    version: "2.2.3"
   path_provider_ios:
     dependency: "direct main"
     description:
@@ -831,10 +831,10 @@ packages:
     dependency: transitive
     description:
       name: path_provider_linux
-      sha256: "525ad5e07622d19447ad740b1ed5070031f7a5437f44355ae915ff56e986429a"
+      sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.9"
+    version: "2.1.10"
   path_provider_platform_interface:
     dependency: transitive
     description:
@@ -847,10 +847,10 @@ packages:
     dependency: transitive
     description:
       name: path_provider_windows
-      sha256: "642ddf65fde5404f83267e8459ddb4556316d3ee6d511ed193357e25caa3632d"
+      sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.4"
+    version: "2.1.6"
   pedantic:
     dependency: transitive
     description:
@@ -871,18 +871,18 @@ packages:
     dependency: transitive
     description:
       name: permission_handler_android
-      sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2"
+      sha256: d8cc6a62ded6d0f49c6eac337e080b066ee3bce4d405bd9439a61e1f1927bfe8
       url: "https://pub.dev"
     source: hosted
-    version: "10.2.0"
+    version: "10.2.1"
   permission_handler_apple:
     dependency: transitive
     description:
       name: permission_handler_apple
-      sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163"
+      sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85
       url: "https://pub.dev"
     source: hosted
-    version: "9.0.7"
+    version: "9.0.8"
   permission_handler_platform_interface:
     dependency: transitive
     description:
@@ -903,18 +903,18 @@ packages:
     dependency: transitive
     description:
       name: petitparser
-      sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4"
+      sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
       url: "https://pub.dev"
     source: hosted
-    version: "5.1.0"
+    version: "5.4.0"
   photo_manager:
     dependency: "direct main"
     description:
       name: photo_manager
-      sha256: "55d50ad1b8f984c57fa7c4bd4980f4760e80d3d9355263cf72624a6ff1bf2b5b"
+      sha256: bdc4ab1fa9fb064d8ccfea6ab44119f55b220293d7ce2e19eb5a5f998db86c88
       url: "https://pub.dev"
     source: hosted
-    version: "2.5.2"
+    version: "2.6.0"
   platform:
     dependency: transitive
     description:
@@ -931,6 +931,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.4"
+  pointycastle:
+    dependency: transitive
+    description:
+      name: pointycastle
+      sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.7.3"
   polylabel:
     dependency: transitive
     description:
@@ -975,26 +983,26 @@ packages:
     dependency: transitive
     description:
       name: pub_semver
-      sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17"
+      sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.1.4"
   pubspec_parse:
     dependency: transitive
     description:
       name: pubspec_parse
-      sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a"
+      sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.1"
+    version: "1.2.3"
   riverpod:
     dependency: transitive
     description:
       name: riverpod
-      sha256: "59a48de9c757aa61aa28e9fd625ffb360d43b6b54606f12536622c55be9e8c4b"
+      sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef"
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.0"
+    version: "2.3.6"
   rxdart:
     dependency: transitive
     description:
@@ -1007,98 +1015,98 @@ packages:
     dependency: "direct main"
     description:
       name: scrollable_positioned_list
-      sha256: ca7fcaa743db712d4f7b1580526f494d0093c77a721a65705ee51fbeac7a2bd3
+      sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287"
       url: "https://pub.dev"
     source: hosted
-    version: "0.3.5"
+    version: "0.3.8"
   share_plus:
     dependency: "direct main"
     description:
       name: share_plus
-      sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625"
+      sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1
       url: "https://pub.dev"
     source: hosted
-    version: "6.3.1"
+    version: "6.3.4"
   share_plus_platform_interface:
     dependency: transitive
     description:
       name: share_plus_platform_interface
-      sha256: "82ddd4ab9260c295e6e39612d4ff00390b9a7a21f1bb1da771e2f232d80ab8a1"
+      sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981"
       url: "https://pub.dev"
     source: hosted
-    version: "3.2.0"
+    version: "3.2.1"
   shared_preferences:
     dependency: transitive
     description:
       name: shared_preferences
-      sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41
+      sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.18"
+    version: "2.1.1"
   shared_preferences_android:
     dependency: transitive
     description:
       name: shared_preferences_android
-      sha256: a51a4f9375097f94df1c6e0a49c0374440d31ab026b59d58a7e7660675879db4
+      sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.16"
+    version: "2.1.4"
   shared_preferences_foundation:
     dependency: transitive
     description:
       name: shared_preferences_foundation
-      sha256: "6b84fdf06b32bb336f972d373cd38b63734f3461ba56ac2ba01b56d052796259"
+      sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.4"
+    version: "2.2.2"
   shared_preferences_linux:
     dependency: transitive
     description:
       name: shared_preferences_linux
-      sha256: d7fb71e6e20cd3dfffcc823a28da3539b392e53ed5fc5c2b90b55fdaa8a7e8fa
+      sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.4"
+    version: "2.2.0"
   shared_preferences_platform_interface:
     dependency: transitive
     description:
       name: shared_preferences_platform_interface
-      sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc"
+      sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.1"
+    version: "2.2.0"
   shared_preferences_web:
     dependency: transitive
     description:
       name: shared_preferences_web
-      sha256: "6737b757e49ba93de2a233df229d0b6a87728cea1684da828cbc718b65dcf9d7"
+      sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.5"
+    version: "2.1.0"
   shared_preferences_windows:
     dependency: transitive
     description:
       name: shared_preferences_windows
-      sha256: bd014168e8484837c39ef21065b78f305810ceabc1d4f90be6e3b392ce81b46d
+      sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.4"
+    version: "2.2.0"
   shelf:
     dependency: transitive
     description:
       name: shelf
-      sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c
+      sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
       url: "https://pub.dev"
     source: hosted
-    version: "1.4.0"
+    version: "1.4.1"
   shelf_web_socket:
     dependency: transitive
     description:
       name: shelf_web_socket
-      sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8
+      sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.3"
+    version: "1.0.4"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -1108,26 +1116,26 @@ packages:
     dependency: "direct main"
     description:
       name: socket_io_client
-      sha256: a9c589d3fe2658506be38ddb36f23348daab73a00ff1645533669d07a5111cfc
+      sha256: "5d2a0a12c2a4a5f48d14e5b6aef7db78d3b425a9b084071059fa54bd12d2576c"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.1"
+    version: "2.0.2"
   socket_io_common:
     dependency: transitive
     description:
       name: socket_io_common
-      sha256: "5a218a784df4d1927ae713e17af619caa736cb2ebac287c59e4e24228b22da29"
+      sha256: "2ab92f8ff3ebbd4b353bf4a98bee45cc157e3255464b2f90f66e09c4472047eb"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.2"
+    version: "2.0.3"
   source_gen:
     dependency: transitive
     description:
       name: source_gen
-      sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d"
+      sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33"
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.6"
+    version: "1.3.2"
   source_span:
     dependency: transitive
     description:
@@ -1140,18 +1148,18 @@ packages:
     dependency: transitive
     description:
       name: sqflite
-      sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f"
+      sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.4+1"
+    version: "2.2.8+4"
   sqflite_common:
     dependency: transitive
     description:
       name: sqflite_common
-      sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f
+      sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.2+2"
+    version: "2.4.5"
   stack_trace:
     dependency: transitive
     description:
@@ -1204,10 +1212,10 @@ packages:
     dependency: transitive
     description:
       name: synchronized
-      sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b"
+      sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.1"
+    version: "3.1.0"
   term_glyph:
     dependency: transitive
     description:
@@ -1252,10 +1260,10 @@ packages:
     dependency: transitive
     description:
       name: typed_data
-      sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
+      sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
       url: "https://pub.dev"
     source: hosted
-    version: "1.3.1"
+    version: "1.3.2"
   unicode:
     dependency: transitive
     description:
@@ -1276,42 +1284,42 @@ packages:
     dependency: "direct main"
     description:
       name: url_launcher
-      sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e"
+      sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3
       url: "https://pub.dev"
     source: hosted
-    version: "6.1.10"
+    version: "6.1.11"
   url_launcher_android:
     dependency: transitive
     description:
       name: url_launcher_android
-      sha256: "1f4d9ebe86f333c15d318f81dcdc08b01d45da44af74552608455ebdc08d9732"
+      sha256: "1a5848f598acc5b7d8f7c18b8cb834ab667e59a13edc3c93e9d09cf38cc6bc87"
       url: "https://pub.dev"
     source: hosted
-    version: "6.0.24"
+    version: "6.0.34"
   url_launcher_ios:
     dependency: transitive
     description:
       name: url_launcher_ios
-      sha256: c9cd648d2f7ab56968e049d4e9116f96a85517f1dd806b96a86ea1018a3a82e5
+      sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
       url: "https://pub.dev"
     source: hosted
-    version: "6.1.1"
+    version: "6.1.4"
   url_launcher_linux:
     dependency: transitive
     description:
       name: url_launcher_linux
-      sha256: e29039160ab3730e42f3d811dc2a6d5f2864b90a70fb765ea60144b03307f682
+      sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.3"
+    version: "3.0.5"
   url_launcher_macos:
     dependency: transitive
     description:
       name: url_launcher_macos
-      sha256: "2dddb3291a57b074dade66b5e07e64401dd2487caefd4e9e2f467138d8c7eb06"
+      sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.3"
+    version: "3.0.5"
   url_launcher_platform_interface:
     dependency: transitive
     description:
@@ -1324,18 +1332,18 @@ packages:
     dependency: transitive
     description:
       name: url_launcher_web
-      sha256: "574cfbe2390666003c3a1d129bdc4574aaa6728f0c00a4829a81c316de69dd9b"
+      sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.15"
+    version: "2.0.16"
   url_launcher_windows:
     dependency: transitive
     description:
       name: url_launcher_windows
-      sha256: "97c9067950a0d09cbd93e2e3f0383d1403989362b97102fbf446473a48079a4b"
+      sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.4"
+    version: "3.0.6"
   uuid:
     dependency: transitive
     description:
@@ -1356,42 +1364,42 @@ packages:
     dependency: "direct main"
     description:
       name: video_player
-      sha256: "6cec15c21974282994577ffcfb5b42e64a699d38583138ec8dcb3d0a6902a41c"
+      sha256: de95f0e9405f29b5582573d4166132e71f83b3158aac14e8ee5767a54f4f1fbd
       url: "https://pub.dev"
     source: hosted
-    version: "2.5.2"
+    version: "2.6.1"
   video_player_android:
     dependency: transitive
     description:
       name: video_player_android
-      sha256: "0fc42778d794465f12456ccdade3e729e4339c8a112f9e58d170dc00f17b75f2"
+      sha256: ae1c7d9a71c236a1bf9e567bd7ed4c90887e389a5f233b2192593f7f7395005c
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.11"
+    version: "2.4.8"
   video_player_avfoundation:
     dependency: transitive
     description:
       name: video_player_avfoundation
-      sha256: "5df5411ff9d316f1dcbfee284e9838aa686e314f2a722b15c02cb7ce40ef9446"
+      sha256: "4c274e439f349a0ee5cb3c42978393ede173a443b98f50de6ffe6900eaa19216"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.9"
+    version: "2.4.6"
   video_player_platform_interface:
     dependency: transitive
     description:
       name: video_player_platform_interface
-      sha256: "72ba04ad0eff76123c6d782ac46621cb8be476a89c33c89173fce982b6ec049b"
+      sha256: a8c4dcae2a7a6e7cc1d7f9808294d968eca1993af34a98e95b9bdfa959bec684
       url: "https://pub.dev"
     source: hosted
-    version: "6.0.2"
+    version: "6.1.0"
   video_player_web:
     dependency: transitive
     description:
       name: video_player_web
-      sha256: d635bb2834f2b14cfd52c7fc9307a95dffbe768d116dd6047a4ecbba203289c8
+      sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.14"
+    version: "2.0.16"
   vm_service:
     dependency: transitive
     description:
@@ -1444,18 +1452,18 @@ packages:
     dependency: transitive
     description:
       name: watcher
-      sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0"
+      sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.2"
+    version: "1.1.0"
   web_socket_channel:
     dependency: transitive
     description:
       name: web_socket_channel
-      sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b
+      sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.0"
+    version: "2.4.0"
   webdriver:
     dependency: transitive
     description:
@@ -1468,10 +1476,10 @@ packages:
     dependency: transitive
     description:
       name: win32
-      sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
+      sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.3"
+    version: "3.1.4"
   wkt_parser:
     dependency: transitive
     description:
@@ -1492,10 +1500,10 @@ packages:
     dependency: transitive
     description:
       name: xml
-      sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
+      sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
       url: "https://pub.dev"
     source: hosted
-    version: "6.2.2"
+    version: "6.3.0"
   xxh3:
     dependency: transitive
     description:
@@ -1508,10 +1516,10 @@ packages:
     dependency: transitive
     description:
       name: yaml
-      sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370"
+      sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.1"
+    version: "3.1.2"
 sdks:
-  dart: ">=3.0.0-0 <4.0.0"
+  dart: ">=3.0.0 <4.0.0"
   flutter: ">=3.3.0"