mirror of
https://github.com/immich-app/immich.git
synced 2025-01-04 02:46:47 +01:00
fix stack handling
This commit is contained in:
parent
3b9a3d4037
commit
d1c7ed5464
4 changed files with 123 additions and 100 deletions
82
mobile/lib/pages/common/gallery_stacked_children.dart
Normal file
82
mobile/lib/pages/common/gallery_stacked_children.dart
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
||||||
|
|
||||||
|
class GalleryStackedChildren extends HookConsumerWidget {
|
||||||
|
final ValueNotifier<int> stackIndex;
|
||||||
|
|
||||||
|
const GalleryStackedChildren(this.stackIndex, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asset = ref.watch(currentAssetProvider);
|
||||||
|
if (asset == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final stackId = asset.stackId;
|
||||||
|
if (stackId == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final stackElements = ref.watch(assetStackStateProvider(stackId));
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 80,
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: stackElements.length,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 5,
|
||||||
|
right: 5,
|
||||||
|
bottom: 30,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final currentAsset = stackElements.elementAt(index);
|
||||||
|
final assetId = currentAsset.remoteId;
|
||||||
|
if (assetId == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
key: ValueKey(assetId),
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
stackIndex.value = index;
|
||||||
|
ref.read(currentAssetProvider.notifier).set(currentAsset);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
decoration: index == stackIndex.value
|
||||||
|
? const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||||
|
border: Border.fromBorderSide(
|
||||||
|
BorderSide(color: Colors.white, width: 2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||||
|
border: null,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
child: Image(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: ImmichRemoteImageProvider(assetId: assetId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,19 +8,18 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
import 'package:immich_mobile/pages/common/download_panel.dart';
|
import 'package:immich_mobile/pages/common/download_panel.dart';
|
||||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/gallery_stacked_children.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart';
|
||||||
|
@ -57,16 +56,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
final totalAssets = useState(renderList.totalAssets);
|
final totalAssets = useState(renderList.totalAssets);
|
||||||
final isZoomed = useState(false);
|
final isZoomed = useState(false);
|
||||||
final isPlayingMotionVideo = useState(false);
|
final isPlayingMotionVideo = useState(false);
|
||||||
final stackIndex = useState(-1);
|
final stackIndex = useState(0);
|
||||||
final localPosition = useRef<Offset?>(null);
|
final localPosition = useRef<Offset?>(null);
|
||||||
final currentIndex = useValueNotifier(initialIndex);
|
final currentIndex = useValueNotifier(initialIndex);
|
||||||
final loadAsset = renderList.loadAsset;
|
final loadAsset = renderList.loadAsset;
|
||||||
final currentAsset = loadAsset(currentIndex.value);
|
|
||||||
|
|
||||||
final stack = showStack && currentAsset.stackCount > 0
|
|
||||||
? ref.watch(assetStackStateProvider(currentAsset))
|
|
||||||
: <Asset>[];
|
|
||||||
final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
|
|
||||||
|
|
||||||
// // Update is playing motion video
|
// // Update is playing motion video
|
||||||
ref.listen(
|
ref.listen(
|
||||||
|
@ -197,58 +190,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget buildStackedChildren() {
|
|
||||||
if (!showStack) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: stackElements.length,
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 5,
|
|
||||||
right: 5,
|
|
||||||
bottom: 30,
|
|
||||||
),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final assetId = stackElements.elementAt(index).remoteId;
|
|
||||||
if (assetId == null) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
return Padding(
|
|
||||||
key: ValueKey(assetId),
|
|
||||||
padding: const EdgeInsets.only(right: 5),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => stackIndex.value = index,
|
|
||||||
child: Container(
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
border: (stackIndex.value == -1 && index == 0) ||
|
|
||||||
index == stackIndex.value
|
|
||||||
? Border.all(
|
|
||||||
color: Colors.white,
|
|
||||||
width: 2,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
child: Image(
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
image: ImmichRemoteImageProvider(assetId: assetId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PhotoViewGalleryPageOptions buildImage(BuildContext context, Asset asset) {
|
PhotoViewGalleryPageOptions buildImage(BuildContext context, Asset asset) {
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
onDragStart: (_, details, __) {
|
onDragStart: (_, details, __) {
|
||||||
|
@ -262,10 +203,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
onLongPressStart: asset.isMotionPhoto
|
onLongPressStart: asset.isMotionPhoto
|
||||||
? (_, __, ___) {
|
? (_, __, ___) {
|
||||||
if (asset.isMotionPhoto) {
|
|
||||||
isPlayingMotionVideo.value = true;
|
isPlayingMotionVideo.value = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
: null,
|
: null,
|
||||||
imageProvider: ImmichImage.imageProvider(asset: asset),
|
imageProvider: ImmichImage.imageProvider(asset: asset),
|
||||||
heroAttributes: _getHeroAttributes(asset),
|
heroAttributes: _getHeroAttributes(asset),
|
||||||
|
@ -316,7 +255,16 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) {
|
PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) {
|
||||||
final newAsset = loadAsset(index);
|
isPlayingMotionVideo.value = false;
|
||||||
|
var newAsset = loadAsset(index);
|
||||||
|
final stackId = newAsset.stackId;
|
||||||
|
if (stackId != null && currentIndex.value == index) {
|
||||||
|
final stackElements =
|
||||||
|
ref.read(assetStackStateProvider(newAsset.stackId!));
|
||||||
|
if (stackIndex.value < stackElements.length) {
|
||||||
|
newAsset = stackElements.elementAt(stackIndex.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newAsset.isImage && !isPlayingMotionVideo.value) {
|
if (newAsset.isImage && !isPlayingMotionVideo.value) {
|
||||||
return buildImage(context, newAsset);
|
return buildImage(context, newAsset);
|
||||||
|
@ -324,8 +272,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
return buildVideo(context, newAsset);
|
return buildVideo(context, newAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('GalleryViewerPage: Building gallery viewer page');
|
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
// Change immersive mode back to normal "edgeToEdge" mode
|
// Change immersive mode back to normal "edgeToEdge" mode
|
||||||
onPopInvokedWithResult: (didPop, _) =>
|
onPopInvokedWithResult: (didPop, _) =>
|
||||||
|
@ -387,7 +333,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
final newAsset = loadAsset(value);
|
final newAsset = loadAsset(value);
|
||||||
|
|
||||||
currentIndex.value = value;
|
currentIndex.value = value;
|
||||||
stackIndex.value = -1;
|
stackIndex.value = 0;
|
||||||
isPlayingMotionVideo.value = false;
|
isPlayingMotionVideo.value = false;
|
||||||
|
|
||||||
ref.read(currentAssetProvider.notifier).set(newAsset);
|
ref.read(currentAssetProvider.notifier).set(newAsset);
|
||||||
|
@ -418,13 +364,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Visibility(
|
GalleryStackedChildren(stackIndex),
|
||||||
visible: stack.isNotEmpty,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 80,
|
|
||||||
child: buildStackedChildren(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BottomGalleryBar(
|
BottomGalleryBar(
|
||||||
key: const ValueKey('bottom-bar'),
|
key: const ValueKey('bottom-bar'),
|
||||||
renderList: renderList,
|
renderList: renderList,
|
||||||
|
|
|
@ -7,49 +7,49 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
part 'asset_stack.provider.g.dart';
|
part 'asset_stack.provider.g.dart';
|
||||||
|
|
||||||
class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
class AssetStackNotifier extends StateNotifier<List<Asset>> {
|
||||||
final Asset _asset;
|
final String _stackId;
|
||||||
final Ref _ref;
|
final Ref _ref;
|
||||||
|
|
||||||
AssetStackNotifier(
|
AssetStackNotifier(this._stackId, this._ref) : super([]) {
|
||||||
this._asset,
|
_fetchStack(_stackId);
|
||||||
this._ref,
|
|
||||||
) : super([]) {
|
|
||||||
fetchStackChildren();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void fetchStackChildren() async {
|
void _fetchStack(String stackId) async {
|
||||||
if (mounted) {
|
if (!mounted) {
|
||||||
state = await _ref.read(assetStackProvider(_asset).future);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final stack = await _ref.read(assetStackProvider(stackId).future);
|
||||||
|
if (stack.isNotEmpty) {
|
||||||
|
state = stack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeChild(int index) {
|
void removeChild(int index) {
|
||||||
if (index < state.length) {
|
if (index < state.length) {
|
||||||
state.removeAt(index);
|
state.removeAt(index);
|
||||||
|
state = List<Asset>.from(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetStackStateProvider = StateNotifierProvider.autoDispose
|
final assetStackStateProvider = StateNotifierProvider.autoDispose
|
||||||
.family<AssetStackNotifier, List<Asset>, Asset>(
|
.family<AssetStackNotifier, List<Asset>, String>(
|
||||||
(ref, asset) => AssetStackNotifier(asset, ref),
|
(ref, stackId) => AssetStackNotifier(stackId, ref),
|
||||||
);
|
);
|
||||||
|
|
||||||
final assetStackProvider =
|
final assetStackProvider =
|
||||||
FutureProvider.autoDispose.family<List<Asset>, Asset>((ref, asset) async {
|
FutureProvider.autoDispose.family<List<Asset>, String>((ref, stackId) {
|
||||||
// Guard [local asset]
|
return ref
|
||||||
if (asset.remoteId == null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ref
|
|
||||||
.watch(dbProvider)
|
.watch(dbProvider)
|
||||||
.assets
|
.assets
|
||||||
.filter()
|
.filter()
|
||||||
.isArchivedEqualTo(false)
|
.isArchivedEqualTo(false)
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
.stackPrimaryAssetIdEqualTo(asset.remoteId)
|
.stackIdEqualTo(stackId)
|
||||||
.sortByFileCreatedAtDesc()
|
// orders primary asset first as its ID is null
|
||||||
|
.sortByStackPrimaryAssetId()
|
||||||
|
.thenByFileCreatedAtDesc()
|
||||||
.findAll();
|
.findAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -52,9 +52,10 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||||
}
|
}
|
||||||
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
|
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
|
||||||
final showControls = ref.watch(showControlsProvider);
|
final showControls = ref.watch(showControlsProvider);
|
||||||
|
final stackId = asset.stackId;
|
||||||
|
|
||||||
final stackItems = showStack && asset.stackCount > 0
|
final stackItems = showStack && stackId != null
|
||||||
? ref.watch(assetStackStateProvider(asset))
|
? ref.watch(assetStackStateProvider(stackId))
|
||||||
: <Asset>[];
|
: <Asset>[];
|
||||||
bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null;
|
bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null;
|
||||||
final navStack = AutoRouter.of(context).stackData;
|
final navStack = AutoRouter.of(context).stackData;
|
||||||
|
@ -66,9 +67,9 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||||
final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false;
|
final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false;
|
||||||
|
|
||||||
void removeAssetFromStack() {
|
void removeAssetFromStack() {
|
||||||
if (stackIndex > 0 && showStack) {
|
if (stackIndex > 0 && showStack && stackId != null) {
|
||||||
ref
|
ref
|
||||||
.read(assetStackStateProvider(asset).notifier)
|
.read(assetStackStateProvider(stackId).notifier)
|
||||||
.removeChild(stackIndex - 1);
|
.removeChild(stackIndex - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,7 +138,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(stackServiceProvider)
|
.read(stackServiceProvider)
|
||||||
.deleteStack(asset.stackId!, [asset, ...stackItems]);
|
.deleteStack(asset.stackId!, stackItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showStackActionItems() {
|
void showStackActionItems() {
|
||||||
|
|
Loading…
Reference in a new issue