mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
feat(mobile): drag to select assets (#8004)
fear(mobile): drag to select assets Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
9274c0701b
commit
9e4bab7494
3 changed files with 420 additions and 21 deletions
223
mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart
Normal file
223
mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart
Normal file
|
@ -0,0 +1,223 @@
|
|||
// ignore_for_file: library_private_types_in_public_api
|
||||
// Based on https://stackoverflow.com/a/52625182
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class AssetDragRegion extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
final void Function(AssetIndex valueKey)? onStart;
|
||||
final void Function(AssetIndex valueKey)? onAssetEnter;
|
||||
final void Function()? onEnd;
|
||||
final void Function()? onScrollStart;
|
||||
final void Function(ScrollDirection direction)? onScroll;
|
||||
|
||||
const AssetDragRegion({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onStart,
|
||||
this.onAssetEnter,
|
||||
this.onEnd,
|
||||
this.onScrollStart,
|
||||
this.onScroll,
|
||||
});
|
||||
@override
|
||||
State createState() => _AssetDragRegionState();
|
||||
}
|
||||
|
||||
class _AssetDragRegionState extends State<AssetDragRegion> {
|
||||
late AssetIndex? assetUnderPointer;
|
||||
late AssetIndex? anchorAsset;
|
||||
|
||||
// Scroll related state
|
||||
static const double scrollOffset = 0.10;
|
||||
double? topScrollOffset;
|
||||
double? bottomScrollOffset;
|
||||
Timer? scrollTimer;
|
||||
late bool scrollNotified;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
assetUnderPointer = null;
|
||||
anchorAsset = null;
|
||||
scrollNotified = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
topScrollOffset = null;
|
||||
bottomScrollOffset = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawGestureDetector(
|
||||
gestures: {
|
||||
_CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<
|
||||
_CustomLongPressGestureRecognizer>(
|
||||
() => _CustomLongPressGestureRecognizer(),
|
||||
_registerCallbacks,
|
||||
),
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) {
|
||||
recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details);
|
||||
recognizer.onLongPressStart = (details) => _onLongPressStart(details);
|
||||
recognizer.onLongPressUp = _onLongPressEnd;
|
||||
recognizer.onLongPressCancel = _onLongPressEnd;
|
||||
}
|
||||
|
||||
AssetIndex? _getValueKeyAtPositon(Offset position) {
|
||||
final box = context.findAncestorRenderObjectOfType<RenderBox>();
|
||||
if (box == null) return null;
|
||||
|
||||
final hitTestResult = BoxHitTestResult();
|
||||
final local = box.globalToLocal(position);
|
||||
if (!box.hitTest(hitTestResult, position: local)) return null;
|
||||
|
||||
return (hitTestResult.path
|
||||
.firstWhereOrNull((hit) => hit.target is _AssetIndexProxy)
|
||||
?.target as _AssetIndexProxy?)
|
||||
?.index;
|
||||
}
|
||||
|
||||
void _onLongPressStart(LongPressStartDetails event) {
|
||||
/// Calculate widget height and scroll offset when long press starting instead of in [initState]
|
||||
/// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size
|
||||
final height = context.size?.height;
|
||||
if (height != null &&
|
||||
(topScrollOffset == null || bottomScrollOffset == null)) {
|
||||
topScrollOffset = height * scrollOffset;
|
||||
bottomScrollOffset = height - topScrollOffset!;
|
||||
}
|
||||
|
||||
final initialHit = _getValueKeyAtPositon(event.globalPosition);
|
||||
anchorAsset = initialHit;
|
||||
if (initialHit == null) return;
|
||||
|
||||
if (anchorAsset != null) {
|
||||
widget.onStart?.call(anchorAsset!);
|
||||
}
|
||||
}
|
||||
|
||||
void _onLongPressEnd() {
|
||||
scrollNotified = false;
|
||||
scrollTimer?.cancel();
|
||||
widget.onEnd?.call();
|
||||
}
|
||||
|
||||
void _onLongPressMove(LongPressMoveUpdateDetails event) {
|
||||
if (anchorAsset == null) return;
|
||||
if (topScrollOffset == null || bottomScrollOffset == null) return;
|
||||
|
||||
final currentDy = event.localPosition.dy;
|
||||
|
||||
if (currentDy > bottomScrollOffset!) {
|
||||
scrollTimer ??= Timer.periodic(
|
||||
const Duration(milliseconds: 50),
|
||||
(_) => widget.onScroll?.call(ScrollDirection.forward),
|
||||
);
|
||||
} else if (currentDy < topScrollOffset!) {
|
||||
scrollTimer ??= Timer.periodic(
|
||||
const Duration(milliseconds: 50),
|
||||
(_) => widget.onScroll?.call(ScrollDirection.reverse),
|
||||
);
|
||||
} else {
|
||||
scrollTimer?.cancel();
|
||||
scrollTimer = null;
|
||||
}
|
||||
|
||||
final currentlyTouchingAsset = _getValueKeyAtPositon(event.globalPosition);
|
||||
if (currentlyTouchingAsset == null) return;
|
||||
|
||||
if (assetUnderPointer != currentlyTouchingAsset) {
|
||||
if (!scrollNotified) {
|
||||
scrollNotified = true;
|
||||
widget.onScrollStart?.call();
|
||||
}
|
||||
|
||||
widget.onAssetEnter?.call(currentlyTouchingAsset);
|
||||
assetUnderPointer = currentlyTouchingAsset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer {
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
acceptGesture(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: prefer-single-widget-per-file
|
||||
class AssetIndexWrapper extends SingleChildRenderObjectWidget {
|
||||
final int rowIndex;
|
||||
final int sectionIndex;
|
||||
|
||||
const AssetIndexWrapper({
|
||||
required Widget super.child,
|
||||
required this.rowIndex,
|
||||
required this.sectionIndex,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
_AssetIndexProxy createRenderObject(BuildContext context) {
|
||||
return _AssetIndexProxy(
|
||||
index: AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
_AssetIndexProxy renderObject,
|
||||
) {
|
||||
renderObject.index =
|
||||
AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetIndexProxy extends RenderProxyBox {
|
||||
AssetIndex index;
|
||||
|
||||
_AssetIndexProxy({
|
||||
required this.index,
|
||||
});
|
||||
}
|
||||
|
||||
class AssetIndex {
|
||||
final int rowIndex;
|
||||
final int sectionIndex;
|
||||
|
||||
const AssetIndex({
|
||||
required this.rowIndex,
|
||||
required this.sectionIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(covariant AssetIndex other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.rowIndex == rowIndex && other.sectionIndex == sectionIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => rowIndex.hashCode ^ sectionIndex.hashCode;
|
||||
}
|
|
@ -5,12 +5,15 @@ import 'dart:math';
|
|||
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:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_drag_region.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
|
@ -73,6 +76,8 @@ class ImmichAssetGridView extends StatefulWidget {
|
|||
|
||||
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
final ScrollOffsetController _scrollOffsetController =
|
||||
ScrollOffsetController();
|
||||
final ItemPositionsListener _itemPositionsListener =
|
||||
ItemPositionsListener.create();
|
||||
|
||||
|
@ -83,6 +88,12 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||
final Set<Asset> _selectedAssets =
|
||||
LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
||||
|
||||
bool _dragging = false;
|
||||
int? _dragAnchorAssetIndex;
|
||||
int? _dragAnchorSectionIndex;
|
||||
final Set<Asset> _draggedAssets =
|
||||
HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
||||
|
||||
Set<Asset> _getSelectedAssets() {
|
||||
return Set.from(_selectedAssets);
|
||||
}
|
||||
|
@ -93,20 +104,26 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||
|
||||
void _selectAssets(List<Asset> assets) {
|
||||
setState(() {
|
||||
if (_dragging) {
|
||||
_draggedAssets.addAll(assets);
|
||||
}
|
||||
_selectedAssets.addAll(assets);
|
||||
_callSelectionListener(true);
|
||||
});
|
||||
}
|
||||
|
||||
void _deselectAssets(List<Asset> assets) {
|
||||
final assetsToDeselect = assets.where(
|
||||
(a) =>
|
||||
widget.canDeselect ||
|
||||
!(widget.preselectedAssets?.contains(a) ?? false),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_selectedAssets.removeAll(
|
||||
assets.where(
|
||||
(a) =>
|
||||
widget.canDeselect ||
|
||||
!(widget.preselectedAssets?.contains(a) ?? false),
|
||||
),
|
||||
);
|
||||
_selectedAssets.removeAll(assetsToDeselect);
|
||||
if (_dragging) {
|
||||
_draggedAssets.removeAll(assetsToDeselect);
|
||||
}
|
||||
_callSelectionListener(_selectedAssets.isNotEmpty);
|
||||
});
|
||||
}
|
||||
|
@ -114,6 +131,10 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||
void _deselectAll() {
|
||||
setState(() {
|
||||
_selectedAssets.clear();
|
||||
_dragAnchorAssetIndex = null;
|
||||
_dragAnchorSectionIndex = null;
|
||||
_draggedAssets.clear();
|
||||
_dragging = false;
|
||||
if (!widget.canDeselect &&
|
||||
widget.preselectedAssets != null &&
|
||||
widget.preselectedAssets!.isNotEmpty) {
|
||||
|
@ -142,6 +163,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||
showStorageIndicator: widget.showStorageIndicator,
|
||||
selectedAssets: _selectedAssets,
|
||||
selectionActive: widget.selectionActive,
|
||||
sectionIndex: index,
|
||||
section: section,
|
||||
margin: widget.margin,
|
||||
renderList: widget.renderList,
|
||||
|
@ -199,6 +221,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||
itemBuilder: _itemBuilder,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
itemScrollController: _itemScrollController,
|
||||
scrollOffsetController: _scrollOffsetController,
|
||||
itemCount: widget.renderList.elements.length +
|
||||
(widget.topWidget != null ? 1 : 0),
|
||||
addRepaintBoundaries: true,
|
||||
|
@ -253,6 +276,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||
if (widget.visibleItemsListener != null) {
|
||||
_itemPositionsListener.itemPositions.removeListener(_positionListener);
|
||||
}
|
||||
_itemPositionsListener.itemPositions.removeListener(_hapticsListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -308,6 +332,107 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||
);
|
||||
}
|
||||
|
||||
void _setDragStartIndex(AssetIndex index) {
|
||||
setState(() {
|
||||
_dragAnchorAssetIndex = index.rowIndex;
|
||||
_dragAnchorSectionIndex = index.sectionIndex;
|
||||
_dragging = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _stopDrag() {
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
_draggedAssets.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void _dragDragScroll(ScrollDirection direction) {
|
||||
_scrollOffsetController.animateScroll(
|
||||
offset: direction == ScrollDirection.forward ? 175 : -175,
|
||||
duration: const Duration(milliseconds: 125),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDragAssetEnter(AssetIndex index) {
|
||||
if (_dragAnchorSectionIndex == null || _dragAnchorAssetIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final dragAnchorSectionIndex = _dragAnchorSectionIndex!;
|
||||
final dragAnchorAssetIndex = _dragAnchorAssetIndex!;
|
||||
|
||||
late final int startSectionIndex;
|
||||
late final int startSectionAssetIndex;
|
||||
late final int endSectionIndex;
|
||||
late final int endSectionAssetIndex;
|
||||
|
||||
if (index.sectionIndex < dragAnchorSectionIndex) {
|
||||
startSectionIndex = index.sectionIndex;
|
||||
startSectionAssetIndex = index.rowIndex;
|
||||
endSectionIndex = dragAnchorSectionIndex;
|
||||
endSectionAssetIndex = dragAnchorAssetIndex;
|
||||
} else if (index.sectionIndex > dragAnchorSectionIndex) {
|
||||
startSectionIndex = dragAnchorSectionIndex;
|
||||
startSectionAssetIndex = dragAnchorAssetIndex;
|
||||
endSectionIndex = index.sectionIndex;
|
||||
endSectionAssetIndex = index.rowIndex;
|
||||
} else {
|
||||
startSectionIndex = dragAnchorSectionIndex;
|
||||
endSectionIndex = dragAnchorSectionIndex;
|
||||
|
||||
// If same section, assign proper start / end asset Index
|
||||
if (dragAnchorAssetIndex < index.rowIndex) {
|
||||
startSectionAssetIndex = dragAnchorAssetIndex;
|
||||
endSectionAssetIndex = index.rowIndex;
|
||||
} else {
|
||||
startSectionAssetIndex = index.rowIndex;
|
||||
endSectionAssetIndex = dragAnchorAssetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
final selectedAssets = <Asset>{};
|
||||
var currentSectionIndex = startSectionIndex;
|
||||
while (currentSectionIndex < endSectionIndex) {
|
||||
final section =
|
||||
widget.renderList.elements.elementAtOrNull(currentSectionIndex);
|
||||
if (section == null) continue;
|
||||
|
||||
final sectionAssets =
|
||||
widget.renderList.loadAssets(section.offset, section.count);
|
||||
|
||||
if (currentSectionIndex == startSectionIndex) {
|
||||
selectedAssets.addAll(
|
||||
sectionAssets.slice(startSectionAssetIndex, sectionAssets.length),
|
||||
);
|
||||
} else {
|
||||
selectedAssets.addAll(sectionAssets);
|
||||
}
|
||||
|
||||
currentSectionIndex += 1;
|
||||
}
|
||||
|
||||
final section = widget.renderList.elements.elementAtOrNull(endSectionIndex);
|
||||
if (section != null) {
|
||||
final sectionAssets =
|
||||
widget.renderList.loadAssets(section.offset, section.count);
|
||||
if (startSectionIndex == endSectionIndex) {
|
||||
selectedAssets.addAll(
|
||||
sectionAssets.slice(startSectionAssetIndex, endSectionAssetIndex + 1),
|
||||
);
|
||||
} else {
|
||||
selectedAssets.addAll(
|
||||
sectionAssets.slice(0, endSectionAssetIndex + 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_deselectAssets(_draggedAssets.toList());
|
||||
_draggedAssets.clear();
|
||||
_draggedAssets.addAll(selectedAssets);
|
||||
_selectAssets(_draggedAssets.toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
|
@ -315,7 +440,16 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||
onPopInvoked: (didPop) => !didPop ? _deselectAll() : null,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildAssetGrid(),
|
||||
AssetDragRegion(
|
||||
onStart: _setDragStartIndex,
|
||||
onAssetEnter: _handleDragAssetEnter,
|
||||
onEnd: _stopDrag,
|
||||
onScroll: _dragDragScroll,
|
||||
onScrollStart: () => WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => controlBottomAppBarNotifier.minimize(),
|
||||
),
|
||||
child: _buildAssetGrid(),
|
||||
),
|
||||
if (widget.showMultiSelectIndicator && widget.selectionActive)
|
||||
_buildMultiSelectIndicator(),
|
||||
],
|
||||
|
@ -361,6 +495,7 @@ class _PlaceholderRow extends StatelessWidget {
|
|||
/// A section for the render grid
|
||||
class _Section extends StatelessWidget {
|
||||
final RenderAssetGridElement section;
|
||||
final int sectionIndex;
|
||||
final Set<Asset> selectedAssets;
|
||||
final bool scrolling;
|
||||
final double margin;
|
||||
|
@ -377,6 +512,7 @@ class _Section extends StatelessWidget {
|
|||
|
||||
const _Section({
|
||||
required this.section,
|
||||
required this.sectionIndex,
|
||||
required this.scrolling,
|
||||
required this.margin,
|
||||
required this.assetsPerRow,
|
||||
|
@ -435,6 +571,8 @@ class _Section extends StatelessWidget {
|
|||
)
|
||||
: _AssetRow(
|
||||
key: ValueKey(i),
|
||||
rowStartIndex: i * assetsPerRow,
|
||||
sectionIndex: sectionIndex,
|
||||
assets: assetsToRender.nestedSlice(
|
||||
i * assetsPerRow,
|
||||
min((i + 1) * assetsPerRow, section.count),
|
||||
|
@ -522,6 +660,8 @@ class _Title extends StatelessWidget {
|
|||
/// The row of assets
|
||||
class _AssetRow extends StatelessWidget {
|
||||
final List<Asset> assets;
|
||||
final int rowStartIndex;
|
||||
final int sectionIndex;
|
||||
final Set<Asset> selectedAssets;
|
||||
final int absoluteOffset;
|
||||
final double width;
|
||||
|
@ -539,6 +679,8 @@ class _AssetRow extends StatelessWidget {
|
|||
|
||||
const _AssetRow({
|
||||
super.key,
|
||||
required this.rowStartIndex,
|
||||
required this.sectionIndex,
|
||||
required this.assets,
|
||||
required this.absoluteOffset,
|
||||
required this.width,
|
||||
|
@ -594,18 +736,22 @@ class _AssetRow extends StatelessWidget {
|
|||
bottom: margin,
|
||||
right: last ? 0.0 : margin,
|
||||
),
|
||||
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: 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
|
@ -11,8 +12,17 @@ import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
|||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
|
||||
|
||||
class ControlBottomAppBar extends ConsumerWidget {
|
||||
final controlBottomAppBarNotifier = ControlBottomAppBarNotifier();
|
||||
|
||||
class ControlBottomAppBarNotifier with ChangeNotifier {
|
||||
void minimize() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class ControlBottomAppBar extends HookConsumerWidget {
|
||||
final void Function(bool shareLocal) onShare;
|
||||
final void Function()? onFavorite;
|
||||
final void Function()? onArchive;
|
||||
|
@ -64,6 +74,25 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||
const bottomPadding = 0.20;
|
||||
final scrollController = useDraggableScrollController();
|
||||
|
||||
void minimize() {
|
||||
scrollController.animateTo(
|
||||
bottomPadding,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
controlBottomAppBarNotifier.addListener(minimize);
|
||||
return () {
|
||||
controlBottomAppBarNotifier.removeListener(minimize);
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
void showForceDeleteDialog(
|
||||
Function(bool) deleteCb, {
|
||||
|
@ -242,6 +271,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||
}
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
controller: scrollController,
|
||||
initialChildSize: hasRemote ? 0.35 : bottomPadding,
|
||||
minChildSize: bottomPadding,
|
||||
maxChildSize: hasRemote ? 0.65 : bottomPadding,
|
||||
|
|
Loading…
Reference in a new issue