From 68db17028b712d911cbf9a467016ffba080458b1 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 10 Jul 2025 11:59:15 -0500 Subject: [PATCH] feat: new create album page (#19731) * feat: new create album page * finished create album flow * refactor into stateful widgets * refactor * focus fix * lint * default sort * pr feedback --- mobile/ios/Runner.xcodeproj/project.pbxproj | 12 +- .../domain/services/remote_album.service.dart | 20 +- .../repositories/remote_album.repository.dart | 44 +- .../presentation/pages/drift_album.page.dart | 15 +- .../pages/drift_create_album.page.dart | 500 ++++++++++++++++++ .../infrastructure/album.provider.dart | 6 +- .../infrastructure/remote_album.provider.dart | 27 + .../repositories/album_api.repository.dart | 39 ++ .../drift_album_api_repository.dart | 53 ++ mobile/lib/routing/router.dart | 6 + mobile/lib/routing/router.gr.dart | 16 + 11 files changed, 730 insertions(+), 8 deletions(-) create mode 100644 mobile/lib/presentation/pages/drift_create_album.page.dart create mode 100644 mobile/lib/repositories/drift_album_api_repository.dart diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index fb0908e8b6..1a39f98db3 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -117,8 +117,6 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = Sync; sourceTree = ""; }; @@ -473,10 +471,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -505,10 +507,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 9ff00e1ce3..ae9e8b5336 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -1,12 +1,14 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/utils/remote_album.utils.dart'; class RemoteAlbumService { final DriftRemoteAlbumRepository _repository; + final DriftAlbumApiRepository _albumApiRepository; - const RemoteAlbumService(this._repository); + const RemoteAlbumService(this._repository, this._albumApiRepository); Future> getAll() { return _repository.getAll(); @@ -57,4 +59,20 @@ class RemoteAlbumService { return filtered; } + + Future createAlbum({ + required String title, + required List assetIds, + String? description, + }) async { + final album = await _albumApiRepository.createDriftAlbum( + title, + description: description, + assetIds: assetIds, + ); + + await _repository.create(album, assetIds); + + return album; + } } diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index de55626b30..49d6a2661d 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -1,16 +1,17 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -enum SortRemoteAlbumsBy { id } +enum SortRemoteAlbumsBy { id, updatedAt } class DriftRemoteAlbumRepository extends DriftDatabaseRepository { final Drift _db; const DriftRemoteAlbumRepository(this._db) : super(_db); Future> getAll({ - Set sortBy = const {}, + Set sortBy = const {SortRemoteAlbumsBy.updatedAt}, }) { final assetCount = _db.remoteAlbumAssetEntity.assetId.count(); @@ -43,6 +44,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { orderings.add( switch (sort) { SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id), + SortRemoteAlbumsBy.updatedAt => + OrderingTerm.desc(_db.remoteAlbumEntity.updatedAt), }, ); } @@ -58,6 +61,43 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { ) .get(); } + + Future create( + RemoteAlbum album, + List assetIds, + ) async { + await _db.transaction(() async { + final entity = RemoteAlbumEntityCompanion( + id: Value(album.id), + name: Value(album.name), + ownerId: Value(album.ownerId), + createdAt: Value(album.createdAt), + updatedAt: Value(album.updatedAt), + description: Value(album.description), + thumbnailAssetId: Value(album.thumbnailAssetId), + isActivityEnabled: Value(album.isActivityEnabled), + order: Value(album.order), + ); + + await _db.remoteAlbumEntity.insertOne(entity); + + if (assetIds.isNotEmpty) { + final albumAssets = assetIds.map( + (assetId) => RemoteAlbumAssetEntityCompanion( + albumId: Value(album.id), + assetId: Value(assetId), + ), + ); + + await _db.batch((batch) { + batch.insertAll( + _db.remoteAlbumAssetEntity, + albumAssets, + ); + }); + } + }); + } } extension on RemoteAlbumEntityData { diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index 4a7b12126a..8ae3c79138 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -97,7 +97,20 @@ class _DriftAlbumsPageState extends ConsumerState { onRefresh: onRefresh, child: CustomScrollView( slivers: [ - const ImmichSliverAppBar(), + ImmichSliverAppBar( + actions: [ + IconButton( + icon: const Icon( + Icons.add_rounded, + size: 28, + ), + onPressed: () => context.pushRoute( + const DriftCreateAlbumRoute(), + ), + ), + ], + showUploadButton: false, + ), _SearchBar( searchController: searchController, searchFocusNode: searchFocusNode, diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart new file mode 100644 index 0000000000..93dfb265c6 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -0,0 +1,500 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; + +@RoutePage() +class DriftCreateAlbumPage extends ConsumerStatefulWidget { + const DriftCreateAlbumPage({super.key}); + + @override + ConsumerState createState() => + _DriftCreateAlbumPageState(); +} + +class _DriftCreateAlbumPageState extends ConsumerState { + TextEditingController albumTitleController = TextEditingController(); + TextEditingController albumDescriptionController = TextEditingController(); + FocusNode albumTitleTextFieldFocusNode = FocusNode(); + FocusNode albumDescriptionTextFieldFocusNode = FocusNode(); + bool isAlbumTitleTextFieldFocus = false; + Set selectedAssets = {}; + + @override + void dispose() { + albumTitleController.dispose(); + albumDescriptionController.dispose(); + albumTitleTextFieldFocusNode.dispose(); + albumDescriptionTextFieldFocusNode.dispose(); + super.dispose(); + } + + bool get _canCreateAlbum => albumTitleController.text.isNotEmpty; + + String _getEffectiveTitle() { + return albumTitleController.text.isNotEmpty + ? albumTitleController.text + : 'create_album_page_untitled'.t(context: context); + } + + Widget _buildSliverAppBar() { + return SliverAppBar( + backgroundColor: context.scaffoldBackgroundColor, + elevation: 0, + automaticallyImplyLeading: false, + pinned: true, + snap: false, + floating: false, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(200.0), + child: SizedBox( + height: 200, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + buildTitleInputField(), + buildDescriptionInputField(), + if (selectedAssets.isNotEmpty) buildControlButton(), + ], + ), + ), + ), + ); + } + + Widget _buildContent() { + if (selectedAssets.isEmpty) { + return SliverList( + delegate: SliverChildListDelegate([ + _buildEmptyState(), + _buildSelectPhotosButton(), + ]), + ); + } else { + return _buildSelectedImageGrid(); + } + } + + Widget _buildEmptyState() { + return Padding( + padding: const EdgeInsets.only(top: 0, left: 18), + child: Text( + 'create_shared_album_page_share_add_assets', + style: context.textTheme.labelLarge, + ).t(), + ); + } + + Widget _buildSelectPhotosButton() { + return Padding( + padding: const EdgeInsets.all(16.0), + child: FilledButton.icon( + style: FilledButton.styleFrom( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + vertical: 24.0, + horizontal: 16.0, + ), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + backgroundColor: context.colorScheme.surfaceContainerHigh, + ), + onPressed: onSelectPhotos, + icon: Icon(Icons.add_rounded, color: context.primaryColor), + label: Padding( + padding: const EdgeInsets.only( + left: 8.0, + ), + child: Text( + 'create_shared_album_page_share_select_photos', + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).t(), + ), + ), + ); + } + + Widget _buildSelectedImageGrid() { + return SliverPadding( + padding: const EdgeInsets.only(top: 16.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 1.0, + mainAxisSpacing: 1.0, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final asset = selectedAssets.elementAt(index); + return GestureDetector( + onTap: onBackgroundTapped, + child: Thumbnail(asset: asset), + ); + }, + childCount: selectedAssets.length, + ), + ), + ); + } + + void onBackgroundTapped() { + albumTitleTextFieldFocusNode.unfocus(); + albumDescriptionTextFieldFocusNode.unfocus(); + setState(() { + isAlbumTitleTextFieldFocus = false; + }); + + if (albumTitleController.text.isEmpty) { + final untitledText = 'create_album_page_untitled'.t(); + albumTitleController.text = untitledText; + } + } + + Future onSelectPhotos() async { + final assets = await context.pushRoute>( + DriftAssetSelectionTimelineRoute( + lockedSelectionAssets: selectedAssets, + ), + ); + + if (assets == null || assets.isEmpty) { + return; + } + + setState(() { + selectedAssets = selectedAssets.union(assets); + }); + } + + Future createAlbum() async { + onBackgroundTapped(); + + final title = _getEffectiveTitle().trim(); + if (title.isEmpty) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('create_album_title_required'.t()), + backgroundColor: context.colorScheme.error, + ), + ); + } + return; + } + + final album = await ref.watch(remoteAlbumProvider.notifier).createAlbum( + title: title, + description: albumDescriptionController.text.trim(), + assetIds: selectedAssets.map((asset) { + final remoteAsset = asset as RemoteAsset; + return remoteAsset.id; + }).toList(), + ); + + if (album != null) { + context.replaceRoute( + RemoteTimelineRoute(album: album), + ); + } + } + + Widget buildTitleInputField() { + return Padding( + padding: const EdgeInsets.only( + right: 10.0, + left: 10.0, + ), + child: _AlbumTitleTextField( + focusNode: albumTitleTextFieldFocusNode, + textController: albumTitleController, + isFocus: isAlbumTitleTextFieldFocus, + onFocusChanged: (focus) { + setState(() { + isAlbumTitleTextFieldFocus = focus; + }); + }, + ), + ); + } + + Widget buildDescriptionInputField() { + return Padding( + padding: const EdgeInsets.only( + right: 10.0, + left: 10.0, + top: 8, + ), + child: _AlbumViewerEditableDescription( + textController: albumDescriptionController, + focusNode: albumDescriptionTextFieldFocusNode, + ), + ); + } + + Widget buildControlButton() { + return Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 8.0, + bottom: 8.0, + ), + child: SizedBox( + height: 42.0, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + AlbumActionFilledButton( + iconData: Icons.add_photo_alternate_outlined, + onPressed: onSelectPhotos, + labelText: "add_photos".t(), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + centerTitle: false, + backgroundColor: context.scaffoldBackgroundColor, + leading: IconButton( + onPressed: () => context.maybePop(), + icon: const Icon(Icons.close_rounded), + ), + title: const Text('create_album').t(), + actions: [ + TextButton( + onPressed: _canCreateAlbum ? createAlbum : null, + child: Text( + 'create'.t(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: _canCreateAlbum + ? context.primaryColor + : context.themeData.disabledColor, + ), + ), + ), + ], + ), + body: GestureDetector( + onTap: onBackgroundTapped, + child: CustomScrollView( + slivers: [ + _buildSliverAppBar(), + _buildContent(), + ], + ), + ), + ); + } +} + +class _AlbumTitleTextField extends StatefulWidget { + const _AlbumTitleTextField({ + required this.focusNode, + required this.textController, + required this.isFocus, + required this.onFocusChanged, + }); + + final FocusNode focusNode; + final TextEditingController textController; + final bool isFocus; + final ValueChanged onFocusChanged; + + @override + State<_AlbumTitleTextField> createState() => _AlbumTitleTextFieldState(); +} + +class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> { + @override + void initState() { + super.initState(); + widget.focusNode.addListener(_onFocusChange); + } + + @override + void dispose() { + widget.focusNode.removeListener(_onFocusChange); + super.dispose(); + } + + void _onFocusChange() { + widget.onFocusChanged(widget.focusNode.hasFocus); + } + + @override + Widget build(BuildContext context) { + return TextField( + focusNode: widget.focusNode, + style: TextStyle( + fontSize: 28.0, + color: context.colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + controller: widget.textController, + onTap: () { + if (widget.textController.text == + 'create_album_page_untitled'.t(context: context)) { + widget.textController.clear(); + } + }, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 16.0, + ), + suffixIcon: widget.textController.text.isNotEmpty && widget.isFocus + ? IconButton( + onPressed: () { + widget.textController.clear(); + }, + icon: Icon( + Icons.cancel_rounded, + color: context.primaryColor, + ), + splashRadius: 10.0, + ) + : null, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.all( + Radius.circular(16.0), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.primaryColor.withValues(alpha: 0.3), + ), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + ), + hintText: 'add_a_title'.t(), + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( + fontSize: 28.0, + fontWeight: FontWeight.bold, + height: 1.2, + ), + focusColor: Colors.grey[300], + fillColor: context.colorScheme.surfaceContainerHigh, + filled: true, + ), + ); + } +} + +class _AlbumViewerEditableDescription extends StatefulWidget { + const _AlbumViewerEditableDescription({ + required this.textController, + required this.focusNode, + }); + + final TextEditingController textController; + final FocusNode focusNode; + + @override + State<_AlbumViewerEditableDescription> createState() => + _AlbumViewerEditableDescriptionState(); +} + +class _AlbumViewerEditableDescriptionState + extends State<_AlbumViewerEditableDescription> { + @override + void initState() { + super.initState(); + widget.focusNode.addListener(_onFocusModeChange); + widget.textController.addListener(_onTextChange); + } + + @override + void dispose() { + widget.focusNode.removeListener(_onFocusModeChange); + widget.textController.removeListener(_onTextChange); + super.dispose(); + } + + void _onFocusModeChange() { + setState(() { + if (!widget.focusNode.hasFocus && widget.textController.text.isEmpty) { + widget.textController.clear(); + } + }); + } + + void _onTextChange() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: TextField( + focusNode: widget.focusNode, + style: context.textTheme.bodyLarge, + maxLines: 3, + minLines: 1, + controller: widget.textController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 16.0, + ), + suffixIcon: + widget.focusNode.hasFocus && widget.textController.text.isNotEmpty + ? IconButton( + onPressed: () { + widget.textController.clear(); + }, + icon: Icon( + Icons.cancel_rounded, + color: context.primaryColor, + ), + splashRadius: 10.0, + ) + : null, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.colorScheme.outline.withValues(alpha: 0.3), + ), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.primaryColor.withValues(alpha: 0.3), + ), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + ), + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( + fontSize: 16.0, + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + focusColor: Colors.grey[300], + fillColor: context.scaffoldBackgroundColor, + filled: widget.focusNode.hasFocus, + hintText: 'add_a_description'.t(), + ), + ), + ); + } +} diff --git a/mobile/lib/providers/infrastructure/album.provider.dart b/mobile/lib/providers/infrastructure/album.provider.dart index 4a6db50697..4ec3453d16 100644 --- a/mobile/lib/providers/infrastructure/album.provider.dart +++ b/mobile/lib/providers/infrastructure/album.provider.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; final localAlbumRepository = Provider( (ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)), @@ -30,7 +31,10 @@ final remoteAlbumRepository = Provider( ); final remoteAlbumServiceProvider = Provider( - (ref) => RemoteAlbumService(ref.watch(remoteAlbumRepository)), + (ref) => RemoteAlbumService( + ref.watch(remoteAlbumRepository), + ref.watch(driftAlbumApiRepositoryProvider), + ), dependencies: [remoteAlbumRepository], ); diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index b80d791b0a..84db53ab9f 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -118,4 +118,31 @@ class RemoteAlbumNotifier extends Notifier { .sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse); state = state.copyWith(filteredAlbums: sortedAlbums); } + + Future createAlbum({ + required String title, + String? description, + List assetIds = const [], + }) async { + state = state.copyWith(isLoading: true, error: null); + + try { + final album = await _remoteAlbumService.createAlbum( + title: title, + description: description, + assetIds: assetIds, + ); + + state = state.copyWith( + albums: [...state.albums, album], + filteredAlbums: [...state.filteredAlbums, album], + ); + + state = state.copyWith(isLoading: false); + return album; + } catch (e) { + state = state.copyWith(isLoading: false, error: e.toString()); + rethrow; + } + } } diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart index 019e4dc63c..20365534c2 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -1,5 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart' + show AlbumAssetOrder, RemoteAlbum; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart' @@ -50,6 +52,25 @@ class AlbumApiRepository extends ApiRepository { return _toAlbum(responseDto); } + // TODO: Change name after removing old method + Future createDriftAlbum( + String name, { + required Iterable assetIds, + String? description, + }) async { + final responseDto = await checkNull( + _api.createAlbum( + CreateAlbumDto( + albumName: name, + description: description, + assetIds: assetIds.toList(), + ), + ), + ); + + return _toRemoteAlbum(responseDto); + } + Future update( String albumId, { String? name, @@ -170,4 +191,22 @@ class AlbumApiRepository extends ApiRepository { return album; } + + static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) { + return RemoteAlbum( + id: dto.id, + name: dto.albumName, + ownerId: dto.owner.id, + description: dto.description, + createdAt: dto.createdAt, + updatedAt: dto.updatedAt, + thumbnailAssetId: dto.albumThumbnailAssetId, + isActivityEnabled: dto.isActivityEnabled, + order: dto.order == AssetOrder.asc + ? AlbumAssetOrder.asc + : AlbumAssetOrder.desc, + assetCount: dto.assetCount, + ownerName: dto.owner.name, + ); + } } diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart new file mode 100644 index 0000000000..3539012895 --- /dev/null +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -0,0 +1,53 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/api.repository.dart'; +// ignore: import_rule_openapi +import 'package:openapi/api.dart'; + +final driftAlbumApiRepositoryProvider = Provider( + (ref) => DriftAlbumApiRepository(ref.watch(apiServiceProvider).albumsApi), +); + +class DriftAlbumApiRepository extends ApiRepository { + final AlbumsApi _api; + + DriftAlbumApiRepository(this._api); + + Future createDriftAlbum( + String name, { + required Iterable assetIds, + String? description, + }) async { + final responseDto = await checkNull( + _api.createAlbum( + CreateAlbumDto( + albumName: name, + description: description, + assetIds: assetIds.toList(), + ), + ), + ); + + return responseDto.toRemoteAlbum(); + } +} + +extension on AlbumResponseDto { + RemoteAlbum toRemoteAlbum() { + return RemoteAlbum( + id: id, + name: albumName, + ownerId: owner.id, + description: description, + createdAt: createdAt, + updatedAt: updatedAt, + thumbnailAssetId: albumThumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: + order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, + assetCount: assetCount, + ownerName: owner.name, + ); + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 7becbd4804..63dc194ac6 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -85,6 +85,7 @@ import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -448,6 +449,11 @@ class AppRouter extends RootStackRouter { page: DriftLocalAlbumsRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: DriftCreateAlbumRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 8e8da4d8d0..51d171c582 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -683,6 +683,22 @@ class DriftAssetSelectionTimelineRouteArgs { } } +/// generated route for +/// [DriftCreateAlbumPage] +class DriftCreateAlbumRoute extends PageRouteInfo { + const DriftCreateAlbumRoute({List? children}) + : super(DriftCreateAlbumRoute.name, initialChildren: children); + + static const String name = 'DriftCreateAlbumRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftCreateAlbumPage(); + }, + ); +} + /// generated route for /// [DriftFavoritePage] class DriftFavoriteRoute extends PageRouteInfo {