Просмотр исходного кода

chore(mobile) Improve mobile UI (#1038)

Alex 2 лет назад
Родитель
Сommit
d31eddf32f

+ 1 - 1
mobile/assets/i18n/en-US.json

@@ -17,7 +17,7 @@
   "backup_album_selection_page_albums_device": "Albums on device ({})",
   "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
   "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
-  "backup_album_selection_page_select_albums": "Select Albums",
+  "backup_album_selection_page_select_albums": "Select albums",
   "backup_album_selection_page_selection_info": "Selection Info",
   "backup_album_selection_page_total_assets": "Total unique assets",
   "backup_all": "All",

+ 35 - 14
mobile/lib/modules/album/ui/album_thumbnail_card.dart

@@ -21,8 +21,39 @@ class AlbumThumbnailCard extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     var box = Hive.box(userInfoBox);
+    var cardSize = MediaQuery.of(context).size.width / 2 - 18;
+    var isDarkMode = Theme.of(context).brightness == Brightness.dark;
 
-    final cardSize = MediaQuery.of(context).size.width / 2 - 18;
+    buildEmptyThumbnail() {
+      return Container(
+        decoration: BoxDecoration(
+          color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
+        ),
+        child: SizedBox(
+          height: cardSize,
+          width: cardSize,
+          child: const Center(
+            child: Icon(Icons.no_photography),
+          ),
+        ),
+      );
+    }
+
+    buildAlbumThumbnail() {
+      return CachedNetworkImage(
+        memCacheHeight: max(400, cardSize.toInt() * 3),
+        width: cardSize,
+        height: cardSize,
+        fit: BoxFit.cover,
+        fadeInDuration: const Duration(milliseconds: 200),
+        imageUrl: getAlbumThumbnailUrl(
+          album,
+          type: ThumbnailFormat.JPEG,
+        ),
+        httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
+        cacheKey: "${album.albumThumbnailAssetId}",
+      );
+    }
 
     return GestureDetector(
       onTap: () {
@@ -35,19 +66,9 @@ class AlbumThumbnailCard extends StatelessWidget {
           children: [
             ClipRRect(
               borderRadius: BorderRadius.circular(8),
-              child: CachedNetworkImage(
-                memCacheHeight: max(400, cardSize.toInt() * 3),
-                width: cardSize,
-                height: cardSize,
-                fit: BoxFit.cover,
-                fadeInDuration: const Duration(milliseconds: 200),
-                imageUrl:
-                    getAlbumThumbnailUrl(album, type: ThumbnailFormat.JPEG),
-                httpHeaders: {
-                  "Authorization": "Bearer ${box.get(accessTokenKey)}"
-                },
-                cacheKey: "${album.albumThumbnailAssetId}",
-              ),
+              child: album.albumThumbnailAssetId == null
+                  ? buildEmptyThumbnail()
+                  : buildAlbumThumbnail(),
             ),
             Padding(
               padding: const EdgeInsets.only(top: 8.0),

+ 22 - 13
mobile/lib/modules/backup/providers/backup.provider.dart

@@ -1,6 +1,7 @@
 import 'dart:io';
 
 import 'package:cancellation_token_http/http.dart';
+import 'package:flutter/widgets.dart';
 import 'package:hive_flutter/hive_flutter.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/constants/hive_box.dart';
@@ -68,6 +69,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
   final AuthenticationState _authState;
   final BackgroundService _backgroundService;
   final Ref ref;
+  var isGettingBackupInfo = false;
 
   ///
   /// UI INTERACTION
@@ -172,9 +174,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
   /// Get all album on the device
   /// Get all selected and excluded album from the user's persistent storage
   /// If this is the first time performing backup - set the default selected album to be
-  /// the one that has all assets (Recent on Android, Recents on iOS)
+  /// the one that has all assets (`Recent` on Android, `Recents` on iOS)
   ///
   Future<void> _getBackupAlbumsInfo() async {
+    Stopwatch stopwatch = Stopwatch()..start();
     // Get all albums on the device
     List<AvailableAlbum> availableAlbums = [];
     List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
@@ -182,6 +185,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       type: RequestType.common,
     );
 
+    log.info('Found ${albums.length} local albums');
+
     for (AssetPathEntity album in albums) {
       AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
 
@@ -293,6 +298,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     } catch (e, stackTrace) {
       log.severe("Failed to generate album from id", e, stackTrace);
     }
+
+    debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
   }
 
   ///
@@ -364,25 +371,29 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     return;
   }
 
-  ///
   /// Get all necessary information for calculating the available albums,
   /// which albums are selected or excluded
   /// and then update the UI according to those information
-  ///
   Future<void> getBackupInfo() async {
-    final bool isEnabled = await _backgroundService.isBackgroundBackupEnabled();
-    state = state.copyWith(backgroundBackup: isEnabled);
-    if (state.backupProgress != BackUpProgressEnum.inBackground) {
-      await _getBackupAlbumsInfo();
-      await _updateServerInfo();
-      await _updateBackupAssetCount();
+    if (!isGettingBackupInfo) {
+      isGettingBackupInfo = true;
+
+      var isEnabled = await _backgroundService.isBackgroundBackupEnabled();
+
+      state = state.copyWith(backgroundBackup: isEnabled);
+
+      if (state.backupProgress != BackUpProgressEnum.inBackground) {
+        await _getBackupAlbumsInfo();
+        await _updateServerInfo();
+        await _updateBackupAssetCount();
+      }
+
+      isGettingBackupInfo = false;
     }
   }
 
-  ///
   /// Save user selection of selected albums and excluded albums to
   /// Hive database
-  ///
   void _updatePersistentAlbumsSelection() {
     final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
     Box<HiveBackupAlbums> backupAlbumInfoBox =
@@ -402,9 +413,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     );
   }
 
-  ///
   /// Invoke backup process
-  ///
   Future<void> startBackupProcess() async {
     assert(state.backupProgress == BackUpProgressEnum.idle);
     state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);

+ 1 - 1
mobile/lib/modules/backup/views/album_preview_page.dart

@@ -36,7 +36,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
         title: Column(
           children: [
             Text(
-              "${album.name} (${album.assetCountAsync})",
+              album.name,
               style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
             ),
             Padding(

+ 61 - 14
mobile/lib/modules/backup/views/backup_album_selection_page.dart

@@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/constants/immich_colors.dart';
+import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
@@ -14,10 +15,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
   const BackupAlbumSelectionPage({Key? key}) : super(key: key);
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final availableAlbums = ref.watch(backupProvider).availableAlbums;
+    // final availableAlbums = ref.watch(backupProvider).availableAlbums;
     final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
     final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
     final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+    final albums = useState<List<AvailableAlbum>>(
+      ref.watch(backupProvider).availableAlbums,
+    );
 
     useEffect(
       () {
@@ -28,7 +32,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
     );
 
     buildAlbumSelectionList() {
-      if (availableAlbums.isEmpty) {
+      if (albums.value.isEmpty) {
         return const Center(
           child: ImmichLoadingIndicator(),
         );
@@ -38,17 +42,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
         height: 265,
         child: ListView.builder(
           scrollDirection: Axis.horizontal,
-          itemCount: availableAlbums.length,
+          itemCount: albums.value.length,
           physics: const BouncingScrollPhysics(),
           itemBuilder: ((context, index) {
-            var thumbnailData = availableAlbums[index].thumbnailData;
+            var thumbnailData = albums.value[index].thumbnailData;
             return Padding(
               padding: index == 0
                   ? const EdgeInsets.only(left: 16.00)
                   : const EdgeInsets.all(0),
               child: AlbumInfoCard(
                 imageData: thumbnailData,
-                albumInfo: availableAlbums[index],
+                albumInfo: albums.value[index],
               ),
             );
           }),
@@ -79,15 +83,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
             child: Chip(
               visualDensity: VisualDensity.compact,
               shape: RoundedRectangleBorder(
-                borderRadius: BorderRadius.circular(5),
+                borderRadius: BorderRadius.circular(10),
               ),
               label: Text(
                 album.name,
                 style: TextStyle(
                   fontSize: 10,
-                  color: Theme.of(context).brightness == Brightness.dark
-                      ? Colors.black
-                      : Colors.white,
+                  color: isDarkTheme ? Colors.black : Colors.white,
                   fontWeight: FontWeight.bold,
                 ),
               ),
@@ -119,7 +121,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
             child: Chip(
               visualDensity: VisualDensity.compact,
               shape: RoundedRectangleBorder(
-                borderRadius: BorderRadius.circular(5),
+                borderRadius: BorderRadius.circular(10),
               ),
               label: Text(
                 album.name,
@@ -143,6 +145,46 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
       }).toSet();
     }
 
+    buildSearchBar() {
+      return Padding(
+        padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
+        child: TextFormField(
+          onChanged: (searchValue) {
+            albums.value = ref
+                .watch(backupProvider)
+                .availableAlbums
+                .where(
+                  (album) => album.name
+                      .toLowerCase()
+                      .contains(searchValue.toLowerCase()),
+                )
+                .toList();
+          },
+          decoration: InputDecoration(
+            contentPadding: const EdgeInsets.symmetric(
+              horizontal: 8.0,
+              vertical: 8.0,
+            ),
+            hintText: "Search",
+            hintStyle: TextStyle(
+              color: isDarkTheme ? Colors.white : Colors.grey,
+              fontSize: 14.0,
+            ),
+            prefixIcon: const Icon(
+              Icons.search,
+              color: Colors.grey,
+            ),
+            border: OutlineInputBorder(
+              borderRadius: BorderRadius.circular(10),
+              borderSide: BorderSide.none,
+            ),
+            filled: true,
+            fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200],
+          ),
+        ),
+      );
+    }
+
     return Scaffold(
       appBar: AppBar(
         leading: IconButton(
@@ -188,7 +230,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
             child: Card(
               margin: const EdgeInsets.all(0),
               shape: RoundedRectangleBorder(
-                borderRadius: BorderRadius.circular(5),
+                borderRadius: BorderRadius.circular(10),
                 side: BorderSide(
                   color: isDarkTheme
                       ? const Color.fromARGB(255, 0, 0, 0)
@@ -225,8 +267,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 
           ListTile(
             title: Text(
-              "backup_album_selection_page_albums_device"
-                  .tr(args: [availableAlbums.length.toString()]),
+              "backup_album_selection_page_albums_device".tr(
+                args: [
+                  ref.watch(backupProvider).availableAlbums.length.toString()
+                ],
+              ),
               style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
             ),
             subtitle: Padding(
@@ -254,7 +299,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
                   builder: (BuildContext context) {
                     return AlertDialog(
                       shape: RoundedRectangleBorder(
-                        borderRadius: BorderRadius.circular(12),
+                        borderRadius: BorderRadius.circular(10),
                       ),
                       elevation: 5,
                       title: Text(
@@ -284,6 +329,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
             ),
           ),
 
+          buildSearchBar(),
+
           Padding(
             padding: const EdgeInsets.only(bottom: 16.0),
             child: buildAlbumSelectionList(),

+ 14 - 7
mobile/lib/modules/home/services/asset.service.dart

@@ -12,6 +12,7 @@ import 'package:immich_mobile/shared/providers/api.provider.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
 import 'package:immich_mobile/utils/openapi_extensions.dart';
 import 'package:immich_mobile/utils/tuple.dart';
+import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
 
 final assetServiceProvider = Provider(
@@ -26,20 +27,26 @@ class AssetService {
   final ApiService _apiService;
   final BackupService _backupService;
   final BackgroundService _backgroundService;
+  final log = Logger('AssetService');
 
   AssetService(this._apiService, this._backupService, this._backgroundService);
 
   /// Returns `null` if the server state did not change, else list of assets
   Future<List<Asset>?> getRemoteAssets({required bool hasCache}) async {
-    final Box box = Hive.box(userInfoBox);
-    final Pair<List<AssetResponseDto>, String?>? remote = await _apiService
-        .assetApi
-        .getAllAssetsWithETag(eTag: hasCache ? box.get(assetEtagKey) : null);
-    if (remote == null) {
+    try {
+      final Box box = Hive.box(userInfoBox);
+      final Pair<List<AssetResponseDto>, String?>? remote = await _apiService
+          .assetApi
+          .getAllAssetsWithETag(eTag: hasCache ? box.get(assetEtagKey) : null);
+      if (remote == null) {
+        return null;
+      }
+      box.put(assetEtagKey, remote.second);
+      return remote.first.map(Asset.remote).toList(growable: false);
+    } catch (e, stack) {
+      log.severe('Error while getting remote assets', e, stack);
       return null;
     }
-    box.put(assetEtagKey, remote.second);
-    return remote.first.map(Asset.remote).toList(growable: false);
   }
 
   /// if [urgent] is `true`, do not block by waiting on the background service

+ 3 - 1
mobile/lib/modules/home/ui/immich_sliver_appbar.dart

@@ -32,7 +32,9 @@ class ImmichSliverAppBar extends ConsumerWidget {
       snap: false,
       backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
       shape: const RoundedRectangleBorder(
-        borderRadius: BorderRadius.all(Radius.circular(5)),
+        borderRadius: BorderRadius.all(
+          Radius.circular(5),
+        ),
       ),
       leading: Builder(
         builder: (BuildContext context) {

+ 59 - 9
mobile/lib/modules/home/views/home_page.dart

@@ -1,3 +1,5 @@
+import 'dart:async';
+
 import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
@@ -20,6 +22,7 @@ import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 import 'package:immich_mobile/shared/services/share.service.dart';
+import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
 import 'package:openapi/api.dart';
 
@@ -37,6 +40,8 @@ class HomePage extends HookConsumerWidget {
     final albums = ref.watch(albumProvider);
     final albumService = ref.watch(albumServiceProvider);
 
+    final tipOneOpacity = useState(0.0);
+
     useEffect(
       () {
         ref.read(websocketProvider.notifier).connect();
@@ -146,6 +151,49 @@ class HomePage extends HookConsumerWidget {
         }
       }
 
+      buildLoadingIndicator() {
+        Timer(const Duration(seconds: 2), () {
+          tipOneOpacity.value = 1;
+        });
+
+        return Center(
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              const ImmichLoadingIndicator(),
+              Padding(
+                padding: const EdgeInsets.only(top: 16.0),
+                child: Text(
+                  'Building the timeline',
+                  style: TextStyle(
+                    fontWeight: FontWeight.w600,
+                    fontSize: 16,
+                    color: Theme.of(context).primaryColor,
+                  ),
+                ),
+              ),
+              AnimatedOpacity(
+                duration: const Duration(milliseconds: 500),
+                opacity: tipOneOpacity.value,
+                child: const SizedBox(
+                  width: 250,
+                  child: Padding(
+                    padding: EdgeInsets.only(top: 8.0),
+                    child: Text(
+                      'If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).',
+                      textAlign: TextAlign.justify,
+                      style: TextStyle(
+                        fontSize: 12,
+                      ),
+                    ),
+                  ),
+                ),
+              )
+            ],
+          ),
+        );
+      }
+
       return SafeArea(
         bottom: !multiselectEnabled.state,
         top: true,
@@ -164,15 +212,17 @@ class HomePage extends HookConsumerWidget {
                 top: selectionEnabledHook.value ? 0 : 60,
                 bottom: 0.0,
               ),
-              child: ImmichAssetGrid(
-                renderList: renderList,
-                assetsPerRow:
-                    appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
-                showStorageIndicator: appSettingService
-                    .getSetting(AppSettingsEnum.storageIndicator),
-                listener: selectionListener,
-                selectionActive: selectionEnabledHook.value,
-              ),
+              child: ref.watch(assetProvider).isEmpty
+                  ? buildLoadingIndicator()
+                  : ImmichAssetGrid(
+                      renderList: renderList,
+                      assetsPerRow: appSettingService
+                          .getSetting(AppSettingsEnum.tilesPerRow),
+                      showStorageIndicator: appSettingService
+                          .getSetting(AppSettingsEnum.storageIndicator),
+                      listener: selectionListener,
+                      selectionActive: selectionEnabledHook.value,
+                    ),
             ),
             if (selectionEnabledHook.value)
               ControlBottomAppBar(

+ 14 - 4
mobile/lib/modules/login/ui/login_form.dart

@@ -83,6 +83,13 @@ class LoginForm extends HookConsumerWidget {
       [],
     );
 
+    populateTestLoginInfo() {
+      usernameController.text = 'testuser@email.com';
+      passwordController.text = 'password';
+      serverEndpointController.text = 'http://10.1.15.216:2283/api';
+      isSaveLoginInfo.value = true;
+    }
+
     return Center(
       child: ConstrainedBox(
         constraints: const BoxConstraints(maxWidth: 300),
@@ -92,10 +99,13 @@ class LoginForm extends HookConsumerWidget {
             runSpacing: 16,
             alignment: WrapAlignment.center,
             children: [
-              const Image(
-                image: AssetImage('assets/immich-logo-no-outline.png'),
-                width: 100,
-                filterQuality: FilterQuality.high,
+              GestureDetector(
+                onDoubleTap: () => populateTestLoginInfo(),
+                child: const Image(
+                  image: AssetImage('assets/immich-logo-no-outline.png'),
+                  width: 100,
+                  filterQuality: FilterQuality.high,
+                ),
               ),
               Text(
                 'IMMICH',

+ 4 - 1
mobile/lib/shared/ui/immich_loading_indicator.dart

@@ -15,7 +15,10 @@ class ImmichLoadingIndicator extends StatelessWidget {
         borderRadius: BorderRadius.circular(10),
       ),
       padding: const EdgeInsets.all(15),
-      child: const CircularProgressIndicator(color: Colors.white),
+      child: const CircularProgressIndicator(
+        color: Colors.white,
+        strokeWidth: 2,
+      ),
     );
   }
 }

+ 53 - 41
mobile/openapi/lib/model/album_response_dto.dart

@@ -43,48 +43,51 @@ class AlbumResponseDto {
   List<AssetResponseDto> assets;
 
   @override
-  bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
-     other.assetCount == assetCount &&
-     other.id == id &&
-     other.ownerId == ownerId &&
-     other.albumName == albumName &&
-     other.createdAt == createdAt &&
-     other.albumThumbnailAssetId == albumThumbnailAssetId &&
-     other.shared == shared &&
-     other.sharedUsers == sharedUsers &&
-     other.assets == assets;
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is AlbumResponseDto &&
+          other.assetCount == assetCount &&
+          other.id == id &&
+          other.ownerId == ownerId &&
+          other.albumName == albumName &&
+          other.createdAt == createdAt &&
+          other.albumThumbnailAssetId == albumThumbnailAssetId &&
+          other.shared == shared &&
+          other.sharedUsers == sharedUsers &&
+          other.assets == assets;
 
   @override
   int get hashCode =>
-    // ignore: unnecessary_parenthesis
-    (assetCount.hashCode) +
-    (id.hashCode) +
-    (ownerId.hashCode) +
-    (albumName.hashCode) +
-    (createdAt.hashCode) +
-    (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
-    (shared.hashCode) +
-    (sharedUsers.hashCode) +
-    (assets.hashCode);
+      // ignore: unnecessary_parenthesis
+      (assetCount.hashCode) +
+      (id.hashCode) +
+      (ownerId.hashCode) +
+      (albumName.hashCode) +
+      (createdAt.hashCode) +
+      (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
+      (shared.hashCode) +
+      (sharedUsers.hashCode) +
+      (assets.hashCode);
 
   @override
-  String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
+  String toString() =>
+      'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
 
   Map<String, dynamic> toJson() {
     final _json = <String, dynamic>{};
-      _json[r'assetCount'] = assetCount;
-      _json[r'id'] = id;
-      _json[r'ownerId'] = ownerId;
-      _json[r'albumName'] = albumName;
-      _json[r'createdAt'] = createdAt;
+    _json[r'assetCount'] = assetCount;
+    _json[r'id'] = id;
+    _json[r'ownerId'] = ownerId;
+    _json[r'albumName'] = albumName;
+    _json[r'createdAt'] = createdAt;
     if (albumThumbnailAssetId != null) {
       _json[r'albumThumbnailAssetId'] = albumThumbnailAssetId;
     } else {
       _json[r'albumThumbnailAssetId'] = null;
     }
-      _json[r'shared'] = shared;
-      _json[r'sharedUsers'] = sharedUsers;
-      _json[r'assets'] = assets;
+    _json[r'shared'] = shared;
+    _json[r'sharedUsers'] = sharedUsers;
+    _json[r'assets'] = assets;
     return _json;
   }
 
@@ -98,13 +101,13 @@ class AlbumResponseDto {
       // Ensure that the map contains the required keys.
       // Note 1: the values aren't checked for validity beyond being non-null.
       // Note 2: this code is stripped in release mode!
-      assert(() {
-        requiredKeys.forEach((key) {
-          assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
-          assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
-        });
-        return true;
-      }());
+      // assert(() {
+      //   requiredKeys.forEach((key) {
+      //     assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
+      //     assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
+      //   });
+      //   return true;
+      // }());
 
       return AlbumResponseDto(
         assetCount: mapValueOfType<int>(json, r'assetCount')!,
@@ -112,7 +115,8 @@ class AlbumResponseDto {
         ownerId: mapValueOfType<String>(json, r'ownerId')!,
         albumName: mapValueOfType<String>(json, r'albumName')!,
         createdAt: mapValueOfType<String>(json, r'createdAt')!,
-        albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
+        albumThumbnailAssetId:
+            mapValueOfType<String>(json, r'albumThumbnailAssetId'),
         shared: mapValueOfType<bool>(json, r'shared')!,
         sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers'])!,
         assets: AssetResponseDto.listFromJson(json[r'assets'])!,
@@ -121,7 +125,10 @@ class AlbumResponseDto {
     return null;
   }
 
-  static List<AlbumResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
+  static List<AlbumResponseDto>? listFromJson(
+    dynamic json, {
+    bool growable = false,
+  }) {
     final result = <AlbumResponseDto>[];
     if (json is List && json.isNotEmpty) {
       for (final row in json) {
@@ -149,12 +156,18 @@ class AlbumResponseDto {
   }
 
   // maps a json object with a list of AlbumResponseDto-objects as value to a dart map
-  static Map<String, List<AlbumResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+  static Map<String, List<AlbumResponseDto>> mapListFromJson(
+    dynamic json, {
+    bool growable = false,
+  }) {
     final map = <String, List<AlbumResponseDto>>{};
     if (json is Map && json.isNotEmpty) {
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       for (final entry in json.entries) {
-        final value = AlbumResponseDto.listFromJson(entry.value, growable: growable,);
+        final value = AlbumResponseDto.listFromJson(
+          entry.value,
+          growable: growable,
+        );
         if (value != null) {
           map[entry.key] = value;
         }
@@ -176,4 +189,3 @@ class AlbumResponseDto {
     'assets',
   };
 }
-