mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02: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
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:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_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/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_image.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.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:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
@ -73,6 +76,8 @@ class ImmichAssetGridView extends StatefulWidget {
|
|||||||
|
|
||||||
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||||
|
final ScrollOffsetController _scrollOffsetController =
|
||||||
|
ScrollOffsetController();
|
||||||
final ItemPositionsListener _itemPositionsListener =
|
final ItemPositionsListener _itemPositionsListener =
|
||||||
ItemPositionsListener.create();
|
ItemPositionsListener.create();
|
||||||
|
|
||||||
@ -83,6 +88,12 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
final Set<Asset> _selectedAssets =
|
final Set<Asset> _selectedAssets =
|
||||||
LinkedHashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
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() {
|
Set<Asset> _getSelectedAssets() {
|
||||||
return Set.from(_selectedAssets);
|
return Set.from(_selectedAssets);
|
||||||
}
|
}
|
||||||
@ -93,20 +104,26 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
|
|
||||||
void _selectAssets(List<Asset> assets) {
|
void _selectAssets(List<Asset> assets) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
if (_dragging) {
|
||||||
|
_draggedAssets.addAll(assets);
|
||||||
|
}
|
||||||
_selectedAssets.addAll(assets);
|
_selectedAssets.addAll(assets);
|
||||||
_callSelectionListener(true);
|
_callSelectionListener(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deselectAssets(List<Asset> assets) {
|
void _deselectAssets(List<Asset> assets) {
|
||||||
|
final assetsToDeselect = assets.where(
|
||||||
|
(a) =>
|
||||||
|
widget.canDeselect ||
|
||||||
|
!(widget.preselectedAssets?.contains(a) ?? false),
|
||||||
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedAssets.removeAll(
|
_selectedAssets.removeAll(assetsToDeselect);
|
||||||
assets.where(
|
if (_dragging) {
|
||||||
(a) =>
|
_draggedAssets.removeAll(assetsToDeselect);
|
||||||
widget.canDeselect ||
|
}
|
||||||
!(widget.preselectedAssets?.contains(a) ?? false),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_callSelectionListener(_selectedAssets.isNotEmpty);
|
_callSelectionListener(_selectedAssets.isNotEmpty);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -114,6 +131,10 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
void _deselectAll() {
|
void _deselectAll() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedAssets.clear();
|
_selectedAssets.clear();
|
||||||
|
_dragAnchorAssetIndex = null;
|
||||||
|
_dragAnchorSectionIndex = null;
|
||||||
|
_draggedAssets.clear();
|
||||||
|
_dragging = false;
|
||||||
if (!widget.canDeselect &&
|
if (!widget.canDeselect &&
|
||||||
widget.preselectedAssets != null &&
|
widget.preselectedAssets != null &&
|
||||||
widget.preselectedAssets!.isNotEmpty) {
|
widget.preselectedAssets!.isNotEmpty) {
|
||||||
@ -142,6 +163,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
showStorageIndicator: widget.showStorageIndicator,
|
showStorageIndicator: widget.showStorageIndicator,
|
||||||
selectedAssets: _selectedAssets,
|
selectedAssets: _selectedAssets,
|
||||||
selectionActive: widget.selectionActive,
|
selectionActive: widget.selectionActive,
|
||||||
|
sectionIndex: index,
|
||||||
section: section,
|
section: section,
|
||||||
margin: widget.margin,
|
margin: widget.margin,
|
||||||
renderList: widget.renderList,
|
renderList: widget.renderList,
|
||||||
@ -199,6 +221,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
itemBuilder: _itemBuilder,
|
itemBuilder: _itemBuilder,
|
||||||
itemPositionsListener: _itemPositionsListener,
|
itemPositionsListener: _itemPositionsListener,
|
||||||
itemScrollController: _itemScrollController,
|
itemScrollController: _itemScrollController,
|
||||||
|
scrollOffsetController: _scrollOffsetController,
|
||||||
itemCount: widget.renderList.elements.length +
|
itemCount: widget.renderList.elements.length +
|
||||||
(widget.topWidget != null ? 1 : 0),
|
(widget.topWidget != null ? 1 : 0),
|
||||||
addRepaintBoundaries: true,
|
addRepaintBoundaries: true,
|
||||||
@ -253,6 +276,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
if (widget.visibleItemsListener != null) {
|
if (widget.visibleItemsListener != null) {
|
||||||
_itemPositionsListener.itemPositions.removeListener(_positionListener);
|
_itemPositionsListener.itemPositions.removeListener(_positionListener);
|
||||||
}
|
}
|
||||||
|
_itemPositionsListener.itemPositions.removeListener(_hapticsListener);
|
||||||
super.dispose();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
@ -315,7 +440,16 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
onPopInvoked: (didPop) => !didPop ? _deselectAll() : null,
|
onPopInvoked: (didPop) => !didPop ? _deselectAll() : null,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildAssetGrid(),
|
AssetDragRegion(
|
||||||
|
onStart: _setDragStartIndex,
|
||||||
|
onAssetEnter: _handleDragAssetEnter,
|
||||||
|
onEnd: _stopDrag,
|
||||||
|
onScroll: _dragDragScroll,
|
||||||
|
onScrollStart: () => WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) => controlBottomAppBarNotifier.minimize(),
|
||||||
|
),
|
||||||
|
child: _buildAssetGrid(),
|
||||||
|
),
|
||||||
if (widget.showMultiSelectIndicator && widget.selectionActive)
|
if (widget.showMultiSelectIndicator && widget.selectionActive)
|
||||||
_buildMultiSelectIndicator(),
|
_buildMultiSelectIndicator(),
|
||||||
],
|
],
|
||||||
@ -361,6 +495,7 @@ class _PlaceholderRow extends StatelessWidget {
|
|||||||
/// A section for the render grid
|
/// A section for the render grid
|
||||||
class _Section extends StatelessWidget {
|
class _Section extends StatelessWidget {
|
||||||
final RenderAssetGridElement section;
|
final RenderAssetGridElement section;
|
||||||
|
final int sectionIndex;
|
||||||
final Set<Asset> selectedAssets;
|
final Set<Asset> selectedAssets;
|
||||||
final bool scrolling;
|
final bool scrolling;
|
||||||
final double margin;
|
final double margin;
|
||||||
@ -377,6 +512,7 @@ class _Section extends StatelessWidget {
|
|||||||
|
|
||||||
const _Section({
|
const _Section({
|
||||||
required this.section,
|
required this.section,
|
||||||
|
required this.sectionIndex,
|
||||||
required this.scrolling,
|
required this.scrolling,
|
||||||
required this.margin,
|
required this.margin,
|
||||||
required this.assetsPerRow,
|
required this.assetsPerRow,
|
||||||
@ -435,6 +571,8 @@ class _Section extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: _AssetRow(
|
: _AssetRow(
|
||||||
key: ValueKey(i),
|
key: ValueKey(i),
|
||||||
|
rowStartIndex: i * assetsPerRow,
|
||||||
|
sectionIndex: sectionIndex,
|
||||||
assets: assetsToRender.nestedSlice(
|
assets: assetsToRender.nestedSlice(
|
||||||
i * assetsPerRow,
|
i * assetsPerRow,
|
||||||
min((i + 1) * assetsPerRow, section.count),
|
min((i + 1) * assetsPerRow, section.count),
|
||||||
@ -522,6 +660,8 @@ class _Title extends StatelessWidget {
|
|||||||
/// The row of assets
|
/// The row of assets
|
||||||
class _AssetRow extends StatelessWidget {
|
class _AssetRow extends StatelessWidget {
|
||||||
final List<Asset> assets;
|
final List<Asset> assets;
|
||||||
|
final int rowStartIndex;
|
||||||
|
final int sectionIndex;
|
||||||
final Set<Asset> selectedAssets;
|
final Set<Asset> selectedAssets;
|
||||||
final int absoluteOffset;
|
final int absoluteOffset;
|
||||||
final double width;
|
final double width;
|
||||||
@ -539,6 +679,8 @@ class _AssetRow extends StatelessWidget {
|
|||||||
|
|
||||||
const _AssetRow({
|
const _AssetRow({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.rowStartIndex,
|
||||||
|
required this.sectionIndex,
|
||||||
required this.assets,
|
required this.assets,
|
||||||
required this.absoluteOffset,
|
required this.absoluteOffset,
|
||||||
required this.width,
|
required this.width,
|
||||||
@ -594,18 +736,22 @@ class _AssetRow extends StatelessWidget {
|
|||||||
bottom: margin,
|
bottom: margin,
|
||||||
right: last ? 0.0 : margin,
|
right: last ? 0.0 : margin,
|
||||||
),
|
),
|
||||||
child: ThumbnailImage(
|
child: AssetIndexWrapper(
|
||||||
asset: asset,
|
rowIndex: rowStartIndex + index,
|
||||||
index: absoluteOffset + index,
|
sectionIndex: sectionIndex,
|
||||||
loadAsset: renderList.loadAsset,
|
child: ThumbnailImage(
|
||||||
totalAssets: renderList.totalAssets,
|
asset: asset,
|
||||||
multiselectEnabled: selectionActive,
|
index: absoluteOffset + index,
|
||||||
isSelected: isSelectionActive && selectedAssets.contains(asset),
|
loadAsset: renderList.loadAsset,
|
||||||
onSelect: () => onSelect?.call(asset),
|
totalAssets: renderList.totalAssets,
|
||||||
onDeselect: () => onDeselect?.call(asset),
|
multiselectEnabled: selectionActive,
|
||||||
showStorageIndicator: showStorageIndicator,
|
isSelected: isSelectionActive && selectedAssets.contains(asset),
|
||||||
heroOffset: heroOffset,
|
onSelect: () => onSelect?.call(asset),
|
||||||
showStack: showStack,
|
onDeselect: () => onDeselect?.call(asset),
|
||||||
|
showStorageIndicator: showStorageIndicator,
|
||||||
|
heroOffset: heroOffset,
|
||||||
|
showStack: showStack,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.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/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.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(bool shareLocal) onShare;
|
||||||
final void Function()? onFavorite;
|
final void Function()? onFavorite;
|
||||||
final void Function()? onArchive;
|
final void Function()? onArchive;
|
||||||
@ -64,6 +74,25 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
const bottomPadding = 0.20;
|
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(
|
void showForceDeleteDialog(
|
||||||
Function(bool) deleteCb, {
|
Function(bool) deleteCb, {
|
||||||
@ -242,6 +271,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return DraggableScrollableSheet(
|
return DraggableScrollableSheet(
|
||||||
|
controller: scrollController,
|
||||||
initialChildSize: hasRemote ? 0.35 : bottomPadding,
|
initialChildSize: hasRemote ? 0.35 : bottomPadding,
|
||||||
minChildSize: bottomPadding,
|
minChildSize: bottomPadding,
|
||||||
maxChildSize: hasRemote ? 0.65 : bottomPadding,
|
maxChildSize: hasRemote ? 0.65 : bottomPadding,
|
||||||
|
Loading…
Reference in New Issue
Block a user