quick chooser changes

This commit is contained in:
Thibault Deckers 2022-12-03 12:28:08 +01:00
parent 7cd2c3fa8b
commit 3726b7334a
12 changed files with 173 additions and 129 deletions

View file

@ -18,7 +18,7 @@ class MoveButton extends ChooserQuickButton<String> {
const MoveButton({
super.key,
required this.copy,
super.chooserPosition,
required super.blurred,
super.onChooserValue,
required super.onPressed,
});
@ -57,6 +57,7 @@ class _MoveButtonState extends ChooserQuickButtonState<MoveButton, String> {
child: AlbumQuickChooser(
valueNotifier: chooserValueNotifier,
options: options,
blurred: widget.blurred,
chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition,
),

View file

@ -10,6 +10,7 @@ import 'package:provider/provider.dart';
class AlbumQuickChooser extends StatelessWidget {
final ValueNotifier<String?> valueNotifier;
final List<String> options;
final bool blurred;
final PopupMenuPosition chooserPosition;
final Stream<Offset> pointerGlobalPosition;
@ -17,6 +18,7 @@ class AlbumQuickChooser extends StatelessWidget {
super.key,
required this.valueNotifier,
required this.options,
required this.blurred,
required this.chooserPosition,
required this.pointerGlobalPosition,
});
@ -27,6 +29,7 @@ class AlbumQuickChooser extends StatelessWidget {
return FilterQuickChooser<String>(
valueNotifier: valueNotifier,
options: options,
blurred: blurred,
chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition,
buildFilterChip: (context, album) => AvesFilterChip(

View file

@ -6,16 +6,16 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
abstract class ChooserQuickButton<T> extends StatefulWidget {
final PopupMenuPosition? chooserPosition;
final bool blurred;
final ValueSetter<T>? onChooserValue;
final VoidCallback? onPressed;
const ChooserQuickButton({
super.key,
this.chooserPosition,
required this.blurred,
this.onChooserValue,
required this.onPressed,
}) : assert((chooserPosition == null) == (onChooserValue == null));
});
}
abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> extends State<T> with SingleTickerProviderStateMixin {
@ -50,13 +50,12 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
@override
Widget build(BuildContext context) {
final chooserPosition = widget.chooserPosition;
final onChooserValue = widget.onChooserValue;
final isChooserEnabled = chooserPosition != null && onChooserValue != null;
final isChooserEnabled = onChooserValue != null;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPressStart: isChooserEnabled ? (details) => _showChooser() : null,
onLongPressStart: isChooserEnabled ? _showChooser : null,
onLongPressMoveUpdate: isChooserEnabled ? _moveUpdateStreamController.add : null,
onLongPressEnd: isChooserEnabled
? (details) {
@ -83,10 +82,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
}
}
void _showChooser() {
final chooserPosition = widget.chooserPosition;
if (chooserPosition == null) return;
void _showChooser(LongPressStartDetails details) {
final overlay = Overlay.of(context)!;
final triggerBox = context.findRenderObject() as RenderBox;
final overlayBox = overlay.context.findRenderObject() as RenderBox;
@ -98,13 +94,14 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
_chooserValueNotifier.value = defaultValue;
_chooserOverlayEntry = OverlayEntry(
builder: (context) {
final mediaQuery = MediaQuery.of(context);
final mq = MediaQuery.of(context);
final chooserPosition = (details.globalPosition.dy > mq.size.height / 2) ? PopupMenuPosition.over : PopupMenuPosition.under;
return CustomSingleChildLayout(
delegate: QuickChooserRouteLayout(
triggerRect,
chooserPosition,
mediaQuery.padding,
DisplayFeatureSubScreen.avoidBounds(mediaQuery).toSet(),
mq.padding,
DisplayFeatureSubScreen.avoidBounds(mq).toSet(),
),
child: buildChooser(_animation!, chooserPosition),
);

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:ui';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/common/app_bar/quick_choosers/quick_chooser.dart';
import 'package:aves_ui/aves_ui.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@ -13,6 +13,7 @@ import 'package:provider/provider.dart';
class FilterQuickChooser<T> extends StatefulWidget {
final ValueNotifier<T?> valueNotifier;
final List<T> options;
final bool blurred;
final PopupMenuPosition chooserPosition;
final Stream<Offset> pointerGlobalPosition;
final Widget Function(BuildContext context, T album) buildFilterChip;
@ -23,6 +24,7 @@ class FilterQuickChooser<T> extends StatefulWidget {
super.key,
required this.valueNotifier,
required List<T> options,
required this.blurred,
required this.chooserPosition,
required this.pointerGlobalPosition,
required this.buildFilterChip,
@ -42,8 +44,6 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
bool get reversed => widget.chooserPosition == PopupMenuPosition.over;
static const margin = EdgeInsets.all(8);
static const padding = EdgeInsets.symmetric(horizontal: 8);
static const double intraPadding = 8;
@override
@ -78,93 +78,88 @@ class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
@override
Widget build(BuildContext context) {
return Padding(
padding: margin,
child: Material(
shape: AvesDialog.shape(context),
clipBehavior: Clip.antiAlias,
child: Padding(
padding: padding,
child: ValueListenableBuilder<T?>(
valueListenable: valueNotifier,
builder: (context, selectedValue, child) {
final durations = context.watch<DurationsData>();
return QuickChooser(
blurred: widget.blurred,
child: ValueListenableBuilder<T?>(
valueListenable: valueNotifier,
builder: (context, selectedValue, child) {
final durations = context.watch<DurationsData>();
List<Widget> optionChildren = options.mapIndexed((index, value) {
final isFirst = index == (reversed ? options.length - 1 : 0);
return Padding(
padding: EdgeInsets.only(top: isFirst ? intraPadding : 0, bottom: intraPadding),
child: widget.buildFilterChip(context, value),
);
}).toList();
List<Widget> optionChildren = options.mapIndexed((index, value) {
final isFirst = index == (reversed ? options.length - 1 : 0);
return Padding(
padding: EdgeInsets.only(top: isFirst ? intraPadding : 0, bottom: intraPadding),
child: widget.buildFilterChip(context, value),
);
}).toList();
optionChildren = AnimationConfiguration.toStaggeredList(
duration: durations.staggeredAnimation * .5,
delay: durations.staggeredAnimationDelay * .5 * timeDilation,
childAnimationBuilder: (child) => SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
optionChildren = AnimationConfiguration.toStaggeredList(
duration: durations.staggeredAnimation * .5,
delay: durations.staggeredAnimationDelay * .5 * timeDilation,
childAnimationBuilder: (child) => SlideAnimation(
verticalOffset: 50.0 * (widget.chooserPosition == PopupMenuPosition.over ? 1 : -1),
child: FadeInAnimation(
child: child,
),
),
children: optionChildren,
);
if (reversed) {
optionChildren = optionChildren.reversed.toList();
}
return Stack(
children: [
ValueListenableBuilder<Rect>(
valueListenable: _selectedRowRect,
builder: (context, selectedRowRect, child) {
Widget child = const Center(child: AvesDot());
child = AnimatedOpacity(
opacity: selectedValue != null ? 1 : 0,
curve: Curves.easeInOutCubic,
duration: const Duration(milliseconds: 200),
child: child,
),
);
child = AnimatedPositioned(
top: selectedRowRect.top,
height: selectedRowRect.height,
curve: Curves.easeInOutCubic,
duration: const Duration(milliseconds: 200),
child: child,
);
return child;
},
),
Padding(
padding: const EdgeInsetsDirectional.only(start: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: optionChildren,
),
children: optionChildren,
);
if (reversed) {
optionChildren = optionChildren.reversed.toList();
}
return Stack(
children: [
ValueListenableBuilder<Rect>(
valueListenable: _selectedRowRect,
builder: (context, selectedRowRect, child) {
Widget child = const Center(child: AvesDot());
child = AnimatedOpacity(
opacity: selectedValue != null ? 1 : 0,
curve: Curves.easeInOutCubic,
duration: const Duration(milliseconds: 200),
child: child,
);
child = AnimatedPositioned(
top: selectedRowRect.top,
height: selectedRowRect.height,
curve: Curves.easeInOutCubic,
duration: const Duration(milliseconds: 200),
child: child,
);
return child;
},
),
Padding(
padding: const EdgeInsetsDirectional.only(start: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: optionChildren,
),
),
],
);
},
),
),
),
],
);
},
),
);
}
void _onPointerMove(Offset globalPosition) {
final padding = QuickChooser.margin.vertical + QuickChooser.padding.vertical;
final chooserBox = context.findRenderObject() as RenderBox;
final chooserSize = chooserBox.size;
final contentWidth = chooserSize.width;
final contentHeight = chooserSize.height - (margin.vertical + padding.vertical);
final contentHeight = chooserSize.height - padding;
final optionCount = options.length;
final itemHeight = (contentHeight - (optionCount + 1) * intraPadding) / optionCount;
final local = chooserBox.globalToLocal(globalPosition);
final dx = local.dx;
final dy = local.dy - (margin.vertical + padding.vertical) / 2;
final dy = local.dy - padding / 2;
T? selectedValue;
if (0 < dx && dx < contentWidth && 0 < dy && dy < contentHeight) {

View file

@ -0,0 +1,47 @@
import 'package:aves/theme/themes.dart';
import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:flutter/material.dart';
class QuickChooser extends StatelessWidget {
final bool blurred;
final Widget child;
static const margin = EdgeInsets.all(8);
static const padding = EdgeInsets.symmetric(horizontal: 8);
const QuickChooser({
super.key,
required this.blurred,
required this.child,
});
@override
Widget build(BuildContext context) {
final brightness = Theme.of(context).brightness;
final backgroundColor = blurred ? Themes.overlayBackgroundColor(brightness: brightness, blurred: blurred) : null;
const borderRadius = BorderRadius.all(AvesDialog.cornerRadius);
return Padding(
padding: margin,
child: BlurredRRect(
enabled: blurred,
borderRadius: borderRadius,
child: Material(
borderRadius: borderRadius,
color: backgroundColor,
child: Ink(
decoration: BoxDecoration(
border: AvesBorder.border(context),
borderRadius: borderRadius,
),
child: Padding(
padding: padding,
child: child,
),
),
),
),
);
}
}

View file

@ -1,15 +1,17 @@
import 'dart:async';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/common/app_bar/quick_choosers/quick_chooser.dart';
import 'package:flutter/material.dart';
class RateQuickChooser extends StatefulWidget {
final bool blurred;
final ValueNotifier<int?> valueNotifier;
final Stream<Offset> pointerGlobalPosition;
const RateQuickChooser({
super.key,
required this.blurred,
required this.valueNotifier,
required this.pointerGlobalPosition,
});
@ -23,9 +25,6 @@ class _RateQuickChooserState extends State<RateQuickChooser> {
ValueNotifier<int?> get valueNotifier => widget.valueNotifier;
static const margin = EdgeInsets.all(8);
static const padding = EdgeInsets.all(8);
@override
void initState() {
super.initState();
@ -57,43 +56,42 @@ class _RateQuickChooserState extends State<RateQuickChooser> {
@override
Widget build(BuildContext context) {
return Padding(
padding: margin,
child: Material(
shape: AvesDialog.shape(context),
child: Padding(
padding: padding,
child: ValueListenableBuilder<int?>(
valueListenable: valueNotifier,
builder: (context, selectedValue, child) {
final _rating = selectedValue ?? 0;
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (i) {
final thisRating = i + 1;
return Padding(
padding: const EdgeInsets.all(4),
child: Icon(
_rating < thisRating ? AIcons.rating : AIcons.ratingFull,
color: _rating < thisRating ? Colors.grey : Colors.amber,
),
);
}).toList(),
);
},
),
return QuickChooser(
blurred: widget.blurred,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ValueListenableBuilder<int?>(
valueListenable: valueNotifier,
builder: (context, selectedValue, child) {
final _rating = selectedValue ?? 0;
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (i) {
final thisRating = i + 1;
return Padding(
padding: const EdgeInsets.all(4),
child: Icon(
_rating < thisRating ? AIcons.rating : AIcons.ratingFull,
color: _rating < thisRating ? Colors.grey : Colors.amber,
),
);
}).toList(),
);
},
),
),
);
}
void _onPointerMove(Offset globalPosition) {
final padding = QuickChooser.margin.horizontal + QuickChooser.padding.horizontal;
final chooserBox = context.findRenderObject() as RenderBox;
final chooserSize = chooserBox.size;
final contentWidth = chooserSize.width - (margin.horizontal + padding.horizontal);
final contentWidth = chooserSize.width - padding;
final local = chooserBox.globalToLocal(globalPosition);
final dx = local.dx - (margin.horizontal + padding.horizontal) / 2;
final dx = local.dx - padding / 2;
valueNotifier.value = (5 * dx / contentWidth).ceil().clamp(0, 5);
}

View file

@ -35,7 +35,7 @@ class QuickChooserRouteLayout extends SingleChildLayoutDelegate {
y = triggerRect.top - childSize.height;
break;
case PopupMenuPosition.under:
y = triggerRect.bottom;
y = size.height - triggerRect.bottom;
break;
}
double x = (triggerRect.left + (size.width - triggerRect.right) - childSize.width) / 2;

View file

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
class TagQuickChooser extends StatelessWidget {
final ValueNotifier<CollectionFilter?> valueNotifier;
final List<CollectionFilter> options;
final bool blurred;
final PopupMenuPosition chooserPosition;
final Stream<Offset> pointerGlobalPosition;
@ -15,6 +16,7 @@ class TagQuickChooser extends StatelessWidget {
super.key,
required this.valueNotifier,
required this.options,
required this.blurred,
required this.chooserPosition,
required this.pointerGlobalPosition,
});
@ -24,6 +26,7 @@ class TagQuickChooser extends StatelessWidget {
return FilterQuickChooser<CollectionFilter>(
valueNotifier: valueNotifier,
options: options,
blurred: blurred,
chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition,
buildFilterChip: (context, filter) => AvesFilterChip(

View file

@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
class RateButton extends ChooserQuickButton<int> {
const RateButton({
super.key,
super.chooserPosition,
required super.blurred,
super.onChooserValue,
required super.onPressed,
});
@ -35,6 +35,7 @@ class _RateButtonState extends ChooserQuickButtonState<RateButton, int> {
scale: animation,
alignment: chooserPosition == PopupMenuPosition.over ? Alignment.bottomCenter : Alignment.topCenter,
child: RateQuickChooser(
blurred: widget.blurred,
valueNotifier: chooserValueNotifier,
pointerGlobalPosition: pointerGlobalPosition,
),

View file

@ -15,7 +15,7 @@ import 'package:provider/provider.dart';
class TagButton extends ChooserQuickButton<CollectionFilter> {
const TagButton({
super.key,
super.chooserPosition,
required super.blurred,
super.onChooserValue,
required super.onPressed,
});
@ -54,6 +54,7 @@ class _TagButtonState extends ChooserQuickButtonState<TagButton, CollectionFilte
child: TagQuickChooser(
valueNotifier: chooserValueNotifier,
options: options,
blurred: widget.blurred,
chooserPosition: chooserPosition,
pointerGlobalPosition: pointerGlobalPosition,
),

View file

@ -34,8 +34,6 @@ class BasicSection extends StatelessWidget {
final ValueNotifier<EntryAction?> isEditingMetadataNotifier;
final FilterCallback onFilter;
static const quickChooserPosition = PopupMenuPosition.over;
const BasicSection({
super.key,
required this.entry,
@ -135,14 +133,14 @@ class BasicSection extends StatelessWidget {
switch (action) {
case EntryAction.editRating:
button = RateButton(
chooserPosition: quickChooserPosition,
blurred: false,
onChooserValue: (rating) => actionDelegate.quickRate(context, entry, rating),
onPressed: onPressed,
);
break;
case EntryAction.editTags:
button = TagButton(
chooserPosition: quickChooserPosition,
blurred: false,
onChooserValue: (filter) => actionDelegate.quickTag(context, entry, filter),
onPressed: onPressed,
);

View file

@ -91,7 +91,6 @@ class ViewerButtonRowContent extends StatelessWidget {
AvesEntry get favouriteTargetEntry => mainEntry.isBurst ? pageEntry : mainEntry;
static const double padding = 8;
static const quickChooserPosition = PopupMenuPosition.over;
ViewerButtonRowContent({
super.key,
@ -206,11 +205,12 @@ class ViewerButtonRowContent extends StatelessWidget {
);
}
final blurred = settings.enableBlurEffect;
switch (action) {
case EntryAction.copy:
child = MoveButton(
copy: true,
chooserPosition: quickChooserPosition,
blurred: blurred,
onChooserValue: (album) => _entryActionDelegate.quickMove(context, album, copy: true),
onPressed: onPressed,
);
@ -218,7 +218,7 @@ class ViewerButtonRowContent extends StatelessWidget {
case EntryAction.move:
child = MoveButton(
copy: false,
chooserPosition: quickChooserPosition,
blurred: blurred,
onChooserValue: (album) => _entryActionDelegate.quickMove(context, album, copy: false),
onPressed: onPressed,
);
@ -252,14 +252,14 @@ class ViewerButtonRowContent extends StatelessWidget {
break;
case EntryAction.editRating:
child = RateButton(
chooserPosition: quickChooserPosition,
blurred: blurred,
onChooserValue: (rating) => _entryActionDelegate.quickRate(context, rating),
onPressed: onPressed,
);
break;
case EntryAction.editTags:
child = TagButton(
chooserPosition: quickChooserPosition,
blurred: blurred,
onChooserValue: (filter) => _entryActionDelegate.quickTag(context, filter),
onPressed: onPressed,
);