diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 0075f65de0557..20360cbb00d5f 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -387,6 +387,7 @@ "partner_page_title": "Partner", "partners": "Partners", "people": "People", + "partner_sharing_dialog_title": "Partner Sharing", "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", @@ -574,11 +575,16 @@ "sync_albums": "Sync albums", "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "sharing_silver_appbar_shared_links": "Shared links", + "storage_asset_local": "On Device (Not backed up)", + "storage_asset_remote": "Immich Server only", + "storage_asset_merged": "Backed up to Immich Server", + "storage_asset_partner": "From Partner Sharing", "tab_controller_nav_library": "Library", "tab_controller_nav_photos": "Photos", "tab_controller_nav_search": "Search", "tab_controller_nav_sharing": "Sharing", - "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", + "theme_setting_asset_list_storage_indicator_title": "Show storage indicator", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", "theme_setting_colorful_interface_title": "Colorful interface", @@ -593,6 +599,8 @@ "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", + "timeline_settings_title": "Tile Options", + "timeline_show_partner_switch": "Show user thumbnails", "translated_text_options": "Options", "trash": "Trash", "trash_emptied": "Emptied trash", diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index 1dda2b9a12a03..88c63ea43fc64 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -218,6 +218,7 @@ enum StoreKey { logLevel(115, type: int), preferRemoteImage(116, type: bool), loopVideo(117, type: bool), + showPartnerIconInTimeline(118, type: bool), // map related settings mapShowFavoriteOnly(118, type: bool), mapRelativeDate(119, type: int), diff --git a/mobile/lib/interfaces/user.interface.dart b/mobile/lib/interfaces/user.interface.dart index e6175a7dc9906..0aad7b9315e0b 100644 --- a/mobile/lib/interfaces/user.interface.dart +++ b/mobile/lib/interfaces/user.interface.dart @@ -4,6 +4,8 @@ import 'package:immich_mobile/interfaces/database.interface.dart'; abstract interface class IUserRepository implements IDatabaseRepository { Future get(String id); + Future getByIsarId(int id); + Future> getByIds(List ids); Future> getAll({bool self = true, UserSort? sortBy}); diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 3915ac3991460..da773eafda3b4 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -14,7 +14,7 @@ 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/common/user_circle_avatar.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -171,7 +171,7 @@ class PartnerList extends ConsumerWidget { left: 12.0, right: 18.0, ), - leading: userAvatar(context, partner, radius: 16), + leading: UserCircleAvatar(user: partner, radius: 16), title: Text( "partner_list_user_photos", style: TextStyle( diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart index 1e9e801210e5e..4d308f3f6eb08 100644 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ b/mobile/lib/pages/library/partner/partner.page.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/services/partner.service.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/user_avatar.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @RoutePage() class PartnerPage extends HookConsumerWidget { @@ -42,7 +42,7 @@ class PartnerPage extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.only(right: 8), - child: userAvatar(context, u), + child: UserCircleAvatar(user: u), ), Text(u.name), ], @@ -99,7 +99,7 @@ class PartnerPage extends HookConsumerWidget { itemCount: users.length, itemBuilder: ((context, index) { return ListTile( - leading: userAvatar(context, users[index]), + leading: UserCircleAvatar(user: users[index]), title: Text( users[index].email, style: context.textTheme.bodyLarge, diff --git a/mobile/lib/pages/library/partner/partner_detail.page.dart b/mobile/lib/pages/library/partner/partner_detail.page.dart index 0874aacfa7f53..a740fbca95dc8 100644 --- a/mobile/lib/pages/library/partner/partner_detail.page.dart +++ b/mobile/lib/pages/library/partner/partner_detail.page.dart @@ -114,6 +114,7 @@ class PartnerDetailPage extends HookConsumerWidget { onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(), deleteEnabled: false, favoriteEnabled: false, + showUserThumbnail: false, ), ); } diff --git a/mobile/lib/repositories/user.repository.dart b/mobile/lib/repositories/user.repository.dart index fb4df84fe7c4a..c33bd881d7da9 100644 --- a/mobile/lib/repositories/user.repository.dart +++ b/mobile/lib/repositories/user.repository.dart @@ -19,6 +19,11 @@ class UserRepository extends DatabaseRepository implements IUserRepository { @override Future get(String id) => db.users.getById(id); + @override + Future getByIsarId(int id) { + return db.users.where().isarIdEqualTo(id).findFirst(); + } + @override Future> getAll({bool self = true, UserSort? sortBy}) { final baseQuery = db.users.where(); diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 8f773e1bb33a9..247c6ee8103bc 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -70,6 +70,11 @@ enum AppSettingsEnum { mapRelativeDate(StoreKey.mapRelativeDate, null, 0), allowSelfSignedSSLCert(StoreKey.selfSignedCert, null, false), ignoreIcloudAssets(StoreKey.ignoreIcloudAssets, null, false), + showPartnerIconInTimeline( + StoreKey.showPartnerIconInTimeline, + null, + true, + ), selectedAlbumSortReverse( StoreKey.selectedAlbumSortReverse, null, diff --git a/mobile/lib/services/user.service.dart b/mobile/lib/services/user.service.dart index 4c2b3cbbd00fb..6309c799c9ede 100644 --- a/mobile/lib/services/user.service.dart +++ b/mobile/lib/services/user.service.dart @@ -38,6 +38,10 @@ class UserService { Future> getUsers({bool self = false}) => _userRepository.getAll(self: self); + Future getUser(String id) async => await _userRepository.get(id); + Future getUserbyIsarId(int id) async => + await _userRepository.getByIsarId(id); + Future<({String profileImagePath})?> uploadProfileImage(XFile image) async { try { return await _userApiRepository.createProfileImage( diff --git a/mobile/lib/utils/storage_indicator.dart b/mobile/lib/utils/storage_indicator.dart index 4764c4538543e..65e79fefce8c4 100644 --- a/mobile/lib/utils/storage_indicator.dart +++ b/mobile/lib/utils/storage_indicator.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -12,3 +13,14 @@ IconData storageIcon(Asset asset) { return Icons.cloud_done_outlined; } } + +String storageText(Asset asset) { + switch (asset.storage) { + case AssetState.local: + return "storage_asset_local".tr(); + case AssetState.remote: + return "storage_asset_remote".tr(); + case AssetState.merged: + return "storage_asset_merged".tr(); + } +} diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart index 17872852e5af7..d201c476c05b1 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart @@ -18,6 +18,7 @@ class ImmichAssetGrid extends HookConsumerWidget { final int? assetsPerRow; final double margin; final bool? showStorageIndicator; + final bool? showUserThumbnail; final ImmichAssetGridSelectionListener? listener; final bool selectionActive; final List? assets; @@ -41,6 +42,7 @@ class ImmichAssetGrid extends HookConsumerWidget { this.renderList, this.assetsPerRow, this.showStorageIndicator, + this.showUserThumbnail, this.listener, this.margin = 2.0, this.selectionActive = false, @@ -105,6 +107,8 @@ class ImmichAssetGrid extends HookConsumerWidget { listener: listener, showStorageIndicator: showStorageIndicator ?? settings.getSetting(AppSettingsEnum.storageIndicator), + showUserThumbnail: showUserThumbnail ?? + settings.getSetting(AppSettingsEnum.showPartnerIconInTimeline), renderList: renderList, margin: margin, selectionActive: selectionActive, diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart index 38e499b5dec8e..f3c3507c37bb8 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart @@ -41,6 +41,7 @@ class ImmichAssetGridView extends ConsumerStatefulWidget { final int assetsPerRow; final double margin; final bool showStorageIndicator; + final bool showUserThumbnail; final ImmichAssetGridSelectionListener? listener; final bool selectionActive; final Future Function()? onRefresh; @@ -61,6 +62,7 @@ class ImmichAssetGridView extends ConsumerStatefulWidget { required this.renderList, required this.assetsPerRow, required this.showStorageIndicator, + required this.showUserThumbnail, this.listener, this.margin = 5.0, this.selectionActive = false, @@ -187,6 +189,7 @@ class ImmichAssetGridViewState extends ConsumerState { final section = widget.renderList.elements[index]; return _Section( showStorageIndicator: widget.showStorageIndicator, + showUserThumbnail: widget.showUserThumbnail, selectedAssets: _selectedAssets, selectionActive: widget.selectionActive, sectionIndex: index, @@ -601,6 +604,7 @@ class _Section extends StatelessWidget { final bool showStack; final int heroOffset; final bool showStorageIndicator; + final bool showUserThumbnail; const _Section({ required this.section, @@ -618,6 +622,7 @@ class _Section extends StatelessWidget { required this.showStack, required this.heroOffset, required this.showStorageIndicator, + required this.showUserThumbnail, }); @override @@ -680,6 +685,7 @@ class _Section extends StatelessWidget { showStack: showStack, heroOffset: heroOffset, showStorageIndicator: showStorageIndicator, + showUserThumbnail: showUserThumbnail, selectionActive: selectionActive, onSelect: (asset) => selectAssets([asset]), onDeselect: (asset) => deselectAssets([asset]), @@ -763,6 +769,7 @@ class _AssetRow extends StatelessWidget { final RenderList renderList; final bool selectionActive; final bool showStorageIndicator; + final bool showUserThumbnail; final int heroOffset; final bool showStack; final Function(Asset)? onSelect; @@ -782,6 +789,7 @@ class _AssetRow extends StatelessWidget { required this.renderList, required this.selectionActive, required this.showStorageIndicator, + required this.showUserThumbnail, required this.heroOffset, required this.showStack, required this.isSelectionActive, @@ -859,6 +867,7 @@ class _AssetRow extends StatelessWidget { asset: asset, multiselectEnabled: selectionActive, isSelected: isSelectionActive && selectedAssets.contains(asset), + showUserThumbnail: showUserThumbnail, showStorageIndicator: showStorageIndicator, heroOffset: heroOffset, showStack: showStack, diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index eeecfa9b58435..b4d3ea5f34bf2 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -35,6 +35,7 @@ class MultiselectGrid extends HookConsumerWidget { this.buildLoadingIndicator, this.onRemoveFromAlbum, this.topWidget, + this.showUserThumbnail, this.stackEnabled = false, this.archiveEnabled = false, this.deleteEnabled = true, @@ -51,6 +52,7 @@ class MultiselectGrid extends HookConsumerWidget { final Future Function(Iterable)? onRemoveFromAlbum; final Widget? topWidget; final bool stackEnabled; + final bool? showUserThumbnail; final bool archiveEnabled; final bool unarchive; final bool deleteEnabled; @@ -406,6 +408,7 @@ class MultiselectGrid extends HookConsumerWidget { : ImmichAssetGrid( renderList: data, listener: selectionListener, + showUserThumbnail: showUserThumbnail, selectionActive: selectionEnabledHook.value, onRefresh: onRefresh == null ? null diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index 6cadef763d730..1506b2701a751 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/services/user.service.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; import 'package:immich_mobile/utils/storage_indicator.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; class ThumbnailImage extends ConsumerWidget { /// The asset to show the thumbnail image for @@ -14,6 +18,9 @@ class ThumbnailImage extends ConsumerWidget { /// Whether to show the storage indicator icont over the image or not final bool showStorageIndicator; + /// Whether to show the user thumbnail for partner assets over the image or not + final bool showUserThumbnail; + /// Whether to show the show stack icon over the image or not final bool showStack; @@ -33,6 +40,7 @@ class ThumbnailImage extends ConsumerWidget { super.key, required this.asset, this.showStorageIndicator = true, + this.showUserThumbnail = true, this.showStack = false, this.isSelected = false, this.multiselectEnabled = false, @@ -47,6 +55,7 @@ class ThumbnailImage extends ConsumerWidget { : context.primaryColor.lighten(amount: 0.8); // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id final isFromDto = asset.id == noDbId; + final userService = ref.watch(userServiceProvider); Widget buildSelectionIcon(Asset asset) { if (isSelected) { @@ -164,52 +173,71 @@ class ThumbnailImage extends ConsumerWidget { ); } - return Stack( - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - decoration: BoxDecoration( - border: multiselectEnabled && isSelected - ? Border.all( - color: canDeselect ? assetContainerColor : Colors.grey, - width: 8, - ) - : const Border(), - ), - child: buildImage(), - ), - if (multiselectEnabled) - Padding( - padding: const EdgeInsets.all(3.0), - child: Align( - alignment: Alignment.topLeft, - child: buildSelectionIcon(asset), + Future userFuture = userService.getUserbyIsarId(asset.ownerId); + return FutureBuilder( + future: userFuture, + builder: (BuildContext context, AsyncSnapshot userSnapshot) => + Stack( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.decelerate, + decoration: BoxDecoration( + border: multiselectEnabled && isSelected + ? Border.all( + color: canDeselect ? assetContainerColor : Colors.grey, + width: 8, + ) + : const Border(), ), + child: buildImage(), ), - if (showStorageIndicator) - Positioned( - right: 8, - bottom: 5, - child: Icon( - storageIcon(asset), - color: Colors.white.withOpacity(.8), - size: 16, + if (multiselectEnabled) + Padding( + padding: const EdgeInsets.all(3.0), + child: Align( + alignment: Alignment.topLeft, + child: buildSelectionIcon(asset), + ), ), - ), - if (asset.isFavorite) - const Positioned( - left: 8, - bottom: 5, - child: Icon( - Icons.favorite, - color: Colors.white, - size: 18, + if (showStorageIndicator) + Positioned( + right: 8, + bottom: 5, + child: Icon( + storageIcon(asset), + color: Colors.white.withOpacity(.8), + size: 16, + ), ), - ), - if (!asset.isImage) buildVideoIcon(), - if (asset.stackCount > 0) buildStackIcon(), - ], + if (asset.isFavorite) + const Positioned( + left: 8, + bottom: 5, + child: Icon( + Icons.favorite, + color: Colors.white, + size: 18, + ), + ), + // Not possible to favorite photos belonging to other users, so reuse lower left corner for partner images + if (asset.ownerId != Store.get(StoreKey.currentUser).isarId && + showUserThumbnail && + userSnapshot.hasData) + Positioned( + left: 8, + bottom: 5, + child: UserCircleAvatar( + user: userSnapshot.data, + radius: 8, + size: 18, + ).build(context), + // userAvatar(context, Store.get(StoreKey.currentUser), radius: 8), + ), + if (!asset.isImage) buildVideoIcon(), + if (asset.stackCount > 0) buildStackIcon(), + ], + ), ); } } diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart index a78a309512d37..ed294dab65375 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart @@ -3,6 +3,7 @@ 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/asset.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_state_details.dart'; import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; @@ -37,6 +38,7 @@ class AssetDetails extends ConsumerWidget { ).tr(), FileInfo(asset: asset), if (exifInfo?.make != null) CameraInfo(exifInfo: exifInfo!), + AssetStateInfo(asset: asset), ], ), ); diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_state_details.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_state_details.dart new file mode 100644 index 0000000000000..21537dbb3fa9a --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/asset_state_details.dart @@ -0,0 +1,70 @@ +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/entities/user.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/services/user.service.dart'; +import 'package:immich_mobile/utils/storage_indicator.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; + +class AssetStateInfo extends ConsumerWidget { + final Asset asset; + + const AssetStateInfo({ + super.key, + required this.asset, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textColor = context.isDarkTheme ? Colors.white : Colors.black; + final userService = ref.watch(userServiceProvider); + final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false; + + Future userFuture = userService.getUserbyIsarId(asset.ownerId); + return FutureBuilder( + future: userFuture, + builder: (BuildContext context, AsyncSnapshot userSnapshot) { + (userSnapshot.hasData); + final User? user = + (asset.ownerId == Store.get(StoreKey.currentUser).isarId) + ? null + : (userSnapshot.hasData) + ? userSnapshot.data + : null; + + return ListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + leading: (user == null) + ? Icon( + storageIcon(asset), + color: textColor.withAlpha(200), + ) + : UserCircleAvatar( + user: user, + radius: 12, + size: 30, + ).build(context), + title: Text( + (user == null) + ? storageText(asset) + : isInAlbum + ? "album_thumbnail_shared_by".tr(args: [user.name]) + : user.name, + style: context.textTheme.labelLarge, + ), + subtitle: (user == null || isInAlbum) + ? null + : Text( + "storage_asset_partner".tr(), + style: context.textTheme.bodySmall, + ), + ); + }, + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index f400224e0a0be..63d6e3c5186c4 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; @@ -17,6 +18,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; class GalleryAppBar extends ConsumerWidget { final Asset asset; @@ -35,6 +37,7 @@ class GalleryAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final album = ref.watch(currentAlbumProvider); + final isInAlbum = album?.isRemote ?? false; final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; final isPartner = ref @@ -93,6 +96,61 @@ class GalleryAppBar extends ConsumerWidget { ); } + showPartnerInfo(Asset asset, User user) { + showDialog( + context: context, + builder: (BuildContext _) { + return SimpleDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 5, + title: Center( + heightFactor: 0.8, + child: Text( + isInAlbum + ? "album_thumbnail_shared_by".tr(args: [""]) + : "partner_sharing_dialog_title".tr(), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + children: [ + SimpleDialogOption( + onPressed: () { + // Need to determine if we are viewing from the partner sharing view + // Navigator.of(context, rootNavigator: true).pop(); + // context.pushRoute((PartnerDetailRoute(partner: user))); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + UserCircleAvatar( + user: user, + radius: 15, + size: 30, + ).build(context), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text( + user.name, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + handleDownloadAsset() { ref.read(downloadStateProvider.notifier).downloadAsset(asset, context); } @@ -117,6 +175,7 @@ class GalleryAppBar extends ConsumerWidget { onToggleMotionVideo: onToggleMotionVideo, onAddToAlbumPressed: () => addToAlbum(asset), onActivitiesPressed: handleActivities, + onPartnerPressed: showPartnerInfo, ), ), ), diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 984b61f50cc05..c243516032dd7 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/providers/activity_statistics.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -21,6 +22,7 @@ class TopControlAppBar extends HookConsumerWidget { required this.isOwner, required this.onActivitiesPressed, required this.isPartner, + required this.onPartnerPressed, }); final Asset asset; @@ -31,6 +33,7 @@ class TopControlAppBar extends HookConsumerWidget { final VoidCallback onAddToAlbumPressed; final VoidCallback onRestorePressed; final VoidCallback onActivitiesPressed; + final Function(Asset, User) onPartnerPressed; final Function(Asset) onFavorite; final bool isPlayingMotionVideo; final bool isOwner; diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index bf3bd8a5a8ff5..1f787f2fd4363 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -2,15 +2,14 @@ import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/widgets/common/transparent_image.dart'; // ignore: must_be_immutable -class UserCircleAvatar extends ConsumerWidget { - final User user; +class UserCircleAvatar extends StatelessWidget { + final User? user; double radius; double size; @@ -18,35 +17,46 @@ class UserCircleAvatar extends ConsumerWidget { super.key, this.radius = 22, this.size = 44, - required this.user, + this.user, }); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { + if (user == null) { + return Icon( + Icons.person_off, + color: Colors.red, + size: size, + ); + } bool isDarkTheme = Theme.of(context).brightness == Brightness.dark; final profileImageUrl = - '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}'; + '${Store.get(StoreKey.serverEndpoint)}/users/${user!.id}/profile-image?d=${Random().nextInt(1024)}'; final textIcon = Text( - user.name[0].toUpperCase(), + user!.name[0].toUpperCase(), style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 12, - color: isDarkTheme && user.avatarColor == AvatarColorEnum.primary + fontSize: radius <= 16 + ? radius < 12 + ? radius + : 14 + : 16, + color: isDarkTheme && user!.avatarColor == AvatarColorEnum.primary ? Colors.black : Colors.white, ), ); return CircleAvatar( - backgroundColor: user.avatarColor.toColor(), + backgroundColor: user!.avatarColor.toColor(), radius: radius, - child: user.profileImagePath.isEmpty + child: user!.profileImagePath.isEmpty ? textIcon : ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(50)), child: CachedNetworkImage( fit: BoxFit.cover, - cacheKey: user.profileImagePath, + cacheKey: user!.profileImagePath, width: size, height: size, placeholder: (_, __) => Image.memory(kTransparentImage), diff --git a/mobile/lib/widgets/partner/partner_list.dart b/mobile/lib/widgets/partner/partner_list.dart new file mode 100644 index 0000000000000..72c7477c42499 --- /dev/null +++ b/mobile/lib/widgets/partner/partner_list.dart @@ -0,0 +1,52 @@ +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_circle_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: UserCircleAvatar( + user: p, + radius: 15, + size: 30, + ).build(context), + 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/settings/asset_list_settings/asset_list_indicators_setting.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_indicators_setting.dart new file mode 100644 index 0000000000000..a18098a781f90 --- /dev/null +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_indicators_setting.dart @@ -0,0 +1,40 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; + +class TimelineSetting extends HookConsumerWidget { + const TimelineSetting({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final showPartnerInTimelineSetting = + useAppSettingsState(AppSettingsEnum.showPartnerIconInTimeline); + + final showStorageIndicatorSetting = + useAppSettingsState(AppSettingsEnum.storageIndicator); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSubTitle(title: "timeline_settings_title".tr()), + SettingsSwitchListTile( + valueNotifier: showStorageIndicatorSetting, + title: 'theme_setting_asset_list_storage_indicator_title'.tr(), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), + SettingsSwitchListTile( + valueNotifier: showPartnerInTimelineSetting, + title: 'timeline_show_partner_switch'.tr(), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart index cd12ea3eb2356..3dc9958b9b65d 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart @@ -1,12 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart'; +import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_indicators_setting.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'asset_list_layout_settings.dart'; class AssetListSettings extends HookConsumerWidget { @@ -16,15 +12,8 @@ class AssetListSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final showStorageIndicator = - useAppSettingsState(AppSettingsEnum.storageIndicator); - final assetListSetting = [ - SettingsSwitchListTile( - valueNotifier: showStorageIndicator, - title: 'theme_setting_asset_list_storage_indicator_title'.tr(), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), - ), + const TimelineSetting(), const LayoutSettings(), const GroupSettings(), ];