mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
fix(mobile): asset state remain in gallery view after being deleted (#10603)
* fix(mobile): asset doesn't get removed from state renderList * fix delete last assets * refactor
This commit is contained in:
parent
922430da36
commit
d8175d8da8
8 changed files with 158 additions and 140 deletions
|
|
@ -311,4 +311,12 @@ class RenderList {
|
|||
GroupAssetsBy groupBy,
|
||||
) =>
|
||||
_buildRenderList(assets, null, groupBy);
|
||||
|
||||
/// Deletes an asset from the render list and clears the buffer
|
||||
/// This is only a workaround for deleted images still appearing in the gallery
|
||||
void deleteAsset(Asset deleteAsset) {
|
||||
allAssets?.remove(deleteAsset);
|
||||
_buf.clear();
|
||||
_bufOffset = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ import 'dart:collection';
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
|
|
@ -815,6 +817,7 @@ class _AssetRow extends StatelessWidget {
|
|||
key: key,
|
||||
children: assets.mapIndexed((int index, Asset asset) {
|
||||
final bool last = index + 1 == assetsPerRow;
|
||||
final isSelected = isSelectionActive && selectedAssets.contains(asset);
|
||||
return Container(
|
||||
width: width * widthDistribution[index],
|
||||
height: width,
|
||||
|
|
@ -822,21 +825,40 @@ class _AssetRow extends StatelessWidget {
|
|||
bottom: margin,
|
||||
right: last ? 0.0 : margin,
|
||||
),
|
||||
child: AssetIndexWrapper(
|
||||
rowIndex: rowStartIndex + index,
|
||||
sectionIndex: sectionIndex,
|
||||
child: ThumbnailImage(
|
||||
asset: asset,
|
||||
index: absoluteOffset + index,
|
||||
loadAsset: renderList.loadAsset,
|
||||
totalAssets: renderList.totalAssets,
|
||||
multiselectEnabled: selectionActive,
|
||||
isSelected: isSelectionActive && selectedAssets.contains(asset),
|
||||
onSelect: () => onSelect?.call(asset),
|
||||
onDeselect: () => onDeselect?.call(asset),
|
||||
showStorageIndicator: showStorageIndicator,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (selectionActive) {
|
||||
if (isSelected) {
|
||||
onDeselect?.call(asset);
|
||||
} else {
|
||||
onSelect?.call(asset);
|
||||
}
|
||||
} else {
|
||||
context.pushRoute(
|
||||
GalleryViewerRoute(
|
||||
renderList: renderList,
|
||||
initialIndex: absoluteOffset + index,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
onSelect?.call(asset);
|
||||
HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: AssetIndexWrapper(
|
||||
rowIndex: rowStartIndex + index,
|
||||
sectionIndex: sectionIndex,
|
||||
child: ThumbnailImage(
|
||||
asset: asset,
|
||||
multiselectEnabled: selectionActive,
|
||||
isSelected: isSelectionActive && selectedAssets.contains(asset),
|
||||
showStorageIndicator: showStorageIndicator,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,40 +1,42 @@
|
|||
import 'package:auto_route/auto_route.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/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/utils/storage_indicator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class ThumbnailImage extends ConsumerWidget {
|
||||
/// The asset to show the thumbnail image for
|
||||
final Asset asset;
|
||||
final int index;
|
||||
final Asset Function(int index) loadAsset;
|
||||
final int totalAssets;
|
||||
|
||||
/// Whether to show the storage indicator icont over the image or not
|
||||
final bool showStorageIndicator;
|
||||
|
||||
/// Whether to show the show stack icon over the image or not
|
||||
final bool showStack;
|
||||
|
||||
/// Whether to show the checkmark indicating that this image is selected
|
||||
final bool isSelected;
|
||||
|
||||
/// Can override [isSelected] and never show the selection indicator
|
||||
final bool multiselectEnabled;
|
||||
final Function? onSelect;
|
||||
final Function? onDeselect;
|
||||
|
||||
/// If we are allowed to deselect this image
|
||||
final bool canDeselect;
|
||||
|
||||
/// The offset index to apply to this hero tag for animation
|
||||
final int heroOffset;
|
||||
|
||||
const ThumbnailImage({
|
||||
super.key,
|
||||
required this.asset,
|
||||
required this.index,
|
||||
required this.loadAsset,
|
||||
required this.totalAssets,
|
||||
this.showStorageIndicator = true,
|
||||
this.showStack = false,
|
||||
this.isSelected = false,
|
||||
this.multiselectEnabled = false,
|
||||
this.onDeselect,
|
||||
this.onSelect,
|
||||
this.heroOffset = 0,
|
||||
this.canDeselect = true,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -147,11 +149,7 @@ class ThumbnailImage extends ConsumerWidget {
|
|||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 0,
|
||||
color: onDeselect == null ? Colors.grey : assetContainerColor,
|
||||
),
|
||||
color: onDeselect == null ? Colors.grey : assetContainerColor,
|
||||
color: canDeselect ? assetContainerColor : Colors.grey,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
|
|
@ -165,79 +163,52 @@ class ThumbnailImage extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (multiselectEnabled) {
|
||||
if (isSelected) {
|
||||
onDeselect?.call();
|
||||
} else {
|
||||
onSelect?.call();
|
||||
}
|
||||
} else {
|
||||
context.pushRoute(
|
||||
GalleryViewerRoute(
|
||||
initialIndex: index,
|
||||
loadAsset: loadAsset,
|
||||
totalAssets: totalAssets,
|
||||
heroOffset: heroOffset,
|
||||
showStack: showStack,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
onSelect?.call();
|
||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.decelerate,
|
||||
decoration: BoxDecoration(
|
||||
border: multiselectEnabled && isSelected
|
||||
? Border.all(
|
||||
color: onDeselect == null
|
||||
? Colors.grey
|
||||
: assetContainerColor,
|
||||
width: 8,
|
||||
)
|
||||
: const Border(),
|
||||
),
|
||||
child: buildImage(),
|
||||
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(),
|
||||
),
|
||||
if (multiselectEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: buildSelectionIcon(asset),
|
||||
),
|
||||
child: buildImage(),
|
||||
),
|
||||
if (multiselectEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: buildSelectionIcon(asset),
|
||||
),
|
||||
if (showStorageIndicator)
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
storageIcon(asset),
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
if (showStorageIndicator)
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
storageIcon(asset),
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
if (asset.isFavorite)
|
||||
const Positioned(
|
||||
left: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
if (asset.isFavorite)
|
||||
const Positioned(
|
||||
left: 8,
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
if (!asset.isImage) buildVideoIcon(),
|
||||
if (asset.stackChildrenCount > 0) buildStackIcon(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!asset.isImage) buildVideoIcon(),
|
||||
if (asset.stackChildrenCount > 0) buildStackIcon(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
|||
import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/services/asset_stack.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
|
@ -21,20 +22,24 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||
|
||||
class BottomGalleryBar extends ConsumerWidget {
|
||||
final Asset asset;
|
||||
final int assetIndex;
|
||||
final bool showStack;
|
||||
final int stackIndex;
|
||||
final int totalAssets;
|
||||
final ValueNotifier<int> totalAssets;
|
||||
final bool showVideoPlayerControls;
|
||||
final PageController controller;
|
||||
final RenderList renderList;
|
||||
|
||||
const BottomGalleryBar({
|
||||
super.key,
|
||||
required this.showStack,
|
||||
required this.stackIndex,
|
||||
required this.asset,
|
||||
required this.assetIndex,
|
||||
required this.controller,
|
||||
required this.totalAssets,
|
||||
required this.showVideoPlayerControls,
|
||||
required this.renderList,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -108,16 +113,17 @@ class BottomGalleryBar extends ConsumerWidget {
|
|||
force: force,
|
||||
);
|
||||
if (isDeleted && isParent) {
|
||||
if (totalAssets == 1) {
|
||||
// Workaround for asset remaining in the gallery
|
||||
renderList.deleteAsset(asset);
|
||||
|
||||
// `assetIndex == totalAssets.value - 1` handle the case of removing the last asset
|
||||
// to not throw the error when the next preCache index is called
|
||||
if (totalAssets.value == 1 || assetIndex == totalAssets.value - 1) {
|
||||
// Handle only one asset
|
||||
context.maybePop();
|
||||
} else {
|
||||
// Go to next page otherwise
|
||||
controller.nextPage(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
);
|
||||
}
|
||||
|
||||
totalAssets.value -= 1;
|
||||
}
|
||||
return isDeleted;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue