split widgets, listen to collection by ChangeNotifierProvider instead of AnimatedWidget

This commit is contained in:
Thibault Deckers 2019-12-31 11:59:01 +09:00
parent 07f073bd77
commit 935227f2e3
7 changed files with 178 additions and 148 deletions

View file

@ -14,55 +14,61 @@ class AllCollectionPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint('$runtimeType build'); debugPrint('$runtimeType build');
final collection = Provider.of<ImageCollection>(context);
return ThumbnailCollection( return ThumbnailCollection(
collection: collection, appBar: _AllCollectionAppBar(),
appBar: SliverAppBar( );
title: const Text('All'), }
actions: [ }
IconButton(
icon: Icon(OMIcons.search), class _AllCollectionAppBar extends StatelessWidget {
onPressed: () => showSearch( @override
context: context, Widget build(BuildContext context) {
delegate: ImageSearchDelegate(collection), final collection = Provider.of<ImageCollection>(context);
), return SliverAppBar(
title: const Text('All'),
actions: [
IconButton(
icon: Icon(OMIcons.search),
onPressed: () => showSearch(
context: context,
delegate: ImageSearchDelegate(collection),
), ),
PopupMenuButton<AlbumAction>( ),
itemBuilder: (context) => [ PopupMenuButton<AlbumAction>(
itemBuilder: (context) => [
PopupMenuItem(
value: AlbumAction.sortByDate,
child: MenuRow(text: 'Sort by date', checked: collection.sortFactor == SortFactor.date),
),
PopupMenuItem(
value: AlbumAction.sortBySize,
child: MenuRow(text: 'Sort by size', checked: collection.sortFactor == SortFactor.size),
),
const PopupMenuDivider(),
if (collection.sortFactor == SortFactor.date) ...[
PopupMenuItem( PopupMenuItem(
value: AlbumAction.sortByDate, value: AlbumAction.groupByAlbum,
child: MenuRow(text: 'Sort by date', checked: collection.sortFactor == SortFactor.date), child: MenuRow(text: 'Group by album', checked: collection.groupFactor == GroupFactor.album),
), ),
PopupMenuItem( PopupMenuItem(
value: AlbumAction.sortBySize, value: AlbumAction.groupByMonth,
child: MenuRow(text: 'Sort by size', checked: collection.sortFactor == SortFactor.size), child: MenuRow(text: 'Group by month', checked: collection.groupFactor == GroupFactor.month),
),
PopupMenuItem(
value: AlbumAction.groupByDay,
child: MenuRow(text: 'Group by day', checked: collection.groupFactor == GroupFactor.day),
), ),
const PopupMenuDivider(), const PopupMenuDivider(),
if (collection.sortFactor == SortFactor.date) ...[
PopupMenuItem(
value: AlbumAction.groupByAlbum,
child: MenuRow(text: 'Group by album', checked: collection.groupFactor == GroupFactor.album),
),
PopupMenuItem(
value: AlbumAction.groupByMonth,
child: MenuRow(text: 'Group by month', checked: collection.groupFactor == GroupFactor.month),
),
PopupMenuItem(
value: AlbumAction.groupByDay,
child: MenuRow(text: 'Group by day', checked: collection.groupFactor == GroupFactor.day),
),
const PopupMenuDivider(),
],
PopupMenuItem(
value: AlbumAction.debug,
child: MenuRow(text: 'Debug', icon: OMIcons.whatshot),
),
], ],
onSelected: (action) => _onActionSelected(context, collection, action), PopupMenuItem(
), value: AlbumAction.debug,
], child: MenuRow(text: 'Debug', icon: OMIcons.whatshot),
floating: true, ),
), ],
onSelected: (action) => _onActionSelected(context, collection, action),
),
],
floating: true,
); );
} }

View file

@ -3,6 +3,7 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FilteredCollectionPage extends StatelessWidget { class FilteredCollectionPage extends StatelessWidget {
final ImageCollection collection; final ImageCollection collection;
@ -17,11 +18,13 @@ class FilteredCollectionPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: Scaffold( child: Scaffold(
body: ThumbnailCollection( body: ChangeNotifierProvider<ImageCollection>.value(
collection: collection, value: collection,
appBar: SliverAppBar( child: ThumbnailCollection(
title: Text(title), appBar: SliverAppBar(
floating: true, title: Text(title),
floating: true,
),
), ),
), ),
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,

View file

@ -4,6 +4,7 @@ import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:provider/provider.dart';
class ImageSearchDelegate extends SearchDelegate<ImageEntry> { class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
final ImageCollection collection; final ImageCollection collection;
@ -59,12 +60,13 @@ class ImageSearchDelegate extends SearchDelegate<ImageEntry> {
return _EmptyContent(); return _EmptyContent();
} }
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: ThumbnailCollection( child: ChangeNotifierProvider<ImageCollection>.value(
collection: ImageCollection( value: ImageCollection(
entries: matches, entries: matches,
groupFactor: collection.groupFactor, groupFactor: collection.groupFactor,
sortFactor: collection.sortFactor, sortFactor: collection.sortFactor,
), ),
child: ThumbnailCollection(),
), ),
); );
} }

View file

@ -1,46 +1,24 @@
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/collection_section.dart'; import 'package:aves/widgets/album/collection_section.dart';
import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class ThumbnailCollection extends AnimatedWidget { class ThumbnailCollection extends StatelessWidget {
final ImageCollection collection;
final Widget appBar; final Widget appBar;
const ThumbnailCollection({
Key key,
this.collection,
this.appBar,
}) : super(key: key, listenable: collection);
@override
Widget build(BuildContext context) {
return ThumbnailCollectionContent(
collection: collection,
appBar: appBar,
);
}
}
class ThumbnailCollectionContent extends StatelessWidget {
final ImageCollection collection;
final Widget appBar;
final Map<dynamic, List<ImageEntry>> _sections;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
ThumbnailCollectionContent({ ThumbnailCollection({
Key key, Key key,
@required this.collection, this.appBar,
@required this.appBar, }) : super(key: key);
}) : _sections = collection.sections,
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sectionKeys = _sections.keys.toList(); final collection = Provider.of<ImageCollection>(context);
final sections = collection.sections;
final sectionKeys = sections.keys.toList();
double topPadding = 0; double topPadding = 0;
if (appBar != null) { if (appBar != null) {
final topWidget = appBar; final topWidget = appBar;
@ -51,40 +29,43 @@ class ThumbnailCollectionContent extends StatelessWidget {
} }
} }
final scrollView = CustomScrollView(
controller: _scrollController,
slivers: [
if (appBar != null) appBar,
...sectionKeys.map((sectionKey) => SectionSliver(
// need key to prevent section header mismatch
// but it should not be unique key, otherwise sections are rebuilt when changing page
key: ValueKey(sectionKey),
collection: collection,
sections: sections,
sectionKey: sectionKey,
)),
SliverToBoxAdapter(
child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.viewInsets.bottom,
builder: (c, mqViewInsetsBottom, child) {
return SizedBox(height: mqViewInsetsBottom);
},
),
),
],
);
return SafeArea( return SafeArea(
child: Selector<MediaQueryData, double>( child: Selector<MediaQueryData, double>(
selector: (c, mq) => mq.viewInsets.bottom, selector: (c, mq) => mq.viewInsets.bottom,
builder: (c, mqViewInsetsBottom, child) => DraggableScrollbar.arrows( builder: (c, mqViewInsetsBottom, child) {
controller: _scrollController, return DraggableScrollbar.arrows(
padding: EdgeInsets.only(
// top padding to adjust scroll thumb
top: topPadding,
bottom: mqViewInsetsBottom,
),
child: CustomScrollView(
controller: _scrollController, controller: _scrollController,
slivers: [ padding: EdgeInsets.only(
if (appBar != null) appBar, // top padding to adjust scroll thumb
...sectionKeys.map((sectionKey) { top: topPadding,
Widget sliver = SectionSliver( bottom: mqViewInsetsBottom,
// need key to prevent section header mismatch ),
// but it should not be unique key, otherwise sections are rebuilt when changing page child: scrollView,
key: ValueKey(sectionKey), );
collection: collection, },
sections: _sections,
sectionKey: sectionKey,
);
if (sectionKey == sectionKeys.last) {
sliver = SliverPadding(
padding: EdgeInsets.only(bottom: mqViewInsetsBottom),
sliver: sliver,
);
}
return sliver;
}),
],
),
),
), ),
); );
} }

View file

@ -36,7 +36,7 @@ class ImagePreviewState extends State<ImagePreview> with AfterInitMixin {
@override @override
void initState() { void initState() {
debugPrint('$runtimeType initState path=${entry.path}'); // debugPrint('$runtimeType initState path=${entry.path}');
super.initState(); super.initState();
_entryChangeNotifier = Listenable.merge([ _entryChangeNotifier = Listenable.merge([
entry.imageChangeNotifier, entry.imageChangeNotifier,

View file

@ -10,12 +10,25 @@ import 'package:provider/provider.dart';
final _stopwatch = Stopwatch()..start(); final _stopwatch = Stopwatch()..start();
class MediaStoreCollectionProvider extends StatelessWidget { class MediaStoreCollectionProvider extends StatefulWidget {
final Widget child; final Widget child;
const MediaStoreCollectionProvider({@required this.child});
@override
_MediaStoreCollectionProviderState createState() => _MediaStoreCollectionProviderState();
}
class _MediaStoreCollectionProviderState extends State<MediaStoreCollectionProvider> {
Future<ImageCollection> collectionFuture;
static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore'); static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore');
const MediaStoreCollectionProvider({@required this.child}); @override
void initState() {
super.initState();
collectionFuture = _create();
}
Future<ImageCollection> _create() async { Future<ImageCollection> _create() async {
debugPrint('$runtimeType _create, elapsed=${_stopwatch.elapsed}'); debugPrint('$runtimeType _create, elapsed=${_stopwatch.elapsed}');
@ -57,10 +70,15 @@ class MediaStoreCollectionProvider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureProvider<ImageCollection>( return FutureBuilder(
create: (context) => _create(), future: collectionFuture,
initialData: ImageCollection(entries: []), builder: (futureContext, AsyncSnapshot<ImageCollection> snapshot) {
child: child, final collection = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : ImageCollection(entries: []);
return ChangeNotifierProvider<ImageCollection>.value(
value: collection,
child: widget.child,
);
},
); );
} }
} }

View file

@ -18,8 +18,6 @@ class MetadataSection extends StatefulWidget {
class MetadataSectionState extends State<MetadataSection> { class MetadataSectionState extends State<MetadataSection> {
Future<Map> _metadataLoader; Future<Map> _metadataLoader;
static const int maxValueLength = 140;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -46,49 +44,71 @@ class MetadataSectionState extends State<MetadataSection> {
if (snapshot.hasError) return Text(snapshot.error.toString()); if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
final metadataMap = snapshot.data.cast<String, Map>(); final metadataMap = snapshot.data.cast<String, Map>();
final directoryNames = metadataMap.keys.toList()..sort();
Widget content;
if (mqWidth > 400) {
final first = <String>[], second = <String>[];
var firstItemCount = 0, secondItemCount = 0;
var firstIndex = 0, secondIndex = directoryNames.length - 1;
while (firstIndex <= secondIndex) {
if (firstItemCount <= secondItemCount) {
final directoryName = directoryNames[firstIndex++];
first.add(directoryName);
firstItemCount += 2 + metadataMap[directoryName].length;
} else {
final directoryName = directoryNames[secondIndex--];
second.insert(0, directoryName);
secondItemCount += 2 + metadataMap[directoryName].length;
}
}
content = Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: getMetadataColumn(metadataMap, first)),
const SizedBox(width: 8),
Expanded(child: getMetadataColumn(metadataMap, second)),
],
);
} else {
content = getMetadataColumn(metadataMap, directoryNames);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SectionRow('Metadata'), const SectionRow('Metadata'),
content, _MetadataSectionContent(
metadataMap: metadataMap,
split: mqWidth > 400,
),
], ],
); );
}, },
), ),
); );
} }
}
Widget getMetadataColumn(Map<String, Map> metadataMap, Iterable<String> directoryNames) { class _MetadataSectionContent extends StatelessWidget {
final Map<String, Map> metadataMap;
final List<String> directoryNames;
final bool split;
_MetadataSectionContent({@required this.metadataMap, @required this.split}) : directoryNames = metadataMap.keys.toList()..sort();
@override
Widget build(BuildContext context) {
if (split) {
final first = <String>[], second = <String>[];
var firstItemCount = 0, secondItemCount = 0;
var firstIndex = 0, secondIndex = directoryNames.length - 1;
while (firstIndex <= secondIndex) {
if (firstItemCount <= secondItemCount) {
final directoryName = directoryNames[firstIndex++];
first.add(directoryName);
firstItemCount += 2 + metadataMap[directoryName].length;
} else {
final directoryName = directoryNames[secondIndex--];
second.insert(0, directoryName);
secondItemCount += 2 + metadataMap[directoryName].length;
}
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _MetadataColumn(metadataMap: metadataMap, directoryNames: first)),
const SizedBox(width: 8),
Expanded(child: _MetadataColumn(metadataMap: metadataMap, directoryNames: second)),
],
);
} else {
return _MetadataColumn(metadataMap: metadataMap, directoryNames: directoryNames);
}
}
}
class _MetadataColumn extends StatelessWidget {
final Map<String, Map> metadataMap;
final List<String> directoryNames;
const _MetadataColumn({@required this.metadataMap, @required this.directoryNames});
static const int maxValueLength = 140;
@override
Widget build(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [