fix(lists): reorderable lists could not be reordered on some platforms

This commit is contained in:
Jagandeep Brar
2022-06-14 21:18:43 -04:00
parent 6ad93549b1
commit 95a645c06c
5 changed files with 48 additions and 598 deletions

View File

@@ -3,5 +3,5 @@ class LunaEnvironment {
static const String commit = 'master';
static const String flavor = 'stable';
static const String flavor = 'edge';
}

View File

@@ -3,65 +3,67 @@ import 'package:lunasea/core.dart';
part 'indexer_icon.g.dart';
const _GENERIC = 'generic';
const _DOGNZB = 'dognzb';
const _DRUNKENSLUG = 'drunkenslug';
const _NZBFINDER = 'nzbfinder';
const _NZBGEEK = 'nzbgeek';
const _NZBHYDRA = 'nzbhydra';
const _NZBSU = 'nzbsu';
@JsonEnum()
@HiveType(typeId: 22, adapterName: 'LunaIndexerIconAdapter')
enum LunaIndexerIcon {
@JsonValue(_GENERIC)
@HiveField(0)
GENERIC,
@HiveField(1)
DOGNZB,
@HiveField(2)
DRUNKENSLUG,
@HiveField(3)
NZBFINDER,
@HiveField(4)
NZBGEEK,
@HiveField(5)
NZBHYDRA,
@HiveField(6)
NZBSU,
}
GENERIC(_GENERIC),
extension LunaIndexerIconExtension on LunaIndexerIcon {
LunaIndexerIcon fromKey(String key) {
@JsonValue(_DOGNZB)
@HiveField(1)
DOGNZB(_DOGNZB),
@JsonValue(_DRUNKENSLUG)
@HiveField(2)
DRUNKENSLUG(_DRUNKENSLUG),
@JsonValue(_NZBFINDER)
@HiveField(3)
NZBFINDER(_NZBFINDER),
@JsonValue(_NZBGEEK)
@HiveField(4)
NZBGEEK(_NZBGEEK),
@JsonValue(_NZBHYDRA)
@HiveField(5)
NZBHYDRA(_NZBHYDRA),
@JsonValue(_NZBSU)
@HiveField(6)
NZBSU(_NZBSU);
final String key;
const LunaIndexerIcon(this.key);
static LunaIndexerIcon fromKey(String key) {
switch (key) {
case 'generic':
return LunaIndexerIcon.GENERIC;
case 'dognzb':
case _DOGNZB:
return LunaIndexerIcon.DOGNZB;
case 'drunkenslug':
case _DRUNKENSLUG:
return LunaIndexerIcon.DRUNKENSLUG;
case 'nzbfinder':
case _NZBFINDER:
return LunaIndexerIcon.NZBFINDER;
case 'nzbgeek':
case _NZBGEEK:
return LunaIndexerIcon.NZBGEEK;
case 'nzbhydra':
case _NZBHYDRA:
return LunaIndexerIcon.NZBHYDRA;
case 'nzbsu':
case _NZBSU:
return LunaIndexerIcon.NZBSU;
default:
return LunaIndexerIcon.GENERIC;
}
}
String get key {
switch (this) {
case LunaIndexerIcon.GENERIC:
return 'generic';
case LunaIndexerIcon.DOGNZB:
return 'dognzb';
case LunaIndexerIcon.DRUNKENSLUG:
return 'drunkenslug';
case LunaIndexerIcon.NZBFINDER:
return 'nzbfinder';
case LunaIndexerIcon.NZBGEEK:
return 'nzbgeek';
case LunaIndexerIcon.NZBHYDRA:
return 'nzbhydra';
case LunaIndexerIcon.NZBSU:
return 'nzbsu';
}
}
String get name {
switch (this) {
case LunaIndexerIcon.GENERIC:

View File

@@ -1,550 +0,0 @@
/// This is a modified version of [ReorderableListView] that was originally pulled from Flutter stable v2.0.4.
///
/// Change includes ensuring that the `padding` is pulled and applied to the inner padding/list padding, and not applied to the list externally.
/// This fixes the bug that caused the list to get clipped from the top padding.
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: unnecessary_null_comparison
import 'dart:ui' show lerpDouble;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
/// A list whose items the user can interactively reorder by dragging.
///
/// This class is appropriate for views with a small number of
/// children because constructing the [List] requires doing work for every
/// child that could possibly be displayed in the list view instead of just
/// those children that are actually visible.
///
/// All list items must have a key.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=3fB1mxOsqJE}
///
/// This sample shows by dragging the user can reorder the items of the list.
/// The [onReorder] parameter is required and will be called when a child
/// widget is dragged to a new position.
///
/// {@tool dartpad --template=stateful_widget_scaffold}
///
/// ```dart
/// final List<int> _items = List<int>.generate(50, (int index) => index);
///
/// @override
/// Widget build(BuildContext context){
/// final ColorScheme colorScheme = Theme.of(context).colorScheme;
/// final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
/// final Color evenItemColor = colorScheme.primary.withOpacity(0.15);
///
/// return ReorderableListView(
/// padding: const EdgeInsets.symmetric(horizontal: 40),
/// children: <Widget>[
/// for (int index = 0; index < _items.length; index++)
/// ListTile(
/// key: Key('$index'),
/// tileColor: _items[index].isOdd ? oddItemColor : evenItemColor,
/// title: Text('Item ${_items[index]}'),
/// ),
/// ],
/// onReorder: (int oldIndex, int newIndex) {
/// setState(() {
/// if (oldIndex < newIndex) {
/// newIndex -= 1;
/// }
/// final int item = _items.removeAt(oldIndex);
/// _items.insert(newIndex, item);
/// });
/// },
/// );
/// }
///
/// ```
///
///{@end-tool}
class FlutterReorderableListView extends StatefulWidget {
/// Creates a reorderable list from a pre-built list of widgets.
///
/// See also:
///
/// * [ReorderableListView.builder], which allows you to build a reorderable
/// list where the items are built as needed when scrolling the list.
FlutterReorderableListView({
Key? key,
required List<Widget> children,
required this.onReorder,
this.proxyDecorator,
this.buildDefaultDragHandles = true,
this.padding,
this.header,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.anchor = 0.0,
this.cacheExtent,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : assert(scrollDirection != null),
assert(onReorder != null),
assert(children != null),
assert(
children.every((Widget w) => w.key != null),
'All children of this widget must have a key.',
),
assert(buildDefaultDragHandles != null),
itemBuilder = ((BuildContext context, int index) => children[index]),
itemCount = children.length,
super(key: key);
/// Creates a reorderable list from widget items that are created on demand.
///
/// This constructor is appropriate for list views with a large number of
/// children because the builder is called only for those children
/// that are actually visible.
///
/// The `itemBuilder` callback will be called only with indices greater than
/// or equal to zero and less than `itemCount`.
///
/// The `itemBuilder` should always return a non-null widget, and actually
/// create the widget instances when called. Avoid using a builder that
/// returns a previously-constructed widget; if the list view's children are
/// created in advance, or all at once when the [ReorderableListView] itself
/// is created, it is more efficient to use the [ReorderableListView]
/// constructor. Even more efficient, however, is to create the instances
/// on demand using this constructor's `itemBuilder` callback.
///
/// See also:
///
/// * [ReorderableListView], which allows you to build a reorderable
/// list with all the items passed into the constructor.
const FlutterReorderableListView.builder({
Key? key,
required this.itemBuilder,
required this.itemCount,
required this.onReorder,
this.proxyDecorator,
this.buildDefaultDragHandles = true,
this.padding,
this.header,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.anchor = 0.0,
this.cacheExtent,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : assert(scrollDirection != null),
assert(itemCount >= 0),
assert(onReorder != null),
assert(buildDefaultDragHandles != null),
super(key: key);
/// {@macro flutter.widgets.reorderable_list.itemBuilder}
final IndexedWidgetBuilder itemBuilder;
/// {@macro flutter.widgets.reorderable_list.itemCount}
final int itemCount;
/// {@macro flutter.widgets.reorderable_list.onReorder}
final ReorderCallback onReorder;
/// {@macro flutter.widgets.reorderable_list.proxyDecorator}
final ReorderItemProxyDecorator? proxyDecorator;
/// If true: on desktop platforms, a drag handle is stacked over the
/// center of each item's trailing edge; on mobile platforms, a long
/// press anywhere on the item starts a drag.
///
/// The default desktop drag handle is just an [Icons.drag_handle]
/// wrapped by a [ReorderableDragStartListener]. On mobile
/// platforms, the entire item is wrapped with a
/// [ReorderableDelayedDragStartListener].
///
/// To change the appearance or the layout of the drag handles, make
/// this parameter false and wrap each list item, or a widget within
/// each list item, with [ReorderableDragStartListener] or
/// [ReorderableDelayedDragStartListener], or a custom subclass
/// of [ReorderableDragStartListener].
///
/// The following sample specifies `buildDefaultDragHandles: false`, and
/// uses a [Card] at the leading edge of each item for the item's drag handle.
///
/// {@tool dartpad --template=stateful_widget_scaffold}
///
/// ```dart
/// final List<int> _items = List<int>.generate(50, (int index) => index);
///
/// @override
/// Widget build(BuildContext context){
/// final ColorScheme colorScheme = Theme.of(context).colorScheme;
/// final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
/// final Color evenItemColor = colorScheme.primary.withOpacity(0.15);
///
/// return ReorderableListView(
/// buildDefaultDragHandles: false,
/// children: <Widget>[
/// for (int index = 0; index < _items.length; index++)
/// Container(
/// key: Key('$index'),
/// color: _items[index].isOdd ? oddItemColor : evenItemColor,
/// child: Row(
/// children: <Widget>[
/// Container(
/// width: 64,
/// height: 64,
/// padding: const EdgeInsets.all(8),
/// child: ReorderableDragStartListener(
/// index: index,
/// child: Card(
/// color: colorScheme.primary,
/// elevation: 2,
/// ),
/// ),
/// ),
/// Text('Item ${_items[index]}'),
/// ],
/// ),
/// ),
/// ],
/// onReorder: (int oldIndex, int newIndex) {
/// setState(() {
/// if (oldIndex < newIndex) {
/// newIndex -= 1;
/// }
/// final int item = _items.removeAt(oldIndex);
/// _items.insert(newIndex, item);
/// });
/// },
/// );
/// }
/// ```
///{@end-tool}
final bool buildDefaultDragHandles;
/// {@macro flutter.widgets.reorderable_list.padding}
final EdgeInsets? padding;
/// A non-reorderable header item to show before the items of the list.
///
/// If null, no header will appear before the list.
final Widget? header;
/// {@macro flutter.widgets.scroll_view.scrollDirection}
final Axis scrollDirection;
/// {@macro flutter.widgets.scroll_view.reverse}
final bool reverse;
/// {@macro flutter.widgets.scroll_view.controller}
final ScrollController? scrollController;
/// {@macro flutter.widgets.scroll_view.primary}
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [scrollController] is null.
final bool? primary;
/// {@macro flutter.widgets.scroll_view.physics}
final ScrollPhysics? physics;
/// {@macro flutter.widgets.scroll_view.shrinkWrap}
final bool shrinkWrap;
/// {@macro flutter.widgets.scroll_view.anchor}
final double anchor;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior}
///
/// The default is [ScrollViewKeyboardDismissBehavior.manual]
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
_ReorderableListViewState createState() => _ReorderableListViewState();
}
class _ReorderableListViewState extends State<FlutterReorderableListView> {
Widget _wrapWithSemantics(Widget child, int index) {
void reorder(int startIndex, int endIndex) {
if (startIndex != endIndex) widget.onReorder(startIndex, endIndex);
}
// First, determine which semantics actions apply.
final Map<CustomSemanticsAction, VoidCallback> semanticsActions =
<CustomSemanticsAction, VoidCallback>{};
// Create the appropriate semantics actions.
void moveToStart() => reorder(index, 0);
void moveToEnd() => reorder(index, widget.itemCount);
void moveBefore() => reorder(index, index - 1);
// To move after, we go to index+2 because we are moving it to the space
// before index+2, which is after the space at index+1.
void moveAfter() => reorder(index, index + 2);
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
// If the item can move to before its current position in the list.
if (index > 0) {
semanticsActions[
CustomSemanticsAction(label: localizations.reorderItemToStart)] =
moveToStart;
String reorderItemBefore = localizations.reorderItemUp;
if (widget.scrollDirection == Axis.horizontal) {
reorderItemBefore = Directionality.of(context) == TextDirection.ltr
? localizations.reorderItemLeft
: localizations.reorderItemRight;
}
semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] =
moveBefore;
}
// If the item can move to after its current position in the list.
if (index < widget.itemCount - 1) {
String reorderItemAfter = localizations.reorderItemDown;
if (widget.scrollDirection == Axis.horizontal) {
reorderItemAfter = Directionality.of(context) == TextDirection.ltr
? localizations.reorderItemRight
: localizations.reorderItemLeft;
}
semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] =
moveAfter;
semanticsActions[
CustomSemanticsAction(label: localizations.reorderItemToEnd)] =
moveToEnd;
}
// We pass toWrap with a GlobalKey into the item so that when it
// gets dragged, the accessibility framework can preserve the selected
// state of the dragging item.
//
// We also apply the relevant custom accessibility actions for moving the item
// up, down, to the start, and to the end of the list.
return MergeSemantics(
child: Semantics(
customSemanticsActions: semanticsActions,
child: child,
),
);
}
Widget _itemBuilder(BuildContext context, int index) {
final Widget item = widget.itemBuilder(context, index);
assert(() {
if (item.key == null) {
throw FlutterError(
'Every item of ReorderableListView must have a key.',
);
}
return true;
}());
// _ReorderableItem so the widget versions can have them as well.
final Widget itemWithSemantics = _wrapWithSemantics(item, index);
final Key itemGlobalKey =
_ReorderableListViewChildGlobalKey(item.key!, this);
if (widget.buildDefaultDragHandles) {
switch (Theme.of(context).platform) {
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
case TargetPlatform.macOS:
switch (widget.scrollDirection) {
case Axis.horizontal:
return Stack(
key: itemGlobalKey,
children: <Widget>[
itemWithSemantics,
Positioned.directional(
textDirection: Directionality.of(context),
start: 0,
end: 0,
bottom: 8,
child: Align(
alignment: AlignmentDirectional.bottomCenter,
child: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
),
),
],
);
case Axis.vertical:
return Stack(
key: itemGlobalKey,
children: <Widget>[
itemWithSemantics,
Positioned.directional(
textDirection: Directionality.of(context),
top: 0,
bottom: 0,
end: 8,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
),
),
],
);
}
case TargetPlatform.iOS:
case TargetPlatform.android:
return ReorderableDelayedDragStartListener(
key: itemGlobalKey,
index: index,
child: itemWithSemantics,
);
}
}
return KeyedSubtree(
key: itemGlobalKey,
child: itemWithSemantics,
);
}
Widget _proxyDecorator(Widget child, int index, Animation<double> animation) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
final double animValue = Curves.easeInOut.transform(animation.value);
final double elevation = lerpDouble(0, 6, animValue)!;
return Material(
child: child,
elevation: elevation,
);
},
child: child,
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
assert(debugCheckHasOverlay(context));
// If there is a header we can't just apply the padding to the list,
// so if header is not present, we wrap the CustomScrollView in the padding form the left and right
// and add the padding from the bottom and top to the sliver list. If header if present
// we only padd top of the header and bottom of the sliver list (or the equivalent for other axis directions).
final EdgeInsets padding = widget.padding ?? EdgeInsets.zero;
late EdgeInsets outerPadding;
late EdgeInsets listPadding;
switch (widget.scrollDirection) {
case Axis.horizontal:
if (widget.reverse) {
outerPadding = EdgeInsets.fromLTRB(
0, padding.top, padding.right, padding.bottom);
listPadding = EdgeInsets.fromLTRB(padding.left, 0, 0, 0);
} else {
outerPadding =
EdgeInsets.fromLTRB(padding.left, padding.top, 0, padding.bottom);
listPadding = EdgeInsets.fromLTRB(0, 0, padding.right, 0);
}
break;
case Axis.vertical:
if (widget.reverse) {
outerPadding = EdgeInsets.fromLTRB(padding.left, 0, padding.right, 0);
listPadding = EdgeInsets.fromLTRB(
0, padding.top, 0, widget.header != null ? 0 : padding.bottom);
} else {
outerPadding = EdgeInsets.fromLTRB(padding.left, 0, padding.right, 0);
listPadding = EdgeInsets.fromLTRB(
0, widget.header != null ? 0 : padding.top, 0, padding.bottom);
}
break;
}
return Padding(
padding: outerPadding,
child: CustomScrollView(
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: widget.scrollController,
primary: widget.primary,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
anchor: widget.anchor,
cacheExtent: widget.cacheExtent,
dragStartBehavior: widget.dragStartBehavior,
keyboardDismissBehavior: widget.keyboardDismissBehavior,
restorationId: widget.restorationId,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
if (widget.header != null)
SliverPadding(
padding: EdgeInsets.fromLTRB(0, widget.reverse ? 0 : padding.top,
0, widget.reverse ? padding.bottom : 0),
sliver: SliverToBoxAdapter(child: widget.header),
),
SliverPadding(
padding: listPadding,
sliver: SliverReorderableList(
itemBuilder: _itemBuilder,
itemCount: widget.itemCount,
onReorder: widget.onReorder,
proxyDecorator: widget.proxyDecorator ?? _proxyDecorator,
),
),
],
),
);
}
}
// A global key that takes its identity from the object and uses a value of a
// particular type to identify itself.
//
// The difference with GlobalObjectKey is that it uses [==] instead of [identical]
// of the objects used to generate widgets.
@optionalTypeArgs
class _ReorderableListViewChildGlobalKey extends GlobalObjectKey {
const _ReorderableListViewChildGlobalKey(this.subKey, this.state)
: super(subKey);
final Key subKey;
final State state;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is _ReorderableListViewChildGlobalKey &&
other.subKey == subKey &&
other.state == state;
}
@override
int get hashCode => hashValues(subKey, state);
}

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/widgets/ui/list_view/flutter_reorderable_list_view.dart';
class LunaReorderableListView extends StatelessWidget {
final List<Widget> children;
@@ -25,7 +24,7 @@ class LunaReorderableListView extends StatelessWidget {
return Scrollbar(
controller: controller,
interactive: true,
child: FlutterReorderableListView(
child: ReorderableListView(
scrollController: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/widgets/ui/list_view/flutter_reorderable_list_view.dart';
class LunaReorderableListViewBuilder extends StatelessWidget {
final int itemCount;
@@ -27,7 +26,7 @@ class LunaReorderableListViewBuilder extends StatelessWidget {
return Scrollbar(
controller: controller,
interactive: true,
child: FlutterReorderableListView.builder(
child: ReorderableListView.builder(
scrollController: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: padding as EdgeInsets? ??