diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 60a6e4ff50..bc62a48a61 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -108,12 +108,17 @@ "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_title": "Experimental", + "favorites_page_title": "Favorites", "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_success": "Added {added} assets to album {album}.", "home_page_building_timeline": "Building the timeline", "home_page_first_time_notice": "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).", "library_page_albums": "Albums", "library_page_new_album": "New album", + "library_page_favorites": "Favorites", + "library_page_sharing": "Sharing", + "library_page_sort_created": "Most recently created", + "library_page_sort_title": "Album title", "login_form_button_text": "Login", "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "http://your-server-ip:port/api", diff --git a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart b/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart index ff4062aae6..df3fc5ac0a 100644 --- a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart +++ b/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -26,6 +27,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer; final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable; + final isFavorite = ref.watch(favoriteProvider).contains(asset.id); viewAsset() { AutoRouter.of(context).push( @@ -96,6 +98,18 @@ class AlbumViewerThumbnail extends HookConsumerWidget { ); } + buildAssetFavoriteIcon() { + return const Positioned( + left: 10, + bottom: 5, + child: Icon( + Icons.star, + color: Colors.white, + size: 18, + ), + ); + } + buildAssetSelectionIcon() { bool isSelected = selectedAssetsInAlbumViewer.contains(asset); @@ -143,6 +157,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { child: Stack( children: [ buildThumbnailImage(), + if (isFavorite) buildAssetFavoriteIcon(), if (showStorageIndicator) buildAssetStoreLocationIcon(), if (!asset.isImage) buildVideoLabel(), if (isMultiSelectionEnable) buildAssetSelectionIcon(), diff --git a/mobile/lib/modules/album/views/library_page.dart b/mobile/lib/modules/album/views/library_page.dart index 8b40fbf7bc..1dc8ab64ab 100644 --- a/mobile/lib/modules/album/views/library_page.dart +++ b/mobile/lib/modules/album/views/library_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -6,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:openapi/api.dart'; class LibraryPage extends HookConsumerWidget { const LibraryPage({Key? key}) : super(key: key); @@ -22,14 +24,11 @@ class LibraryPage extends HookConsumerWidget { [], ); - Widget buildAppBar() { - return const SliverAppBar( + AppBar buildAppBar() { + return AppBar( centerTitle: true, - floating: true, - pinned: false, - snap: false, automaticallyImplyLeading: false, - title: Text( + title: const Text( 'IMMICH', style: TextStyle( fontFamily: 'SnowburstOne', @@ -40,6 +39,74 @@ class LibraryPage extends HookConsumerWidget { ); } + final selectedAlbumSortOrder = useState(0); + + List sortedAlbums() { + if (selectedAlbumSortOrder.value == 0) { + return albums.sortedBy((album) => album.createdAt).reversed.toList(); + } + return albums.sortedBy((album) => album.albumName); + } + + Widget buildSortButton() { + final options = [ + "library_page_sort_created".tr(), + "library_page_sort_title".tr() + ]; + + return PopupMenuButton( + position: PopupMenuPosition.over, + itemBuilder: (BuildContext context) { + return options.mapIndexed>((index, option) { + final selected = selectedAlbumSortOrder.value == index; + return PopupMenuItem( + value: index, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Icon( + Icons.check, + color: selected + ? Theme.of(context).primaryColor + : Colors.transparent, + ), + ), + Text( + option, + style: TextStyle( + color: selected ? Theme.of(context).primaryColor : null, + fontSize: 12.0, + ), + ) + ], + ), + ); + }).toList(); + }, + onSelected: (int value) { + selectedAlbumSortOrder.value = value; + }, + child: Row( + children: [ + Icon( + Icons.swap_vert_rounded, + size: 18, + color: Theme.of(context).primaryColor, + ), + Text( + options[selectedAlbumSortOrder.value], + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + fontSize: 12.0, + ), + ), + ], + ), + ); + } + Widget buildCreateAlbumButton() { return GestureDetector( onTap: () { @@ -80,17 +147,90 @@ class LibraryPage extends HookConsumerWidget { ); } + Widget buildLibraryNavButton( + String label, + IconData icon, + Function() onClick, + ) { + return Expanded( + child: OutlinedButton.icon( + onPressed: onClick, + label: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12.0, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + ), + ), + ), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.all(12), + side: BorderSide( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[600]! + : Colors.grey[300]!, + ), + alignment: Alignment.centerLeft, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6.0), + ), + ), + icon: Icon(icon, color: Theme.of(context).primaryColor), + ), + ); + } + return Scaffold( + appBar: buildAppBar(), body: CustomScrollView( slivers: [ - buildAppBar(), SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(12.0), - child: const Text( - 'library_page_albums', - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + top: 24.0, + bottom: 12.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + buildLibraryNavButton( + "library_page_favorites".tr(), Icons.star_border, () { + AutoRouter.of(context).navigate(const FavoritesRoute()); + }), + const SizedBox(width: 12.0), + buildLibraryNavButton( + "library_page_sharing".tr(), Icons.group_outlined, () { + AutoRouter.of(context).navigate(const SharingRoute()); + }), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + top: 12.0, + left: 12.0, + right: 12.0, + bottom: 20.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'library_page_albums', + style: TextStyle(fontWeight: FontWeight.bold), + ).tr(), + buildSortButton(), + ], + ), ), ), SliverPadding( @@ -100,7 +240,7 @@ class LibraryPage extends HookConsumerWidget { spacing: 12, children: [ buildCreateAlbumButton(), - for (var album in albums) + for (var album in sortedAlbums()) AlbumThumbnailCard( album: album, ), diff --git a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart index 2ae0c131cc..61a9b64d62 100644 --- a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart +++ b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart @@ -14,6 +14,8 @@ class TopControlAppBar extends HookConsumerWidget { required this.onAddToAlbumPressed, required this.onToggleMotionVideo, required this.isPlayingMotionVideo, + required this.onFavorite, + required this.isFavorite, }) : super(key: key); final Asset asset; @@ -22,13 +24,29 @@ class TopControlAppBar extends HookConsumerWidget { final VoidCallback onToggleMotionVideo; final VoidCallback onDeletePressed; final VoidCallback onAddToAlbumPressed; + final VoidCallback onFavorite; final Function onSharePressed; final bool isPlayingMotionVideo; + final bool isFavorite; @override Widget build(BuildContext context, WidgetRef ref) { double iconSize = 18.0; + Widget buildFavoriteButton() { + return IconButton( + iconSize: iconSize, + splashRadius: iconSize, + onPressed: () { + onFavorite(); + }, + icon: Icon( + isFavorite ? Icons.star : Icons.star_border, + color: Colors.grey[200], + ), + ); + } + return AppBar( foregroundColor: Colors.grey[100], backgroundColor: Colors.transparent, @@ -43,6 +61,7 @@ class TopControlAppBar extends HookConsumerWidget { ), ), actions: [ + if (asset.isRemote) buildFavoriteButton(), if (asset.livePhotoVideoId != null) IconButton( iconSize: iconSize, diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 44e2f4cea0..0a9097dd2d 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_s import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; +import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; import 'package:immich_mobile/shared/services/asset.service.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; @@ -69,7 +70,11 @@ class GalleryViewerPage extends HookConsumerWidget { [], ); - void getAssetExif() async { + void toggleFavorite(Asset asset) { + ref.watch(favoriteProvider.notifier).toggleFavorite(asset); + } + + getAssetExif() async { if (assetList[indexOfAsset.value].isRemote) { assetDetail = await ref .watch(assetServiceProvider) @@ -236,9 +241,15 @@ class GalleryViewerPage extends HookConsumerWidget { child: TopControlAppBar( isPlayingMotionVideo: isPlayingMotionVideo.value, asset: assetList[indexOfAsset.value], + isFavorite: ref.watch(favoriteProvider).contains( + assetList[indexOfAsset.value].id, + ), onMoreInfoPressed: () { showInfo(); }, + onFavorite: () { + toggleFavorite(assetList[indexOfAsset.value]); + }, onDownloadPressed: assetList[indexOfAsset.value].isLocal ? null : () { diff --git a/mobile/lib/modules/favorite/providers/favorite_provider.dart b/mobile/lib/modules/favorite/providers/favorite_provider.dart new file mode 100644 index 0000000000..49176cd067 --- /dev/null +++ b/mobile/lib/modules/favorite/providers/favorite_provider.dart @@ -0,0 +1,52 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/asset.provider.dart'; + +class FavoriteSelectionNotifier extends StateNotifier> { + FavoriteSelectionNotifier(this.ref) : super({}) { + state = ref.watch(assetProvider).allAssets + .where((asset) => asset.isFavorite) + .map((asset) => asset.id) + .toSet(); + } + + final Ref ref; + + void _setFavoriteForAssetId(String id, bool favorite) { + if (!favorite) { + state = state.difference({id}); + } else { + state = state.union({id}); + } + } + + bool _isFavorite(String id) { + return state.contains(id); + } + + Future toggleFavorite(Asset asset) async { + if (!asset.isRemote) return; // TODO support local favorite assets + + _setFavoriteForAssetId(asset.id, !_isFavorite(asset.id)); + + await ref.watch(assetProvider.notifier).toggleFavorite( + asset, + state.contains(asset.id), + ); + } +} + +final favoriteProvider = + StateNotifierProvider>((ref) { + return FavoriteSelectionNotifier(ref); +}); + +final favoriteAssetProvider = StateProvider((ref) { + final favorites = ref.watch(favoriteProvider); + + return ref + .watch(assetProvider) + .allAssets + .where((element) => favorites.contains(element.id)) + .toList(); +}); diff --git a/mobile/lib/modules/favorite/ui/favorite_image.dart b/mobile/lib/modules/favorite/ui/favorite_image.dart new file mode 100644 index 0000000000..26bef32f96 --- /dev/null +++ b/mobile/lib/modules/favorite/ui/favorite_image.dart @@ -0,0 +1,36 @@ + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/ui/immich_image.dart'; + +class FavoriteImage extends HookConsumerWidget { + final Asset asset; + final List assets; + + const FavoriteImage(this.asset, this.assets, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + void viewAsset() { + AutoRouter.of(context).push( + GalleryViewerRoute( + asset: asset, + assetList: assets, + ), + ); + } + + return GestureDetector( + onTap: viewAsset, + child: ImmichImage( + asset, + width: 300, + height: 300, + ), + ); + } + +} \ No newline at end of file diff --git a/mobile/lib/modules/favorite/views/favorites_page.dart b/mobile/lib/modules/favorite/views/favorites_page.dart new file mode 100644 index 0000000000..657bfe7939 --- /dev/null +++ b/mobile/lib/modules/favorite/views/favorites_page.dart @@ -0,0 +1,68 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; +import 'package:immich_mobile/modules/favorite/ui/favorite_image.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; + +class FavoritesPage extends HookConsumerWidget { + const FavoritesPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + AppBar buildAppBar() { + return AppBar( + leading: IconButton( + onPressed: () => AutoRouter.of(context).pop(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), + centerTitle: true, + automaticallyImplyLeading: false, + title: const Text( + 'favorites_page_title', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ).tr(), + ); + } + + Widget buildImageGrid() { + final appSettingService = ref.watch(appSettingsServiceProvider); + + if (ref.watch(favoriteAssetProvider).isNotEmpty) { + return SliverPadding( + padding: const EdgeInsets.only(top: 10.0), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: + appSettingService.getSetting(AppSettingsEnum.tilesPerRow), + crossAxisSpacing: 5.0, + mainAxisSpacing: 5, + ), + delegate: SliverChildBuilderDelegate( + ( + BuildContext context, + int index, + ) { + return FavoriteImage( + ref.watch(favoriteAssetProvider)[index], + ref.watch(favoriteAssetProvider), + ); + }, + childCount: ref.watch(favoriteAssetProvider).length, + ), + ), + ); + } + return const SliverToBoxAdapter(); + } + + return Scaffold( + appBar: buildAppBar(), + body: CustomScrollView( + slivers: [buildImageGrid()], + ), + ); + } +} diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index 3dd5aca99c..e2d6f54b7d 100644 --- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/asset.dart'; @@ -110,6 +111,16 @@ class ThumbnailImage extends HookConsumerWidget { size: 18, ), ), + if (ref.watch(favoriteProvider).contains(asset.id)) + const Positioned( + left: 10, + bottom: 5, + child: Icon( + Icons.star, + color: Colors.white, + size: 18, + ), + ), if (!asset.isImage) Positioned( top: 5, diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 873873524f..ace3629288 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -15,6 +15,7 @@ import 'package:immich_mobile/modules/backup/views/album_preview_page.dart'; import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart'; import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart'; import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart'; +import 'package:immich_mobile/modules/favorite/views/favorites_page.dart'; import 'package:immich_mobile/modules/home/views/home_page.dart'; import 'package:immich_mobile/modules/login/views/change_password_page.dart'; import 'package:immich_mobile/modules/login/views/login_page.dart'; @@ -55,6 +56,7 @@ part 'router.gr.dart'; AutoRoute(page: BackupControllerPage, guards: [AuthGuard]), AutoRoute(page: SearchResultPage, guards: [AuthGuard]), AutoRoute(page: CreateAlbumPage, guards: [AuthGuard]), + AutoRoute(page: FavoritesPage, guards: [AuthGuard]), CustomRoute( page: AssetSelectionPage, guards: [AuthGuard], diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index b307fc6cb5..0a1311460d 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -77,6 +77,10 @@ class _$AppRouter extends RootStackRouter { isSharedAlbum: args.isSharedAlbum, initialAssets: args.initialAssets)); }, + FavoritesRoute.name: (routeData) { + return MaterialPageX( + routeData: routeData, child: const FavoritesPage()); + }, AssetSelectionRoute.name: (routeData) { return CustomPage( routeData: routeData, @@ -197,6 +201,8 @@ class _$AppRouter extends RootStackRouter { path: '/search-result-page', guards: [authGuard]), RouteConfig(CreateAlbumRoute.name, path: '/create-album-page', guards: [authGuard]), + RouteConfig(FavoritesRoute.name, + path: '/favorites-page', guards: [authGuard]), RouteConfig(AssetSelectionRoute.name, path: '/asset-selection-page', guards: [authGuard]), RouteConfig(SelectUserForSharingRoute.name, @@ -386,6 +392,14 @@ class CreateAlbumRouteArgs { } } +/// generated route for +/// [FavoritesPage] +class FavoritesRoute extends PageRouteInfo { + const FavoritesRoute() : super(FavoritesRoute.name, path: '/favorites-page'); + + static const String name = 'FavoritesRoute'; +} + /// generated route for /// [AssetSelectionPage] class AssetSelectionRoute extends PageRouteInfo { diff --git a/mobile/lib/shared/models/asset.dart b/mobile/lib/shared/models/asset.dart index e3e8afbed7..567e42a1a0 100644 --- a/mobile/lib/shared/models/asset.dart +++ b/mobile/lib/shared/models/asset.dart @@ -23,7 +23,8 @@ class Asset { latitude = remote.exifInfo?.latitude?.toDouble(), longitude = remote.exifInfo?.longitude?.toDouble(), exifInfo = - remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null; + remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null, + isFavorite = remote.isFavorite; Asset.local(AssetEntity local, String owner) : localId = local.id, @@ -37,6 +38,7 @@ class Asset { deviceId = Hive.box(userInfoBox).get(deviceIdKey), ownerId = owner, modifiedAt = local.modifiedDateTime.toUtc(), + isFavorite = local.isFavorite, createdAt = local.createDateTime.toUtc() { if (createdAt.year == 1970) { createdAt = modifiedAt; @@ -59,6 +61,7 @@ class Asset { required this.fileName, this.livePhotoVideoId, this.exifInfo, + required this.isFavorite, }); AssetEntity? _local; @@ -111,6 +114,8 @@ class Asset { ExifInfo? exifInfo; + bool isFavorite; + String get id => isLocal ? localId.toString() : remoteId!; String get name => p.withoutExtension(fileName); @@ -150,6 +155,7 @@ class Asset { json["height"] = height; json["fileName"] = fileName; json["livePhotoVideoId"] = livePhotoVideoId; + json["isFavorite"] = isFavorite; if (exifInfo != null) { json["exifInfo"] = exifInfo!.toJson(); } @@ -179,6 +185,7 @@ class Asset { fileName: json["fileName"], livePhotoVideoId: json["livePhotoVideoId"], exifInfo: ExifInfo.fromJson(json["exifInfo"]), + isFavorite: json["isFavorite"], ); } return null; diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 4376cf36ab..6655955850 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -268,6 +268,26 @@ class AssetNotifier extends StateNotifier { .where((a) => a.status == DeleteAssetStatus.SUCCESS) .map((a) => a.id); } + + Future toggleFavorite(Asset asset, bool status) async { + final newAsset = await _assetService.changeFavoriteStatus(asset, status); + + if (newAsset == null) { + log.severe("Change favorite status failed for asset ${asset.id}"); + return asset.isFavorite; + } + + await _updateAssetsState( + state.allAssets.map((a) { + if (asset.id == a.id) { + return Asset.remote(newAsset); + } + return a; + }).toList(), + ); + + return newAsset.isFavorite; + } } final assetProvider = StateNotifierProvider((ref) { diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart index ca33c5fb8c..0cc04936fb 100644 --- a/mobile/lib/shared/services/asset.service.dart +++ b/mobile/lib/shared/services/asset.service.dart @@ -104,4 +104,13 @@ class AssetService { return null; } } + + Future updateAsset(Asset asset, UpdateAssetDto updateAssetDto) async { + return await _apiService.assetApi.updateAsset(asset.id, updateAssetDto); + } + + Future changeFavoriteStatus(Asset asset, bool isFavorite) { + return updateAsset(asset, UpdateAssetDto(isFavorite: isFavorite)); + } + } diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 282344a16f..7858e857d6 100644 Binary files a/mobile/openapi/lib/model/album_response_dto.dart and b/mobile/openapi/lib/model/album_response_dto.dart differ diff --git a/mobile/test/asset_grid_data_structure_test.dart b/mobile/test/asset_grid_data_structure_test.dart index e957796de6..20aa2b64d3 100644 --- a/mobile/test/asset_grid_data_structure_test.dart +++ b/mobile/test/asset_grid_data_structure_test.dart @@ -20,6 +20,7 @@ void main() { modifiedAt: date, durationInSeconds: 0, fileName: '', + isFavorite: false, ), ); }