diff --git a/i18n/en.json b/i18n/en.json index bd59c37fac..2f98ab4bcb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1039,6 +1039,7 @@ "exif_bottom_sheet_description_error": "Error updating description", "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "LOCATION", + "exif_bottom_sheet_no_description": "No description", "exif_bottom_sheet_people": "PEOPLE", "exif_bottom_sheet_person_add_person": "Add name", "exit_slideshow": "Exit Slideshow", diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index cc28dfafd5..13dfadb8d8 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -120,6 +120,10 @@ class RemoteAlbumService { return _repository.getSharedUsers(albumId); } + Future getUserRole(String albumId, String userId) { + return _repository.getUserRole(albumId, userId); + } + Future> getAssets(String albumId) { return _repository.getAssets(albumId); } diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index 22d4715c1e..0526cfb7aa 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -221,6 +221,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { .get(); } + Future getUserRole(String albumId, String userId) async { + final query = _db.remoteAlbumUserEntity.select() + ..where((row) => row.albumId.equals(albumId) & row.userId.equals(userId)) + ..limit(1); + + final result = await query.getSingleOrNull(); + return result?.role; + } + Future> getAssets(String albumId) { final query = _db.remoteAlbumAssetEntity.select().join([ innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index 34d8919674..23d82dcb92 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -169,9 +169,11 @@ class _RemoteAlbumPageState extends ConsumerState { context.pushRoute(const DriftActivitiesRoute()); } - void showOptionSheet(BuildContext context) { + Future showOptionSheet(BuildContext context) async { final user = ref.watch(currentUserProvider); final isOwner = user != null ? user.id == _album.ownerId : false; + final canAddPhotos = + await ref.read(remoteAlbumServiceProvider).getUserRole(_album.id, user?.id ?? '') == AlbumUserRole.editor; showModalBottomSheet( context: context, @@ -193,22 +195,30 @@ class _RemoteAlbumPageState extends ConsumerState { context.pop(); } : null, - onAddPhotos: () async { - await addAssets(context); - context.pop(); - }, - onToggleAlbumOrder: () async { - await toggleAlbumOrder(); - context.pop(); - }, - onEditAlbum: () async { - context.pop(); - await showEditTitleAndDescription(context); - }, - onCreateSharedLink: () async { - context.pop(); - context.pushRoute(SharedLinkEditRoute(albumId: _album.id)); - }, + onAddPhotos: isOwner || canAddPhotos + ? () async { + await addAssets(context); + context.pop(); + } + : null, + onToggleAlbumOrder: isOwner + ? () async { + await toggleAlbumOrder(); + context.pop(); + } + : null, + onEditAlbum: isOwner + ? () async { + context.pop(); + await showEditTitleAndDescription(context); + } + : null, + onCreateSharedLink: isOwner + ? () async { + context.pop(); + context.pushRoute(SharedLinkEditRoute(albumId: _album.id)); + } + : null, onShowOptions: () { context.pop(); context.pushRoute(const DriftAlbumOptionsRoute()); @@ -220,6 +230,9 @@ class _RemoteAlbumPageState extends ConsumerState { @override Widget build(BuildContext context) { + final user = ref.watch(currentUserProvider); + final isOwner = user != null ? user.id == _album.ownerId : false; + return PopScope( onPopInvokedWithResult: (didPop, _) { if (didPop) { @@ -243,8 +256,8 @@ class _RemoteAlbumPageState extends ConsumerState { appBar: RemoteAlbumSliverAppBar( icon: Icons.photo_album_outlined, onShowOptions: () => showOptionSheet(context), - onToggleAlbumOrder: () => toggleAlbumOrder(), - onEditTitle: () => showEditTitleAndDescription(context), + onToggleAlbumOrder: isOwner ? () => toggleAlbumOrder() : null, + onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null, onActivity: () => showActivity(context), ), bottomSheet: RemoteAlbumBottomSheet(album: _album), diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 3111512823..44660e440c 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -43,7 +43,7 @@ class ViewerBottomBar extends ConsumerWidget { final actions = [ const ShareActionButton(source: ActionSource.viewer), if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), - if (asset.type == AssetType.image) const EditImageActionButton(), + if (asset.type == AssetType.image && isOwner) const EditImageActionButton(), if (isOwner) ...[ if (asset.hasRemote && isOwner && isArchived) const UnArchiveActionButton(source: ActionSource.viewer) diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index bdd7fb9b48..9d29b19bff 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -140,6 +140,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; final cameraTitle = _getCameraInfoTitle(exifInfo); + final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null); return SliverList.list( children: [ @@ -147,10 +148,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget { _SheetTile( title: _getDateTime(context, asset), titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null, - onTap: asset.hasRemote ? () async => await _editDateTime(context, ref) : null, + trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null, + onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null, ), - if (exifInfo != null) _SheetAssetDescription(exif: exifInfo), + if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner), const SheetPeopleDetails(), const SheetLocationDetails(), // Details header @@ -265,8 +266,9 @@ class _SheetTile extends ConsumerWidget { class _SheetAssetDescription extends ConsumerStatefulWidget { final ExifInfo exif; + final bool isEditable; - const _SheetAssetDescription({required this.exif}); + const _SheetAssetDescription({required this.exif, this.isEditable = true}); @override ConsumerState<_SheetAssetDescription> createState() => _SheetAssetDescriptionState(); @@ -312,27 +314,33 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> // Update controller text when EXIF data changes final currentDescription = currentExifInfo?.description ?? ''; + final hintText = (widget.isEditable ? 'exif_bottom_sheet_description' : 'exif_bottom_sheet_no_description').t( + context: context, + ); if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) { _controller.text = currentDescription; } return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), - child: TextField( - controller: _controller, - keyboardType: TextInputType.multiline, - focusNode: _descriptionFocus, - maxLines: null, // makes it grow as text is added - decoration: InputDecoration( - hintText: 'exif_bottom_sheet_description'.t(context: context), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, + child: IgnorePointer( + ignoring: !widget.isEditable, + child: TextField( + controller: _controller, + keyboardType: TextInputType.multiline, + focusNode: _descriptionFocus, + maxLines: null, // makes it grow as text is added + decoration: InputDecoration( + hintText: hintText, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + ), + onTapOutside: (_) => saveDescription(currentExifInfo?.description), ), - onTapOutside: (_) => saveDescription(currentExifInfo?.description), ), ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart index 149252ab17..c0e93a73cc 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -45,7 +45,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { (previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) && previousRouteName != AssetViewerRoute.name && previousRouteName != null && - previousRouteName != LocalTimelineRoute.name; + previousRouteName != LocalTimelineRoute.name && + isOwner; final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 0ab419a56b..7db8a80af2 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -24,6 +24,7 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class RemoteAlbumBottomSheet extends ConsumerStatefulWidget { @@ -53,6 +54,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState Widget build(BuildContext context) { final multiselect = ref.watch(multiSelectProvider); final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); + final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId; Future addAssetsToAlbum(RemoteAlbum album) async { final selectedAssets = multiselect.selectedAssets; @@ -93,28 +95,35 @@ class _RemoteAlbumBottomSheetState extends ConsumerState const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), - const ArchiveActionButton(source: ActionSource.timeline), - const FavoriteActionButton(source: ActionSource.timeline), + + if (ownsAlbum) ...[ + const ArchiveActionButton(source: ActionSource.timeline), + const FavoriteActionButton(source: ActionSource.timeline), + ], const DownloadActionButton(source: ActionSource.timeline), - isTrashEnable - ? const TrashActionButton(source: ActionSource.timeline) - : const DeletePermanentActionButton(source: ActionSource.timeline), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), - if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), - if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), + if (ownsAlbum) ...[ + isTrashEnable + ? const TrashActionButton(source: ActionSource.timeline) + : const DeletePermanentActionButton(source: ActionSource.timeline), + const EditDateTimeActionButton(source: ActionSource.timeline), + const EditLocationActionButton(source: ActionSource.timeline), + const MoveToLockFolderActionButton(source: ActionSource.timeline), + if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), + if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), + ], ], if (multiselect.hasLocal) ...[ const DeleteLocalActionButton(source: ActionSource.timeline), const UploadActionButton(source: ActionSource.timeline), ], - RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), - ], - slivers: [ - const AddToAlbumHeader(), - AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), + if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), ], + slivers: ownsAlbum + ? [ + const AddToAlbumHeader(), + AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), + ] + : null, ); } }