mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix: shared album control permissions (#22435)
* fix: shared album control permissions * fix: properly display "add photos" * fix: dont allow modification of album order * fix: album title/description edit from app bar * chore: code review changes * chore: format translations * chore: lintings
This commit is contained in:
parent
146973b072
commit
8473dab684
8 changed files with 99 additions and 54 deletions
|
|
@ -1039,6 +1039,7 @@
|
||||||
"exif_bottom_sheet_description_error": "Error updating description",
|
"exif_bottom_sheet_description_error": "Error updating description",
|
||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "LOCATION",
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
|
"exif_bottom_sheet_no_description": "No description",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
"exif_bottom_sheet_people": "PEOPLE",
|
||||||
"exif_bottom_sheet_person_add_person": "Add name",
|
"exif_bottom_sheet_person_add_person": "Add name",
|
||||||
"exit_slideshow": "Exit Slideshow",
|
"exit_slideshow": "Exit Slideshow",
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,10 @@ class RemoteAlbumService {
|
||||||
return _repository.getSharedUsers(albumId);
|
return _repository.getSharedUsers(albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<AlbumUserRole?> getUserRole(String albumId, String userId) {
|
||||||
|
return _repository.getUserRole(albumId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<RemoteAsset>> getAssets(String albumId) {
|
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||||
return _repository.getAssets(albumId);
|
return _repository.getAssets(albumId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<AlbumUserRole?> 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<List<RemoteAsset>> getAssets(String albumId) {
|
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||||
final query = _db.remoteAlbumAssetEntity.select().join([
|
final query = _db.remoteAlbumAssetEntity.select().join([
|
||||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||||
|
|
|
||||||
|
|
@ -169,9 +169,11 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||||
context.pushRoute(const DriftActivitiesRoute());
|
context.pushRoute(const DriftActivitiesRoute());
|
||||||
}
|
}
|
||||||
|
|
||||||
void showOptionSheet(BuildContext context) {
|
Future<void> showOptionSheet(BuildContext context) async {
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
final isOwner = user != null ? user.id == _album.ownerId : false;
|
final isOwner = user != null ? user.id == _album.ownerId : false;
|
||||||
|
final canAddPhotos =
|
||||||
|
await ref.read(remoteAlbumServiceProvider).getUserRole(_album.id, user?.id ?? '') == AlbumUserRole.editor;
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -193,22 +195,30 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
onAddPhotos: () async {
|
onAddPhotos: isOwner || canAddPhotos
|
||||||
await addAssets(context);
|
? () async {
|
||||||
context.pop();
|
await addAssets(context);
|
||||||
},
|
context.pop();
|
||||||
onToggleAlbumOrder: () async {
|
}
|
||||||
await toggleAlbumOrder();
|
: null,
|
||||||
context.pop();
|
onToggleAlbumOrder: isOwner
|
||||||
},
|
? () async {
|
||||||
onEditAlbum: () async {
|
await toggleAlbumOrder();
|
||||||
context.pop();
|
context.pop();
|
||||||
await showEditTitleAndDescription(context);
|
}
|
||||||
},
|
: null,
|
||||||
onCreateSharedLink: () async {
|
onEditAlbum: isOwner
|
||||||
context.pop();
|
? () async {
|
||||||
context.pushRoute(SharedLinkEditRoute(albumId: _album.id));
|
context.pop();
|
||||||
},
|
await showEditTitleAndDescription(context);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
onCreateSharedLink: isOwner
|
||||||
|
? () async {
|
||||||
|
context.pop();
|
||||||
|
context.pushRoute(SharedLinkEditRoute(albumId: _album.id));
|
||||||
|
}
|
||||||
|
: null,
|
||||||
onShowOptions: () {
|
onShowOptions: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
context.pushRoute(const DriftAlbumOptionsRoute());
|
context.pushRoute(const DriftAlbumOptionsRoute());
|
||||||
|
|
@ -220,6 +230,9 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
final isOwner = user != null ? user.id == _album.ownerId : false;
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
onPopInvokedWithResult: (didPop, _) {
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
if (didPop) {
|
if (didPop) {
|
||||||
|
|
@ -243,8 +256,8 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||||
appBar: RemoteAlbumSliverAppBar(
|
appBar: RemoteAlbumSliverAppBar(
|
||||||
icon: Icons.photo_album_outlined,
|
icon: Icons.photo_album_outlined,
|
||||||
onShowOptions: () => showOptionSheet(context),
|
onShowOptions: () => showOptionSheet(context),
|
||||||
onToggleAlbumOrder: () => toggleAlbumOrder(),
|
onToggleAlbumOrder: isOwner ? () => toggleAlbumOrder() : null,
|
||||||
onEditTitle: () => showEditTitleAndDescription(context),
|
onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null,
|
||||||
onActivity: () => showActivity(context),
|
onActivity: () => showActivity(context),
|
||||||
),
|
),
|
||||||
bottomSheet: RemoteAlbumBottomSheet(album: _album),
|
bottomSheet: RemoteAlbumBottomSheet(album: _album),
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(source: ActionSource.viewer),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
if (asset.isLocalOnly) const UploadActionButton(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 (isOwner) ...[
|
||||||
if (asset.hasRemote && isOwner && isArchived)
|
if (asset.hasRemote && isOwner && isArchived)
|
||||||
const UnArchiveActionButton(source: ActionSource.viewer)
|
const UnArchiveActionButton(source: ActionSource.viewer)
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||||
|
|
||||||
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||||
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
||||||
|
final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null);
|
||||||
|
|
||||||
return SliverList.list(
|
return SliverList.list(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -147,10 +148,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: _getDateTime(context, asset),
|
title: _getDateTime(context, asset),
|
||||||
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||||
trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null,
|
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null,
|
||||||
onTap: asset.hasRemote ? () async => await _editDateTime(context, ref) : 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 SheetPeopleDetails(),
|
||||||
const SheetLocationDetails(),
|
const SheetLocationDetails(),
|
||||||
// Details header
|
// Details header
|
||||||
|
|
@ -265,8 +266,9 @@ class _SheetTile extends ConsumerWidget {
|
||||||
|
|
||||||
class _SheetAssetDescription extends ConsumerStatefulWidget {
|
class _SheetAssetDescription extends ConsumerStatefulWidget {
|
||||||
final ExifInfo exif;
|
final ExifInfo exif;
|
||||||
|
final bool isEditable;
|
||||||
|
|
||||||
const _SheetAssetDescription({required this.exif});
|
const _SheetAssetDescription({required this.exif, this.isEditable = true});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<_SheetAssetDescription> createState() => _SheetAssetDescriptionState();
|
ConsumerState<_SheetAssetDescription> createState() => _SheetAssetDescriptionState();
|
||||||
|
|
@ -312,27 +314,33 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription>
|
||||||
|
|
||||||
// Update controller text when EXIF data changes
|
// Update controller text when EXIF data changes
|
||||||
final currentDescription = currentExifInfo?.description ?? '';
|
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) {
|
if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) {
|
||||||
_controller.text = currentDescription;
|
_controller.text = currentDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||||
child: TextField(
|
child: IgnorePointer(
|
||||||
controller: _controller,
|
ignoring: !widget.isEditable,
|
||||||
keyboardType: TextInputType.multiline,
|
child: TextField(
|
||||||
focusNode: _descriptionFocus,
|
controller: _controller,
|
||||||
maxLines: null, // makes it grow as text is added
|
keyboardType: TextInputType.multiline,
|
||||||
decoration: InputDecoration(
|
focusNode: _descriptionFocus,
|
||||||
hintText: 'exif_bottom_sheet_description'.t(context: context),
|
maxLines: null, // makes it grow as text is added
|
||||||
border: InputBorder.none,
|
decoration: InputDecoration(
|
||||||
enabledBorder: InputBorder.none,
|
hintText: hintText,
|
||||||
focusedBorder: InputBorder.none,
|
border: InputBorder.none,
|
||||||
disabledBorder: InputBorder.none,
|
enabledBorder: InputBorder.none,
|
||||||
errorBorder: InputBorder.none,
|
focusedBorder: InputBorder.none,
|
||||||
focusedErrorBorder: InputBorder.none,
|
disabledBorder: InputBorder.none,
|
||||||
|
errorBorder: InputBorder.none,
|
||||||
|
focusedErrorBorder: InputBorder.none,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => saveDescription(currentExifInfo?.description),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => saveDescription(currentExifInfo?.description),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
|
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
|
||||||
previousRouteName != AssetViewerRoute.name &&
|
previousRouteName != AssetViewerRoute.name &&
|
||||||
previousRouteName != null &&
|
previousRouteName != null &&
|
||||||
previousRouteName != LocalTimelineRoute.name;
|
previousRouteName != LocalTimelineRoute.name &&
|
||||||
|
isOwner;
|
||||||
|
|
||||||
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
||||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||||
|
|
|
||||||
|
|
@ -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/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.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/timeline/multiselect.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/immich_toast.dart';
|
||||||
|
|
||||||
class RemoteAlbumBottomSheet extends ConsumerStatefulWidget {
|
class RemoteAlbumBottomSheet extends ConsumerStatefulWidget {
|
||||||
|
|
@ -53,6 +54,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final multiselect = ref.watch(multiSelectProvider);
|
final multiselect = ref.watch(multiSelectProvider);
|
||||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||||
|
final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId;
|
||||||
|
|
||||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||||
final selectedAssets = multiselect.selectedAssets;
|
final selectedAssets = multiselect.selectedAssets;
|
||||||
|
|
@ -93,28 +95,35 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||||
const ShareActionButton(source: ActionSource.timeline),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
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),
|
const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
if (ownsAlbum) ...[
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
isTrashEnable
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||||
|
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasLocal) ...[
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||||
],
|
|
||||||
slivers: [
|
|
||||||
const AddToAlbumHeader(),
|
|
||||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
|
||||||
],
|
],
|
||||||
|
slivers: ownsAlbum
|
||||||
|
? [
|
||||||
|
const AddToAlbumHeader(),
|
||||||
|
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue