diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 477dfbd39ff13..5938bc6ff17d1 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -1,4 +1,24 @@ { + "all": "All", + "shared_with_me": "Shared with me", + "my_albums": "My albums", + "create_new": "CREATE NEW", + "create_album": "Create album", + "videos": "Videos", + "recently_added": "Recently added", + "partners": "Partners", + "partner_page_title": "Partners", + "library": "Library", + "on_this_device": "On this device", + "add_a_name": "Add a name", + "places": "Places", + "albums": "Albums", + "people": "People", + "shared_links": "Shared links", + "trash": "Trash", + "archived": "Archived", + "favorites": "Favorites", + "search_albums": "Search albums", "action_common_back": "Back", "action_common_cancel": "Cancel", "action_common_clear": "Clear", @@ -353,7 +373,6 @@ "notification_permission_list_tile_enable_button": "Enable Notifications", "notification_permission_list_tile_title": "Notification Permission", "partner_list_user_photos": "{user}'s photos", - "partner_list_view_all": "View all", "partner_page_add_partner": "Add partner", "partner_page_empty_message": "Your photos are not yet shared with any partner.", "partner_page_no_more_users": "No more users to add", @@ -362,7 +381,6 @@ "partner_page_shared_to_title": "Shared to", "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", - "partner_page_title": "Partner", "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 6a9d34ab83bfe..567406aef0df2 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -3,7 +3,7 @@ PODS: - Flutter - connectivity_plus (0.0.1): - Flutter - - ReachabilitySwift + - FlutterMacOS - device_info_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.9): @@ -77,7 +77,6 @@ PODS: - photo_manager (2.0.0): - Flutter - FlutterMacOS - - ReachabilitySwift (5.0.0) - SAMKeychain (1.5.3) - SDWebImage (5.19.4): - SDWebImage/Core (= 5.19.4) @@ -102,7 +101,7 @@ PODS: DEPENDENCIES: - background_downloader (from `.symlinks/plugins/background_downloader/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) @@ -133,7 +132,6 @@ SPEC REPOS: - DKImagePickerController - DKPhotoGallery - MapLibre - - ReachabilitySwift - SAMKeychain - SDWebImage - SwiftyGif @@ -143,7 +141,7 @@ EXTERNAL SOURCES: background_downloader: :path: ".symlinks/plugins/background_downloader/ios" connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" + :path: ".symlinks/plugins/connectivity_plus/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -195,8 +193,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf - connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 @@ -217,7 +215,6 @@ SPEC CHECKSUMS: path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index 38deac3f0ec61..6f6d1a6a31e88 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -24,7 +24,10 @@ final Map _themePresetsMap = { ImmichColorPreset.indigo: ImmichTheme( light: ColorScheme.fromSeed( seedColor: immichBrandColorLight, - ).copyWith(primary: immichBrandColorLight), + ).copyWith( + primary: immichBrandColorLight, + onSurface: const Color.fromARGB(255, 34, 31, 32), + ), dark: ColorScheme.fromSeed( seedColor: immichBrandColorDark, brightness: Brightness.dark, diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart index 8be636efb659b..23bf23604635d 100644 --- a/mobile/lib/entities/asset.entity.g.dart +++ b/mobile/lib/entities/asset.entity.g.dart @@ -57,64 +57,69 @@ const AssetSchema = CollectionSchema( name: r'isFavorite', type: IsarType.bool, ), - r'isTrashed': PropertySchema( + r'isOffline': PropertySchema( id: 8, + name: r'isOffline', + type: IsarType.bool, + ), + r'isTrashed': PropertySchema( + id: 9, name: r'isTrashed', type: IsarType.bool, ), r'livePhotoVideoId': PropertySchema( - id: 9, + id: 10, name: r'livePhotoVideoId', type: IsarType.string, ), r'localId': PropertySchema( - id: 10, + id: 11, name: r'localId', type: IsarType.string, ), r'ownerId': PropertySchema( - id: 11, + id: 12, name: r'ownerId', type: IsarType.long, ), r'remoteId': PropertySchema( - id: 12, + id: 13, name: r'remoteId', type: IsarType.string, ), r'stackCount': PropertySchema( - id: 13, + id: 14, name: r'stackCount', type: IsarType.long, ), r'stackId': PropertySchema( - id: 14, + id: 15, name: r'stackId', type: IsarType.string, ), r'stackPrimaryAssetId': PropertySchema( - id: 15, + id: 16, name: r'stackPrimaryAssetId', type: IsarType.string, ), r'thumbhash': PropertySchema( - id: 16, + id: 17, name: r'thumbhash', type: IsarType.string, ), r'type': PropertySchema( - id: 17, + id: 18, name: r'type', type: IsarType.byte, enumMap: _AssettypeEnumValueMap, ), r'updatedAt': PropertySchema( - id: 18, + id: 19, name: r'updatedAt', type: IsarType.dateTime, ), r'width': PropertySchema( - id: 19, + id: 20, name: r'width', type: IsarType.int, ) @@ -239,18 +244,19 @@ void _assetSerialize( writer.writeInt(offsets[5], object.height); writer.writeBool(offsets[6], object.isArchived); writer.writeBool(offsets[7], object.isFavorite); - writer.writeBool(offsets[8], object.isTrashed); - writer.writeString(offsets[9], object.livePhotoVideoId); - writer.writeString(offsets[10], object.localId); - writer.writeLong(offsets[11], object.ownerId); - writer.writeString(offsets[12], object.remoteId); - writer.writeLong(offsets[13], object.stackCount); - writer.writeString(offsets[14], object.stackId); - writer.writeString(offsets[15], object.stackPrimaryAssetId); - writer.writeString(offsets[16], object.thumbhash); - writer.writeByte(offsets[17], object.type.index); - writer.writeDateTime(offsets[18], object.updatedAt); - writer.writeInt(offsets[19], object.width); + writer.writeBool(offsets[8], object.isOffline); + writer.writeBool(offsets[9], object.isTrashed); + writer.writeString(offsets[10], object.livePhotoVideoId); + writer.writeString(offsets[11], object.localId); + writer.writeLong(offsets[12], object.ownerId); + writer.writeString(offsets[13], object.remoteId); + writer.writeLong(offsets[14], object.stackCount); + writer.writeString(offsets[15], object.stackId); + writer.writeString(offsets[16], object.stackPrimaryAssetId); + writer.writeString(offsets[17], object.thumbhash); + writer.writeByte(offsets[18], object.type.index); + writer.writeDateTime(offsets[19], object.updatedAt); + writer.writeInt(offsets[20], object.width); } Asset _assetDeserialize( @@ -269,19 +275,20 @@ Asset _assetDeserialize( id: id, isArchived: reader.readBoolOrNull(offsets[6]) ?? false, isFavorite: reader.readBoolOrNull(offsets[7]) ?? false, - isTrashed: reader.readBoolOrNull(offsets[8]) ?? false, - livePhotoVideoId: reader.readStringOrNull(offsets[9]), - localId: reader.readStringOrNull(offsets[10]), - ownerId: reader.readLong(offsets[11]), - remoteId: reader.readStringOrNull(offsets[12]), - stackCount: reader.readLongOrNull(offsets[13]) ?? 0, - stackId: reader.readStringOrNull(offsets[14]), - stackPrimaryAssetId: reader.readStringOrNull(offsets[15]), - thumbhash: reader.readStringOrNull(offsets[16]), - type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ?? + isOffline: reader.readBoolOrNull(offsets[8]) ?? false, + isTrashed: reader.readBoolOrNull(offsets[9]) ?? false, + livePhotoVideoId: reader.readStringOrNull(offsets[10]), + localId: reader.readStringOrNull(offsets[11]), + ownerId: reader.readLong(offsets[12]), + remoteId: reader.readStringOrNull(offsets[13]), + stackCount: reader.readLongOrNull(offsets[14]) ?? 0, + stackId: reader.readStringOrNull(offsets[15]), + stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), + thumbhash: reader.readStringOrNull(offsets[17]), + type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? AssetType.other, - updatedAt: reader.readDateTime(offsets[18]), - width: reader.readIntOrNull(offsets[19]), + updatedAt: reader.readDateTime(offsets[19]), + width: reader.readIntOrNull(offsets[20]), ); return object; } @@ -312,27 +319,29 @@ P _assetDeserializeProp

( case 8: return (reader.readBoolOrNull(offset) ?? false) as P; case 9: - return (reader.readStringOrNull(offset)) as P; + return (reader.readBoolOrNull(offset) ?? false) as P; case 10: return (reader.readStringOrNull(offset)) as P; case 11: - return (reader.readLong(offset)) as P; - case 12: return (reader.readStringOrNull(offset)) as P; + case 12: + return (reader.readLong(offset)) as P; case 13: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 14: return (reader.readStringOrNull(offset)) as P; + case 14: + return (reader.readLongOrNull(offset) ?? 0) as P; case 15: return (reader.readStringOrNull(offset)) as P; case 16: return (reader.readStringOrNull(offset)) as P; case 17: + return (reader.readStringOrNull(offset)) as P; + case 18: return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? AssetType.other) as P; - case 18: - return (reader.readDateTime(offset)) as P; case 19: + return (reader.readDateTime(offset)) as P; + case 20: return (reader.readIntOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -1353,6 +1362,16 @@ extension AssetQueryFilter on QueryBuilder { }); } + QueryBuilder isOfflineEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isOffline', + value: value, + )); + }); + } + QueryBuilder isTrashedEqualTo( bool value) { return QueryBuilder.apply(this, (query) { @@ -2628,6 +2647,18 @@ extension AssetQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByIsOffline() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isOffline', Sort.asc); + }); + } + + QueryBuilder sortByIsOfflineDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isOffline', Sort.desc); + }); + } + QueryBuilder sortByIsTrashed() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isTrashed', Sort.asc); @@ -2882,6 +2913,18 @@ extension AssetQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByIsOffline() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isOffline', Sort.asc); + }); + } + + QueryBuilder thenByIsOfflineDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isOffline', Sort.desc); + }); + } + QueryBuilder thenByIsTrashed() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isTrashed', Sort.asc); @@ -3078,6 +3121,12 @@ extension AssetQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByIsOffline() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isOffline'); + }); + } + QueryBuilder distinctByIsTrashed() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'isTrashed'); @@ -3214,6 +3263,12 @@ extension AssetQueryProperty on QueryBuilder { }); } + QueryBuilder isOfflineProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isOffline'); + }); + } + QueryBuilder isTrashedProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'isTrashed'); diff --git a/mobile/lib/interfaces/album.interface.dart b/mobile/lib/interfaces/album.interface.dart index ba188f127009a..bdf11f18de8ac 100644 --- a/mobile/lib/interfaces/album.interface.dart +++ b/mobile/lib/interfaces/album.interface.dart @@ -2,6 +2,7 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/database.interface.dart'; +import 'package:immich_mobile/models/albums/album_search.model.dart'; abstract interface class IAlbumRepository implements IDatabaseRepository { Future create(Album album); @@ -38,6 +39,8 @@ abstract interface class IAlbumRepository implements IDatabaseRepository { Future removeAssets(Album album, List assets); Future recalculateMetadata(Album album); + + Future> search(String searchTerm, QuickFilterMode filterMode); } enum AlbumSort { remoteId, localId } diff --git a/mobile/lib/models/albums/album_search.model.dart b/mobile/lib/models/albums/album_search.model.dart new file mode 100644 index 0000000000000..ac4eedbff1bd8 --- /dev/null +++ b/mobile/lib/models/albums/album_search.model.dart @@ -0,0 +1,5 @@ +enum QuickFilterMode { + all, + sharedWithMe, + myAlbums, +} diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart new file mode 100644 index 0000000000000..e466149ac3ee8 --- /dev/null +++ b/mobile/lib/pages/albums/albums.page.dart @@ -0,0 +1,469 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; +import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; +import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; + +@RoutePage() +class AlbumsPage extends HookConsumerWidget { + const AlbumsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albums = + ref.watch(albumProvider).where((album) => album.isRemote).toList(); + final albumSortOption = ref.watch(albumSortByOptionsProvider); + final albumSortIsReverse = ref.watch(albumSortOrderProvider); + final sorted = albumSortOption.sortFn(albums, albumSortIsReverse); + final isGrid = useState(false); + final searchController = useTextEditingController(); + final debounceTimer = useRef(null); + final filterMode = useState(QuickFilterMode.all); + final userId = ref.watch(currentUserProvider)?.id; + final searchFocusNode = useFocusNode(); + + toggleViewMode() { + isGrid.value = !isGrid.value; + } + + onSearch(String searchTerm, QuickFilterMode mode) { + debounceTimer.value?.cancel(); + debounceTimer.value = Timer(const Duration(milliseconds: 300), () { + ref.read(albumProvider.notifier).searchAlbums(searchTerm, mode); + }); + } + + changeFilter(QuickFilterMode mode) { + filterMode.value = mode; + } + + useEffect( + () { + searchController.addListener(() { + onSearch(searchController.text, filterMode.value); + }); + + return () { + searchController.removeListener(() { + onSearch(searchController.text, filterMode.value); + }); + debounceTimer.value?.cancel(); + }; + }, + [], + ); + + clearSearch() { + filterMode.value = QuickFilterMode.all; + searchController.clear(); + onSearch('', QuickFilterMode.all); + } + + return Scaffold( + appBar: ImmichAppBar( + showUploadButton: false, + actions: [ + IconButton( + icon: Icon( + Icons.add_rounded, + size: 28, + ), + onPressed: () => context.pushRoute( + CreateAlbumRoute(), + ), + ), + ], + ), + body: RefreshIndicator( + displacement: 70, + onRefresh: () async { + await ref.read(albumProvider.notifier).refreshRemoteAlbums(); + }, + child: ListView( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + color: context.colorScheme.onSurface.withAlpha(0), + width: 0, + ), + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withOpacity(0.075), + context.colorScheme.primary.withOpacity(0.09), + context.colorScheme.primary.withOpacity(0.075), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + transform: GradientRotation(0.5 * pi), + ), + ), + child: TextField( + autofocus: false, + decoration: InputDecoration( + contentPadding: EdgeInsets.all(16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.surfaceDim, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.surfaceContainer, + ), + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.surfaceDim, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.primary.withAlpha(100), + ), + ), + hintText: 'search_albums'.tr(), + hintStyle: context.textTheme.bodyLarge?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + prefixIcon: const Icon(Icons.search_rounded), + suffixIcon: searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear_rounded), + onPressed: clearSearch, + ) + : const SizedBox.shrink(), + ), + controller: searchController, + onChanged: (_) => + onSearch(searchController.text, filterMode.value), + focusNode: searchFocusNode, + onTapOutside: (_) => searchFocusNode.unfocus(), + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + QuickFilterButton( + label: 'all'.tr(), + isSelected: filterMode.value == QuickFilterMode.all, + onTap: () { + changeFilter(QuickFilterMode.all); + onSearch(searchController.text, QuickFilterMode.all); + }, + ), + QuickFilterButton( + label: 'shared_with_me'.tr(), + isSelected: filterMode.value == QuickFilterMode.sharedWithMe, + onTap: () { + changeFilter(QuickFilterMode.sharedWithMe); + onSearch( + searchController.text, + QuickFilterMode.sharedWithMe, + ); + }, + ), + QuickFilterButton( + label: 'my_albums'.tr(), + isSelected: filterMode.value == QuickFilterMode.myAlbums, + onTap: () { + changeFilter(QuickFilterMode.myAlbums); + onSearch( + searchController.text, + QuickFilterMode.myAlbums, + ); + }, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SortButton(), + IconButton( + icon: Icon( + isGrid.value + ? Icons.view_list_outlined + : Icons.grid_view_outlined, + size: 24, + ), + onPressed: toggleViewMode, + ), + ], + ), + const SizedBox(height: 5), + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: isGrid.value + ? GridView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: .7, + ), + itemBuilder: (context, index) { + return AlbumThumbnailCard( + album: sorted[index], + onTap: () => context.pushRoute( + AlbumViewerRoute(albumId: sorted[index].id), + ), + showOwner: true, + ); + }, + itemCount: sorted.length, + ) + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: sorted.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: LargeLeadingTile( + title: Text( + sorted[index].name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + subtitle: sorted[index].ownerId == userId + ? Text( + '${sorted[index].assetCount} items', + overflow: TextOverflow.ellipsis, + style: + context.textTheme.bodyMedium?.copyWith( + color: context + .colorScheme.onSurfaceSecondary, + ), + ) + : sorted[index].ownerName != null + ? Text( + '${sorted[index].assetCount} items • ${'album_thumbnail_shared_by'.tr( + args: [ + sorted[index].ownerName!, + ], + )}', + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodyMedium + ?.copyWith( + color: context + .colorScheme.onSurfaceSecondary, + ), + ) + : null, + onTap: () => context.pushRoute( + AlbumViewerRoute(albumId: sorted[index].id), + ), + leadingPadding: const EdgeInsets.only( + right: 16, + ), + leading: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(15), + ), + child: ImmichThumbnail( + asset: sorted[index].thumbnail.value, + width: 80, + height: 80, + ), + ), + // minVerticalPadding: 1, + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} + +class QuickFilterButton extends StatelessWidget { + const QuickFilterButton({ + super.key, + required this.isSelected, + required this.onTap, + required this.label, + }); + + final bool isSelected; + final VoidCallback onTap; + final String label; + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: onTap, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isSelected ? context.colorScheme.primary : Colors.transparent, + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: context.colorScheme.onSurface.withAlpha(25), + width: 1, + ), + ), + ), + ), + child: Text( + label, + style: TextStyle( + color: isSelected + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + fontSize: 14, + ), + ), + ); + } +} + +class SortButton extends ConsumerWidget { + const SortButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albumSortOption = ref.watch(albumSortByOptionsProvider); + final albumSortIsReverse = ref.watch(albumSortOrderProvider); + + return MenuAnchor( + style: MenuStyle( + elevation: WidgetStatePropertyAll(1), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + padding: WidgetStatePropertyAll( + EdgeInsets.all(4), + ), + ), + consumeOutsideTap: true, + menuChildren: AlbumSortMode.values + .map( + (mode) => MenuItemButton( + leadingIcon: albumSortOption == mode + ? albumSortIsReverse + ? Icon( + Icons.keyboard_arrow_down, + color: albumSortOption == mode + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + ) + : Icon( + Icons.keyboard_arrow_up_rounded, + color: albumSortOption == mode + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + ) + : const Icon(Icons.abc, color: Colors.transparent), + onPressed: () { + final selected = albumSortOption == mode; + // Switch direction + if (selected) { + ref + .read(albumSortOrderProvider.notifier) + .changeSortDirection(!albumSortIsReverse); + } else { + ref + .read(albumSortByOptionsProvider.notifier) + .changeSortMode(mode); + } + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.fromLTRB(16, 16, 32, 16), + ), + backgroundColor: WidgetStateProperty.all( + albumSortOption == mode + ? context.colorScheme.primary + : Colors.transparent, + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + ), + child: Text( + mode.label.tr(), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: albumSortOption == mode + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface.withAlpha(185), + ), + ), + ), + ) + .toList(), + builder: (context, controller, child) { + return GestureDetector( + onTap: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 5), + child: Transform.rotate( + angle: 90 * pi / 180, + child: Icon( + Icons.compare_arrows_rounded, + size: 18, + color: context.colorScheme.onSurface.withAlpha(225), + ), + ), + ), + Text( + albumSortOption.label.tr(), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.onSurface.withAlpha(225), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 8dccece325d8f..0869e75e9fc14 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -151,7 +151,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { handleSyncAlbumToggle(bool isEnable) async { if (isEnable) { - await ref.read(albumProvider.notifier).getAllAlbums(); + await ref.read(albumProvider.notifier).refreshRemoteAlbums(); for (final album in selectedBackupAlbums) { await ref.read(albumProvider.notifier).createSyncAlbum(album.name); } diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index bb9d462e50bc4..d8baecf808d63 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -212,7 +212,7 @@ class BackupControllerPage extends HookConsumerWidget { .read(backupProvider.notifier) .backupAlbumSelectionDone(); // waited until backup albums are stored in DB - ref.read(albumProvider.notifier).getDeviceAlbums(); + ref.read(albumProvider.notifier).refreshDeviceAlbums(); }, child: const Text( "backup_controller_page_select", diff --git a/mobile/lib/pages/common/album_options.page.dart b/mobile/lib/pages/common/album_options.page.dart index 3cc30af7a97f1..93e4c180fed6b 100644 --- a/mobile/lib/pages/common/album_options.page.dart +++ b/mobile/lib/pages/common/album_options.page.dart @@ -6,7 +6,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/authentication.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -45,11 +45,11 @@ class AlbumOptionsPage extends HookConsumerWidget { try { final isSuccess = - await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album); + await ref.read(albumProvider.notifier).leaveAlbum(album); if (isSuccess) { context.navigateTo( - const TabControllerRoute(children: [SharingRoute()]), + TabControllerRoute(children: [AlbumsRoute()]), ); } else { showErrorMessage(); @@ -65,9 +65,7 @@ class AlbumOptionsPage extends HookConsumerWidget { isProcessing.value = true; try { - await ref - .read(sharedAlbumProvider.notifier) - .removeUserFromAlbum(album, user); + await ref.read(albumProvider.notifier).removeUser(album, user); album.sharedUsers.remove(user); sharedUsers.value = album.sharedUsers.toList(); } catch (error) { @@ -200,8 +198,8 @@ class AlbumOptionsPage extends HookConsumerWidget { onChanged: (bool value) async { activityEnabled.value = value; if (await ref - .read(sharedAlbumProvider.notifier) - .setActivityEnabled(album, value)) { + .read(albumProvider.notifier) + .setActivitystatus(album, value)) { album.activityEnabled = value; } }, diff --git a/mobile/lib/pages/common/album_shared_user_selection.page.dart b/mobile/lib/pages/common/album_shared_user_selection.page.dart index aefa8e273612c..9dadef1a76f8a 100644 --- a/mobile/lib/pages/common/album_shared_user_selection.page.dart +++ b/mobile/lib/pages/common/album_shared_user_selection.page.dart @@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_title.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -25,20 +25,15 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget { final suggestedShareUsers = ref.watch(otherUsersProvider); createSharedAlbum() async { - var newAlbum = - await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum( - ref.watch(albumTitleProvider), - assets, - sharedUsersList.value, - ); + var newAlbum = await ref.watch(albumProvider.notifier).createAlbum( + ref.watch(albumTitleProvider), + assets, + ); if (newAlbum != null) { - await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); - // ref.watch(assetSelectionProvider.notifier).removeAll(); ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); context.maybePop(true); - context - .navigateTo(const TabControllerRoute(children: [SharingRoute()])); + context.navigateTo(TabControllerRoute(children: [AlbumsRoute()])); } ScaffoldMessenger( diff --git a/mobile/lib/pages/common/album_viewer.page.dart b/mobile/lib/pages/common/album_viewer.page.dart index 33b314f3b105b..b977128cfa25c 100644 --- a/mobile/lib/pages/common/album_viewer.page.dart +++ b/mobile/lib/pages/common/album_viewer.page.dart @@ -11,9 +11,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; @@ -50,9 +48,7 @@ class AlbumViewerPage extends HookConsumerWidget { Future onRemoveFromAlbumPressed(Iterable assets) async { final a = album.valueOrNull; final bool isSuccess = a != null && - await ref - .read(sharedAlbumProvider.notifier) - .removeAssetFromAlbum(a, assets); + await ref.read(albumProvider.notifier).removeAsset(a, assets); if (!isSuccess) { ImmichToast.show( @@ -81,9 +77,9 @@ class AlbumViewerPage extends HookConsumerWidget { // Check if there is new assets add isProcessing.value = true; - await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum( - returnPayload.selectedAssets, + await ref.watch(albumProvider.notifier).addAssets( albumInfo, + returnPayload.selectedAssets, ); isProcessing.value = false; @@ -98,9 +94,7 @@ class AlbumViewerPage extends HookConsumerWidget { if (sharedUserIds != null) { isProcessing.value = true; - await ref - .watch(albumServiceProvider) - .addAdditionalUserToAlbum(sharedUserIds, album); + await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds); isProcessing.value = false; } @@ -184,27 +178,29 @@ class AlbumViewerPage extends HookConsumerWidget { } Widget buildSharedUserIconsRow(Album album) { - return GestureDetector( - onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)), - child: SizedBox( - height: 50, - child: ListView.builder( - padding: const EdgeInsets.only(left: 16), - scrollDirection: Axis.horizontal, - itemBuilder: ((context, index) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: UserCircleAvatar( - user: album.sharedUsers.toList()[index], - radius: 18, - size: 36, + return album.sharedUsers.isNotEmpty + ? GestureDetector( + onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)), + child: SizedBox( + height: 50, + child: ListView.builder( + padding: const EdgeInsets.only(left: 16), + scrollDirection: Axis.horizontal, + itemBuilder: ((context, index) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: UserCircleAvatar( + user: album.sharedUsers.toList()[index], + radius: 18, + size: 36, + ), + ); + }), + itemCount: album.sharedUsers.length, ), - ); - }), - itemCount: album.sharedUsers.length, - ), - ), - ); + ), + ) + : const SizedBox.shrink(); } Widget buildHeader(Album album) { @@ -214,7 +210,7 @@ class AlbumViewerPage extends HookConsumerWidget { children: [ buildTitle(album), if (album.assets.isNotEmpty == true) buildAlbumDateRange(album), - if (album.shared) buildSharedUserIconsRow(album), + buildSharedUserIconsRow(album), ], ); } @@ -231,17 +227,17 @@ class AlbumViewerPage extends HookConsumerWidget { body: Stack( children: [ album.widgetWhen( - onData: (data) => MultiselectGrid( + onData: (albumInfo) => MultiselectGrid( renderListProvider: albumRenderlistProvider(albumId), topWidget: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildHeader(data), - if (data.isRemote) buildControlButton(data), + buildHeader(albumInfo), + if (albumInfo.isRemote) buildControlButton(albumInfo), ], ), onRemoveFromAlbum: onRemoveFromAlbumPressed, - editEnabled: data.ownerId == userId, + editEnabled: albumInfo.ownerId == userId, ), ), AnimatedPositioned( diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart index 1fd860520d5c7..55261f6d55304 100644 --- a/mobile/lib/pages/common/create_album.page.dart +++ b/mobile/lib/pages/common/create_album.page.dart @@ -17,13 +17,11 @@ import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; @RoutePage() // ignore: must_be_immutable class CreateAlbumPage extends HookConsumerWidget { - final bool isSharedAlbum; - final List? initialAssets; + final List? assets; const CreateAlbumPage({ super.key, - required this.isSharedAlbum, - this.initialAssets, + this.assets, }); @override @@ -34,18 +32,9 @@ class CreateAlbumPage extends HookConsumerWidget { final isAlbumTitleTextFieldFocus = useState(false); final isAlbumTitleEmpty = useState(true); final selectedAssets = useState>( - initialAssets != null ? Set.from(initialAssets!) : const {}, + assets != null ? Set.from(assets!) : const {}, ); - showSelectUserPage() async { - final bool? ok = await context.pushRoute( - AlbumSharedUserSelectionRoute(assets: selectedAssets.value), - ); - if (ok == true) { - selectedAssets.value = {}; - } - } - void onBackgroundTapped() { albumTitleTextFieldFocusNode.unfocus(); isAlbumTitleTextFieldFocus.value = false; @@ -199,7 +188,7 @@ class CreateAlbumPage extends HookConsumerWidget { ); if (newAlbum != null) { - ref.watch(albumProvider.notifier).getAllAlbums(); + ref.watch(albumProvider.notifier).refreshRemoteAlbums(); selectedAssets.value = {}; ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); @@ -223,36 +212,20 @@ class CreateAlbumPage extends HookConsumerWidget { 'share_create_album', ).tr(), actions: [ - if (isSharedAlbum) - TextButton( - onPressed: albumTitleController.text.isNotEmpty - ? showSelectUserPage - : null, - child: Text( - 'create_shared_album_page_share'.tr(), - style: TextStyle( - fontWeight: FontWeight.bold, - color: albumTitleController.text.isEmpty - ? context.themeData.disabledColor - : context.primaryColor, - ), - ), - ), - if (!isSharedAlbum) - TextButton( - onPressed: albumTitleController.text.isNotEmpty - ? createNonSharedAlbum - : null, - child: Text( - 'create_shared_album_page_create'.tr(), - style: TextStyle( - fontWeight: FontWeight.bold, - color: albumTitleController.text.isNotEmpty - ? context.primaryColor - : context.themeData.disabledColor, - ), + TextButton( + onPressed: albumTitleController.text.isNotEmpty + ? createNonSharedAlbum + : null, + child: Text( + 'create_shared_album_page_create'.tr(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: albumTitleController.text.isNotEmpty + ? context.primaryColor + : context.themeData.disabledColor, ), ), + ), ], ), body: GestureDetector( diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart new file mode 100644 index 0000000000000..8213ca423f268 --- /dev/null +++ b/mobile/lib/pages/common/large_leading_tile.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +class LargeLeadingTile extends StatelessWidget { + const LargeLeadingTile({ + super.key, + required this.leading, + required this.onTap, + required this.title, + this.subtitle, + this.leadingPadding = const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16.0, + ), + this.borderRadius = 20.0, + }); + + final Widget leading; + final VoidCallback onTap; + final Widget title; + final Widget? subtitle; + final EdgeInsetsGeometry leadingPadding; + final double borderRadius; + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(borderRadius), + onTap: onTap, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: leadingPadding, + child: leading, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.6, + child: title, + ), + subtitle ?? const SizedBox.shrink(), + ], + ), + ], + ), + ); + } +} diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index b619e003d2c3a..e9a870af471b3 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -16,10 +17,11 @@ class TabControllerPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final refreshing = ref.watch(assetProvider); + final isRefreshingAssets = ref.watch(assetProvider); + final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider); - Widget buildIcon(Widget icon) { - if (!refreshing) return icon; + Widget buildIcon({required Widget icon, required bool isProcessing}) { + if (!isProcessing) return icon; return Stack( alignment: Alignment.center, clipBehavior: Clip.none, @@ -84,15 +86,15 @@ class TabControllerPage extends HookConsumerWidget { ), NavigationRailDestination( padding: const EdgeInsets.all(4), - icon: const Icon(Icons.share_rounded), - selectedIcon: const Icon(Icons.share), - label: const Text('tab_controller_nav_sharing').tr(), + icon: const Icon(Icons.photo_album_outlined), + selectedIcon: const Icon(Icons.photo_album), + label: const Text('albums').tr(), ), NavigationRailDestination( padding: const EdgeInsets.all(4), - icon: const Icon(Icons.photo_album_outlined), - selectedIcon: const Icon(Icons.photo_album), - label: const Text('tab_controller_nav_library').tr(), + icon: const Icon(Icons.space_dashboard_outlined), + selectedIcon: const Icon(Icons.space_dashboard_rounded), + label: const Text('library').tr(), ), ], ); @@ -118,7 +120,8 @@ class TabControllerPage extends HookConsumerWidget { Icons.photo_library_outlined, ), selectedIcon: buildIcon( - Icon( + isProcessing: isRefreshingAssets, + icon: Icon( Icons.photo_library, color: context.primaryColor, ), @@ -135,23 +138,27 @@ class TabControllerPage extends HookConsumerWidget { ), ), NavigationDestination( - label: 'tab_controller_nav_sharing'.tr(), + label: 'albums'.tr(), icon: const Icon( - Icons.group_outlined, + Icons.photo_album_outlined, ), - selectedIcon: Icon( - Icons.group, - color: context.primaryColor, + selectedIcon: buildIcon( + isProcessing: isRefreshingRemoteAlbums, + icon: Icon( + Icons.photo_album_rounded, + color: context.primaryColor, + ), ), ), NavigationDestination( - label: 'tab_controller_nav_library'.tr(), + label: 'library'.tr(), icon: const Icon( - Icons.photo_album_outlined, + Icons.space_dashboard_outlined, ), selectedIcon: buildIcon( - Icon( - Icons.photo_album_rounded, + isProcessing: isRefreshingAssets, + icon: Icon( + Icons.space_dashboard_rounded, color: context.primaryColor, ), ), @@ -162,11 +169,11 @@ class TabControllerPage extends HookConsumerWidget { final multiselectEnabled = ref.watch(multiselectProvider); return AutoTabsRouter( - routes: const [ - PhotosRoute(), - SearchRoute(), - SharingRoute(), - LibraryRoute(), + routes: [ + const PhotosRoute(), + SearchInputRoute(), + const AlbumsRoute(), + const LibraryRoute(), ], duration: const Duration(milliseconds: 600), transitionBuilder: (context, child, animation) => FadeTransition( diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart index 32d3aa6ba9021..650d2dc912db9 100644 --- a/mobile/lib/pages/editing/edit.page.dart +++ b/mobile/lib/pages/editing/edit.page.dart @@ -69,7 +69,7 @@ class EditImagePage extends ConsumerWidget { imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg", ); - await ref.read(albumProvider.notifier).getDeviceAlbums(); + await ref.read(albumProvider.notifier).refreshDeviceAlbums(); Navigator.of(context).popUntil((route) => route.isFirst); ImmichToast.show( durationInSecond: 3, diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 5f03ed68714c8..368f3d2ec37a6 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -1,324 +1,354 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/providers/partner.provider.dart'; +import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; +import 'package:immich_mobile/widgets/common/user_avatar.dart'; +import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() -class LibraryPage extends HookConsumerWidget { +class LibraryPage extends ConsumerWidget { const LibraryPage({super.key}); - @override Widget build(BuildContext context, WidgetRef ref) { final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final albums = ref.watch(albumProvider); - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - - useEffect( - () { - ref.read(albumProvider.notifier).getAllAlbums(); - return null; - }, - [], - ); - Widget buildSortButton() { - return PopupMenuButton( - position: PopupMenuPosition.over, - itemBuilder: (BuildContext context) { - return AlbumSortMode.values - .map>((option) { - final selected = albumSortOption == option; - return PopupMenuItem( - value: option, + return Scaffold( + appBar: ImmichAppBar(), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0), child: Row( children: [ - Padding( - padding: const EdgeInsets.only(right: 12.0), - child: Icon( - Icons.check, - color: - selected ? context.primaryColor : Colors.transparent, - ), + ActionButton( + onPressed: () => context.pushRoute(const FavoritesRoute()), + icon: Icons.favorite_outline_rounded, + label: 'favorites'.tr(), ), - Text( - option.label.tr(), - style: TextStyle( - color: selected ? context.primaryColor : null, - fontSize: 14.0, - ), + const SizedBox(width: 8), + ActionButton( + onPressed: () => context.pushRoute(const ArchiveRoute()), + icon: Icons.archive_outlined, + label: 'archived'.tr(), ), ], ), - ); - }).toList(); - }, - onSelected: (AlbumSortMode value) { - final selected = albumSortOption == value; - // Switch direction - if (selected) { - ref - .read(albumSortOrderProvider.notifier) - .changeSortDirection(!albumSortIsReverse); - } else { - ref.read(albumSortByOptionsProvider.notifier).changeSortMode(value); - } - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 5), - child: Icon( - albumSortIsReverse - ? Icons.arrow_downward_rounded - : Icons.arrow_upward_rounded, - size: 14, - color: context.primaryColor, - ), ), - Text( - albumSortOption.label.tr(), - style: context.textTheme.labelLarge?.copyWith( - color: context.primaryColor, - ), + const SizedBox(height: 8), + Row( + children: [ + ActionButton( + onPressed: () => context.pushRoute(const SharedLinkRoute()), + icon: Icons.link_outlined, + label: 'shared_links'.tr(), + ), + const SizedBox(width: 8), + trashEnabled + ? ActionButton( + onPressed: () => context.pushRoute(const TrashRoute()), + icon: Icons.delete_outline_rounded, + label: 'trash'.tr(), + ) + : const SizedBox.shrink(), + ], + ), + const SizedBox(height: 12), + const Wrap( + spacing: 8, + runSpacing: 8, + children: [ + PeopleCollectionCard(), + PlacesCollectionCard(), + LocalAlbumsCollectionCard(), + ], + ), + const SizedBox(height: 12), + QuickAccessButtons(), + const SizedBox( + height: 32, ), ], ), - ); - } + ), + ); + } +} - Widget buildCreateAlbumButton() { - return LayoutBuilder( - builder: (context, constraints) { - var cardSize = constraints.maxWidth; +class QuickAccessButtons extends ConsumerWidget { + const QuickAccessButtons({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final partners = ref.watch(partnerSharedWithProvider); - return GestureDetector( - onTap: () => - context.pushRoute(CreateAlbumRoute(isSharedAlbum: false)), - child: Padding( - padding: - const EdgeInsets.only(bottom: 32), // Adjust padding to suit - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: cardSize, - height: cardSize, - decoration: BoxDecoration( - color: context.colorScheme.surfaceContainer, - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - child: Center( - child: Icon( - Icons.add_rounded, - size: 28, - color: context.primaryColor, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 8.0, - bottom: 16, - ), - child: Text( - 'library_page_new_album', - style: context.textTheme.labelLarge?.copyWith( - color: context.colorScheme.onSurface, - ), - ).tr(), - ), - ], + return Container( + decoration: BoxDecoration( + border: Border.all( + color: context.colorScheme.onSurface.withAlpha(10), + width: 1, + ), + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withAlpha(10), + context.colorScheme.primary.withAlpha(15), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0), + bottomRight: Radius.circular(partners.isEmpty ? 20 : 0), ), ), - ); - }, - ); - } - - Widget buildLibraryNavButton( - String label, - IconData icon, - Function() onClick, - ) { - return Expanded( - child: FilledButton.icon( - onPressed: onClick, - label: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - label, - style: TextStyle( - color: context.colorScheme.onSurface, + leading: const Icon( + Icons.group_outlined, + size: 26, + ), + title: Text( + 'partners'.tr(), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, ), ), + onTap: () => context.pushRoute(const PartnerRoute()), ), - style: FilledButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - backgroundColor: context.colorScheme.surfaceContainer, - alignment: Alignment.centerLeft, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), + PartnerList(partners: partners), + ], + ), + ); + } +} + +class PartnerList extends ConsumerWidget { + const PartnerList({super.key, required this.partners}); + + final List partners; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: partners.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final partner = partners[index]; + final isLastItem = index == partners.length - 1; + return ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(isLastItem ? 20 : 0), + bottomRight: Radius.circular(isLastItem ? 20 : 0), ), ), - icon: Icon( - icon, - color: context.primaryColor, + contentPadding: const EdgeInsets.only( + left: 12.0, + right: 18.0, ), - ), - ); - } - - final remote = albums.where((a) => a.isRemote).toList(); - final sorted = albumSortOption.sortFn(remote, albumSortIsReverse); - final local = albums.where((a) => a.isLocal).toList(); + leading: userAvatar(context, partner, radius: 16), + title: Text( + "partner_list_user_photos", + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ).tr( + namedArgs: { + 'user': partner.name, + }, + ), + onTap: () => context.pushRoute( + (PartnerDetailRoute(partner: partner)), + ), + ); + }, + ); + } +} - Widget? shareTrashButton() { - return trashEnabled - ? InkWell( - onTap: () => context.pushRoute(const TrashRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Icon( - Icons.delete_rounded, - size: 25, - semanticLabel: 'profile_drawer_trash'.tr(), - ), - ) - : null; - } +class PeopleCollectionCard extends ConsumerWidget { + const PeopleCollectionCard({super.key}); - return Scaffold( - appBar: ImmichAppBar( - action: shareTrashButton(), - ), - body: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Padding( - 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.favorite_border, () { - context.navigateTo(const FavoritesRoute()); - }), - const SizedBox(width: 12.0), - buildLibraryNavButton( - "library_page_archive".tr(), Icons.archive_outlined, () { - context.navigateTo(const ArchiveRoute()); - }), + @override + Widget build(BuildContext context, WidgetRef ref) { + final people = ref.watch(getAllPeopleProvider); + final size = MediaQuery.of(context).size.width * 0.5 - 20; + return GestureDetector( + onTap: () => context.pushRoute(const PeopleCollectionRoute()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: size, + width: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withAlpha(30), + context.colorScheme.primary.withAlpha(25), ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, ), ), + child: people.widgetWhen( + onData: (people) { + return GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + physics: const NeverScrollableScrollPhysics(), + children: people.take(4).map((person) { + return CircleAvatar( + backgroundImage: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: ApiService.getRequestHeaders(), + ), + ); + }).toList(), + ); + }, + ), ), - 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: [ - Text( - 'library_page_albums', - style: context.textTheme.bodyLarge?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ).tr(), - buildSortButton(), - ], + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'people'.tr(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, ), ), ), - SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - delegate: SliverChildBuilderDelegate( - childCount: sorted.length + 1, - (context, index) { - if (index == 0) { - return buildCreateAlbumButton(); - } + ], + ), + ); + } +} - return AlbumThumbnailCard( - album: sorted[index - 1], - onTap: () => context.pushRoute( - AlbumViewerRoute( - albumId: sorted[index - 1].id, - ), - ), - ); - }, +class LocalAlbumsCollectionCard extends HookConsumerWidget { + const LocalAlbumsCollectionCard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albums = ref.watch(localAlbumsProvider); + + final size = MediaQuery.of(context).size.width * 0.5 - 20; + + return GestureDetector( + onTap: () => context.pushRoute( + const LocalAlbumsRoute(), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: size, + width: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withAlpha(30), + context.colorScheme.primary.withAlpha(25), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, ), ), + child: GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + physics: const NeverScrollableScrollPhysics(), + children: albums.take(4).map((album) { + return AlbumThumbnailCard( + album: album, + showTitle: false, + ); + }).toList(), + ), ), - 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: [ - Text( - 'library_page_device_albums', - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ], + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'on_this_device'.tr(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, ), ), ), - SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - delegate: SliverChildBuilderDelegate( - childCount: local.length, - (context, index) => AlbumThumbnailCard( - album: local[index], - onTap: () => context.pushRoute( - AlbumViewerRoute( - albumId: local[index].id, - ), - ), + ], + ), + ); + } +} + +class PlacesCollectionCard extends StatelessWidget { + const PlacesCollectionCard({super.key}); + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size.width * 0.5 - 20; + return GestureDetector( + onTap: () => context.pushRoute(const PlacesCollectionRoute()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: size, + width: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.colorScheme.secondaryContainer.withAlpha(100), + ), + child: IgnorePointer( + child: MapThumbnail( + zoom: 8, + centre: const LatLng( + 21.44950, + -157.91959, ), + showAttribution: false, + themeMode: + context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'places'.tr(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, ), ), ), @@ -327,3 +357,52 @@ class LibraryPage extends HookConsumerWidget { ); } } + +class ActionButton extends StatelessWidget { + final VoidCallback onPressed; + final IconData icon; + final String label; + + const ActionButton({ + super.key, + required this.onPressed, + required this.icon, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: FilledButton.icon( + onPressed: onPressed, + label: Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + label, + style: TextStyle( + color: context.colorScheme.onSurface, + fontSize: 15, + ), + ), + ), + style: FilledButton.styleFrom( + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + backgroundColor: context.colorScheme.surfaceContainerLow, + alignment: Alignment.centerLeft, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(25)), + side: BorderSide( + color: context.colorScheme.onSurface.withAlpha(10), + width: 1, + ), + ), + ), + icon: Icon( + icon, + color: context.primaryColor, + ), + ), + ); + } +} diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart new file mode 100644 index 0000000000000..164ea3bad883f --- /dev/null +++ b/mobile/lib/pages/library/local_albums.page.dart @@ -0,0 +1,55 @@ +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/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; + +@RoutePage() +class LocalAlbumsPage extends HookConsumerWidget { + const LocalAlbumsPage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final albums = ref.watch(localAlbumsProvider); + + return Scaffold( + appBar: AppBar( + title: Text('on_this_device'.tr()), + ), + body: ListView.builder( + padding: const EdgeInsets.all(18.0), + itemCount: albums.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: LargeLeadingTile( + leadingPadding: const EdgeInsets.only( + right: 16, + ), + leading: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(15)), + child: ImmichThumbnail( + asset: albums[index].thumbnail.value, + width: 80, + height: 80, + ), + ), + title: Text( + albums[index].name, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text('${albums[index].assetCount} items'), + onTap: () => context + .pushRoute(AlbumViewerRoute(albumId: albums[index].id)), + ), + ); + }, + ), + ); + } +} diff --git a/mobile/lib/pages/sharing/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart similarity index 93% rename from mobile/lib/pages/sharing/partner/partner.page.dart rename to mobile/lib/pages/library/partner/partner.page.dart index 8dd31023c7cad..1e9e801210e5e 100644 --- a/mobile/lib/pages/sharing/partner/partner.page.dart +++ b/mobile/lib/pages/library/partner/partner.page.dart @@ -86,12 +86,10 @@ class PartnerPage extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.only(left: 16.0, top: 16.0), - child: const Text( + child: Text( "partner_page_shared_to_title", - style: TextStyle( - fontSize: 14, - color: Colors.grey, - fontWeight: FontWeight.bold, + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface.withAlpha(200), ), ).tr(), ), @@ -104,10 +102,7 @@ class PartnerPage extends HookConsumerWidget { leading: userAvatar(context, users[index]), title: Text( users[index].email, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), + style: context.textTheme.bodyLarge, ), trailing: IconButton( icon: const Icon(Icons.person_remove), @@ -148,7 +143,7 @@ class PartnerPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text("partner_page_title").tr(), + title: const Text("partners").tr(), elevation: 0, centerTitle: false, actions: [ diff --git a/mobile/lib/pages/sharing/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart similarity index 59% rename from mobile/lib/pages/sharing/partner/partner_detail.page.dart rename to mobile/lib/pages/library/partner/partner_detail.page.dart index 8a2dd4b820379..0874aacfa7f53 100644 --- a/mobile/lib/pages/sharing/partner/partner_detail.page.dart +++ b/mobile/lib/pages/library/partner/partner_detail.page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/providers/partner.provider.dart'; import 'package:immich_mobile/entities/user.entity.dart'; @@ -22,7 +23,11 @@ class PartnerDetailPage extends HookConsumerWidget { useEffect( () { - ref.read(assetProvider.notifier).getAllAsset(); + Future.microtask( + () async => { + await ref.read(assetProvider.notifier).getAllAsset(), + }, + ); return null; }, [], @@ -64,19 +69,47 @@ class PartnerDetailPage extends HookConsumerWidget { title: Text(partner.name), elevation: 0, centerTitle: false, - actions: [ - IconButton( - onPressed: toggleInTimeline, - icon: Icon( - inTimeline.value - ? Icons.collections - : Icons.collections_outlined, + ), + body: MultiselectGrid( + topWidget: Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: context.colorScheme.onSurface.withAlpha(10), + width: 1, + ), + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withAlpha(10), + context.colorScheme.primary.withAlpha(15), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ListTile( + title: Text( + "Show in timeline", + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.primary, ), - tooltip: "Show/hide photos on your main timeline", ), - ], + subtitle: Text( + "Show photos and videos from this user in your timeline", + style: context.textTheme.bodyMedium, + ), + trailing: Switch( + value: inTimeline.value, + onChanged: (_) => toggleInTimeline(), + ), + ), ), - body: MultiselectGrid( + ), + ), renderListProvider: assetsProvider(partner.isarId), onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(), deleteEnabled: false, diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart new file mode 100644 index 0000000000000..b3f688280810c --- /dev/null +++ b/mobile/lib/pages/library/people/people_collection.page.dart @@ -0,0 +1,104 @@ +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/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/search/people.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; + +@RoutePage() +class PeopleCollectionPage extends HookConsumerWidget { + const PeopleCollectionPage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final people = ref.watch(getAllPeopleProvider); + final headers = ApiService.getRequestHeaders(); + + showNameEditModel( + String personId, + String personName, + ) { + return showDialog( + context: context, + builder: (BuildContext context) { + return PersonNameEditForm(personId: personId, personName: personName); + }, + ); + } + + return Scaffold( + appBar: AppBar( + title: Text('people'.tr()), + ), + body: people.when( + data: (people) { + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 0.85, + ), + padding: const EdgeInsets.symmetric(vertical: 32), + itemCount: people.length, + itemBuilder: (context, index) { + final person = people[index]; + + return Column( + children: [ + GestureDetector( + onTap: () { + context.pushRoute( + PersonResultRoute( + personId: person.id, + personName: person.name, + ), + ); + }, + child: Material( + shape: const CircleBorder(side: BorderSide.none), + elevation: 3, + child: CircleAvatar( + maxRadius: 96 / 2, + backgroundImage: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: headers, + ), + ), + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () => showNameEditModel(person.id, person.name), + child: person.name.isEmpty + ? Text( + 'add_a_name'.tr(), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.primary, + ), + ) + : Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + person.name, + overflow: TextOverflow.ellipsis, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ); + }, + ); + }, + error: (error, stack) => const Text("error"), + loading: () => const CircularProgressIndicator(), + ), + ); + } +} diff --git a/mobile/lib/pages/library/places/places_collection.part.dart b/mobile/lib/pages/library/places/places_collection.part.dart new file mode 100644 index 0000000000000..e24a9a79ef236 --- /dev/null +++ b/mobile/lib/pages/library/places/places_collection.part.dart @@ -0,0 +1,125 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/search/search_filter.model.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +@RoutePage() +class PlacesCollectionPage extends HookConsumerWidget { + const PlacesCollectionPage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final places = ref.watch(getAllPlacesProvider); + + return Scaffold( + appBar: AppBar( + title: Text('places'.tr()), + ), + body: ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + height: 200, + width: context.width, + child: MapThumbnail( + onTap: (_, __) => context.pushRoute(const MapRoute()), + zoom: 8, + centre: const LatLng( + 21.44950, + -157.91959, + ), + showAttribution: false, + themeMode: + context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, + ), + ), + ), + places.when( + data: (places) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: places.length, + itemBuilder: (context, index) { + final place = places[index]; + + return PlaceTile(id: place.id, name: place.label); + }, + ); + }, + error: (error, stask) => const Text('Error getting places'), + loading: () => Center(child: const CircularProgressIndicator()), + ), + ], + ), + ); + } +} + +class PlaceTile extends StatelessWidget { + const PlaceTile({super.key, required this.id, required this.name}); + + final String id; + final String name; + + @override + Widget build(BuildContext context) { + final thumbnailUrl = + '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail'; + + void navigateToPlace() { + context.pushRoute( + SearchInputRoute( + prefilter: SearchFilter( + people: {}, + location: SearchLocationFilter( + city: name, + ), + camera: SearchCameraFilter(), + date: SearchDateFilter(), + display: SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + mediaType: AssetType.other, + ), + ), + ); + } + + return LargeLeadingTile( + onTap: () => navigateToPlace(), + title: Text( + name, + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + leading: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: CachedNetworkImage( + width: 80, + height: 80, + fit: BoxFit.cover, + imageUrl: thumbnailUrl, + httpHeaders: ApiService.getRequestHeaders(), + errorWidget: (context, url, error) => + const Icon(Icons.image_not_supported_outlined), + ), + ), + ); + } +} diff --git a/mobile/lib/pages/sharing/shared_link/shared_link.page.dart b/mobile/lib/pages/library/shared_link/shared_link.page.dart similarity index 100% rename from mobile/lib/pages/sharing/shared_link/shared_link.page.dart rename to mobile/lib/pages/library/shared_link/shared_link.page.dart diff --git a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart similarity index 100% rename from mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart rename to mobile/lib/pages/library/shared_link/shared_link_edit.page.dart diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart index 3c5ff272962a3..14e5724155da3 100644 --- a/mobile/lib/pages/photos/photos.page.dart +++ b/mobile/lib/pages/photos/photos.page.dart @@ -7,7 +7,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/widgets/memories/memory_lane.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; @@ -33,8 +32,7 @@ class PhotosPage extends HookConsumerWidget { () { ref.read(websocketProvider.notifier).connect(); Future(() => ref.read(assetProvider.notifier).getAllAsset()); - ref.read(albumProvider.notifier).getAllAlbums(); - ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); + Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums()); ref.read(serverInfoProvider.notifier).getServerInfo(); return; }, diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart index 55824b8db91f6..8627c65bcccef 100644 --- a/mobile/lib/pages/search/person_result.page.dart +++ b/mobile/lib/pages/search/person_result.page.dart @@ -92,6 +92,7 @@ class PersonResultPage extends HookConsumerWidget { Text( name.value, style: context.textTheme.titleLarge, + overflow: TextOverflow.ellipsis, ), ], ), @@ -125,9 +126,11 @@ class PersonResultPage extends HookConsumerWidget { headers: ApiService.getRequestHeaders(), ), ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: buildTitleBlock(), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: buildTitleBlock(), + ), ), ], ), diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 173115185bd5a..a8be87cc6a84e 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -1,25 +1,11 @@ -import 'dart:math' as math; - 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/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/widgets/search/curated_people_row.dart'; -import 'package:immich_mobile/widgets/search/curated_places_row.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; -import 'package:immich_mobile/widgets/search/search_row_section.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/scaffold_error_body.dart'; @RoutePage() // ignore: must_be_immutable @@ -28,12 +14,6 @@ class SearchPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final places = ref.watch(getPreviewPlacesProvider); - final curatedPeople = ref.watch(getAllPeopleProvider); - final isMapEnabled = - ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map)); - final double imageSize = math.min(context.width / 3, 150); - TextStyle categoryTitleStyle = const TextStyle( fontWeight: FontWeight.w500, fontSize: 15.0, @@ -41,87 +21,6 @@ class SearchPage extends HookConsumerWidget { Color categoryIconColor = context.colorScheme.onSurface; - showNameEditModel( - String personId, - String personName, - ) { - return showDialog( - context: context, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ); - } - - buildPeople() { - return curatedPeople.widgetWhen( - onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), - onData: (people) { - return SearchRowSection( - onViewAllPressed: () => context.pushRoute(const AllPeopleRoute()), - title: "search_page_people".tr(), - isEmpty: people.isEmpty, - child: CuratedPeopleRow( - padding: const EdgeInsets.symmetric(horizontal: 16), - content: people - .map((e) => SearchCuratedContent(label: e.name, id: e.id)) - .take(12) - .toList(), - onTap: (content, index) { - context.pushRoute( - PersonResultRoute( - personId: content.id, - personName: content.label, - ), - ); - }, - onNameTap: (person, index) => { - showNameEditModel(person.id, person.label), - }, - ), - ); - }, - ); - } - - buildPlaces() { - return places.widgetWhen( - onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), - onData: (data) { - return SearchRowSection( - onViewAllPressed: () => context.pushRoute(const AllPlacesRoute()), - title: "search_page_places".tr(), - isEmpty: !isMapEnabled && data.isEmpty, - child: CuratedPlacesRow( - isMapEnabled: isMapEnabled, - content: data, - imageSize: imageSize, - onTap: (content, index) { - context.pushRoute( - SearchInputRoute( - prefilter: SearchFilter( - people: {}, - location: SearchLocationFilter( - city: content.label, - ), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters( - isNotInAlbum: false, - isArchive: false, - isFavorite: false, - ), - mediaType: AssetType.other, - ), - ), - ); - }, - ), - ); - }, - ); - } - buildSearchButton() { return GestureDetector( onTap: () { @@ -165,20 +64,17 @@ class SearchPage extends HookConsumerWidget { body: ListView( children: [ buildSearchButton(), - const SizedBox(height: 8.0), - buildPeople(), - const SizedBox(height: 8.0), - buildPlaces(), const SizedBox(height: 24.0), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( - 'search_page_your_activity', + 'search_page_categories', style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, ), ).tr(), ), + const SizedBox(height: 12.0), ListTile( leading: Icon( Icons.favorite_border_rounded, @@ -200,16 +96,7 @@ class SearchPage extends HookConsumerWidget { ).tr(), onTap: () => context.pushRoute(const RecentlyAddedRoute()), ), - const SizedBox(height: 24.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - 'search_page_categories', - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ), + const CategoryDivider(), ListTile( title: Text('search_page_videos', style: categoryTitleStyle).tr(), leading: Icon( diff --git a/mobile/lib/pages/search/search_input.page.dart b/mobile/lib/pages/search/search_input.page.dart index 2ca2a379180dd..64e68ddfb4090 100644 --- a/mobile/lib/pages/search/search_input.page.dart +++ b/mobile/lib/pages/search/search_input.page.dart @@ -31,6 +31,7 @@ class SearchInputPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isContextualSearch = useState(true); final textSearchController = useTextEditingController(); + final focusNode = useFocusNode(); final filter = useState( SearchFilter( people: prefilter?.people ?? {}, @@ -440,6 +441,10 @@ class SearchInputPage extends HookConsumerWidget { } handleTextSubmitted(String value) { + if (value.isEmpty) { + return; + } + if (isContextualSearch.value) { filter.value = filter.value.copyWith( context: value, @@ -489,38 +494,82 @@ class SearchInputPage extends HookConsumerWidget { appBar: AppBar( automaticallyImplyLeading: true, actions: [ - IconButton( - icon: isContextualSearch.value - ? const Icon(Icons.abc_rounded) - : const Icon(Icons.image_search_rounded), - onPressed: () { - isContextualSearch.value = !isContextualSearch.value; - textSearchController.clear(); - }, + Padding( + padding: const EdgeInsets.only(right: 14.0), + child: IconButton( + icon: isContextualSearch.value + ? const Icon(Icons.abc_rounded) + : const Icon(Icons.image_search_rounded), + onPressed: () { + isContextualSearch.value = !isContextualSearch.value; + textSearchController.clear(); + }, + ), ), ], - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new_rounded), - onPressed: () => context.router.maybePop(), - ), - title: TextField( - controller: textSearchController, - decoration: InputDecoration( - hintText: isContextualSearch.value - ? 'contextual_search'.tr() - : 'filename_search'.tr(), - hintStyle: context.textTheme.bodyLarge?.copyWith( - color: context.themeData.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.w500, + title: Container( + decoration: BoxDecoration( + border: Border.all( + color: context.colorScheme.onSurface.withAlpha(0), + width: 0, ), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withOpacity(0.075), + context.colorScheme.primary.withOpacity(0.09), + context.colorScheme.primary.withOpacity(0.075), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), + ), + child: TextField( + controller: textSearchController, + decoration: InputDecoration( + contentPadding: EdgeInsets.all(8), + prefixIcon: prefilter != null + ? null + : Icon( + Icons.search_rounded, + color: context.colorScheme.primary, + ), + hintText: isContextualSearch.value + ? 'contextual_search'.tr() + : 'filename_search'.tr(), + hintStyle: context.textTheme.bodyLarge?.copyWith( + color: context.themeData.colorScheme.onSurfaceSecondary, + fontWeight: FontWeight.w500, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.surfaceDim, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.surfaceContainer, + ), + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.surfaceDim, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide( + color: context.colorScheme.primary.withAlpha(100), + ), + ), ), + onSubmitted: handleTextSubmitted, + focusNode: focusNode, + onTapOutside: (_) => focusNode.unfocus(), ), - onSubmitted: handleTextSubmitted, ), ), body: Column( diff --git a/mobile/lib/pages/sharing/sharing.page.dart b/mobile/lib/pages/sharing/sharing.page.dart deleted file mode 100644 index 98d4cfafe9fe5..0000000000000 --- a/mobile/lib/pages/sharing/sharing.page.dart +++ /dev/null @@ -1,283 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; -import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/providers/partner.provider.dart'; -import 'package:immich_mobile/widgets/partner/partner_list.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; -import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; - -@RoutePage() -class SharingPage extends HookConsumerWidget { - const SharingPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final albumSortOption = ref.watch(albumSortByOptionsProvider); - final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final albums = ref.watch(sharedAlbumProvider); - final sharedAlbums = albumSortOption.sortFn(albums, albumSortIsReverse); - final userId = ref.watch(currentUserProvider)?.id; - final partner = ref.watch(partnerSharedWithProvider); - - useEffect( - () { - ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); - return null; - }, - [], - ); - - buildAlbumGrid() { - return SliverPadding( - padding: const EdgeInsets.all(18.0), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - return AlbumThumbnailCard( - album: sharedAlbums[index], - showOwner: true, - onTap: () => context.pushRoute( - AlbumViewerRoute(albumId: sharedAlbums[index].id), - ), - ); - }, - childCount: sharedAlbums.length, - ), - ), - ); - } - - buildAlbumList() { - return SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - final album = sharedAlbums[index]; - final isOwner = album.ownerId == userId; - - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 12), - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: ImmichThumbnail( - asset: album.thumbnail.value, - width: 60, - height: 60, - ), - ), - title: Text( - album.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - subtitle: isOwner - ? Text( - 'album_thumbnail_owned'.tr(), - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceSecondary, - ), - ) - : album.ownerName != null - ? Text( - 'album_thumbnail_shared_by' - .tr(args: [album.ownerName!]), - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceSecondary, - ), - ) - : null, - onTap: () => context - .pushRoute(AlbumViewerRoute(albumId: sharedAlbums[index].id)), - ); - }, - childCount: sharedAlbums.length, - ), - ); - } - - buildTopBottons() { - return Padding( - padding: const EdgeInsets.only( - left: 12.0, - right: 12.0, - top: 24.0, - bottom: 12.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: () => - context.pushRoute(CreateAlbumRoute(isSharedAlbum: true)), - icon: const Icon( - Icons.photo_album_outlined, - size: 20, - ), - label: const Text( - "sharing_silver_appbar_create_shared_album", - maxLines: 1, - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 12, - ), - ).tr(), - ), - ), - const SizedBox(width: 12.0), - Expanded( - child: ElevatedButton.icon( - onPressed: () => context.pushRoute(const SharedLinkRoute()), - icon: const Icon( - Icons.link, - size: 20, - ), - label: const Text( - "sharing_silver_appbar_shared_links", - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 12, - ), - maxLines: 1, - ).tr(), - ), - ), - ], - ), - ); - } - - buildEmptyListIndication() { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(20)), - side: BorderSide( - color: context.isDarkTheme - ? const Color(0xFF383838) - : Colors.black12, - width: 1, - ), - ), - child: Padding( - padding: const EdgeInsets.all(18.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 5.0, bottom: 5), - child: Icon( - Icons.insert_photo_rounded, - size: 50, - color: context.primaryColor, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'sharing_page_empty_list', - style: context.textTheme.displaySmall, - ).tr(), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'sharing_page_description', - style: context.textTheme.bodyMedium, - ).tr(), - ), - ], - ), - ), - ), - ), - ); - } - - Widget sharePartnerButton() { - return InkWell( - onTap: () => context.pushRoute(const PartnerRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Icon( - Icons.swap_horizontal_circle_rounded, - size: 25, - semanticLabel: 'partner_page_title'.tr(), - ), - ); - } - - return RefreshIndicator( - onRefresh: () async { - ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); - }, - child: Scaffold( - appBar: ImmichAppBar( - action: sharePartnerButton(), - ), - body: CustomScrollView( - slivers: [ - SliverToBoxAdapter(child: buildTopBottons()), - if (partner.isNotEmpty) - SliverPadding( - padding: const EdgeInsets.all(12), - sliver: SliverToBoxAdapter( - child: Text( - "partner_page_title", - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ), - ), - if (partner.isNotEmpty) PartnerList(partner: partner), - SliverPadding( - padding: const EdgeInsets.all(12), - sliver: SliverToBoxAdapter( - child: Text( - "sharing_page_album", - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ), - ), - SliverLayoutBuilder( - builder: (context, constraints) { - if (sharedAlbums.isEmpty) { - return buildEmptyListIndication(); - } - - if (constraints.crossAxisExtent < 600) { - return buildAlbumList(); - } else { - return buildAlbumGrid(); - } - }, - ), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index ed9dc07f5e5c0..943671f1885ad 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -1,21 +1,21 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; +final isRefreshingRemoteAlbumProvider = StateProvider((ref) => false); + class AlbumNotifier extends StateNotifier> { - AlbumNotifier(this._albumService, Isar db) : super([]) { - final query = db.albums - .filter() - .owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)); + AlbumNotifier(this._albumService, this.db, this.ref) : super([]) { + final query = db.albums.filter().remoteIdIsNotNull(); query.findAll().then((value) { if (mounted) { state = value; @@ -25,14 +25,22 @@ class AlbumNotifier extends StateNotifier> { } final AlbumService _albumService; + final Isar db; + final Ref ref; late final StreamSubscription> _streamSub; - Future getAllAlbums() => Future.wait([ - _albumService.refreshDeviceAlbums(), - _albumService.refreshRemoteAlbums(isShared: false), - ]); + Future refreshRemoteAlbums() async { + final isRefresing = + ref.read(isRefreshingRemoteAlbumProvider.notifier).state; + + if (isRefresing) return; + + ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true; + await _albumService.refreshRemoteAlbums(); + ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false; + } - Future getDeviceAlbums() => _albumService.refreshDeviceAlbums(); + Future refreshDeviceAlbums() => _albumService.refreshDeviceAlbums(); Future deleteAlbum(Album album) => _albumService.deleteAlbum(album); @@ -59,6 +67,50 @@ class AlbumNotifier extends StateNotifier> { await createAlbum(albumName, {}); } + Future leaveAlbum(Album album) async { + var res = await _albumService.leaveAlbum(album); + + if (res) { + await deleteAlbum(album); + return true; + } else { + return false; + } + } + + void searchAlbums(String searchTerm, QuickFilterMode filterMode) async { + state = await _albumService.search(searchTerm, filterMode); + } + + Future addUsers(Album album, List userIds) async { + await _albumService.addUsers(album, userIds); + } + + Future removeUser(Album album, User user) async { + final isRemoved = await _albumService.removeUser(album, user); + + if (isRemoved && album.sharedUsers.isEmpty) { + state = state.where((element) => element.id != album.id).toList(); + } + + return isRemoved; + } + + Future addAssets(Album album, Iterable assets) async { + await _albumService.addAssets(album, assets); + } + + Future removeAsset(Album album, Iterable assets) async { + return await _albumService.removeAsset(album, assets); + } + + Future setActivitystatus( + Album album, + bool enabled, + ) { + return _albumService.setActivityStatus(album, enabled); + } + @override void dispose() { _streamSub.cancel(); @@ -71,6 +123,7 @@ final albumProvider = return AlbumNotifier( ref.watch(albumServiceProvider), ref.watch(dbProvider), + ref, ); }); @@ -94,3 +147,31 @@ final albumRenderlistProvider = } return const Stream.empty(); }); + +class LocalAlbumsNotifier extends StateNotifier> { + LocalAlbumsNotifier(this.db) : super([]) { + final query = db.albums.where().remoteIdIsNull(); + + query.findAll().then((value) { + if (mounted) { + state = value; + } + }); + + _streamSub = query.watch().listen((data) => state = data); + } + + final Isar db; + late final StreamSubscription> _streamSub; + + @override + void dispose() { + _streamSub.cancel(); + super.dispose(); + } +} + +final localAlbumsProvider = + StateNotifierProvider.autoDispose>((ref) { + return LocalAlbumsNotifier(ref.watch(dbProvider)); +}); diff --git a/mobile/lib/providers/album/album_viewer.provider.dart b/mobile/lib/providers/album/album_viewer.provider.dart index f34ff4ef2257e..e41865778214a 100644 --- a/mobile/lib/providers/album/album_viewer.provider.dart +++ b/mobile/lib/providers/album/album_viewer.provider.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -40,7 +39,6 @@ class AlbumViewerNotifier extends StateNotifier { if (isSuccess) { state = state.copyWith(editTitleText: "", isEditAlbum: false); - ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); return true; } diff --git a/mobile/lib/providers/album/shared_album.provider.dart b/mobile/lib/providers/album/shared_album.provider.dart deleted file mode 100644 index 0d581353757b8..0000000000000 --- a/mobile/lib/providers/album/shared_album.provider.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:isar/isar.dart'; - -class SharedAlbumNotifier extends StateNotifier> { - SharedAlbumNotifier(this._albumService, Isar db) : super([]) { - final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc(); - query.findAll().then((value) { - if (mounted) { - state = value; - } - }); - _streamSub = query.watch().listen((data) => state = data); - } - - final AlbumService _albumService; - late final StreamSubscription> _streamSub; - - Future createSharedAlbum( - String albumName, - Iterable assets, - Iterable sharedUsers, - ) async { - try { - return await _albumService.createAlbum( - albumName, - assets, - sharedUsers, - ); - } catch (e) { - debugPrint("Error createSharedAlbum ${e.toString()}"); - } - return null; - } - - Future getAllSharedAlbums() => - _albumService.refreshRemoteAlbums(isShared: true); - - Future deleteAlbum(Album album) => _albumService.deleteAlbum(album); - - Future leaveAlbum(Album album) async { - var res = await _albumService.leaveAlbum(album); - - if (res) { - await deleteAlbum(album); - return true; - } else { - return false; - } - } - - Future removeAssetFromAlbum(Album album, Iterable assets) { - return _albumService.removeAssetFromAlbum(album, assets); - } - - Future removeUserFromAlbum(Album album, User user) async { - final result = await _albumService.removeUserFromAlbum(album, user); - - if (result && album.sharedUsers.isEmpty) { - state = state.where((element) => element.id != album.id).toList(); - } - - return result; - } - - Future setActivityEnabled(Album album, bool activityEnabled) { - return _albumService.setActivityEnabled(album, activityEnabled); - } - - @override - void dispose() { - _streamSub.cancel(); - super.dispose(); - } -} - -final sharedAlbumProvider = - StateNotifierProvider.autoDispose>((ref) { - return SharedAlbumNotifier( - ref.watch(albumServiceProvider), - ref.watch(dbProvider), - ); -}); diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 5561d3fefd683..c06a99da35b62 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -1,6 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -58,11 +57,10 @@ class AppLifeCycleNotifier extends StateNotifier { _ref.read(assetProvider.notifier).getAllAsset(); case TabEnum.search: // nothing to do - case TabEnum.sharing: - _ref.read(assetProvider.notifier).getAllAsset(); - _ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); + case TabEnum.albums: + _ref.read(albumProvider.notifier).refreshRemoteAlbums(); case TabEnum.library: - _ref.read(albumProvider.notifier).getAllAlbums(); + // nothing to do } } diff --git a/mobile/lib/providers/authentication.provider.dart b/mobile/lib/providers/authentication.provider.dart index b56e71b11b3f6..1fe7db5d46f42 100644 --- a/mobile/lib/providers/authentication.provider.dart +++ b/mobile/lib/providers/authentication.provider.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/authentication/authentication_state.model.dart'; import 'package:immich_mobile/entities/user.entity.dart'; @@ -115,7 +114,6 @@ class AuthenticationNotifier extends StateNotifier { Store.delete(StoreKey.accessToken), ]); _ref.invalidate(albumProvider); - _ref.invalidate(sharedAlbumProvider); state = state.copyWith( deviceId: "", diff --git a/mobile/lib/providers/backup/backup_verification.provider.g.dart b/mobile/lib/providers/backup/backup_verification.provider.g.dart index f222c9bd83e12..e286f434219b5 100644 --- a/mobile/lib/providers/backup/backup_verification.provider.g.dart +++ b/mobile/lib/providers/backup/backup_verification.provider.g.dart @@ -7,7 +7,7 @@ part of 'backup_verification.provider.dart'; // ************************************************************************** String _$backupVerificationHash() => - r'b691e0cc27856eef189258d3c102cc73ce4812a4'; + r'021dfdf65e1903c932e4a1c14967b786dd3516fb'; /// See also [BackupVerification]. @ProviderFor(BackupVerification) diff --git a/mobile/lib/providers/tab.provider.dart b/mobile/lib/providers/tab.provider.dart index 2abed7c395e50..a4875115ce2a0 100644 --- a/mobile/lib/providers/tab.provider.dart +++ b/mobile/lib/providers/tab.provider.dart @@ -1,11 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -enum TabEnum { - home, - search, - sharing, - library, -} +enum TabEnum { home, search, albums, library } /// Provides the currently active tab final tabProvider = StateProvider( diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart index 35f5cae32722c..2c78e4c2389f1 100644 --- a/mobile/lib/repositories/album.repository.dart +++ b/mobile/lib/repositories/album.repository.dart @@ -1,8 +1,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/album.interface.dart'; +import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; import 'package:isar/isar.dart'; @@ -118,4 +120,33 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { @override Future deleteAllLocal() => txn(() => db.albums.where().localIdIsNotNull().deleteAll()); + + @override + Future> search( + String searchTerm, + QuickFilterMode filterMode, + ) async { + var query = db.albums + .filter() + .nameContains(searchTerm, caseSensitive: false) + .remoteIdIsNotNull(); + + switch (filterMode) { + case QuickFilterMode.sharedWithMe: + query = query.owner( + (q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + ); + break; + case QuickFilterMode.myAlbums: + query = query.owner( + (q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + ); + break; + case QuickFilterMode.all: + default: + break; + } + + return await query.findAll(); + } } diff --git a/mobile/lib/repositories/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart index 0b3d164ca3523..1ae16d9d52993 100644 --- a/mobile/lib/repositories/partner_api.repository.dart +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -36,7 +36,7 @@ class PartnerApiRepository extends ApiRepository } @override - Future delete(String id) => checkNull(_api.removePartner(id)); + Future delete(String id) => _api.removePartner(id); @override Future update(String id, {required bool inTimeline}) async { diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 1f26e0d6de1ed..c9970b02c55b1 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -13,6 +13,11 @@ import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; +import 'package:immich_mobile/pages/albums/albums.page.dart'; +import 'package:immich_mobile/pages/library/local_albums.page.dart'; +import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; +import 'package:immich_mobile/pages/library/places/places_collection.part.dart'; +import 'package:immich_mobile/pages/library/library.page.dart'; import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart'; import 'package:immich_mobile/pages/common/album_asset_selection.page.dart'; @@ -32,7 +37,6 @@ import 'package:immich_mobile/pages/editing/crop.page.dart'; import 'package:immich_mobile/pages/editing/filter.page.dart'; import 'package:immich_mobile/pages/library/archive.page.dart'; import 'package:immich_mobile/pages/library/favorite.page.dart'; -import 'package:immich_mobile/pages/library/library.page.dart'; import 'package:immich_mobile/pages/library/trash.page.dart'; import 'package:immich_mobile/pages/login/change_password.page.dart'; import 'package:immich_mobile/pages/login/login.page.dart'; @@ -49,11 +53,10 @@ import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/recently_added.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/search/search_input.page.dart'; -import 'package:immich_mobile/pages/sharing/partner/partner.page.dart'; -import 'package:immich_mobile/pages/sharing/partner/partner_detail.page.dart'; -import 'package:immich_mobile/pages/sharing/shared_link/shared_link.page.dart'; -import 'package:immich_mobile/pages/sharing/shared_link/shared_link_edit.page.dart'; -import 'package:immich_mobile/pages/sharing/sharing.page.dart'; +import 'package:immich_mobile/pages/library/partner/partner.page.dart'; +import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; +import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart'; +import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; @@ -103,15 +106,16 @@ class AppRouter extends RootStackRouter { guards: [_authGuard, _duplicateGuard], ), AutoRoute( - page: SearchRoute.page, + page: SearchInputRoute.page, guards: [_authGuard, _duplicateGuard], + maintainState: false, ), AutoRoute( - page: SharingRoute.page, + page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard], ), AutoRoute( - page: LibraryRoute.page, + page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard], ), ], @@ -137,7 +141,11 @@ class AppRouter extends RootStackRouter { AutoRoute(page: EditImageRoute.page), AutoRoute(page: CropImageRoute.page), AutoRoute(page: FilterImageRoute.page), - AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]), + CustomRoute( + page: FavoritesRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute( page: AllMotionPhotosRoute.page, @@ -183,8 +191,16 @@ class AppRouter extends RootStackRouter { AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]), - AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]), + CustomRoute( + page: ArchiveRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), + CustomRoute( + page: PartnerRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), AutoRoute( page: PartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard], @@ -200,10 +216,15 @@ class AppRouter extends RootStackRouter { page: AlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard], ), - AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute( + CustomRoute( + page: TrashRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), + CustomRoute( page: SharedLinkRoute.page, guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, ), AutoRoute( page: SharedLinkEditRoute.page, @@ -232,6 +253,26 @@ class AppRouter extends RootStackRouter { page: HeaderSettingsRoute.page, guards: [_duplicateGuard], ), + CustomRoute( + page: PeopleCollectionRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), + CustomRoute( + page: AlbumsRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), + CustomRoute( + page: LocalAlbumsRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), + CustomRoute( + page: PlacesCollectionRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 4b27ab155fc31..f230fc3f38013 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -319,6 +319,25 @@ class AlbumViewerRouteArgs { } } +/// generated route for +/// [AlbumsPage] +class AlbumsRoute extends PageRouteInfo { + const AlbumsRoute({List? children}) + : super( + AlbumsRoute.name, + initialChildren: children, + ); + + static const String name = 'AlbumsRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AlbumsPage(); + }, + ); +} + /// generated route for /// [AllMotionPhotosPage] class AllMotionPhotosRoute extends PageRouteInfo { @@ -560,15 +579,13 @@ class ChangePasswordRoute extends PageRouteInfo { class CreateAlbumRoute extends PageRouteInfo { CreateAlbumRoute({ Key? key, - required bool isSharedAlbum, - List? initialAssets, + List? assets, List? children, }) : super( CreateAlbumRoute.name, args: CreateAlbumRouteArgs( key: key, - isSharedAlbum: isSharedAlbum, - initialAssets: initialAssets, + assets: assets, ), initialChildren: children, ); @@ -578,11 +595,11 @@ class CreateAlbumRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = data.argsAs(); + final args = data.argsAs( + orElse: () => const CreateAlbumRouteArgs()); return CreateAlbumPage( key: args.key, - isSharedAlbum: args.isSharedAlbum, - initialAssets: args.initialAssets, + assets: args.assets, ); }, ); @@ -591,19 +608,16 @@ class CreateAlbumRoute extends PageRouteInfo { class CreateAlbumRouteArgs { const CreateAlbumRouteArgs({ this.key, - required this.isSharedAlbum, - this.initialAssets, + this.assets, }); final Key? key; - final bool isSharedAlbum; - - final List? initialAssets; + final List? assets; @override String toString() { - return 'CreateAlbumRouteArgs{key: $key, isSharedAlbum: $isSharedAlbum, initialAssets: $initialAssets}'; + return 'CreateAlbumRouteArgs{key: $key, assets: $assets}'; } } @@ -909,6 +923,25 @@ class LibraryRoute extends PageRouteInfo { ); } +/// generated route for +/// [LocalAlbumsPage] +class LocalAlbumsRoute extends PageRouteInfo { + const LocalAlbumsRoute({List? children}) + : super( + LocalAlbumsRoute.name, + initialChildren: children, + ); + + static const String name = 'LocalAlbumsRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LocalAlbumsPage(); + }, + ); +} + /// generated route for /// [LoginPage] class LoginRoute extends PageRouteInfo { @@ -1111,6 +1144,25 @@ class PartnerRoute extends PageRouteInfo { ); } +/// generated route for +/// [PeopleCollectionPage] +class PeopleCollectionRoute extends PageRouteInfo { + const PeopleCollectionRoute({List? children}) + : super( + PeopleCollectionRoute.name, + initialChildren: children, + ); + + static const String name = 'PeopleCollectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PeopleCollectionPage(); + }, + ); +} + /// generated route for /// [PermissionOnboardingPage] class PermissionOnboardingRoute extends PageRouteInfo { @@ -1201,6 +1253,25 @@ class PhotosRoute extends PageRouteInfo { ); } +/// generated route for +/// [PlacesCollectionPage] +class PlacesCollectionRoute extends PageRouteInfo { + const PlacesCollectionRoute({List? children}) + : super( + PlacesCollectionRoute.name, + initialChildren: children, + ); + + static const String name = 'PlacesCollectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PlacesCollectionPage(); + }, + ); +} + /// generated route for /// [RecentlyAddedPage] class RecentlyAddedRoute extends PageRouteInfo { @@ -1429,25 +1500,6 @@ class SharedLinkRoute extends PageRouteInfo { ); } -/// generated route for -/// [SharingPage] -class SharingRoute extends PageRouteInfo { - const SharingRoute({List? children}) - : super( - SharingRoute.name, - initialChildren: children, - ); - - static const String name = 'SharingRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const SharingPage(); - }, - ); -} - /// generated route for /// [SplashScreenPage] class SplashScreenRoute extends PageRouteInfo { diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index e16fecb32392a..35a2942973054 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -1,12 +1,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -21,14 +19,6 @@ class TabNavigationObserver extends AutoRouterObserver { required this.ref, }); - @override - void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) { - // Perform tasks on first navigation to SearchRoute - if (route.name == 'SearchRoute') { - // ref.refresh(getCuratedLocationProvider); - } - } - @override Future didChangeTabRoute( TabPageRoute route, @@ -41,15 +31,6 @@ class TabNavigationObserver extends AutoRouterObserver { ref.invalidate(getAllPeopleProvider); } - if (route.name == 'SharingRoute') { - ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); - Future(() => ref.read(assetProvider.notifier).getAllAsset()); - } - - if (route.name == 'LibraryRoute') { - ref.read(albumProvider.notifier).getAllAlbums(); - } - if (route.name == 'HomeRoute') { ref.invalidate(memoryFutureProvider); Future(() => ref.read(assetProvider.notifier).getAllAsset()); diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 091049edb59f1..53a65e2869aea 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/repositories/album.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; @@ -152,7 +153,7 @@ class AlbumService { /// Checks remote albums (owned if `isShared` is false) for changes, /// updates the local database and returns `true` if there were any changes - Future refreshRemoteAlbums({required bool isShared}) async { + Future refreshRemoteAlbums() async { if (!_remoteCompleter.isCompleted) { // guard against concurrent calls return _remoteCompleter.future; @@ -162,12 +163,21 @@ class AlbumService { bool changes = false; try { await _userService.refreshUsers(); - final List serverAlbums = - await _albumApiRepository.getAll(shared: isShared ? true : null); - changes = await _syncService.syncRemoteAlbumsToDb( - serverAlbums, - isShared: isShared, + final List sharedAlbum = + await _albumApiRepository.getAll(shared: true); + + final List ownedAlbum = + await _albumApiRepository.getAll(shared: null); + + final albums = HashSet( + equals: (a, b) => a.remoteId == b.remoteId, + hashCode: (a) => a.remoteId.hashCode, ); + + albums.addAll(sharedAlbum); + albums.addAll(ownedAlbum); + + changes = await _syncService.syncRemoteAlbumsToDb(albums.toList()); } finally { _remoteCompleter.complete(changes); } @@ -213,9 +223,9 @@ class AlbumService { ); } - Future addAdditionalAssetToAlbum( - Iterable assets, + Future addAssets( Album album, + Iterable assets, ) async { try { final result = await _albumApiRepository.addAssets( @@ -234,7 +244,7 @@ class AlbumService { successfullyAdded: addedAssets.length, ); } catch (e) { - debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}"); + debugPrint("Error addAssets ${e.toString()}"); } return null; } @@ -253,30 +263,14 @@ class AlbumService { await _albumRepository.update(album); }); - Future addAdditionalUserToAlbum( - List sharedUserIds, - Album album, - ) async { - try { - final updatedAlbum = - await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds); - await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum); - await _albumRepository.update(updatedAlbum); - return true; - } catch (e) { - debugPrint("Error addAdditionalUserToAlbum ${e.toString()}"); - } - return false; - } - - Future setActivityEnabled(Album album, bool enabled) async { + Future setActivityStatus(Album album, bool enabled) async { try { final updatedAlbum = await _albumApiRepository.update( album.remoteId!, activityEnabled: enabled, ); - await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum); - await _albumRepository.update(updatedAlbum); + album.activityEnabled = updatedAlbum.activityEnabled; + await _albumRepository.update(album); return true; } catch (e) { debugPrint("Error setActivityEnabled ${e.toString()}"); @@ -327,7 +321,7 @@ class AlbumService { } } - Future removeAssetFromAlbum( + Future removeAsset( Album album, Iterable assets, ) async { @@ -346,7 +340,7 @@ class AlbumService { return false; } - Future removeUserFromAlbum( + Future removeUser( Album album, User user, ) async { @@ -363,22 +357,44 @@ class AlbumService { await _albumRepository.update(a!); return true; - } catch (e) { - debugPrint("Error removeUserFromAlbum ${e.toString()}"); + } catch (error) { + debugPrint("Error removeUser ${error.toString()}"); return false; } } + Future addUsers( + Album album, + List userIds, + ) async { + try { + final updatedAlbum = + await _albumApiRepository.addUsers(album.remoteId!, userIds); + + album.sharedUsers.addAll(updatedAlbum.remoteUsers); + album.shared = true; + + await _albumRepository.addUsers(album, album.sharedUsers.toList()); + await _albumRepository.update(album); + + return true; + } catch (error) { + debugPrint("Error addUsers ${error.toString()}"); + } + return false; + } + Future changeTitleAlbum( Album album, String newAlbumTitle, ) async { try { - album = await _albumApiRepository.update( + final updatedAlbum = await _albumApiRepository.update( album.remoteId!, name: newAlbumTitle, ); - await _entityService.fillAlbumWithDatabaseEntities(album); + + album.name = updatedAlbum.name; await _albumRepository.update(album); return true; } catch (e) { @@ -405,4 +421,15 @@ class AlbumService { } } } + + Future> getAll() async { + return _albumRepository.getAll(remote: true); + } + + Future> search( + String searchTerm, + QuickFilterMode filterMode, + ) async { + return _albumRepository.search(searchTerm, filterMode); + } } diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart index 8297620bc70e3..ddbe77f8c9493 100644 --- a/mobile/lib/services/entity.service.dart +++ b/mobile/lib/services/entity.service.dart @@ -32,6 +32,7 @@ class EntityService { .getByIds(album.remoteUsers.map((user) => user.id).toList()); album.sharedUsers.clear(); album.sharedUsers.addAll(users); + album.shared = true; } if (album.remoteAssets.isNotEmpty) { // replace all assets with assets from database diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index e7a192e7834ea..d691b006ad0d7 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -95,10 +95,9 @@ class SyncService { /// Syncs remote albums to the database /// returns `true` if there were any changes Future syncRemoteAlbumsToDb( - List remote, { - required bool isShared, - }) => - _lock.run(() => _syncRemoteAlbumsToDb(remote, isShared)); + List remote, + ) => + _lock.run(() => _syncRemoteAlbumsToDb(remote)); /// Syncs all device albums and their assets to the database /// Returns `true` if there were any changes @@ -310,17 +309,14 @@ class SyncService { /// returns `true` if there were any changes Future _syncRemoteAlbumsToDb( List remoteAlbums, - bool isShared, ) async { remoteAlbums.sortBy((e) => e.remoteId!); - final User me = await _userRepository.me(); final List dbAlbums = await _albumRepository.getAll( remote: true, - shared: isShared ? true : null, - ownerId: isShared ? null : me.isarId, sortBy: AlbumSort.remoteId, ); + final List toDelete = []; final List existing = []; @@ -335,7 +331,7 @@ class SyncService { onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete), ); - if (isShared && toDelete.isNotEmpty) { + if (toDelete.isNotEmpty) { final List idsToRemove = sharedAssetsToRemove(toDelete, existing); if (idsToRemove.isNotEmpty) { await _assetRepository.deleteById(idsToRemove); diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index 0aac5b476efda..c0cf60514f04d 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -190,17 +190,14 @@ ThemeData getThemeData({required ColorScheme colorScheme}) { displayLarge: TextStyle( fontSize: 26, fontWeight: FontWeight.bold, - color: isDark ? Colors.white : primaryColor, ), displayMedium: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: isDark ? Colors.white : Colors.black87, ), displaySmall: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, - color: primaryColor, ), titleSmall: const TextStyle( fontSize: 16.0, @@ -241,7 +238,7 @@ ThemeData getThemeData({required ColorScheme colorScheme}) { isDark ? colorScheme.surfaceContainer : colorScheme.surface, labelTextStyle: const WidgetStatePropertyAll( TextStyle( - fontSize: 13, + fontSize: 14, fontWeight: FontWeight.w500, ), ), diff --git a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart b/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart index 46fa0b1fe8ac1..6856ae184d038 100644 --- a/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart +++ b/mobile/lib/widgets/album/add_to_album_bottom_sheet.dart @@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -27,13 +26,11 @@ class AddToAlbumBottomSheet extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); final albumService = ref.watch(albumServiceProvider); - final sharedAlbums = ref.watch(sharedAlbumProvider); useEffect( () { // Fetch album updates, e.g., cover image - ref.read(albumProvider.notifier).getAllAlbums(); - ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); + ref.read(albumProvider.notifier).refreshRemoteAlbums(); return null; }, @@ -41,9 +38,9 @@ class AddToAlbumBottomSheet extends HookConsumerWidget { ); void addToAlbum(Album album) async { - final result = await albumService.addAdditionalAssetToAlbum( - assets, + final result = await albumService.addAssets( album, + assets, ); if (result != null) { @@ -107,8 +104,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget { onPressed: () { context.pushRoute( CreateAlbumRoute( - isSharedAlbum: false, - initialAssets: assets, + assets: assets, ), ); }, @@ -123,7 +119,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 16), sliver: AddToAlbumSliverList( albums: albums, - sharedAlbums: sharedAlbums, + sharedAlbums: albums.where((a) => a.shared).toList(), onAddToAlbum: addToAlbum, ), ), diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 42fa55cdd4459..b728f2b5415fe 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -12,12 +12,14 @@ class AlbumThumbnailCard extends StatelessWidget { /// Whether or not to show the owner of the album (or "Owned") /// in the subtitle of the album final bool showOwner; + final bool showTitle; const AlbumThumbnailCard({ super.key, required this.album, this.onTap, this.showOwner = false, + this.showTitle = true, }); final Album album; @@ -76,7 +78,7 @@ class AlbumThumbnailCard extends StatelessWidget { : 'album_thumbnail_card_items' .tr(args: ['${album.assetCount}']), ), - if (owner != null) const TextSpan(text: ' · '), + if (owner != null) const TextSpan(text: ' • '), if (owner != null) TextSpan(text: owner), ], ), @@ -102,21 +104,23 @@ class AlbumThumbnailCard extends StatelessWidget { : buildAlbumThumbnail(), ), ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: cardSize, - child: Text( - album.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, + if (showTitle) ...[ + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: SizedBox( + width: cardSize, + child: Text( + album.name, + overflow: TextOverflow.ellipsis, + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), ), ), ), - ), - buildAlbumTextRow(), + buildAlbumTextRow(), + ], ], ), ), diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart index 1067d7241e3e4..89528cc4da365 100644 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ b/mobile/lib/widgets/album/album_viewer_appbar.dart @@ -7,7 +7,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/activity_statistics.provider.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -46,10 +45,8 @@ class AlbumViewerAppbar extends HookConsumerWidget final bool success; if (album.shared) { - success = - await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album); - context - .navigateTo(const TabControllerRoute(children: [SharingRoute()])); + success = await ref.watch(albumProvider.notifier).deleteAlbum(album); + context.navigateTo(TabControllerRoute(children: [AlbumsRoute()])); } else { success = await ref.watch(albumProvider.notifier).deleteAlbum(album); context @@ -113,11 +110,10 @@ class AlbumViewerAppbar extends HookConsumerWidget isProcessing.value = true; bool isSuccess = - await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album); + await ref.watch(albumProvider.notifier).leaveAlbum(album); if (isSuccess) { - context - .navigateTo(const TabControllerRoute(children: [SharingRoute()])); + context.navigateTo(TabControllerRoute(children: [AlbumsRoute()])); } else { context.pop(); ImmichToast.show( diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart index e6d769a3d7aa2..ec054d08ee131 100644 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart @@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; import 'package:immich_mobile/models/asset_selection_state.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; @@ -72,7 +71,8 @@ class ControlBottomAppBar extends HookConsumerWidget { final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList(); - final sharedAlbums = ref.watch(sharedAlbumProvider); + final sharedAlbums = + ref.watch(albumProvider).where((a) => a.shared).toList(); const bottomPadding = 0.20; final scrollController = useDraggableScrollController(); diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index 14678903ba298..eeecfa9b58435 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -9,7 +9,6 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/stack.service.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; @@ -272,11 +271,10 @@ class MultiselectGrid extends HookConsumerWidget { if (assets.isEmpty) { return; } - final result = - await ref.read(albumServiceProvider).addAdditionalAssetToAlbum( - assets, - album, - ); + final result = await ref.read(albumServiceProvider).addAssets( + album, + assets, + ); if (result != null) { if (result.alreadyInAlbum.isNotEmpty) { @@ -323,8 +321,7 @@ class MultiselectGrid extends HookConsumerWidget { .createAlbumWithGeneratedName(assets); if (result != null) { - ref.watch(albumProvider.notifier).getAllAlbums(); - ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); + ref.watch(albumProvider.notifier).refreshRemoteAlbums(); selectionEnabledHook.value = false; context.pushRoute(AlbumViewerRoute(albumId: result.id)); diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index c3f1390dba04a..f550857b9d867 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -6,8 +6,8 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; @@ -230,9 +230,7 @@ class BottomGalleryBar extends ConsumerWidget { handleRemoveFromAlbum() async { final album = ref.read(currentAlbumProvider); final bool isSuccess = album != null && - await ref - .read(sharedAlbumProvider.notifier) - .removeAssetFromAlbum(album, [asset]); + await ref.read(albumProvider.notifier).removeAsset(album, [asset]); if (isSuccess) { // Workaround for asset remaining in the gallery diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 8e2465fc9ca3d..1831a2d1689ab 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -18,9 +18,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart'; class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); - final Widget? action; + final List? actions; + final bool showUploadButton; - const ImmichAppBar({super.key, this.action}); + const ImmichAppBar({super.key, this.actions, this.showUploadButton = true}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -184,12 +185,18 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { }, ), actions: [ - if (action != null) - Padding(padding: const EdgeInsets.only(right: 20), child: action!), - Padding( - padding: const EdgeInsets.only(right: 20), - child: buildBackupIndicator(), - ), + if (actions != null) + ...actions!.map( + (action) => Padding( + padding: const EdgeInsets.only(right: 16), + child: action, + ), + ), + if (showUploadButton) + Padding( + padding: const EdgeInsets.only(right: 20), + child: buildBackupIndicator(), + ), Padding( padding: const EdgeInsets.only(right: 20), child: buildProfileIndicator(), diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 01b717ef5b977..46e86718583df 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget { populateTestLoginInfo1() { usernameController.text = 'testuser@email.com'; passwordController.text = 'password'; - serverEndpointController.text = 'http://192.168.1.16:2283/api'; + serverEndpointController.text = 'http://192.168.1.118:2283/api'; } login() async { diff --git a/mobile/lib/widgets/partner/partner_list.dart b/mobile/lib/widgets/partner/partner_list.dart deleted file mode 100644 index 53a27c48abad7..0000000000000 --- a/mobile/lib/widgets/partner/partner_list.dart +++ /dev/null @@ -1,48 +0,0 @@ -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/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; - -class PartnerList extends HookConsumerWidget { - const PartnerList({super.key, required this.partner}); - - final List partner; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SliverList( - delegate: - SliverChildBuilderDelegate(listEntry, childCount: partner.length), - ); - } - - Widget listEntry(BuildContext context, int index) { - final User p = partner[index]; - return ListTile( - contentPadding: const EdgeInsets.only( - left: 12.0, - right: 18.0, - ), - leading: userAvatar(context, p, radius: 24), - title: Text( - "partner_list_user_photos", - style: context.textTheme.labelLarge, - ).tr( - namedArgs: { - 'user': p.name, - }, - ), - trailing: Text( - "partner_list_view_all", - style: context.textTheme.labelLarge?.copyWith( - color: context.primaryColor, - ), - ).tr(), - onTap: () => context.pushRoute((PartnerDetailRoute(partner: p))), - ); - } -} diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart index 20747913fb14a..b4a12ab82634b 100644 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ b/mobile/lib/widgets/search/search_map_thumbnail.dart @@ -13,6 +13,7 @@ class SearchMapThumbnail extends StatelessWidget { }); final double size; + final bool showTitle = true; @override Widget build(BuildContext context) { diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart index fb46dceed592f..848d7cfad7078 100644 --- a/mobile/test/services/album.service_test.dart +++ b/mobile/test/services/album.service_test.dart @@ -79,25 +79,35 @@ void main() { verifyNoMoreInteractions(syncService); }); }); + group('refreshRemoteAlbums', () { - test('isShared: false', () async { + test('is working', () async { when(() => userService.refreshUsers()).thenAnswer((_) async => true); + when(() => albumApiRepository.getAll(shared: true)) + .thenAnswer((_) async => [AlbumStub.sharedWithUser]); + when(() => albumApiRepository.getAll(shared: null)) .thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); + when( - () => syncService.syncRemoteAlbumsToDb( - [AlbumStub.oneAsset, AlbumStub.twoAsset], - isShared: false, - ), + () => syncService.syncRemoteAlbumsToDb([ + AlbumStub.twoAsset, + AlbumStub.oneAsset, + AlbumStub.sharedWithUser, + ]), ).thenAnswer((_) async => true); - final result = await sut.refreshRemoteAlbums(isShared: false); + final result = await sut.refreshRemoteAlbums(); expect(result, true); verify(() => userService.refreshUsers()).called(1); + verify(() => albumApiRepository.getAll(shared: true)).called(1); verify(() => albumApiRepository.getAll(shared: null)).called(1); verify( () => syncService.syncRemoteAlbumsToDb( - [AlbumStub.oneAsset, AlbumStub.twoAsset], - isShared: false, + [ + AlbumStub.twoAsset, + AlbumStub.oneAsset, + AlbumStub.sharedWithUser, + ], ), ).called(1); verifyNoMoreInteractions(userService); @@ -166,9 +176,9 @@ void main() { () => albumRepository.update(AlbumStub.oneAsset), ).thenAnswer((_) async => AlbumStub.oneAsset); - final result = await sut.addAdditionalAssetToAlbum( - [AssetStub.image1, AssetStub.image2], + final result = await sut.addAssets( AlbumStub.oneAsset, + [AssetStub.image1, AssetStub.image2], ); expect(result != null, true); @@ -185,18 +195,23 @@ void main() { ).thenAnswer( (_) async => AlbumStub.sharedWithUser, ); + when( - () => entityService - .fillAlbumWithDatabaseEntities(AlbumStub.sharedWithUser), - ).thenAnswer((_) async => AlbumStub.sharedWithUser); + () => albumRepository.addUsers( + AlbumStub.emptyAlbum, + AlbumStub.emptyAlbum.sharedUsers.toList(), + ), + ).thenAnswer((_) async => AlbumStub.emptyAlbum); + when( - () => albumRepository.update(AlbumStub.sharedWithUser), - ).thenAnswer((_) async => AlbumStub.sharedWithUser); + () => albumRepository.update(AlbumStub.emptyAlbum), + ).thenAnswer((_) async => AlbumStub.emptyAlbum); - final result = await sut.addAdditionalUserToAlbum( - [UserStub.user2.id], + final result = await sut.addUsers( AlbumStub.emptyAlbum, + [UserStub.user2.id], ); + expect(result, true); }); });