mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
parent
7520ffd6c3
commit
5806a3ce25
203 changed files with 318 additions and 318 deletions
222
mobile/lib/widgets/asset_grid/asset_drag_region.dart
Normal file
222
mobile/lib/widgets/asset_grid/asset_drag_region.dart
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue