#9 fast scroll feedback + staggered animation improvements
This commit is contained in:
parent
81c9c8a757
commit
2a4722736b
19 changed files with 337 additions and 70 deletions
|
@ -2,6 +2,9 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Collection / Albums / Countries / Tags: added label when dragging scrollbar thumb
|
||||
|
||||
### Changed
|
||||
- Upgraded Flutter to beta v2.1.0-12.2.pre
|
||||
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
"@welcomeAnalyticsToggle": {},
|
||||
"welcomeTermsToggle": "I agree to the terms and conditions",
|
||||
"@welcomeTermsToggle": {},
|
||||
"itemCount": "{count, plural, =1{1 item} other{{count} items}}",
|
||||
"@itemCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
|
||||
"applyButtonLabel": "APPLY",
|
||||
"@applyButtonLabel": {},
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"welcomeMessage": "아베스 사용을 환영합니다",
|
||||
"welcomeAnalyticsToggle": "진단 데이터를 보내는 것에 동의합니다 (선택)",
|
||||
"welcomeTermsToggle": "이용약관에 동의합니다",
|
||||
"itemCount": "{count, plural, other{{count}개}}",
|
||||
|
||||
"applyButtonLabel": "확인",
|
||||
"deleteButtonLabel": "삭제",
|
||||
|
|
|
@ -9,6 +9,7 @@ class Durations {
|
|||
static const dialogTransitionAnimation = Duration(milliseconds: 150); // ref `transitionDuration` in `showDialog()`
|
||||
|
||||
static const staggeredAnimation = Duration(milliseconds: 375);
|
||||
static const staggeredAnimationPageTarget = Duration(milliseconds: 900);
|
||||
static const dialogFieldReachAnimation = Duration(milliseconds: 300);
|
||||
|
||||
static const appBarTitleAnimation = Duration(milliseconds: 300);
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/ref/mime_types.dart';
|
|||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/collection/app_bar.dart';
|
||||
import 'package:aves/widgets/collection/draggable_thumb_label.dart';
|
||||
import 'package:aves/widgets/collection/grid/section_layout.dart';
|
||||
import 'package:aves/widgets/collection/grid/selector.dart';
|
||||
import 'package:aves/widgets/collection/grid/thumbnail.dart';
|
||||
|
@ -29,9 +30,9 @@ import 'package:aves/widgets/common/providers/tile_extent_controller_provider.da
|
|||
import 'package:aves/widgets/common/scaling.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class CollectionGrid extends StatefulWidget {
|
||||
final String settingsRouteKey;
|
||||
|
@ -74,23 +75,34 @@ class _CollectionGridContent extends StatelessWidget {
|
|||
builder: (context, tileExtent, child) {
|
||||
return ThumbnailTheme(
|
||||
extent: tileExtent,
|
||||
child: SectionedEntryListLayoutProvider(
|
||||
collection: collection,
|
||||
scrollableWidth: context.select<TileExtentController, double>((controller) => controller.viewportSize.width),
|
||||
tileExtent: tileExtent,
|
||||
columnCount: context.select<TileExtentController, int>((controller) => controller.getEffectiveColumnCountForExtent(tileExtent)),
|
||||
tileBuilder: (entry) => InteractiveThumbnail(
|
||||
key: ValueKey(entry.contentId),
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
tileExtent: tileExtent,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
),
|
||||
child: _CollectionSectionedContent(
|
||||
collection: collection,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
scrollController: PrimaryScrollController.of(context),
|
||||
),
|
||||
child: Selector<TileExtentController, Tuple2<double, int>>(
|
||||
selector: (context, c) => Tuple2(c.viewportSize.width, c.columnCount),
|
||||
builder: (context, c, child) {
|
||||
final scrollableWidth = c.item1;
|
||||
final columnCount = c.item2;
|
||||
// do not listen for animation delay change
|
||||
final controller = Provider.of<TileExtentController>(context, listen: false);
|
||||
final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget);
|
||||
return SectionedEntryListLayoutProvider(
|
||||
collection: collection,
|
||||
scrollableWidth: scrollableWidth,
|
||||
tileExtent: tileExtent,
|
||||
columnCount: columnCount,
|
||||
tileBuilder: (entry) => InteractiveThumbnail(
|
||||
key: ValueKey(entry.contentId),
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
tileExtent: tileExtent,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
),
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: _CollectionSectionedContent(
|
||||
collection: collection,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
scrollController: PrimaryScrollController.of(context),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -266,10 +278,10 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollView = _buildScrollView(widget.appBar, widget.collection);
|
||||
return _buildDraggableScrollView(scrollView);
|
||||
return _buildDraggableScrollView(scrollView, widget.collection);
|
||||
}
|
||||
|
||||
Widget _buildDraggableScrollView(ScrollView scrollView) {
|
||||
Widget _buildDraggableScrollView(ScrollView scrollView, CollectionLens collection) {
|
||||
return ValueListenableBuilder<double>(
|
||||
valueListenable: widget.appBarHeightNotifier,
|
||||
builder: (context, appBarHeight, child) => Selector<MediaQueryData, double>(
|
||||
|
@ -287,6 +299,10 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
|
|||
top: appBarHeight,
|
||||
bottom: mqPaddingBottom,
|
||||
),
|
||||
labelTextBuilder: (offsetY) => CollectionDraggableThumbLabel(
|
||||
collection: collection,
|
||||
offsetY: offsetY,
|
||||
),
|
||||
child: scrollView,
|
||||
),
|
||||
child: child,
|
||||
|
|
60
lib/widgets/collection/draggable_thumb_label.dart
Normal file
60
lib/widgets/collection/draggable_thumb_label.dart
Normal file
|
@ -0,0 +1,60 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/utils/file_utils.dart';
|
||||
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CollectionDraggableThumbLabel extends StatelessWidget {
|
||||
final CollectionLens collection;
|
||||
final double offsetY;
|
||||
|
||||
const CollectionDraggableThumbLabel({
|
||||
@required this.collection,
|
||||
@required this.offsetY,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableThumbLabel<AvesEntry>(
|
||||
offsetY: offsetY,
|
||||
lineBuilder: (context, entry) {
|
||||
switch (collection.sortFactor) {
|
||||
case EntrySortFactor.date:
|
||||
switch (collection.groupFactor) {
|
||||
case EntryGroupFactor.album:
|
||||
return [
|
||||
DraggableThumbLabel.formatMonthThumbLabel(context, entry.bestDate),
|
||||
if (_hasMultipleSections(context)) context.read<CollectionSource>().getUniqueAlbumName(context, entry.directory),
|
||||
];
|
||||
case EntryGroupFactor.month:
|
||||
case EntryGroupFactor.none:
|
||||
return [
|
||||
DraggableThumbLabel.formatMonthThumbLabel(context, entry.bestDate),
|
||||
];
|
||||
case EntryGroupFactor.day:
|
||||
return [
|
||||
DraggableThumbLabel.formatDayThumbLabel(context, entry.bestDate),
|
||||
];
|
||||
}
|
||||
break;
|
||||
case EntrySortFactor.name:
|
||||
return [
|
||||
if (_hasMultipleSections(context)) context.read<CollectionSource>().getUniqueAlbumName(context, entry.directory),
|
||||
entry.bestTitle,
|
||||
];
|
||||
case EntrySortFactor.size:
|
||||
return [
|
||||
formatFilesize(entry.sizeBytes, round: 0),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _hasMultipleSections(BuildContext context) => context.read<SectionedListLayout<AvesEntry>>().sections.length > 1;
|
||||
}
|
|
@ -15,12 +15,14 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
|
|||
@required int columnCount,
|
||||
@required double tileExtent,
|
||||
@required Widget Function(AvesEntry entry) tileBuilder,
|
||||
@required Duration tileAnimationDelay,
|
||||
@required Widget child,
|
||||
}) : super(
|
||||
scrollableWidth: scrollableWidth,
|
||||
columnCount: columnCount,
|
||||
tileExtent: tileExtent,
|
||||
tileBuilder: tileBuilder,
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: child,
|
||||
);
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ class DraggableScrollbar extends StatefulWidget {
|
|||
@required this.controller,
|
||||
this.padding,
|
||||
this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
|
||||
this.scrollbarTimeToFade = const Duration(milliseconds: 600),
|
||||
this.scrollbarTimeToFade = const Duration(milliseconds: 1000),
|
||||
this.labelTextBuilder,
|
||||
@required this.child,
|
||||
}) : assert(controller != null),
|
||||
|
@ -91,6 +91,7 @@ class DraggableScrollbar extends StatefulWidget {
|
|||
backgroundColor: backgroundColor,
|
||||
child: labelText,
|
||||
),
|
||||
SizedBox(width: 24),
|
||||
scrollThumb,
|
||||
],
|
||||
);
|
||||
|
@ -133,6 +134,7 @@ class ScrollLabel extends StatelessWidget {
|
|||
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
|
||||
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0);
|
||||
bool _isDragInProcess = false;
|
||||
Offset _longPressLastGlobalPosition;
|
||||
|
||||
AnimationController _thumbAnimationController;
|
||||
Animation<double> _thumbAnimation;
|
||||
|
@ -193,9 +195,19 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
),
|
||||
RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
onVerticalDragStart: _onVerticalDragStart,
|
||||
onVerticalDragUpdate: _onVerticalDragUpdate,
|
||||
onVerticalDragEnd: _onVerticalDragEnd,
|
||||
onLongPressStart: (details) {
|
||||
_longPressLastGlobalPosition = details.globalPosition;
|
||||
_onVerticalDragStart();
|
||||
},
|
||||
onLongPressMoveUpdate: (details) {
|
||||
final dy = (details.globalPosition - _longPressLastGlobalPosition).dy;
|
||||
_longPressLastGlobalPosition = details.globalPosition;
|
||||
_onVerticalDragUpdate(dy);
|
||||
},
|
||||
onLongPressEnd: (_) => _onVerticalDragEnd(),
|
||||
onVerticalDragStart: (_) => _onVerticalDragStart(),
|
||||
onVerticalDragUpdate: (details) => _onVerticalDragUpdate(details.delta.dy),
|
||||
onVerticalDragEnd: (_) => _onVerticalDragEnd(),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _thumbOffsetNotifier,
|
||||
builder: (context, thumbOffset, child) => Container(
|
||||
|
@ -244,17 +256,18 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
}
|
||||
}
|
||||
|
||||
void _onVerticalDragStart(DragStartDetails details) {
|
||||
void _onVerticalDragStart() {
|
||||
_labelAnimationController.forward();
|
||||
_fadeoutTimer?.cancel();
|
||||
_showThumb();
|
||||
setState(() => _isDragInProcess = true);
|
||||
}
|
||||
|
||||
void _onVerticalDragUpdate(DragUpdateDetails details) {
|
||||
void _onVerticalDragUpdate(double deltaY) {
|
||||
_showThumb();
|
||||
if (_isDragInProcess) {
|
||||
// thumb offset
|
||||
_thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + details.delta.dy).clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
|
||||
_thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + deltaY).clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
|
||||
|
||||
// scroll offset
|
||||
final min = controller.position.minScrollExtent;
|
||||
|
@ -263,7 +276,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
}
|
||||
}
|
||||
|
||||
void _onVerticalDragEnd(DragEndDetails details) {
|
||||
void _onVerticalDragEnd() {
|
||||
_scheduleFadeout();
|
||||
setState(() => _isDragInProcess = false);
|
||||
}
|
||||
|
|
67
lib/widgets/common/grid/draggable_thumb_label.dart
Normal file
67
lib/widgets/common/grid/draggable_thumb_label.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DraggableThumbLabel<T> extends StatelessWidget {
|
||||
final double offsetY;
|
||||
final List<String> Function(BuildContext context, T item) lineBuilder;
|
||||
|
||||
const DraggableThumbLabel({
|
||||
@required this.offsetY,
|
||||
@required this.lineBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sll = context.read<SectionedListLayout<T>>();
|
||||
final sectionLayout = sll.getSectionAt(offsetY);
|
||||
if (sectionLayout == null) return null;
|
||||
|
||||
final section = sll.sections[sectionLayout.sectionKey];
|
||||
final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent);
|
||||
final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileExtent + sll.spacing)) * sll.columnCount;
|
||||
final item = section[itemIndex];
|
||||
if (item == null) return SizedBox();
|
||||
|
||||
final lines = lineBuilder(context, item);
|
||||
if (lines.isEmpty) return SizedBox();
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 140),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: lines.length > 1
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: lines.map(_buildText).toList(),
|
||||
)
|
||||
: _buildText(lines.first),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildText(String text) => Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
);
|
||||
|
||||
static String formatMonthThumbLabel(BuildContext context, DateTime date) {
|
||||
final l10n = context.l10n;
|
||||
if (date == null) return l10n.sectionUnknown;
|
||||
return DateFormat.yMMM(l10n.localeName).format(date);
|
||||
}
|
||||
|
||||
static String formatDayThumbLabel(BuildContext context, DateTime date) {
|
||||
final l10n = context.l10n;
|
||||
if (date == null) return l10n.sectionUnknown;
|
||||
return DateFormat.yMMMd(l10n.localeName).format(date);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
final int columnCount;
|
||||
final double spacing, tileExtent;
|
||||
final Widget Function(T item) tileBuilder;
|
||||
final Duration tileAnimationDelay;
|
||||
final Widget child;
|
||||
|
||||
const SectionedListLayoutProvider({
|
||||
|
@ -20,6 +21,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
this.spacing = 0,
|
||||
@required this.tileExtent,
|
||||
@required this.tileBuilder,
|
||||
this.tileAnimationDelay,
|
||||
@required this.child,
|
||||
}) : assert(scrollableWidth != 0);
|
||||
|
||||
|
@ -40,6 +42,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
final _showHeaders = showHeaders;
|
||||
final _sections = sections;
|
||||
final sectionKeys = _sections.keys.toList();
|
||||
final animate = tileAnimationDelay > Duration.zero;
|
||||
|
||||
final sectionLayouts = <SectionLayout>[];
|
||||
var currentIndex = 0, currentOffset = 0.0;
|
||||
|
@ -76,6 +79,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
listIndex - sectionFirstIndex,
|
||||
sectionKey,
|
||||
headerExtent,
|
||||
animate,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -97,10 +101,11 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
int sectionChildIndex,
|
||||
SectionKey sectionKey,
|
||||
double headerExtent,
|
||||
bool animate,
|
||||
) {
|
||||
if (sectionChildIndex == 0) {
|
||||
final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : SizedBox.shrink();
|
||||
return _buildAnimation(sectionGridIndex, header);
|
||||
return animate ? _buildAnimation(sectionGridIndex, header) : header;
|
||||
}
|
||||
sectionChildIndex--;
|
||||
|
||||
|
@ -113,7 +118,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
final itemGridIndex = sectionGridIndex + i - minItemIndex;
|
||||
final item = tileBuilder(section[i]);
|
||||
if (i != minItemIndex) children.add(SizedBox(width: spacing));
|
||||
children.add(_buildAnimation(itemGridIndex, item));
|
||||
children.add(animate ? _buildAnimation(itemGridIndex, item) : item);
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -126,7 +131,7 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
position: index,
|
||||
columnCount: columnCount,
|
||||
duration: Durations.staggeredAnimation,
|
||||
delay: Durations.staggeredAnimationDelay,
|
||||
delay: tileAnimationDelay ?? Durations.staggeredAnimationDelay,
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
|
@ -189,9 +194,11 @@ class SectionedListLayout<T> {
|
|||
return Rect.fromLTWH(left, top, tileExtent, tileExtent);
|
||||
}
|
||||
|
||||
SectionLayout getSectionAt(double offsetY) => sectionLayouts.firstWhere((sl) => offsetY < sl.maxOffset, orElse: () => null);
|
||||
|
||||
T getItemAt(Offset position) {
|
||||
var dy = position.dy;
|
||||
final sectionLayout = sectionLayouts.firstWhere((sl) => dy < sl.maxOffset, orElse: () => null);
|
||||
final sectionLayout = getSectionAt(dy);
|
||||
if (sectionLayout == null) return null;
|
||||
|
||||
final section = sections[sectionLayout.sectionKey];
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class TileExtentController {
|
||||
|
@ -45,7 +46,7 @@ class TileExtentController {
|
|||
? oldUserPreferredExtent
|
||||
: currentExtent;
|
||||
|
||||
final columnCount = getEffectiveColumnCountForExtent(targetExtent);
|
||||
final columnCount = _effectiveColumnCountForExtent(targetExtent);
|
||||
final newExtent = _extentForColumnCount(columnCount);
|
||||
|
||||
if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) {
|
||||
|
@ -67,15 +68,24 @@ class TileExtentController {
|
|||
|
||||
int _effectiveColumnCountMax() => _columnCountForExtent(extentMin).floor();
|
||||
|
||||
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax());
|
||||
|
||||
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());
|
||||
|
||||
int getEffectiveColumnCountForExtent(double extent) {
|
||||
int _effectiveColumnCountForExtent(double extent) {
|
||||
if (extent > 0) {
|
||||
final columnCount = _columnCountForExtent(extent);
|
||||
return columnCount.clamp(_effectiveColumnCountMin(), _effectiveColumnCountMax()).round();
|
||||
}
|
||||
return columnCountDefault;
|
||||
}
|
||||
|
||||
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax());
|
||||
|
||||
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());
|
||||
|
||||
int get columnCount => _effectiveColumnCountForExtent(extentNotifier.value);
|
||||
|
||||
Duration getTileAnimationDelay(Duration pageTarget) {
|
||||
final extent = extentNotifier.value;
|
||||
final columnCount = ((viewportSize.width + spacing) / (extent + spacing)).round();
|
||||
final rowCount = (viewportSize.height + spacing) ~/ (extent + spacing);
|
||||
return pageTarget ~/ (columnCount + rowCount) * timeDilation;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
|
|||
appBar: appBar,
|
||||
appBarHeight: AlbumPickAppBar.preferredHeight,
|
||||
filterSections: AlbumListPage.getAlbumEntries(context, source),
|
||||
sortFactor: settings.albumSortFactor,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
queryNotifier: _queryNotifier,
|
||||
applyQuery: (filters, query) {
|
||||
|
|
|
@ -34,6 +34,7 @@ class AlbumListPage extends StatelessWidget {
|
|||
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter>(
|
||||
source: source,
|
||||
title: context.l10n.albumPageTitle,
|
||||
sortFactor: settings.albumSortFactor,
|
||||
groupable: true,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
chipSetActionDelegate: AlbumChipSetActionDelegate(),
|
||||
|
|
42
lib/widgets/filter_grids/common/draggable_thumb_label.dart
Normal file
42
lib/widgets/filter_grids/common/draggable_thumb_label.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FilterDraggableThumbLabel<T extends CollectionFilter> extends StatelessWidget {
|
||||
final ChipSortFactor sortFactor;
|
||||
final double offsetY;
|
||||
|
||||
const FilterDraggableThumbLabel({
|
||||
@required this.sortFactor,
|
||||
@required this.offsetY,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableThumbLabel<FilterGridItem<T>>(
|
||||
offsetY: offsetY,
|
||||
lineBuilder: (context, filterGridItem) {
|
||||
switch (sortFactor) {
|
||||
case ChipSortFactor.count:
|
||||
return [
|
||||
context.l10n.itemCount(context.read<CollectionSource>().count(filterGridItem.filter)),
|
||||
];
|
||||
break;
|
||||
case ChipSortFactor.date:
|
||||
return [
|
||||
DraggableThumbLabel.formatMonthThumbLabel(context, filterGridItem.entry.bestDate),
|
||||
];
|
||||
case ChipSortFactor.name:
|
||||
return [
|
||||
filterGridItem.filter.getLabel(context),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/covers.dart';
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
||||
import 'package:aves/widgets/common/basic/insets.dart';
|
||||
|
@ -20,12 +21,14 @@ import 'package:aves/widgets/common/scaling.dart';
|
|||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:aves/widgets/drawer/app_drawer.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/section_layout.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
typedef QueryTest<T extends CollectionFilter> = Iterable<FilterGridItem<T>> Function(Iterable<FilterGridItem<T>> filters, String query);
|
||||
|
||||
|
@ -34,6 +37,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
final Widget appBar;
|
||||
final double appBarHeight;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||
final ChipSortFactor sortFactor;
|
||||
final bool showHeaders;
|
||||
final ValueNotifier<String> queryNotifier;
|
||||
final QueryTest<T> applyQuery;
|
||||
|
@ -47,6 +51,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
@required this.appBar,
|
||||
this.appBarHeight = kToolbarHeight,
|
||||
@required this.filterSections,
|
||||
@required this.sortFactor,
|
||||
@required this.showHeaders,
|
||||
@required this.queryNotifier,
|
||||
this.applyQuery,
|
||||
|
@ -72,6 +77,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
appBar: appBar,
|
||||
appBarHeight: appBarHeight,
|
||||
filterSections: filterSections,
|
||||
sortFactor: sortFactor,
|
||||
showHeaders: showHeaders,
|
||||
queryNotifier: queryNotifier,
|
||||
applyQuery: applyQuery,
|
||||
|
@ -95,6 +101,7 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
|||
final Widget appBar;
|
||||
final double appBarHeight;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||
final ChipSortFactor sortFactor;
|
||||
final bool showHeaders;
|
||||
final ValueNotifier<String> queryNotifier;
|
||||
final QueryTest<T> applyQuery;
|
||||
|
@ -108,6 +115,7 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
|||
@required this.appBar,
|
||||
@required this.appBarHeight,
|
||||
@required this.filterSections,
|
||||
@required this.sortFactor,
|
||||
@required this.showHeaders,
|
||||
@required this.queryNotifier,
|
||||
@required this.applyQuery,
|
||||
|
@ -137,6 +145,7 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
|
|||
appBar: widget.appBar,
|
||||
appBarHeight: widget.appBarHeight,
|
||||
filterSections: widget.filterSections,
|
||||
sortFactor: widget.sortFactor,
|
||||
showHeaders: widget.showHeaders,
|
||||
queryNotifier: widget.queryNotifier,
|
||||
applyQuery: widget.applyQuery,
|
||||
|
@ -151,6 +160,7 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
|
|||
class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||
final Widget appBar;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||
final ChipSortFactor sortFactor;
|
||||
final bool showHeaders;
|
||||
final ValueNotifier<String> queryNotifier;
|
||||
final Widget Function() emptyBuilder;
|
||||
|
@ -165,6 +175,7 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
@required this.appBar,
|
||||
@required double appBarHeight,
|
||||
@required this.filterSections,
|
||||
@required this.sortFactor,
|
||||
@required this.showHeaders,
|
||||
@required this.queryNotifier,
|
||||
@required this.applyQuery,
|
||||
|
@ -197,38 +208,48 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
|||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
|
||||
builder: (context, tileExtent, child) {
|
||||
final columnCount = context.select<TileExtentController, int>((controller) => controller.getEffectiveColumnCountForExtent(tileExtent));
|
||||
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||
return SectionedFilterListLayoutProvider<T>(
|
||||
sections: visibleFilterSections,
|
||||
showHeaders: showHeaders,
|
||||
scrollableWidth: context.select<TileExtentController, double>((controller) => controller.viewportSize.width),
|
||||
tileExtent: tileExtent,
|
||||
columnCount: columnCount,
|
||||
spacing: tileSpacing,
|
||||
tileBuilder: (gridItem) {
|
||||
final filter = gridItem.filter;
|
||||
final entry = gridItem.entry;
|
||||
return MetaData(
|
||||
metaData: ScalerMetadata(FilterGridItem<T>(filter, entry)),
|
||||
child: DecoratedFilterChip(
|
||||
key: Key(filter.key),
|
||||
filter: filter,
|
||||
extent: tileExtent,
|
||||
pinned: pinnedFilters.contains(filter),
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _FilterSectionedContent<T>(
|
||||
appBar: appBar,
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
visibleFilterSections: visibleFilterSections,
|
||||
emptyBuilder: emptyBuilder,
|
||||
scrollController: PrimaryScrollController.of(context),
|
||||
),
|
||||
);
|
||||
return Selector<TileExtentController, Tuple3<double, int, double>>(
|
||||
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
|
||||
builder: (context, c, child) {
|
||||
final scrollableWidth = c.item1;
|
||||
final columnCount = c.item2;
|
||||
final tileSpacing = c.item3;
|
||||
// do not listen for animation delay change
|
||||
final controller = Provider.of<TileExtentController>(context, listen: false);
|
||||
final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget);
|
||||
return SectionedFilterListLayoutProvider<T>(
|
||||
sections: visibleFilterSections,
|
||||
showHeaders: showHeaders,
|
||||
scrollableWidth: scrollableWidth,
|
||||
tileExtent: tileExtent,
|
||||
columnCount: columnCount,
|
||||
spacing: tileSpacing,
|
||||
tileBuilder: (gridItem) {
|
||||
final filter = gridItem.filter;
|
||||
final entry = gridItem.entry;
|
||||
return MetaData(
|
||||
metaData: ScalerMetadata(FilterGridItem<T>(filter, entry)),
|
||||
child: DecoratedFilterChip(
|
||||
key: Key(filter.key),
|
||||
filter: filter,
|
||||
extent: tileExtent,
|
||||
pinned: pinnedFilters.contains(filter),
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
),
|
||||
);
|
||||
},
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: _FilterSectionedContent<T>(
|
||||
appBar: appBar,
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
visibleFilterSections: visibleFilterSections,
|
||||
sortFactor: sortFactor,
|
||||
emptyBuilder: emptyBuilder,
|
||||
scrollController: PrimaryScrollController.of(context),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
return sectionedListLayoutProvider;
|
||||
|
@ -241,6 +262,7 @@ class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget
|
|||
final Widget appBar;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> visibleFilterSections;
|
||||
final ChipSortFactor sortFactor;
|
||||
final Widget Function() emptyBuilder;
|
||||
final ScrollController scrollController;
|
||||
|
||||
|
@ -248,6 +270,7 @@ class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget
|
|||
@required this.appBar,
|
||||
@required this.appBarHeightNotifier,
|
||||
@required this.visibleFilterSections,
|
||||
@required this.sortFactor,
|
||||
@required this.emptyBuilder,
|
||||
@required this.scrollController,
|
||||
});
|
||||
|
@ -282,6 +305,7 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
|
|||
scrollableKey: _scrollableKey,
|
||||
appBar: appBar,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
sortFactor: widget.sortFactor,
|
||||
emptyBuilder: emptyBuilder,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
|
@ -380,6 +404,7 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
|
|||
final GlobalKey scrollableKey;
|
||||
final Widget appBar;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final ChipSortFactor sortFactor;
|
||||
final Widget Function() emptyBuilder;
|
||||
final ScrollController scrollController;
|
||||
|
||||
|
@ -387,6 +412,7 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
|
|||
@required this.scrollableKey,
|
||||
@required this.appBar,
|
||||
@required this.appBarHeightNotifier,
|
||||
@required this.sortFactor,
|
||||
@required this.emptyBuilder,
|
||||
@required this.scrollController,
|
||||
});
|
||||
|
@ -413,6 +439,10 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
|
|||
top: appBarHeightNotifier.value,
|
||||
bottom: mqPaddingBottom,
|
||||
),
|
||||
labelTextBuilder: (offsetY) => FilterDraggableThumbLabel<T>(
|
||||
sortFactor: sortFactor,
|
||||
offsetY: offsetY,
|
||||
),
|
||||
child: scrollView,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -28,6 +28,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
final CollectionSource source;
|
||||
final String title;
|
||||
final ChipSetActionDelegate chipSetActionDelegate;
|
||||
final ChipSortFactor sortFactor;
|
||||
final bool groupable, showHeaders;
|
||||
final ChipActionDelegate chipActionDelegate;
|
||||
final List<ChipAction> Function(T filter) chipActionsBuilder;
|
||||
|
@ -37,6 +38,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
const FilterNavigationPage({
|
||||
@required this.source,
|
||||
@required this.title,
|
||||
@required this.sortFactor,
|
||||
this.groupable = false,
|
||||
this.showHeaders = false,
|
||||
@required this.chipSetActionDelegate,
|
||||
|
@ -64,6 +66,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
floating: true,
|
||||
),
|
||||
filterSections: filterSections,
|
||||
sortFactor: sortFactor,
|
||||
showHeaders: showHeaders,
|
||||
queryNotifier: ValueNotifier(''),
|
||||
emptyBuilder: () => ValueListenableBuilder<SourceState>(
|
||||
|
|
|
@ -14,6 +14,7 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
|
|||
double spacing = 0,
|
||||
@required double tileExtent,
|
||||
@required Widget Function(FilterGridItem<T> gridItem) tileBuilder,
|
||||
@required Duration tileAnimationDelay,
|
||||
@required Widget child,
|
||||
}) : super(
|
||||
scrollableWidth: scrollableWidth,
|
||||
|
@ -21,6 +22,7 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
|
|||
spacing: spacing,
|
||||
tileExtent: tileExtent,
|
||||
tileBuilder: tileBuilder,
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: child,
|
||||
);
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ class CountryListPage extends StatelessWidget {
|
|||
builder: (context, snapshot) => FilterNavigationPage<LocationFilter>(
|
||||
source: source,
|
||||
title: context.l10n.countryPageTitle,
|
||||
sortFactor: settings.countrySortFactor,
|
||||
chipSetActionDelegate: CountryChipSetActionDelegate(),
|
||||
chipActionDelegate: ChipActionDelegate(),
|
||||
chipActionsBuilder: (filter) => [
|
||||
|
|
|
@ -31,6 +31,7 @@ class TagListPage extends StatelessWidget {
|
|||
builder: (context, snapshot) => FilterNavigationPage<TagFilter>(
|
||||
source: source,
|
||||
title: context.l10n.tagPageTitle,
|
||||
sortFactor: settings.tagSortFactor,
|
||||
chipSetActionDelegate: TagChipSetActionDelegate(),
|
||||
chipActionDelegate: ChipActionDelegate(),
|
||||
chipActionsBuilder: (filter) => [
|
||||
|
|
Loading…
Reference in a new issue