#101 adapted ScaleGestureRecognizer to eagerly accept multiple pointer gestures
This commit is contained in:
parent
769c8f9f2f
commit
005339094b
27 changed files with 608 additions and 177 deletions
|
@ -454,7 +454,7 @@ class AvesEntry {
|
|||
_catalogMetadata = newMetadata;
|
||||
_bestTitle = null;
|
||||
_tags = null;
|
||||
metadataChangeNotifier.notifyListeners();
|
||||
metadataChangeNotifier.notify();
|
||||
|
||||
_onVisualFieldChanged(oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
||||
}
|
||||
|
@ -496,7 +496,7 @@ class AvesEntry {
|
|||
|
||||
set addressDetails(AddressDetails? newAddress) {
|
||||
_addressDetails = newAddress;
|
||||
addressChangeNotifier.notifyListeners();
|
||||
addressChangeNotifier.notify();
|
||||
}
|
||||
|
||||
Future<void> locate({required bool background, required bool force, required Locale geocoderLocale}) async {
|
||||
|
@ -620,7 +620,7 @@ class AvesEntry {
|
|||
}
|
||||
|
||||
await _onVisualFieldChanged(oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
||||
metadataChangeNotifier.notifyListeners();
|
||||
metadataChangeNotifier.notify();
|
||||
}
|
||||
|
||||
Future<void> refresh({
|
||||
|
@ -717,7 +717,7 @@ class AvesEntry {
|
|||
Future<void> _onVisualFieldChanged(int? oldDateModifiedSecs, int oldRotationDegrees, bool oldIsFlipped) async {
|
||||
if (oldDateModifiedSecs != dateModifiedSecs || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
|
||||
await EntryCache.evict(uri, mimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
||||
imageChangeNotifier.notifyListeners();
|
||||
imageChangeNotifier.notify();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:aves/model/filters/location.dart';
|
|||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/events.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/analysis_controller.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/model/source/events.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/services/analysis_service.dart';
|
||||
|
@ -183,7 +184,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
return;
|
||||
}
|
||||
await _moveEntry(entry, newFields, persist: persist);
|
||||
entry.metadataChangeNotifier.notifyListeners();
|
||||
entry.metadataChangeNotifier.notify();
|
||||
eventBus.fire(EntryMovedEvent({entry}));
|
||||
completer.complete(true);
|
||||
},
|
||||
|
@ -381,46 +382,3 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryAddedEvent {
|
||||
final Set<AvesEntry>? entries;
|
||||
|
||||
const EntryAddedEvent([this.entries]);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryRemovedEvent {
|
||||
final Set<AvesEntry> entries;
|
||||
|
||||
const EntryRemovedEvent(this.entries);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryMovedEvent {
|
||||
final Set<AvesEntry> entries;
|
||||
|
||||
const EntryMovedEvent(this.entries);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryRefreshedEvent {
|
||||
final Set<AvesEntry> entries;
|
||||
|
||||
const EntryRefreshedEvent(this.entries);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class FilterVisibilityChangedEvent {
|
||||
final Set<CollectionFilter> filters;
|
||||
final bool visible;
|
||||
|
||||
const FilterVisibilityChangedEvent(this.filters, this.visible);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ProgressEvent {
|
||||
final int done, total;
|
||||
|
||||
const ProgressEvent({required this.done, required this.total});
|
||||
}
|
||||
|
|
46
lib/model/source/events.dart
Normal file
46
lib/model/source/events.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@immutable
|
||||
class EntryAddedEvent {
|
||||
final Set<AvesEntry>? entries;
|
||||
|
||||
const EntryAddedEvent([this.entries]);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryRemovedEvent {
|
||||
final Set<AvesEntry> entries;
|
||||
|
||||
const EntryRemovedEvent(this.entries);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryMovedEvent {
|
||||
final Set<AvesEntry> entries;
|
||||
|
||||
const EntryMovedEvent(this.entries);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryRefreshedEvent {
|
||||
final Set<AvesEntry> entries;
|
||||
|
||||
const EntryRefreshedEvent(this.entries);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class FilterVisibilityChangedEvent {
|
||||
final Set<CollectionFilter> filters;
|
||||
final bool visible;
|
||||
|
||||
const FilterVisibilityChangedEvent(this.filters, this.visible);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ProgressEvent {
|
||||
final int done, total;
|
||||
|
||||
const ProgressEvent({required this.done, required this.total});
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
// cf flutter/foundation `consolidateHttpClientResponseBytes`
|
||||
// adapted from Flutter `_OutputBuffer` in `/foundation/consolidate_response.dart`
|
||||
class OutputBuffer extends ByteConversionSinkBase {
|
||||
List<List<int>>? _chunks = <List<int>>[];
|
||||
int _contentLength = 0;
|
||||
|
@ -21,8 +21,8 @@ class OutputBuffer extends ByteConversionSinkBase {
|
|||
return;
|
||||
}
|
||||
_bytes = Uint8List(_contentLength);
|
||||
var offset = 0;
|
||||
for (final chunk in _chunks!) {
|
||||
int offset = 0;
|
||||
for (final List<int> chunk in _chunks!) {
|
||||
_bytes!.setRange(offset, offset + chunk.length, chunk);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
|
|
@ -32,10 +32,10 @@ class AndroidFileUtils {
|
|||
downloadPath = pContext.join(primaryStorage, 'Download');
|
||||
moviesPath = pContext.join(primaryStorage, 'Movies');
|
||||
picturesPath = pContext.join(primaryStorage, 'Pictures');
|
||||
avesVideoCapturesPath = pContext.join(dcimPath, 'Videocaptures');
|
||||
avesVideoCapturesPath = pContext.join(dcimPath, 'Video Captures');
|
||||
videoCapturesPaths = {
|
||||
// from Samsung
|
||||
pContext.join(dcimPath, 'Video Captures'),
|
||||
pContext.join(dcimPath, 'Videocaptures'),
|
||||
// from Aves
|
||||
avesVideoCapturesPath,
|
||||
};
|
||||
|
|
|
@ -1,26 +1,9 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// reimplemented ChangeNotifier so that it can be used anywhere, not just as a mixin
|
||||
class AChangeNotifier implements Listenable {
|
||||
ObserverList<VoidCallback>? _listeners = ObserverList<VoidCallback>();
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) => _listeners!.add(listener);
|
||||
|
||||
@override
|
||||
void removeListener(VoidCallback listener) => _listeners!.remove(listener);
|
||||
|
||||
void dispose() => _listeners = null;
|
||||
|
||||
void notifyListeners() {
|
||||
if (_listeners == null) return;
|
||||
final localListeners = List<VoidCallback>.from(_listeners!);
|
||||
for (final listener in localListeners) {
|
||||
try {
|
||||
if (_listeners!.contains(listener)) listener();
|
||||
} catch (error, stack) {
|
||||
debugPrint('$runtimeType failed to notify listeners with error=$error\n$stack');
|
||||
}
|
||||
}
|
||||
// `ChangeNotifier` wrapper so that it can be used anywhere, not just as a mixin
|
||||
class AChangeNotifier extends ChangeNotifier {
|
||||
void notify() {
|
||||
// why is this protected?
|
||||
super.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,8 +42,6 @@ class _EntryQueryBarState extends State<EntryQueryBar> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
// TODO TLAD focus on text field when enabled (`autofocus` is unusable)
|
||||
// TODO TLAD lose focus on navigation to viewer?
|
||||
void _registerWidget(EntryQueryBar widget) {
|
||||
widget.queryNotifier.addListener(_onQueryChanged);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/model/source/events.dart';
|
||||
import 'package:aves/model/source/source_state.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
|
|
|
@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:highlight/highlight.dart' show highlight, Node;
|
||||
|
||||
// TODO TLAD use the TextSpan getter instead of this modified `HighlightView` when this is fixed: https://github.com/git-touch/highlight/issues/6
|
||||
// adapted from package `flutter_highlight` v0.7.0 `HighlightView`
|
||||
// TODO TLAD use the TextSpan getter when this is fixed: https://github.com/git-touch/highlight/issues/6
|
||||
|
||||
/// Highlight Flutter Widget
|
||||
class AvesHighlightView extends StatelessWidget {
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/*
|
||||
This is derived from `draggable_scrollbar` package v0.0.4:
|
||||
adapted from package `draggable_scrollbar` v0.0.4:
|
||||
- removed default thumb builders
|
||||
- allow any `ScrollView` as child
|
||||
- allow any `Widget` as label content
|
||||
|
|
395
lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart
Normal file
395
lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart
Normal file
|
@ -0,0 +1,395 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
// adapted from Flutter `ScaleGestureRecognizer` in `/gestures/scale.dart`
|
||||
// ignore_for_file: curly_braces_in_flow_control_structures, deprecated_member_use, unnecessary_null_comparison
|
||||
|
||||
/// The possible states of a [ScaleGestureRecognizer].
|
||||
enum _ScaleState {
|
||||
/// The recognizer is ready to start recognizing a gesture.
|
||||
ready,
|
||||
|
||||
/// The sequence of pointer events seen thus far is consistent with a scale
|
||||
/// gesture but the gesture has not been accepted definitively.
|
||||
possible,
|
||||
|
||||
/// The sequence of pointer events seen thus far has been accepted
|
||||
/// definitively as a scale gesture.
|
||||
accepted,
|
||||
|
||||
/// The sequence of pointer events seen thus far has been accepted
|
||||
/// definitively as a scale gesture and the pointers established a focal point
|
||||
/// and initial scale.
|
||||
started,
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool _isFlingGesture(Velocity velocity) {
|
||||
assert(velocity != null);
|
||||
final double speedSquared = velocity.pixelsPerSecond.distanceSquared;
|
||||
return speedSquared > kMinFlingVelocity * kMinFlingVelocity;
|
||||
}
|
||||
|
||||
/// Defines a line between two pointers on screen.
|
||||
///
|
||||
/// [_LineBetweenPointers] is an abstraction of a line between two pointers in
|
||||
/// contact with the screen. Used to track the rotation of a scale gesture.
|
||||
class _LineBetweenPointers {
|
||||
/// Creates a [_LineBetweenPointers]. None of the [pointerStartLocation], [pointerStartId]
|
||||
/// [pointerEndLocation] and [pointerEndId] must be null. [pointerStartId] and [pointerEndId]
|
||||
/// should be different.
|
||||
_LineBetweenPointers({
|
||||
this.pointerStartLocation = Offset.zero,
|
||||
this.pointerStartId = 0,
|
||||
this.pointerEndLocation = Offset.zero,
|
||||
this.pointerEndId = 1,
|
||||
}) : assert(pointerStartLocation != null && pointerEndLocation != null),
|
||||
assert(pointerStartId != null && pointerEndId != null),
|
||||
assert(pointerStartId != pointerEndId);
|
||||
|
||||
// The location and the id of the pointer that marks the start of the line.
|
||||
final Offset pointerStartLocation;
|
||||
final int pointerStartId;
|
||||
|
||||
// The location and the id of the pointer that marks the end of the line.
|
||||
final Offset pointerEndLocation;
|
||||
final int pointerEndId;
|
||||
}
|
||||
|
||||
/// Recognizes a scale gesture.
|
||||
///
|
||||
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
|
||||
/// calculates their focal point, indicated scale, and rotation. When a focal
|
||||
/// pointer is established, the recognizer calls [onStart]. As the focal point,
|
||||
/// scale, rotation change, the recognizer calls [onUpdate]. When the pointers
|
||||
/// are no longer in contact with the screen, the recognizer calls [onEnd].
|
||||
class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
/// Create a gesture recognizer for interactions intended for scaling content.
|
||||
///
|
||||
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
|
||||
EagerScaleGestureRecognizer({
|
||||
Object? debugOwner,
|
||||
@Deprecated(
|
||||
'Migrate to supportedDevices. '
|
||||
'This feature was deprecated after v2.3.0-1.0.pre.',
|
||||
)
|
||||
PointerDeviceKind? kind,
|
||||
Set<PointerDeviceKind>? supportedDevices,
|
||||
this.dragStartBehavior = DragStartBehavior.down,
|
||||
}) : assert(dragStartBehavior != null),
|
||||
super(
|
||||
debugOwner: debugOwner,
|
||||
kind: kind,
|
||||
supportedDevices: supportedDevices,
|
||||
);
|
||||
|
||||
/// Determines what point is used as the starting point in all calculations
|
||||
/// involving this gesture.
|
||||
///
|
||||
/// When set to [DragStartBehavior.down], the scale is calculated starting
|
||||
/// from the position where the pointer first contacted the screen.
|
||||
///
|
||||
/// When set to [DragStartBehavior.start], the scale is calculated starting
|
||||
/// from the position where the scale gesture began. The scale gesture may
|
||||
/// begin after the time that the pointer first contacted the screen if there
|
||||
/// are multiple listeners competing for the gesture. In that case, the
|
||||
/// gesture arena waits to determine whether or not the gesture is a scale
|
||||
/// gesture before giving the gesture to this GestureRecognizer. This happens
|
||||
/// in the case of nested GestureDetectors, for example.
|
||||
///
|
||||
/// Defaults to [DragStartBehavior.down].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation,
|
||||
/// which provides more information about the gesture arena.
|
||||
DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// The pointers in contact with the screen have established a focal point and
|
||||
/// initial scale of 1.0.
|
||||
///
|
||||
/// This won't be called until the gesture arena has determined that this
|
||||
/// GestureRecognizer has won the gesture.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation,
|
||||
/// which provides more information about the gesture arena.
|
||||
GestureScaleStartCallback? onStart;
|
||||
|
||||
/// The pointers in contact with the screen have indicated a new focal point
|
||||
/// and/or scale.
|
||||
GestureScaleUpdateCallback? onUpdate;
|
||||
|
||||
/// The pointers are no longer in contact with the screen.
|
||||
GestureScaleEndCallback? onEnd;
|
||||
|
||||
_ScaleState _state = _ScaleState.ready;
|
||||
|
||||
Matrix4? _lastTransform;
|
||||
|
||||
late Offset _initialFocalPoint;
|
||||
late Offset _currentFocalPoint;
|
||||
late double _initialSpan;
|
||||
late double _currentSpan;
|
||||
late double _initialHorizontalSpan;
|
||||
late double _currentHorizontalSpan;
|
||||
late double _initialVerticalSpan;
|
||||
late double _currentVerticalSpan;
|
||||
_LineBetweenPointers? _initialLine;
|
||||
_LineBetweenPointers? _currentLine;
|
||||
late Map<int, Offset> _pointerLocations;
|
||||
late List<int> _pointerQueue; // A queue to sort pointers in order of entrance
|
||||
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
||||
|
||||
double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
|
||||
|
||||
double get _horizontalScaleFactor => _initialHorizontalSpan > 0.0 ? _currentHorizontalSpan / _initialHorizontalSpan : 1.0;
|
||||
|
||||
double get _verticalScaleFactor => _initialVerticalSpan > 0.0 ? _currentVerticalSpan / _initialVerticalSpan : 1.0;
|
||||
|
||||
double _computeRotationFactor() {
|
||||
if (_initialLine == null || _currentLine == null) {
|
||||
return 0.0;
|
||||
}
|
||||
final double fx = _initialLine!.pointerStartLocation.dx;
|
||||
final double fy = _initialLine!.pointerStartLocation.dy;
|
||||
final double sx = _initialLine!.pointerEndLocation.dx;
|
||||
final double sy = _initialLine!.pointerEndLocation.dy;
|
||||
|
||||
final double nfx = _currentLine!.pointerStartLocation.dx;
|
||||
final double nfy = _currentLine!.pointerStartLocation.dy;
|
||||
final double nsx = _currentLine!.pointerEndLocation.dx;
|
||||
final double nsy = _currentLine!.pointerEndLocation.dy;
|
||||
|
||||
final double angle1 = math.atan2(fy - sy, fx - sx);
|
||||
final double angle2 = math.atan2(nfy - nsy, nfx - nsx);
|
||||
|
||||
return angle2 - angle1;
|
||||
}
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
super.addAllowedPointer(event);
|
||||
_velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind);
|
||||
if (_state == _ScaleState.ready) {
|
||||
_state = _ScaleState.possible;
|
||||
_initialSpan = 0.0;
|
||||
_currentSpan = 0.0;
|
||||
_initialHorizontalSpan = 0.0;
|
||||
_currentHorizontalSpan = 0.0;
|
||||
_initialVerticalSpan = 0.0;
|
||||
_currentVerticalSpan = 0.0;
|
||||
_pointerLocations = <int, Offset>{};
|
||||
_pointerQueue = <int>[];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event) {
|
||||
assert(_state != _ScaleState.ready);
|
||||
bool didChangeConfiguration = false;
|
||||
bool shouldStartIfAccepted = false;
|
||||
if (event is PointerMoveEvent) {
|
||||
final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
|
||||
if (!event.synthesized) tracker.addPosition(event.timeStamp, event.position);
|
||||
_pointerLocations[event.pointer] = event.position;
|
||||
shouldStartIfAccepted = true;
|
||||
_lastTransform = event.transform;
|
||||
} else if (event is PointerDownEvent) {
|
||||
_pointerLocations[event.pointer] = event.position;
|
||||
_pointerQueue.add(event.pointer);
|
||||
didChangeConfiguration = true;
|
||||
shouldStartIfAccepted = true;
|
||||
_lastTransform = event.transform;
|
||||
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
|
||||
_pointerLocations.remove(event.pointer);
|
||||
_pointerQueue.remove(event.pointer);
|
||||
didChangeConfiguration = true;
|
||||
_lastTransform = event.transform;
|
||||
}
|
||||
|
||||
_updateLines();
|
||||
_update();
|
||||
|
||||
if (!didChangeConfiguration || _reconfigure(event.pointer)) _advanceStateMachine(shouldStartIfAccepted, event.kind);
|
||||
stopTrackingIfPointerNoLongerDown(event);
|
||||
}
|
||||
|
||||
void _update() {
|
||||
final int count = _pointerLocations.keys.length;
|
||||
|
||||
// Compute the focal point
|
||||
Offset focalPoint = Offset.zero;
|
||||
for (final int pointer in _pointerLocations.keys) focalPoint += _pointerLocations[pointer]!;
|
||||
_currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero;
|
||||
|
||||
// Span is the average deviation from focal point. Horizontal and vertical
|
||||
// spans are the average deviations from the focal point's horizontal and
|
||||
// vertical coordinates, respectively.
|
||||
double totalDeviation = 0.0;
|
||||
double totalHorizontalDeviation = 0.0;
|
||||
double totalVerticalDeviation = 0.0;
|
||||
for (final int pointer in _pointerLocations.keys) {
|
||||
totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]!).distance;
|
||||
totalHorizontalDeviation += (_currentFocalPoint.dx - _pointerLocations[pointer]!.dx).abs();
|
||||
totalVerticalDeviation += (_currentFocalPoint.dy - _pointerLocations[pointer]!.dy).abs();
|
||||
}
|
||||
_currentSpan = count > 0 ? totalDeviation / count : 0.0;
|
||||
_currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0;
|
||||
_currentVerticalSpan = count > 0 ? totalVerticalDeviation / count : 0.0;
|
||||
}
|
||||
|
||||
/// Updates [_initialLine] and [_currentLine] accordingly to the situation of
|
||||
/// the registered pointers.
|
||||
void _updateLines() {
|
||||
final int count = _pointerLocations.keys.length;
|
||||
assert(_pointerQueue.length >= count);
|
||||
|
||||
/// In case of just one pointer registered, reconfigure [_initialLine]
|
||||
if (count < 2) {
|
||||
_initialLine = _currentLine;
|
||||
} else if (_initialLine != null && _initialLine!.pointerStartId == _pointerQueue[0] && _initialLine!.pointerEndId == _pointerQueue[1]) {
|
||||
/// Rotation updated, set the [_currentLine]
|
||||
_currentLine = _LineBetweenPointers(
|
||||
pointerStartId: _pointerQueue[0],
|
||||
pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
|
||||
pointerEndId: _pointerQueue[1],
|
||||
pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
|
||||
);
|
||||
} else {
|
||||
/// A new rotation process is on the way, set the [_initialLine]
|
||||
_initialLine = _LineBetweenPointers(
|
||||
pointerStartId: _pointerQueue[0],
|
||||
pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
|
||||
pointerEndId: _pointerQueue[1],
|
||||
pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
|
||||
);
|
||||
_currentLine = _initialLine;
|
||||
}
|
||||
}
|
||||
|
||||
bool _reconfigure(int pointer) {
|
||||
_initialFocalPoint = _currentFocalPoint;
|
||||
_initialSpan = _currentSpan;
|
||||
_initialLine = _currentLine;
|
||||
_initialHorizontalSpan = _currentHorizontalSpan;
|
||||
_initialVerticalSpan = _currentVerticalSpan;
|
||||
if (_state == _ScaleState.started) {
|
||||
if (onEnd != null) {
|
||||
final VelocityTracker tracker = _velocityTrackers[pointer]!;
|
||||
|
||||
Velocity velocity = tracker.getVelocity();
|
||||
if (_isFlingGesture(velocity)) {
|
||||
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
||||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerQueue.length)));
|
||||
} else {
|
||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: Velocity.zero, pointerCount: _pointerQueue.length)));
|
||||
}
|
||||
}
|
||||
_state = _ScaleState.accepted;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
|
||||
if (_state == _ScaleState.ready) _state = _ScaleState.possible;
|
||||
|
||||
// TLAD insert start
|
||||
if (_pointerQueue.length == 2) {
|
||||
resolve(GestureDisposition.accepted);
|
||||
}
|
||||
// TLAD insert end
|
||||
|
||||
if (_state == _ScaleState.possible) {
|
||||
final double spanDelta = (_currentSpan - _initialSpan).abs();
|
||||
final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance;
|
||||
if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind)) resolve(GestureDisposition.accepted);
|
||||
} else if (_state.index >= _ScaleState.accepted.index) {
|
||||
resolve(GestureDisposition.accepted);
|
||||
}
|
||||
|
||||
if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
|
||||
_state = _ScaleState.started;
|
||||
_dispatchOnStartCallbackIfNeeded();
|
||||
}
|
||||
|
||||
if (_state == _ScaleState.started && onUpdate != null)
|
||||
invokeCallback<void>('onUpdate', () {
|
||||
onUpdate!(ScaleUpdateDetails(
|
||||
scale: _scaleFactor,
|
||||
horizontalScale: _horizontalScaleFactor,
|
||||
verticalScale: _verticalScaleFactor,
|
||||
focalPoint: _currentFocalPoint,
|
||||
localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint),
|
||||
rotation: _computeRotationFactor(),
|
||||
pointerCount: _pointerQueue.length,
|
||||
delta: _currentFocalPoint - _initialFocalPoint,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
void _dispatchOnStartCallbackIfNeeded() {
|
||||
assert(_state == _ScaleState.started);
|
||||
if (onStart != null)
|
||||
invokeCallback<void>('onStart', () {
|
||||
onStart!(ScaleStartDetails(
|
||||
focalPoint: _currentFocalPoint,
|
||||
localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint),
|
||||
pointerCount: _pointerQueue.length,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {
|
||||
if (_state == _ScaleState.possible) {
|
||||
_state = _ScaleState.started;
|
||||
_dispatchOnStartCallbackIfNeeded();
|
||||
if (dragStartBehavior == DragStartBehavior.start) {
|
||||
_initialFocalPoint = _currentFocalPoint;
|
||||
_initialSpan = _currentSpan;
|
||||
_initialLine = _currentLine;
|
||||
_initialHorizontalSpan = _currentHorizontalSpan;
|
||||
_initialVerticalSpan = _currentVerticalSpan;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
stopTrackingPointer(pointer);
|
||||
}
|
||||
|
||||
@override
|
||||
void didStopTrackingLastPointer(int pointer) {
|
||||
switch (_state) {
|
||||
case _ScaleState.possible:
|
||||
resolve(GestureDisposition.rejected);
|
||||
break;
|
||||
case _ScaleState.ready:
|
||||
assert(false); // We should have not seen a pointer yet
|
||||
break;
|
||||
case _ScaleState.accepted:
|
||||
break;
|
||||
case _ScaleState.started:
|
||||
assert(false); // We should be in the accepted state when user is done
|
||||
break;
|
||||
}
|
||||
_state = _ScaleState.ready;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_velocityTrackers.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'scale';
|
||||
}
|
|
@ -3,7 +3,7 @@ import 'dart:ui' as ui;
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// adapted from `RawImage`, `paintImage()` from `DecorationImagePainter`, etc.
|
||||
// adapted from Flutter `RawImage`, `paintImage()` from `DecorationImagePainter`, etc.
|
||||
// to transition between 2 different fits during hero animation:
|
||||
// - BoxFit.cover at t=0
|
||||
// - BoxFit.contain at t=1
|
||||
|
|
|
@ -12,7 +12,7 @@ import 'package:provider/provider.dart';
|
|||
// With the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen
|
||||
// because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 547), as of Flutter v1.17.0.
|
||||
// cf https://github.com/flutter/flutter/issues/49027
|
||||
// adapted from `RenderSliverFixedExtentBoxAdaptor`
|
||||
// adapted from Flutter `RenderSliverFixedExtentBoxAdaptor` in `/rendering/sliver_fixed_extent_list.dart`
|
||||
class SectionedListSliver<T> extends StatelessWidget {
|
||||
const SectionedListSliver({Key? key}) : super(key: key);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'package:aves/widgets/common/magnifier/scale/state.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/*
|
||||
`Magnifier` is derived from `photo_view` package v0.9.2:
|
||||
adapted from package `photo_view` v0.9.2:
|
||||
- removed image related aspects to focus on a general purpose pan/scale viewer (à la `InteractiveViewer`)
|
||||
- removed rotation and many customization parameters
|
||||
- removed ignorable/ignoring partial notifiers
|
||||
|
|
|
@ -268,7 +268,7 @@ class _GeoMapState extends State<GeoMap> {
|
|||
void _onCollectionChanged() {
|
||||
_defaultMarkerCluster = _buildFluster();
|
||||
_slowMarkerCluster = null;
|
||||
_clusterChangeNotifier.notifyListeners();
|
||||
_clusterChangeNotifier.notify();
|
||||
}
|
||||
|
||||
Fluster<GeoEntry> _buildFluster({int nodeSize = 64}) {
|
||||
|
|
|
@ -134,7 +134,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
|||
isReadyToRender: (key) => key.entry.isThumbnailReady(extent: GeoMap.markerImageExtent),
|
||||
onRendered: (key, bitmap) {
|
||||
_markerBitmaps[key] = bitmap;
|
||||
_markerBitmapChangeNotifier.notifyListeners();
|
||||
_markerBitmapChangeNotifier.notify();
|
||||
},
|
||||
),
|
||||
MapDecorator(
|
||||
|
|
|
@ -2,10 +2,12 @@ import 'dart:ui' as ui;
|
|||
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -49,95 +51,116 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onScaleStart: (details) {
|
||||
// the gesture detector wrongly detects a new scaling gesture
|
||||
// when scaling ends and we apply the new extent, so we prevent this
|
||||
// until we scaled and scrolled to the tile in the new grid
|
||||
if (_applyingScale) return;
|
||||
final child = GestureDetector(
|
||||
// Horizontal/vertical drag gestures are interpreted as scaling
|
||||
// if they are not handled by `onHorizontalDragStart`/`onVerticalDragStart`
|
||||
// at the scaling `GestureDetector` level, or handled beforehand down the widget tree.
|
||||
// Setting `onHorizontalDragStart`, `onVerticalDragStart`, and `onScaleStart`
|
||||
// all at once is not allowed, so we use another `GestureDetector` for that.
|
||||
onVerticalDragStart: (details) {},
|
||||
onHorizontalDragStart: (details) {},
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
final tileExtentController = context.read<TileExtentController>();
|
||||
// as of Flutter v2.5.3, `ScaleGestureRecognizer` does not work well
|
||||
// when combined with the `VerticalDragGestureRecognizer` inside a `GridView`,
|
||||
// so it is modified to eagerly accept the gesture
|
||||
// when multiple pointers are involved, and take priority over drag gestures.
|
||||
return RawGestureDetector(
|
||||
gestures: <Type, GestureRecognizerFactory>{
|
||||
EagerScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<EagerScaleGestureRecognizer>(
|
||||
() => EagerScaleGestureRecognizer(debugOwner: this),
|
||||
(instance) {
|
||||
instance
|
||||
..onStart = _onScaleStart
|
||||
..onUpdate = _onScaleUpdate
|
||||
..onEnd = _onScaleEnd
|
||||
..dragStartBehavior = DragStartBehavior.start;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
final scrollableContext = widget.scrollableKey.currentContext!;
|
||||
final scrollableBox = scrollableContext.findRenderObject() as RenderBox;
|
||||
final renderMetaData = _getClosestRenderMetadata(
|
||||
box: scrollableBox,
|
||||
localFocalPoint: details.localFocalPoint,
|
||||
spacing: tileExtentController.spacing,
|
||||
);
|
||||
// abort if we cannot find an image to show on overlay
|
||||
if (renderMetaData == null) return;
|
||||
_metadata = renderMetaData.metaData;
|
||||
_startSize = renderMetaData.size;
|
||||
_scaledSizeNotifier = ValueNotifier(_startSize!);
|
||||
void _onScaleStart(ScaleStartDetails details) {
|
||||
// the gesture detector wrongly detects a new scaling gesture
|
||||
// when scaling ends and we apply the new extent, so we prevent this
|
||||
// until we scaled and scrolled to the tile in the new grid
|
||||
if (_applyingScale) return;
|
||||
|
||||
// not the same as `MediaQuery.size.width`, because of screen insets/padding
|
||||
final gridWidth = scrollableBox.size.width;
|
||||
final tileExtentController = context.read<TileExtentController>();
|
||||
|
||||
_extentMin = tileExtentController.effectiveExtentMin;
|
||||
_extentMax = tileExtentController.effectiveExtentMax;
|
||||
final scrollableContext = widget.scrollableKey.currentContext!;
|
||||
final scrollableBox = scrollableContext.findRenderObject() as RenderBox;
|
||||
final renderMetaData = _getClosestRenderMetadata(
|
||||
box: scrollableBox,
|
||||
localFocalPoint: details.localFocalPoint,
|
||||
spacing: tileExtentController.spacing,
|
||||
);
|
||||
// abort if we cannot find an image to show on overlay
|
||||
if (renderMetaData == null) return;
|
||||
_metadata = renderMetaData.metaData;
|
||||
_startSize = renderMetaData.size;
|
||||
_scaledSizeNotifier = ValueNotifier(_startSize!);
|
||||
|
||||
final halfSize = _startSize! / 2;
|
||||
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => ScaleOverlay(
|
||||
builder: (scaledTileSize) => SizedBox.fromSize(
|
||||
size: scaledTileSize,
|
||||
child: GridTheme(
|
||||
extent: scaledTileSize.width,
|
||||
child: widget.scaledBuilder(_metadata!.item, scaledTileSize),
|
||||
),
|
||||
),
|
||||
center: thumbnailCenter,
|
||||
viewportWidth: gridWidth,
|
||||
gridBuilder: widget.gridBuilder,
|
||||
scaledSizeNotifier: _scaledSizeNotifier!,
|
||||
// not the same as `MediaQuery.size.width`, because of screen insets/padding
|
||||
final gridWidth = scrollableBox.size.width;
|
||||
|
||||
_extentMin = tileExtentController.effectiveExtentMin;
|
||||
_extentMax = tileExtentController.effectiveExtentMax;
|
||||
|
||||
final halfSize = _startSize! / 2;
|
||||
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => ScaleOverlay(
|
||||
builder: (scaledTileSize) => SizedBox.fromSize(
|
||||
size: scaledTileSize,
|
||||
child: GridTheme(
|
||||
extent: scaledTileSize.width,
|
||||
child: widget.scaledBuilder(_metadata!.item, scaledTileSize),
|
||||
),
|
||||
);
|
||||
Overlay.of(scrollableContext)!.insert(_overlayEntry!);
|
||||
},
|
||||
onScaleUpdate: (details) {
|
||||
if (_scaledSizeNotifier == null) return;
|
||||
final s = details.scale;
|
||||
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
|
||||
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
|
||||
},
|
||||
onScaleEnd: (details) {
|
||||
if (_scaledSizeNotifier == null) return;
|
||||
if (_overlayEntry != null) {
|
||||
_overlayEntry!.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
_applyingScale = true;
|
||||
final tileExtentController = context.read<TileExtentController>();
|
||||
final oldExtent = tileExtentController.extentNotifier.value;
|
||||
// sanitize and update grid layout if necessary
|
||||
final newExtent = tileExtentController.setUserPreferredExtent(_scaledSizeNotifier!.value.width);
|
||||
_scaledSizeNotifier = null;
|
||||
if (newExtent == oldExtent) {
|
||||
_applyingScale = false;
|
||||
} else {
|
||||
// scroll to show the focal point thumbnail at its new position
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
final trackItem = _metadata!.item;
|
||||
final highlightItem = widget.highlightItem?.call(trackItem) ?? trackItem;
|
||||
context.read<HighlightInfo>().trackItem(trackItem, animate: false, highlightItem: highlightItem);
|
||||
_applyingScale = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
// Horizontal/vertical drag gestures are interpreted as scaling
|
||||
// if they are not handled by `onHorizontalDragStart`/`onVerticalDragStart`
|
||||
// at the scaling `GestureDetector` level, or handled beforehand down the widget tree.
|
||||
// Setting `onHorizontalDragStart`, `onVerticalDragStart`, and `onScaleStart`
|
||||
// all at once is not allowed, so we use another `GestureDetector` for that.
|
||||
onVerticalDragStart: (details) {},
|
||||
onHorizontalDragStart: (details) {},
|
||||
child: widget.child,
|
||||
),
|
||||
center: thumbnailCenter,
|
||||
viewportWidth: gridWidth,
|
||||
gridBuilder: widget.gridBuilder,
|
||||
scaledSizeNotifier: _scaledSizeNotifier!,
|
||||
),
|
||||
);
|
||||
Overlay.of(scrollableContext)!.insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
if (_scaledSizeNotifier == null) return;
|
||||
final s = details.scale;
|
||||
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
|
||||
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
|
||||
}
|
||||
|
||||
void _onScaleEnd(ScaleEndDetails details) {
|
||||
if (_scaledSizeNotifier == null) return;
|
||||
if (_overlayEntry != null) {
|
||||
_overlayEntry!.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
_applyingScale = true;
|
||||
final tileExtentController = context.read<TileExtentController>();
|
||||
final oldExtent = tileExtentController.extentNotifier.value;
|
||||
// sanitize and update grid layout if necessary
|
||||
final newExtent = tileExtentController.setUserPreferredExtent(_scaledSizeNotifier!.value.width);
|
||||
_scaledSizeNotifier = null;
|
||||
if (newExtent == oldExtent) {
|
||||
_applyingScale = false;
|
||||
} else {
|
||||
// scroll to show the focal point thumbnail at its new position
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
final trackItem = _metadata!.item;
|
||||
final highlightItem = widget.highlightItem?.call(trackItem) ?? trackItem;
|
||||
context.read<HighlightInfo>().trackItem(trackItem, animate: false, highlightItem: highlightItem);
|
||||
_applyingScale = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
RenderMetaData? _getClosestRenderMetadata({
|
||||
|
|
|
@ -5,10 +5,10 @@ import 'package:aves/model/filters/tag.dart';
|
|||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/expandable_filter_row.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/search/expandable_filter_row.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
|
@ -15,9 +15,9 @@ import 'package:aves/model/source/tag.dart';
|
|||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/expandable_filter_row.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/search/expandable_filter_row.dart';
|
||||
import 'package:aves/widgets/search/search_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -273,7 +273,7 @@ class CollectionSearchDelegate {
|
|||
focusNode?.unfocus();
|
||||
}
|
||||
|
||||
// adapted from `SearchDelegate`
|
||||
// adapted from Flutter `SearchDelegate` in `/material/search.dart`
|
||||
|
||||
void showResults(BuildContext context) {
|
||||
focusNode?.unfocus();
|
||||
|
@ -311,10 +311,10 @@ class CollectionSearchDelegate {
|
|||
SearchPageRoute? route;
|
||||
}
|
||||
|
||||
// adapted from `SearchDelegate`
|
||||
// adapted from Flutter `_SearchBody` in `/material/search.dart`
|
||||
enum SearchBody { suggestions, results }
|
||||
|
||||
// adapted from `SearchDelegate`
|
||||
// adapted from Flutter `_SearchPageRoute` in `/material/search.dart`
|
||||
class SearchPageRoute<T> extends PageRoute<T> {
|
||||
SearchPageRoute({
|
||||
required this.delegate,
|
||||
|
|
|
@ -290,7 +290,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
|
|||
targetIndex,
|
||||
duration: Durations.quickActionListAnimation,
|
||||
);
|
||||
_quickActionsChangeNotifier.notifyListeners();
|
||||
_quickActionsChangeNotifier.notify();
|
||||
Future.delayed(Durations.quickActionListAnimation).then((value) => _reordering = false);
|
||||
return true;
|
||||
}
|
||||
|
@ -305,7 +305,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
|
|||
(context, animation) => DraggedPlaceholder(child: _buildQuickActionButton(action, animation)),
|
||||
duration: Durations.quickActionListAnimation,
|
||||
);
|
||||
_quickActionsChangeNotifier.notifyListeners();
|
||||
_quickActionsChangeNotifier.notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -390,7 +390,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
if (!_isEntryTracked && _verticalPager.page?.floor() == transitionPage) {
|
||||
_trackEntry();
|
||||
}
|
||||
_verticalScrollNotifier.notifyListeners();
|
||||
_verticalScrollNotifier.notify();
|
||||
}
|
||||
|
||||
void _goToCollection(CollectionFilter filter) {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
// cf photoshop:ColorMode
|
||||
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md
|
||||
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
|
||||
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md
|
||||
class XmpPhotoshopNamespace extends XmpNamespace {
|
||||
static const ns = 'photoshop';
|
||||
|
||||
|
|
|
@ -75,7 +75,6 @@ class OverlayTextButton extends StatelessWidget {
|
|||
shape: MaterialStateProperty.all<OutlinedBorder>(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(_borderRadius)),
|
||||
)),
|
||||
// shape: MaterialStateProperty.all<OutlinedBorder>(CircleBorder()),
|
||||
),
|
||||
child: Text(buttonLabel),
|
||||
),
|
||||
|
|
|
@ -86,7 +86,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
|
||||
void _startListening() {
|
||||
_instance.addListener(_onValueChanged);
|
||||
_subscriptions.add(_valueStream.where((value) => value.state == FijkState.completed).listen((_) => _completedNotifier.notifyListeners()));
|
||||
_subscriptions.add(_valueStream.where((value) => value.state == FijkState.completed).listen((_) => _completedNotifier.notify()));
|
||||
_subscriptions.add(_instance.onTimedText.listen(_timedTextStreamController.add));
|
||||
}
|
||||
|
||||
|
|
27
pubspec.yaml
27
pubspec.yaml
|
@ -117,3 +117,30 @@ flutter:
|
|||
|
||||
# capture shaders in profile mode (real device only):
|
||||
# % flutter drive --flavor play -t test_driver/driver_play.dart --profile --cache-sksl --write-sksl-on-exit shaders.sksl.json
|
||||
|
||||
################################################################################
|
||||
# Adaptations
|
||||
|
||||
# `DraggableScrollbar` in `/widgets/common/basic/draggable_scrollbar.dart`
|
||||
# adapts from package `draggable_scrollbar` v0.0.4
|
||||
#
|
||||
# `Magnifier` in `/widgets/common/magnifier/magnifier.dart`
|
||||
# adapts from package `photo_view` v0.9.2
|
||||
#
|
||||
# `AvesHighlightView` in `/widgets/common/aves_highlight.dart`
|
||||
# adapts from package `flutter_highlight` v0.7.0
|
||||
#
|
||||
# `OutputBuffer` in `/services/common/output_buffer.dart`
|
||||
# adapts from Flutter `_OutputBuffer` in `/foundation/consolidate_response.dart`
|
||||
#
|
||||
# `EagerScaleGestureRecognizer` in `/widgets/common/behaviour/eager_scale_gesture_recognizer.dart`
|
||||
# adapts from Flutter `ScaleGestureRecognizer` in `/gestures/scale.dart`
|
||||
#
|
||||
# `TransitionImage` in `/widgets/common/fx/transition_image.dart`
|
||||
# adapts from Flutter `RawImage` in `/widgets/basic.dart` and `DecorationImagePainter` in `/painting/decoration_image.dart`
|
||||
#
|
||||
# `_RenderSliverKnownExtentBoxAdaptor` in `/widgets/common/grid/sliver.dart`
|
||||
# adapts from Flutter `RenderSliverFixedExtentBoxAdaptor` in `/rendering/sliver_fixed_extent_list.dart`
|
||||
#
|
||||
# `CollectionSearchDelegate`, `SearchPageRoute` in `/widgets/search/search_delegate.dart`
|
||||
# adapts from Flutter `SearchDelegate`, `_SearchPageRoute` in `/material/search.dart`
|
||||
|
|
Loading…
Reference in a new issue