Procházet zdrojové kódy

Add service provider (#250)

* optimize android side gradle settings

* android minsdk back to 21

* remove unused package, update linter and fix lint error

* clean code of 'shared module' with offical dart style guide

* restore uploadProfileImage method in UserService

* add service provider

* fix searchFocusNode init error
xpwmaosldk před 3 roky
rodič
revize
d02b97e1c1
24 změnil soubory, kde provedl 289 přidání a 158 odebrání
  1. 31 24
      mobile/lib/modules/asset_viewer/views/image_viewer_page.dart
  2. 15 9
      mobile/lib/modules/asset_viewer/views/video_viewer_page.dart
  3. 26 25
      mobile/lib/modules/backup/providers/backup.provider.dart
  4. 23 9
      mobile/lib/modules/backup/services/backup.service.dart
  5. 18 8
      mobile/lib/modules/home/providers/upload_profile_image.provider.dart
  6. 16 6
      mobile/lib/modules/home/services/asset.service.dart
  7. 23 13
      mobile/lib/modules/login/providers/authentication.provider.dart
  8. 5 5
      mobile/lib/modules/search/providers/search_page_state.provider.dart
  9. 28 11
      mobile/lib/modules/search/providers/search_result_page.provider.dart
  10. 12 4
      mobile/lib/modules/search/services/search.service.dart
  11. 1 1
      mobile/lib/modules/search/views/search_page.dart
  12. 9 5
      mobile/lib/modules/sharing/providers/album_viewer.provider.dart
  13. 5 4
      mobile/lib/modules/sharing/providers/shared_album.provider.dart
  14. 3 2
      mobile/lib/modules/sharing/providers/suggested_shared_users.provider.dart
  15. 6 1
      mobile/lib/modules/sharing/services/shared_album.service.dart
  16. 6 3
      mobile/lib/modules/sharing/views/album_viewer_page.dart
  17. 34 16
      mobile/lib/modules/sharing/views/select_user_for_sharing_page.dart
  18. 3 4
      mobile/lib/shared/providers/asset.provider.dart
  19. 3 3
      mobile/lib/shared/providers/server_info.provider.dart
  20. 4 0
      mobile/lib/shared/services/device_info.service.dart
  21. 3 0
      mobile/lib/shared/services/local_storage.service.dart
  22. 3 3
      mobile/lib/shared/services/network.service.dart
  23. 6 1
      mobile/lib/shared/services/server_info.service.dart
  24. 6 1
      mobile/lib/shared/services/user.service.dart

+ 31 - 24
mobile/lib/modules/asset_viewer/views/image_viewer_page.dart

@@ -20,20 +20,26 @@ class ImageViewerPage extends HookConsumerWidget {
   final String heroTag;
   final String thumbnailUrl;
   final ImmichAsset asset;
-  final AssetService _assetService = AssetService();
+
   ImmichAssetWithExif? assetDetail;
 
-  ImageViewerPage(
-      {Key? key, required this.imageUrl, required this.heroTag, required this.thumbnailUrl, required this.asset})
-      : super(key: key);
+  ImageViewerPage({
+    Key? key,
+    required this.imageUrl,
+    required this.heroTag,
+    required this.thumbnailUrl,
+    required this.asset,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
+    final downloadAssetStatus =
+        ref.watch(imageViewerStateProvider).downloadAssetStatus;
     var box = Hive.box(userInfoBox);
 
     getAssetExif() async {
-      assetDetail = await _assetService.getAssetById(asset.id);
+      assetDetail =
+          await ref.watch(assetServiceProvider).getAssetById(asset.id);
     }
 
     showInfo() {
@@ -59,31 +65,32 @@ class ImageViewerPage extends HookConsumerWidget {
         asset: asset,
         onMoreInfoPressed: showInfo,
         onDownloadPressed: () {
-          ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
+          ref
+              .watch(imageViewerStateProvider.notifier)
+              .downloadAsset(asset, context);
         },
       ),
       body: SafeArea(
-          child: Stack(
-            children: [
-              Center(
-                child: Hero(
+        child: Stack(
+          children: [
+            Center(
+              child: Hero(
                   tag: heroTag,
                   child: RemotePhotoView(
-                      thumbnailUrl: thumbnailUrl,
-                      imageUrl: imageUrl,
-                      authToken: "Bearer ${box.get(accessTokenKey)}",
-                      onSwipeDown: () => AutoRouter.of(context).pop(),
-                      onSwipeUp: () => showInfo(),
-                  )
-                ),
+                    thumbnailUrl: thumbnailUrl,
+                    imageUrl: imageUrl,
+                    authToken: "Bearer ${box.get(accessTokenKey)}",
+                    onSwipeDown: () => AutoRouter.of(context).pop(),
+                    onSwipeUp: () => showInfo(),
+                  )),
+            ),
+            if (downloadAssetStatus == DownloadAssetStatus.loading)
+              const Center(
+                child: DownloadLoadingIndicator(),
               ),
-              if (downloadAssetStatus == DownloadAssetStatus.loading)
-                const Center(
-                  child: DownloadLoadingIndicator(),
-                ),
-            ],
-          ),
+          ],
         ),
+      ),
     );
   }
 }

+ 15 - 9
mobile/lib/modules/asset_viewer/views/video_viewer_page.dart

@@ -21,13 +21,14 @@ class VideoViewerPage extends HookConsumerWidget {
   final String videoUrl;
   final ImmichAsset asset;
   ImmichAssetWithExif? assetDetail;
-  final AssetService _assetService = AssetService();
 
-  VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) : super(key: key);
+  VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
+      : super(key: key);
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
+    final downloadAssetStatus =
+        ref.watch(imageViewerStateProvider).downloadAssetStatus;
 
     String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
 
@@ -44,7 +45,8 @@ class VideoViewerPage extends HookConsumerWidget {
     }
 
     getAssetExif() async {
-      assetDetail = await _assetService.getAssetById(asset.id);
+      assetDetail =
+          await ref.watch(assetServiceProvider).getAssetById(asset.id);
     }
 
     useEffect(() {
@@ -60,7 +62,9 @@ class VideoViewerPage extends HookConsumerWidget {
           showInfo();
         },
         onDownloadPressed: () {
-          ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
+          ref
+              .watch(imageViewerStateProvider.notifier)
+              .downloadAsset(asset, context);
         },
       ),
       body: SwipeDetector(
@@ -93,7 +97,8 @@ class VideoThumbnailPlayer extends StatefulWidget {
   final String url;
   final String? jwtToken;
 
-  const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken}) : super(key: key);
+  const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken})
+      : super(key: key);
 
   @override
   State<VideoThumbnailPlayer> createState() => _VideoThumbnailPlayerState();
@@ -111,8 +116,8 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
 
   Future<void> initializePlayer() async {
     try {
-      videoPlayerController =
-          VideoPlayerController.network(widget.url, httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"});
+      videoPlayerController = VideoPlayerController.network(widget.url,
+          httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"});
 
       await videoPlayerController.initialize();
       _createChewieController();
@@ -142,7 +147,8 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
 
   @override
   Widget build(BuildContext context) {
-    return chewieController != null && chewieController!.videoPlayerController.value.isInitialized
+    return chewieController != null &&
+            chewieController!.videoPlayerController.value.isInitialized
         ? SizedBox(
             child: Chewie(
               controller: chewieController!,

+ 26 - 25
mobile/lib/modules/backup/providers/backup.provider.dart

@@ -7,13 +7,14 @@ import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
 import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
 import 'package:immich_mobile/modules/backup/services/backup.service.dart';
+import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/shared/models/server_info.model.dart';
 import 'package:immich_mobile/shared/services/server_info.service.dart';
 import 'package:photo_manager/photo_manager.dart';
 
 class BackupNotifier extends StateNotifier<BackUpState> {
-  BackupNotifier({this.ref})
+  BackupNotifier(this._backupService, this._serverInfoService, this._authState)
       : super(
           BackUpState(
             backupProgress: BackUpProgressEnum.idle,
@@ -37,9 +38,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
           ),
         );
 
-  Ref? ref;
-  final BackupService _backupService = BackupService();
-  final ServerInfoService _serverInfoService = ServerInfoService();
+  final BackupService _backupService;
+  final ServerInfoService _serverInfoService;
+  final AuthenticationState _authState;
 
   ///
   /// UI INTERACTION
@@ -338,38 +339,38 @@ class BackupNotifier extends StateNotifier<BackUpState> {
   }
 
   void resumeBackup() {
-    var authState = ref?.read(authenticationProvider);
-
     // Check if user is login
     var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
 
     // User has been logged out return
-    if (authState != null) {
-      if (accessKey == null || !authState.isAuthenticated) {
-        debugPrint("[resumeBackup] not authenticated - abort");
-        return;
-      }
+    if (accessKey == null || !_authState.isAuthenticated) {
+      debugPrint("[resumeBackup] not authenticated - abort");
+      return;
+    }
 
-      // Check if this device is enable backup by the user
-      if ((authState.deviceInfo.deviceId == authState.deviceId) &&
-          authState.deviceInfo.isAutoBackup) {
-        // check if backup is alreayd in process - then return
-        if (state.backupProgress == BackUpProgressEnum.inProgress) {
-          debugPrint("[resumeBackup] Backup is already in progress - abort");
-          return;
-        }
-
-        // Run backup
-        debugPrint("[resumeBackup] Start back up");
-        startBackupProcess();
+    // Check if this device is enable backup by the user
+    if ((_authState.deviceInfo.deviceId == _authState.deviceId) &&
+        _authState.deviceInfo.isAutoBackup) {
+      // check if backup is alreayd in process - then return
+      if (state.backupProgress == BackUpProgressEnum.inProgress) {
+        debugPrint("[resumeBackup] Backup is already in progress - abort");
+        return;
       }
 
-      return;
+      // Run backup
+      debugPrint("[resumeBackup] Start back up");
+      startBackupProcess();
     }
+
+    return;
   }
 }
 
 final backupProvider =
     StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
-  return BackupNotifier(ref: ref);
+  return BackupNotifier(
+    ref.watch(backupServiceProvider),
+    ref.watch(serverInfoServiceProvider),
+    ref.watch(authenticationProvider),
+  );
 });

+ 23 - 9
mobile/lib/modules/backup/services/backup.service.dart

@@ -5,6 +5,7 @@ import 'dart:io';
 import 'package:dio/dio.dart';
 import 'package:flutter/material.dart';
 import 'package:hive/hive.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/constants/hive_box.dart';
 import 'package:immich_mobile/shared/services/network.service.dart';
 import 'package:immich_mobile/shared/models/device_info.model.dart';
@@ -14,20 +15,28 @@ import 'package:http_parser/http_parser.dart';
 import 'package:path/path.dart' as p;
 import 'package:cancellation_token_http/http.dart' as http;
 
+final backupServiceProvider =
+    Provider((ref) => BackupService(ref.watch(networkServiceProvider)));
+
 class BackupService {
-  final NetworkService _networkService = NetworkService();
+  final NetworkService _networkService;
+  BackupService(this._networkService);
 
   Future<List<String>> getDeviceBackupAsset() async {
     String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
 
-    Response response = await _networkService.getRequest(url: "asset/$deviceId");
+    Response response =
+        await _networkService.getRequest(url: "asset/$deviceId");
     List<dynamic> result = jsonDecode(response.toString());
 
     return result.cast<String>();
   }
 
-  backupAsset(Set<AssetEntity> assetList, http.CancellationToken cancelToken,
-      Function(String, String) singleAssetDoneCb, Function(int, int) uploadProgress) async {
+  backupAsset(
+      Set<AssetEntity> assetList,
+      http.CancellationToken cancelToken,
+      Function(String, String) singleAssetDoneCb,
+      Function(int, int) uploadProgress) async {
     String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
     String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
     File? file;
@@ -44,7 +53,8 @@ class BackupService {
 
         if (file != null) {
           String originalFileName = await entity.titleAsync;
-          String fileNameWithoutPath = originalFileName.toString().split(".")[0];
+          String fileNameWithoutPath =
+              originalFileName.toString().split(".")[0];
           var fileExtension = p.extension(file.path);
           var mimeType = FileHelper.getMimeType(file.path);
           var fileStream = file.openRead();
@@ -60,7 +70,8 @@ class BackupService {
           );
 
           // Build thumbnail multipart data
-          var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
+          var thumbnailData = await entity
+              .thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
           if (thumbnailData != null) {
             thumbnailUploadData = http.MultipartFile.fromBytes(
               "thumbnailData",
@@ -75,8 +86,10 @@ class BackupService {
 
           var box = Hive.box(userInfoBox);
 
-          var req = MultipartRequest('POST', Uri.parse('$savedEndpoint/asset/upload'),
-              onProgress: ((bytes, totalBytes) => uploadProgress(bytes, totalBytes)));
+          var req = MultipartRequest(
+              'POST', Uri.parse('$savedEndpoint/asset/upload'),
+              onProgress: ((bytes, totalBytes) =>
+                  uploadProgress(bytes, totalBytes)));
           req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
 
           req.fields['deviceAssetId'] = entity.id;
@@ -126,7 +139,8 @@ class BackupService {
     }
   }
 
-  Future<DeviceInfoRemote> setAutoBackup(bool status, String deviceId, String deviceType) async {
+  Future<DeviceInfoRemote> setAutoBackup(
+      bool status, String deviceId, String deviceType) async {
     var res = await _networkService.patchRequest(url: 'device-info', data: {
       "isAutoBackup": status,
       "deviceId": deviceId,

+ 18 - 8
mobile/lib/modules/home/providers/upload_profile_image.provider.dart

@@ -50,37 +50,46 @@ class UploadProfileImageState {
 
   String toJson() => json.encode(toMap());
 
-  factory UploadProfileImageState.fromJson(String source) => UploadProfileImageState.fromMap(json.decode(source));
+  factory UploadProfileImageState.fromJson(String source) =>
+      UploadProfileImageState.fromMap(json.decode(source));
 
   @override
-  String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)';
+  String toString() =>
+      'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)';
 
   @override
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
 
-    return other is UploadProfileImageState && other.status == status && other.profileImagePath == profileImagePath;
+    return other is UploadProfileImageState &&
+        other.status == status &&
+        other.profileImagePath == profileImagePath;
   }
 
   @override
   int get hashCode => status.hashCode ^ profileImagePath.hashCode;
 }
 
-class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState> {
-  UploadProfileImageNotifier()
+class UploadProfileImageNotifier
+    extends StateNotifier<UploadProfileImageState> {
+  UploadProfileImageNotifier(this._userSErvice)
       : super(UploadProfileImageState(
           profileImagePath: '',
           status: UploadProfileStatus.idle,
         ));
 
+  final UserService _userSErvice;
+
   Future<bool> upload(XFile file) async {
     state = state.copyWith(status: UploadProfileStatus.loading);
 
-    var res = await UserService().uploadProfileImage(file);
+    var res = await _userSErvice.uploadProfileImage(file);
 
     if (res != null) {
       debugPrint("Succesfully upload profile image");
-      state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: res.profileImagePath);
+      state = state.copyWith(
+          status: UploadProfileStatus.success,
+          profileImagePath: res.profileImagePath);
       return true;
     }
 
@@ -90,4 +99,5 @@ class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState>
 }
 
 final uploadProfileImageProvider =
-    StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(((ref) => UploadProfileImageNotifier()));
+    StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(
+        ((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))));

+ 16 - 6
mobile/lib/modules/home/services/asset.service.dart

@@ -1,21 +1,27 @@
 import 'dart:convert';
 
 import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart';
 import 'package:immich_mobile/modules/home/models/get_all_asset_response.model.dart';
 import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
 import 'package:immich_mobile/shared/services/network.service.dart';
 
+final assetServiceProvider =
+    Provider((ref) => AssetService(ref.watch(networkServiceProvider)));
+
 class AssetService {
-  final NetworkService _networkService = NetworkService();
+  final NetworkService _networkService;
+  AssetService(this._networkService);
 
   Future<List<ImmichAsset>?> getAllAsset() async {
     var res = await _networkService.getRequest(url: "asset/");
     try {
       List<dynamic> decodedData = jsonDecode(res.toString());
 
-      List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
+      List<ImmichAsset> result =
+          List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
       return result;
     } catch (e) {
       debugPrint("Error getAllAsset  ${e.toString()}");
@@ -62,7 +68,8 @@ class AssetService {
 
       List<dynamic> decodedData = jsonDecode(res.toString());
 
-      List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
+      List<ImmichAsset> result =
+          List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
       if (result.isNotEmpty) {
         return result;
       }
@@ -90,7 +97,8 @@ class AssetService {
     }
   }
 
-  Future<List<DeleteAssetResponse>?> deleteAssets(Set<ImmichAsset> deleteAssets) async {
+  Future<List<DeleteAssetResponse>?> deleteAssets(
+      Set<ImmichAsset> deleteAssets) async {
     try {
       var payload = [];
 
@@ -98,11 +106,13 @@ class AssetService {
         payload.add(asset.id);
       }
 
-      var res = await _networkService.deleteRequest(url: "asset/", data: {"ids": payload});
+      var res = await _networkService
+          .deleteRequest(url: "asset/", data: {"ids": payload});
 
       List<dynamic> decodedData = jsonDecode(res.toString());
 
-      List<DeleteAssetResponse> result = List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a)));
+      List<DeleteAssetResponse> result =
+          List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a)));
 
       return result;
     } catch (e) {

+ 23 - 13
mobile/lib/modules/login/providers/authentication.provider.dart

@@ -12,7 +12,8 @@ import 'package:immich_mobile/shared/services/network.service.dart';
 import 'package:immich_mobile/shared/models/device_info.model.dart';
 
 class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
-  AuthenticationNotifier(this.ref)
+  AuthenticationNotifier(
+      this._deviceInfoService, this._backupService, this._networkService)
       : super(
           AuthenticationState(
             deviceId: "",
@@ -37,12 +38,12 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
           ),
         );
 
-  final Ref ref;
-  final DeviceInfoService _deviceInfoService = DeviceInfoService();
-  final BackupService _backupService = BackupService();
-  final NetworkService _networkService = NetworkService();
+  final DeviceInfoService _deviceInfoService;
+  final BackupService _backupService;
+  final NetworkService _networkService;
 
-  Future<bool> login(String email, String password, String serverEndpoint, bool isSavedLoginInfo) async {
+  Future<bool> login(String email, String password, String serverEndpoint,
+      bool isSavedLoginInfo) async {
     // Store server endpoint to Hive and test endpoint
     if (serverEndpoint[serverEndpoint.length - 1] == "/") {
       var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
@@ -71,7 +72,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 
     // Make sign-in request
     try {
-      Response res = await _networkService.postRequest(url: 'auth/login', data: {'email': email, 'password': password});
+      Response res = await _networkService.postRequest(
+          url: 'auth/login', data: {'email': email, 'password': password});
 
       var payload = LogInReponse.fromJson(res.toString());
 
@@ -99,7 +101,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
               serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)),
         );
       } else {
-        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey);
+        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
+            .delete(savedLoginInfoKey);
       }
     } catch (e) {
       return false;
@@ -107,8 +110,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 
     // Register device info
     try {
-      Response res = await _networkService
-          .postRequest(url: 'device-info', data: {'deviceId': state.deviceId, 'deviceType': state.deviceType});
+      Response res = await _networkService.postRequest(
+          url: 'device-info',
+          data: {'deviceId': state.deviceId, 'deviceType': state.deviceType});
 
       DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString());
       state = state.copyWith(deviceInfo: deviceInfo);
@@ -151,7 +155,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
     var deviceId = deviceInfo["deviceId"];
     var deviceType = deviceInfo["deviceType"];
 
-    DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
+    DeviceInfoRemote deviceInfoRemote =
+        await _backupService.setAutoBackup(backupState, deviceId, deviceType);
     state = state.copyWith(deviceInfo: deviceInfoRemote);
   }
 
@@ -160,6 +165,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
   }
 }
 
-final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
-  return AuthenticationNotifier(ref);
+final authenticationProvider =
+    StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
+  return AuthenticationNotifier(
+    ref.watch(deviceInfoServiceProvider),
+    ref.watch(backupServiceProvider),
+    ref.watch(networkServiceProvider),
+  );
 });

+ 5 - 5
mobile/lib/modules/search/providers/search_page_state.provider.dart

@@ -6,7 +6,7 @@ import 'package:immich_mobile/modules/search/models/search_page_state.model.dart
 import 'package:immich_mobile/modules/search/services/search.service.dart';
 
 class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
-  SearchPageStateNotifier()
+  SearchPageStateNotifier(this._searchService)
       : super(
           SearchPageState(
             searchTerm: "",
@@ -16,7 +16,7 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
           ),
         );
 
-  final SearchService _searchService = SearchService();
+  final SearchService _searchService;
 
   void enableSearch() {
     state = state.copyWith(isSearchEnabled: true);
@@ -54,12 +54,12 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
 
 final searchPageStateProvider =
     StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
-  return SearchPageStateNotifier();
+  return SearchPageStateNotifier(ref.watch(searchServiceProvider));
 });
 
 final getCuratedLocationProvider =
     FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
-  final SearchService searchService = SearchService();
+  final SearchService searchService = ref.watch(searchServiceProvider);
 
   var curatedLocation = await searchService.getCuratedLocation();
   if (curatedLocation != null) {
@@ -71,7 +71,7 @@ final getCuratedLocationProvider =
 
 final getCuratedObjectProvider =
     FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
-  final SearchService searchService = SearchService();
+  final SearchService searchService = ref.watch(searchServiceProvider);
 
   var curatedObject = await searchService.getCuratedObjects();
   if (curatedObject != null) {

+ 28 - 11
mobile/lib/modules/search/providers/search_result_page.provider.dart

@@ -7,31 +7,48 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 import 'package:intl/intl.dart';
 
 class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
-  SearchResultPageNotifier()
-      : super(SearchResultPageState(searchResult: [], isError: false, isLoading: true, isSuccess: false));
-
-  final SearchService _searchService = SearchService();
+  SearchResultPageNotifier(this._searchService)
+      : super(
+          SearchResultPageState(
+            searchResult: [],
+            isError: false,
+            isLoading: true,
+            isSuccess: false,
+          ),
+        );
+
+  final SearchService _searchService;
 
   void search(String searchTerm) async {
-    state = state.copyWith(searchResult: [], isError: false, isLoading: true, isSuccess: false);
+    state = state.copyWith(
+        searchResult: [], isError: false, isLoading: true, isSuccess: false);
 
     List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm);
 
     if (assets != null) {
-      state = state.copyWith(searchResult: assets, isError: false, isLoading: false, isSuccess: true);
+      state = state.copyWith(
+          searchResult: assets,
+          isError: false,
+          isLoading: false,
+          isSuccess: true);
     } else {
-      state = state.copyWith(searchResult: [], isError: true, isLoading: false, isSuccess: false);
+      state = state.copyWith(
+          searchResult: [], isError: true, isLoading: false, isSuccess: false);
     }
   }
 }
 
-final searchResultPageProvider = StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>((ref) {
-  return SearchResultPageNotifier();
+final searchResultPageProvider =
+    StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>(
+        (ref) {
+  return SearchResultPageNotifier(ref.watch(searchServiceProvider));
 });
 
 final searchResultGroupByDateTimeProvider = StateProvider((ref) {
   var assets = ref.watch(searchResultPageProvider).searchResult;
 
-  assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
-  return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
+  assets.sortByCompare<DateTime>(
+      (e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
+  return assets.groupListsBy((element) =>
+      DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
 });

+ 12 - 4
mobile/lib/modules/search/services/search.service.dart

@@ -1,13 +1,18 @@
 import 'dart:convert';
 
 import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
 import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
 import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 import 'package:immich_mobile/shared/services/network.service.dart';
 
+final searchServiceProvider =
+    Provider((ref) => SearchService(ref.watch(networkServiceProvider)));
+
 class SearchService {
-  final NetworkService _networkService = NetworkService();
+  final NetworkService _networkService;
+  SearchService(this._networkService);
 
   Future<List<String>?> getUserSuggestedSearchTerms() async {
     try {
@@ -30,7 +35,8 @@ class SearchService {
 
       List<dynamic> decodedData = jsonDecode(res.toString());
 
-      List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
+      List<ImmichAsset> result =
+          List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
 
       return result;
     } catch (e) {
@@ -45,7 +51,8 @@ class SearchService {
 
       List<dynamic> decodedData = jsonDecode(res.toString());
 
-      List<CuratedLocation> result = List.from(decodedData.map((a) => CuratedLocation.fromMap(a)));
+      List<CuratedLocation> result =
+          List.from(decodedData.map((a) => CuratedLocation.fromMap(a)));
 
       return result;
     } catch (e) {
@@ -60,7 +67,8 @@ class SearchService {
 
       List<dynamic> decodedData = jsonDecode(res.toString());
 
-      List<CuratedObject> result = List.from(decodedData.map((a) => CuratedObject.fromMap(a)));
+      List<CuratedObject> result =
+          List.from(decodedData.map((a) => CuratedObject.fromMap(a)));
 
       return result;
     } catch (e) {

+ 1 - 1
mobile/lib/modules/search/views/search_page.dart

@@ -18,7 +18,7 @@ import 'package:immich_mobile/utils/capitalize_first_letter.dart';
 class SearchPage extends HookConsumerWidget {
   SearchPage({Key? key}) : super(key: key);
 
-  late FocusNode searchFocusNode;
+  FocusNode searchFocusNode = FocusNode();
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {

+ 9 - 5
mobile/lib/modules/sharing/providers/album_viewer.provider.dart

@@ -4,7 +4,8 @@ import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.da
 import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
 
 class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
-  AlbumViewerNotifier(this.ref) : super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
+  AlbumViewerNotifier(this.ref)
+      : super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
 
   final Ref ref;
 
@@ -28,10 +29,12 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
     state = state.copyWith(editTitleText: "", isEditAlbum: false);
   }
 
-  Future<bool> changeAlbumTitle(String albumId, String ownerId, String newAlbumTitle) async {
-    SharedAlbumService service = SharedAlbumService();
+  Future<bool> changeAlbumTitle(
+      String albumId, String ownerId, String newAlbumTitle) async {
+    SharedAlbumService service = ref.watch(sharedAlbumServiceProvider);
 
-    bool isSuccess = await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
+    bool isSuccess =
+        await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
 
     if (isSuccess) {
       state = state.copyWith(editTitleText: "", isEditAlbum: false);
@@ -45,6 +48,7 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
   }
 }
 
-final albumViewerProvider = StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
+final albumViewerProvider =
+    StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
   return AlbumViewerNotifier(ref);
 });

+ 5 - 4
mobile/lib/modules/sharing/providers/shared_album.provider.dart

@@ -3,9 +3,9 @@ import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
 import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
 
 class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
-  SharedAlbumNotifier() : super([]);
+  SharedAlbumNotifier(this._sharedAlbumService) : super([]);
 
-  final SharedAlbumService _sharedAlbumService = SharedAlbumService();
+  final SharedAlbumService _sharedAlbumService;
 
   getAllSharedAlbums() async {
     List<SharedAlbum> sharedAlbums =
@@ -50,12 +50,13 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
 
 final sharedAlbumProvider =
     StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
-  return SharedAlbumNotifier();
+  return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider));
 });
 
 final sharedAlbumDetailProvider = FutureProvider.autoDispose
     .family<SharedAlbum, String>((ref, albumId) async {
-  final SharedAlbumService sharedAlbumService = SharedAlbumService();
+  final SharedAlbumService sharedAlbumService =
+      ref.watch(sharedAlbumServiceProvider);
 
   return await sharedAlbumService.getAlbumDetail(albumId);
 });

+ 3 - 2
mobile/lib/modules/sharing/providers/suggested_shared_users.provider.dart

@@ -2,8 +2,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/shared/models/user.model.dart';
 import 'package:immich_mobile/shared/services/user.service.dart';
 
-final suggestedSharedUsersProvider = FutureProvider.autoDispose<List<User>>((ref) async {
-  UserService userService = UserService();
+final suggestedSharedUsersProvider =
+    FutureProvider.autoDispose<List<User>>((ref) async {
+  UserService userService = ref.watch(userServiceProvider);
 
   return await userService.getAllUsersInfo();
 });

+ 6 - 1
mobile/lib/modules/sharing/services/shared_album.service.dart

@@ -3,12 +3,17 @@ import 'dart:convert';
 
 import 'package:dio/dio.dart';
 import 'package:flutter/foundation.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
 import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 import 'package:immich_mobile/shared/services/network.service.dart';
 
+final sharedAlbumServiceProvider =
+    Provider((ref) => SharedAlbumService(ref.watch(networkServiceProvider)));
+
 class SharedAlbumService {
-  final NetworkService _networkService = NetworkService();
+  final NetworkService _networkService;
+  SharedAlbumService(this._networkService);
 
   Future<List<SharedAlbum>> getAllSharedAlbum() async {
     try {

+ 6 - 3
mobile/lib/modules/sharing/views/album_viewer_page.dart

@@ -53,8 +53,10 @@ class AlbumViewerPage extends HookConsumerWidget {
         if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
           ImmichLoadingOverlayController.appLoader.show();
 
-          var isSuccess = await SharedAlbumService().addAdditionalAssetToAlbum(
-              returnPayload.selectedAdditionalAsset, albumId);
+          var isSuccess = await ref
+              .watch(sharedAlbumServiceProvider)
+              .addAdditionalAssetToAlbum(
+                  returnPayload.selectedAdditionalAsset, albumId);
 
           if (isSuccess) {
             ref.refresh(sharedAlbumDetailProvider(albumId));
@@ -77,7 +79,8 @@ class AlbumViewerPage extends HookConsumerWidget {
       if (sharedUserIds != null) {
         ImmichLoadingOverlayController.appLoader.show();
 
-        var isSuccess = await SharedAlbumService()
+        var isSuccess = await ref
+            .watch(sharedAlbumServiceProvider)
             .addAdditionalUserToAlbum(sharedUserIds, albumId);
 
         if (isSuccess) {

+ 34 - 16
mobile/lib/modules/sharing/views/select_user_for_sharing_page.dart

@@ -16,24 +16,28 @@ class SelectUserForSharingPage extends HookConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final sharedUsersList = useState<Set<User>>({});
-    AsyncValue<List<User>> suggestedShareUsers = ref.watch(suggestedSharedUsersProvider);
+    AsyncValue<List<User>> suggestedShareUsers =
+        ref.watch(suggestedSharedUsersProvider);
 
     _createSharedAlbum() async {
-      var isSuccess = await SharedAlbumService().createSharedAlbum(
-        ref.watch(albumTitleProvider),
-        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
-        sharedUsersList.value.map((userInfo) => userInfo.id).toList(),
-      );
+      var isSuccess =
+          await ref.watch(sharedAlbumServiceProvider).createSharedAlbum(
+                ref.watch(albumTitleProvider),
+                ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
+                sharedUsersList.value.map((userInfo) => userInfo.id).toList(),
+              );
 
       if (isSuccess) {
         await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
         ref.watch(assetSelectionProvider.notifier).removeAll();
         ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
 
-        AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
+        AutoRouter.of(context)
+            .navigate(const TabControllerRoute(children: [SharingRoute()]));
       }
 
-      const ScaffoldMessenger(child: SnackBar(content: Text('Failed to create album')));
+      const ScaffoldMessenger(
+          child: SnackBar(content: Text('Failed to create album')));
     }
 
     _buildTileIcon(User user) {
@@ -47,7 +51,8 @@ class SelectUserForSharingPage extends HookConsumerWidget {
         );
       } else {
         return CircleAvatar(
-          backgroundImage: const AssetImage('assets/immich-logo-no-outline.png'),
+          backgroundImage:
+              const AssetImage('assets/immich-logo-no-outline.png'),
           backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
         );
       }
@@ -64,7 +69,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
               backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
               label: Text(
                 user.email,
-                style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold),
+                style: const TextStyle(
+                    fontSize: 12,
+                    color: Colors.black87,
+                    fontWeight: FontWeight.bold),
               ),
             ),
           ),
@@ -80,7 +88,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
             padding: EdgeInsets.all(16.0),
             child: Text(
               'Suggestions',
-              style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold),
+              style: TextStyle(
+                  fontSize: 14,
+                  color: Colors.grey,
+                  fontWeight: FontWeight.bold),
             ),
           ),
           ListView.builder(
@@ -90,14 +101,20 @@ class SelectUserForSharingPage extends HookConsumerWidget {
                 leading: _buildTileIcon(users[index]),
                 title: Text(
                   users[index].email,
-                  style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
+                  style: const TextStyle(
+                      fontSize: 14, fontWeight: FontWeight.bold),
                 ),
                 onTap: () {
                   if (sharedUsersList.value.contains(users[index])) {
-                    sharedUsersList.value =
-                        sharedUsersList.value.where((selectedUser) => selectedUser.id != users[index].id).toSet();
+                    sharedUsersList.value = sharedUsersList.value
+                        .where((selectedUser) =>
+                            selectedUser.id != users[index].id)
+                        .toSet();
                   } else {
-                    sharedUsersList.value = {...sharedUsersList.value, users[index]};
+                    sharedUsersList.value = {
+                      ...sharedUsersList.value,
+                      users[index]
+                    };
                   }
                 },
               );
@@ -124,7 +141,8 @@ class SelectUserForSharingPage extends HookConsumerWidget {
         ),
         actions: [
           TextButton(
-              onPressed: sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
+              onPressed:
+                  sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
               child: const Text(
                 "Create Album",
                 style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),

+ 3 - 4
mobile/lib/shared/providers/asset.provider.dart

@@ -9,11 +9,10 @@ import 'package:intl/intl.dart';
 import 'package:photo_manager/photo_manager.dart';
 
 class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
-  final AssetService _assetService = AssetService();
+  final AssetService _assetService;
   final DeviceInfoService _deviceInfoService = DeviceInfoService();
-  final Ref ref;
 
-  AssetNotifier(this.ref) : super([]);
+  AssetNotifier(this._assetService) : super([]);
 
   getAllAsset() async {
     List<ImmichAsset>? allAssets = await _assetService.getAllAsset();
@@ -71,7 +70,7 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
 
 final assetProvider =
     StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) {
-  return AssetNotifier(ref);
+  return AssetNotifier(ref.watch(assetServiceProvider));
 });
 
 final assetGroupByDateTimeProvider = StateProvider((ref) {

+ 3 - 3
mobile/lib/shared/providers/server_info.provider.dart

@@ -7,7 +7,7 @@ import 'package:immich_mobile/shared/services/server_info.service.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 
 class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
-  ServerInfoNotifier()
+  ServerInfoNotifier(this._serverInfoService)
       : super(
           ServerInfoState(
             mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
@@ -18,7 +18,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
           ),
         );
 
-  final ServerInfoService _serverInfoService = ServerInfoService();
+  final ServerInfoService _serverInfoService;
 
   getServerVersion() async {
     ServerVersion? serverVersion = await _serverInfoService.getServerVersion();
@@ -79,5 +79,5 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
 
 final serverInfoProvider =
     StateNotifierProvider<ServerInfoNotifier, ServerInfoState>((ref) {
-  return ServerInfoNotifier();
+  return ServerInfoNotifier(ref.watch(serverInfoServiceProvider));
 });

+ 4 - 0
mobile/lib/shared/services/device_info.service.dart

@@ -1,6 +1,10 @@
 import 'package:flutter_udid/flutter_udid.dart';
 import 'dart:io' show Platform;
 
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+final deviceInfoServiceProvider = Provider((_) => DeviceInfoService());
+
 class DeviceInfoService {
   Future<Map<String, dynamic>> getDeviceInfo() async {
     // Get device info

+ 3 - 0
mobile/lib/shared/services/local_storage.service.dart

@@ -1,6 +1,9 @@
 import 'package:hive/hive.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/constants/hive_box.dart';
 
+final localStorageServiceProvider = Provider((_) => LocalStorageService());
+
 class LocalStorageService {
   late Box _box;
 

+ 3 - 3
mobile/lib/shared/services/network.service.dart

@@ -4,11 +4,11 @@ import 'dart:convert';
 import 'package:dio/dio.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:hive/hive.dart';
-import 'package:http_parser/http_parser.dart';
-import 'package:image_picker/image_picker.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/constants/hive_box.dart';
 import 'package:immich_mobile/utils/dio_http_interceptor.dart';
-import 'package:immich_mobile/utils/files_helper.dart';
+
+final networkServiceProvider = Provider((_) => NetworkService());
 
 class NetworkService {
   late final Dio dio;

+ 6 - 1
mobile/lib/shared/services/server_info.service.dart

@@ -1,11 +1,16 @@
 import 'package:dio/dio.dart';
 import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/shared/models/server_info.model.dart';
 import 'package:immich_mobile/shared/models/server_version.model.dart';
 import 'package:immich_mobile/shared/services/network.service.dart';
 
+final serverInfoServiceProvider =
+    Provider((ref) => ServerInfoService(ref.watch(networkServiceProvider)));
+
 class ServerInfoService {
-  final NetworkService _networkService = NetworkService();
+  final NetworkService _networkService;
+  ServerInfoService(this._networkService);
 
   Future<ServerInfo> getServerInfo() async {
     Response response = await _networkService.getRequest(url: 'server-info');

+ 6 - 1
mobile/lib/shared/services/user.service.dart

@@ -3,6 +3,7 @@ import 'dart:convert';
 import 'package:dio/dio.dart';
 import 'package:flutter/material.dart';
 import 'package:hive/hive.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:http_parser/http_parser.dart';
 import 'package:image_picker/image_picker.dart';
 import 'package:immich_mobile/constants/hive_box.dart';
@@ -12,8 +13,12 @@ import 'package:immich_mobile/shared/services/network.service.dart';
 import 'package:immich_mobile/utils/dio_http_interceptor.dart';
 import 'package:immich_mobile/utils/files_helper.dart';
 
+final userServiceProvider =
+    Provider((ref) => UserService(ref.watch(networkServiceProvider)));
+
 class UserService {
-  final NetworkService _networkService = NetworkService();
+  final NetworkService _networkService;
+  UserService(this._networkService);
 
   Future<List<User>> getAllUsersInfo() async {
     try {