From 02c9ac6a8e828cc6818fe7bcd5e0ef638027eb3b Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 10 Apr 2020 17:10:40 +0900 Subject: [PATCH] custom SliverList to avoid performing layout on children --- lib/labs/sliver_known_extent_list.dart | 252 ++++++++++++++++++ lib/model/collection_lens.dart | 5 +- lib/model/collection_source.dart | 13 +- lib/widgets/album/collection_drawer.dart | 6 +- lib/widgets/album/collection_list_sliver.dart | 109 ++++++-- lib/widgets/album/collection_section.dart | 68 +---- lib/widgets/album/grid/header_album.dart | 31 +++ .../{sections.dart => grid/header_date.dart} | 33 +-- lib/widgets/album/grid/header_generic.dart | 95 +++++++ .../scaling.dart} | 0 lib/widgets/album/search/search_delegate.dart | 3 +- lib/widgets/album/thumbnail_collection.dart | 5 +- .../fullscreen/info/basic_section.dart | 3 +- 13 files changed, 483 insertions(+), 140 deletions(-) create mode 100644 lib/labs/sliver_known_extent_list.dart create mode 100644 lib/widgets/album/grid/header_album.dart rename lib/widgets/album/{sections.dart => grid/header_date.dart} (56%) create mode 100644 lib/widgets/album/grid/header_generic.dart rename lib/widgets/album/{collection_scaling.dart => grid/scaling.dart} (100%) diff --git a/lib/labs/sliver_known_extent_list.dart b/lib/labs/sliver_known_extent_list.dart new file mode 100644 index 000000000..244109c69 --- /dev/null +++ b/lib/labs/sliver_known_extent_list.dart @@ -0,0 +1,252 @@ +import 'dart:math' as math; + +import 'package:aves/widgets/album/collection_list_sliver.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +class SliverKnownExtentList extends SliverMultiBoxAdaptorWidget { + final List sectionLayouts; + + const SliverKnownExtentList({ + Key key, + @required SliverChildDelegate delegate, + @required this.sectionLayouts, + }) : super(key: key, delegate: delegate); + + @override + RenderSliverKnownExtentBoxAdaptor createRenderObject(BuildContext context) { + final element = context as SliverMultiBoxAdaptorElement; + return RenderSliverKnownExtentBoxAdaptor(childManager: element, sectionLayouts: sectionLayouts); + } + + @override + void updateRenderObject(BuildContext context, RenderSliverKnownExtentBoxAdaptor renderObject) { + renderObject.sectionLayouts = sectionLayouts; + } +} + +class RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { + List _sectionLayouts; + + List get sectionLayouts => _sectionLayouts; + + set sectionLayouts(List value) { + assert(value != null); + if (_sectionLayouts == value) return; + _sectionLayouts = value; + markNeedsLayout(); + } + + RenderSliverKnownExtentBoxAdaptor({ + @required RenderSliverBoxChildManager childManager, + @required List sectionLayouts, + }) : _sectionLayouts = sectionLayouts, + super(childManager: childManager); + + SectionLayout sectionAtIndex(int index) => sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null); + + SectionLayout sectionAtOffset(double scrollOffset) => sectionLayouts.firstWhere((section) => section.hasChildAtOffset(scrollOffset), orElse: () => null); + + double indexToLayoutOffset(int index) { + return sectionAtIndex(index).indexToLayoutOffset(index); + } + + int getMinChildIndexForScrollOffset(double scrollOffset) { + return sectionAtOffset(scrollOffset).getMinChildIndexForScrollOffset(scrollOffset); + } + + int getMaxChildIndexForScrollOffset(double scrollOffset) { + return (sectionAtOffset(scrollOffset) ?? sectionLayouts.last).getMaxChildIndexForScrollOffset(scrollOffset); + } + + double estimateMaxScrollOffset( + SliverConstraints constraints, { + int firstIndex, + int lastIndex, + double leadingScrollOffset, + double trailingScrollOffset, + }) { + return childManager.estimateMaxScrollOffset( + constraints, + firstIndex: firstIndex, + lastIndex: lastIndex, + leadingScrollOffset: leadingScrollOffset, + trailingScrollOffset: trailingScrollOffset, + ); + } + + double computeMaxScrollOffset(SliverConstraints constraints) { + return sectionLayouts.last.maxOffset; + } + + int _calculateLeadingGarbage(int firstIndex) { + var walker = firstChild; + var leadingGarbage = 0; + while (walker != null && indexOf(walker) < firstIndex) { + leadingGarbage += 1; + walker = childAfter(walker); + } + return leadingGarbage; + } + + int _calculateTrailingGarbage(int targetLastIndex) { + var walker = lastChild; + var trailingGarbage = 0; + while (walker != null && indexOf(walker) > targetLastIndex) { + trailingGarbage += 1; + walker = childBefore(walker); + } + return trailingGarbage; + } + + @override + void performLayout() { + final constraints = this.constraints; + childManager.didStartLayout(); + childManager.setDidUnderflow(false); + + final scrollOffset = constraints.scrollOffset + constraints.cacheOrigin; + assert(scrollOffset >= 0.0); + final remainingExtent = constraints.remainingCacheExtent; + assert(remainingExtent >= 0.0); + final targetEndScrollOffset = scrollOffset + remainingExtent; + + final childConstraints = constraints.asBoxConstraints(); + + final firstIndex = getMinChildIndexForScrollOffset(scrollOffset); + final targetLastIndex = targetEndScrollOffset.isFinite ? getMaxChildIndexForScrollOffset(targetEndScrollOffset) : null; + + if (firstChild != null) { + final leadingGarbage = _calculateLeadingGarbage(firstIndex); + final trailingGarbage = _calculateTrailingGarbage(targetLastIndex); + collectGarbage(leadingGarbage, trailingGarbage); + } else { + collectGarbage(0, 0); + } + + if (firstChild == null) { + if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(firstIndex))) { + // There are either no children, or we are past the end of all our children. + // If it is the latter, we will need to find the first available child. + double max; + if (childManager.childCount != null) { + max = computeMaxScrollOffset(constraints); + } else if (firstIndex <= 0) { + max = 0.0; + } else { + // We will have to find it manually. + var possibleFirstIndex = firstIndex - 1; + while (possibleFirstIndex > 0 && + !addInitialChild( + index: possibleFirstIndex, + layoutOffset: indexToLayoutOffset(possibleFirstIndex), + )) { + possibleFirstIndex -= 1; + } + max = sectionAtIndex(possibleFirstIndex).indexToLayoutOffset(possibleFirstIndex); + } + geometry = SliverGeometry( + scrollExtent: max, + maxPaintExtent: max, + ); + childManager.didFinishLayout(); + return; + } + } + + RenderBox trailingChildWithLayout; + + for (var index = indexOf(firstChild) - 1; index >= firstIndex; --index) { + final child = insertAndLayoutLeadingChild(childConstraints); + if (child == null) { + // Items before the previously first child are no longer present. + // Reset the scroll offset to offset all items prior and up to the + // missing item. Let parent re-layout everything. + final layout = sectionAtIndex(index) ?? sectionLayouts.first; + geometry = SliverGeometry(scrollOffsetCorrection: layout.indexToMaxScrollOffset(index)); + return; + } + final childParentData = child.parentData as SliverMultiBoxAdaptorParentData; + childParentData.layoutOffset = indexToLayoutOffset(index); + assert(childParentData.index == index); + trailingChildWithLayout ??= child; + } + + if (trailingChildWithLayout == null) { + firstChild.layout(childConstraints); + final childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData; + childParentData.layoutOffset = indexToLayoutOffset(firstIndex); + trailingChildWithLayout = firstChild; + } + + var estimatedMaxScrollOffset = double.infinity; + for (var index = indexOf(trailingChildWithLayout) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { + var child = childAfter(trailingChildWithLayout); + if (child == null || indexOf(child) != index) { + child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout); + if (child == null) { + // We have run out of children. + final layout = sectionAtIndex(index) ?? sectionLayouts.last; + estimatedMaxScrollOffset = layout.indexToMaxScrollOffset(index); + break; + } + } else { + child.layout(childConstraints); + } + trailingChildWithLayout = child; + assert(child != null); + final childParentData = child.parentData as SliverMultiBoxAdaptorParentData; + assert(childParentData.index == index); + childParentData.layoutOffset = indexToLayoutOffset(childParentData.index); + } + + final lastIndex = indexOf(lastChild); + final leadingScrollOffset = indexToLayoutOffset(firstIndex); + final trailingScrollOffset = indexToLayoutOffset(lastIndex + 1); + + assert(firstIndex == 0 || childScrollOffset(firstChild) - scrollOffset <= precisionErrorTolerance); + assert(debugAssertChildListIsNonEmptyAndContiguous()); + assert(indexOf(firstChild) == firstIndex); + assert(targetLastIndex == null || lastIndex <= targetLastIndex); + + estimatedMaxScrollOffset = math.min( + estimatedMaxScrollOffset, + estimateMaxScrollOffset( + constraints, + firstIndex: firstIndex, + lastIndex: lastIndex, + leadingScrollOffset: leadingScrollOffset, + trailingScrollOffset: trailingScrollOffset, + ), + ); + + final paintExtent = calculatePaintOffset( + constraints, + from: leadingScrollOffset, + to: trailingScrollOffset, + ); + + final cacheExtent = calculateCacheOffset( + constraints, + from: leadingScrollOffset, + to: trailingScrollOffset, + ); + + final targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent; + final targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite ? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint) : null; + geometry = SliverGeometry( + scrollExtent: estimatedMaxScrollOffset, + paintExtent: paintExtent, + cacheExtent: cacheExtent, + maxPaintExtent: estimatedMaxScrollOffset, + // Conservative to avoid flickering away the clip during scroll. + hasVisualOverflow: (targetLastIndexForPaint != null && lastIndex >= targetLastIndexForPaint) || constraints.scrollOffset > 0.0, + ); + + // We may have started the layout while scrolled to the end, which would not + // expose a new child. + if (estimatedMaxScrollOffset == trailingScrollOffset) childManager.setDidUnderflow(true); + childManager.didFinishLayout(); + } +} diff --git a/lib/model/collection_lens.dart b/lib/model/collection_lens.dart index 32e9b1e48..cc500fb4a 100644 --- a/lib/model/collection_lens.dart +++ b/lib/model/collection_lens.dart @@ -162,10 +162,9 @@ class CollectionLens with ChangeNotifier { break; case SortFactor.name: final byAlbum = groupBy(_filteredEntries, (ImageEntry entry) => entry.directory); - final albums = byAlbum.keys.toSet(); final compare = (a, b) { - final ua = CollectionSource.getUniqueAlbumName(a, albums); - final ub = CollectionSource.getUniqueAlbumName(b, albums); + final ua = source.getUniqueAlbumName(a); + final ub = source.getUniqueAlbumName(b); return compareAsciiUpperCase(ua, ub); }; sections = Map.unmodifiable(SplayTreeMap.of(byAlbum, compare)); diff --git a/lib/model/collection_source.dart b/lib/model/collection_source.dart index 50fcaa0f9..cdbb14cfc 100644 --- a/lib/model/collection_source.dart +++ b/lib/model/collection_source.dart @@ -8,6 +8,7 @@ import 'package:path/path.dart'; class CollectionSource { final List _rawEntries; + final Set _folderPaths = {}; final EventBus _eventBus = EventBus(); List sortedAlbums = List.unmodifiable(const Iterable.empty()); @@ -106,11 +107,10 @@ class CollectionSource { } void updateAlbums() { - final albums = _rawEntries.map((entry) => entry.directory).toSet(); - final sorted = albums.toList() + final sorted = _folderPaths.toList() ..sort((a, b) { - final ua = getUniqueAlbumName(a, albums); - final ub = getUniqueAlbumName(b, albums); + final ua = getUniqueAlbumName(a); + final ub = getUniqueAlbumName(b); return compareAsciiUpperCase(ua, ub); }); sortedAlbums = List.unmodifiable(sorted); @@ -134,6 +134,7 @@ class CollectionSource { entry.catalogDateMillis = savedDates.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null)?.dateMillis; }); _rawEntries.addAll(entries); + _folderPaths.addAll(_rawEntries.map((entry) => entry.directory).toSet()); eventBus.fire(const EntryAddedEvent()); } @@ -146,8 +147,8 @@ class CollectionSource { return success; } - static String getUniqueAlbumName(String album, Iterable albums) { - final otherAlbums = albums?.where((item) => item != album) ?? []; + String getUniqueAlbumName(String album) { + final otherAlbums = _folderPaths.where((item) => item != album); final parts = album.split(separator); var partCount = 0; String testName; diff --git a/lib/widgets/album/collection_drawer.dart b/lib/widgets/album/collection_drawer.dart index f2ee703a6..462bf5ccf 100644 --- a/lib/widgets/album/collection_drawer.dart +++ b/lib/widgets/album/collection_drawer.dart @@ -5,8 +5,8 @@ import 'package:aves/model/collection_source.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/mime_types.dart'; import 'package:aves/model/settings.dart'; @@ -96,9 +96,9 @@ class _CollectionDrawerState extends State { final buildAlbumEntry = (album) => _FilteredCollectionNavTile( source: source, leading: IconUtils.getAlbumIcon(context: context, album: album), - title: CollectionSource.getUniqueAlbumName(album, source.sortedAlbums), + title: source.getUniqueAlbumName(album), dense: true, - filter: AlbumFilter(album, CollectionSource.getUniqueAlbumName(album, source.sortedAlbums)), + filter: AlbumFilter(album, source.getUniqueAlbumName(album)), ); final buildTagEntry = (tag) => _FilteredCollectionNavTile( source: source, diff --git a/lib/widgets/album/collection_list_sliver.dart b/lib/widgets/album/collection_list_sliver.dart index 6d623d12d..37ad35801 100644 --- a/lib/widgets/album/collection_list_sliver.dart +++ b/lib/widgets/album/collection_list_sliver.dart @@ -1,8 +1,12 @@ import 'dart:math'; +import 'package:aves/labs/sliver_known_extent_list.dart'; import 'package:aves/model/collection_lens.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/album/collection_section.dart'; +import 'package:aves/widgets/album/grid/header_generic.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; // use a `SliverList` instead of multiple `SliverGrid` because having one `SliverGrid` per section does not scale up // with the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen @@ -10,33 +14,56 @@ import 'package:flutter/material.dart'; class CollectionListSliver extends StatelessWidget { final CollectionLens collection; final bool showHeader; + final double scrollableWidth; final int columnCount; final double tileExtent; - const CollectionListSliver({ + CollectionListSliver({ Key key, @required this.collection, @required this.showHeader, - @required this.columnCount, + @required this.scrollableWidth, @required this.tileExtent, - }) : super(key: key); + }) : columnCount = (scrollableWidth / tileExtent).round(), + super(key: key); @override Widget build(BuildContext context) { - final sectionLayouts = <_SectionLayout>[]; + final sectionLayouts = []; final sectionKeys = collection.sections.keys.toList(); - var firstIndex = 0; + final headerPadding = TitleSectionHeader.padding; + var currentIndex = 0, currentOffset = 0.0; sectionKeys.forEach((sectionKey) { final sectionEntryCount = collection.sections[sectionKey].length; - final rowCount = (sectionEntryCount / columnCount).ceil(); - final widgetCount = rowCount + (showHeader ? 1 : 0); - // closure of `firstIndex` on `sectionFirstIndex` - final sectionFirstIndex = firstIndex; + final sectionChildCount = 1 + (sectionEntryCount / columnCount).ceil(); + + var headerExtent = 0.0; + if (showHeader) { + // only compute height for album headers, as they're the only likely ones to split on multiple lines + if (sectionKey is String) { + final text = collection.source.getUniqueAlbumName(sectionKey); + headerExtent = SectionLayout.computeHeaderExtent(text, scrollableWidth - headerPadding.horizontal); + } + headerExtent = max(headerExtent, TitleSectionHeader.leadingDimension) + headerPadding.vertical; + } + + final sectionFirstIndex = currentIndex; + currentIndex += sectionChildCount; + final sectionLastIndex = currentIndex - 1; + + final sectionMinOffset = currentOffset; + currentOffset += headerExtent + tileExtent * (sectionChildCount - 1); + final sectionMaxOffset = currentOffset; + sectionLayouts.add( - _SectionLayout( + SectionLayout( sectionKey: sectionKey, - widgetCount: widgetCount, firstIndex: sectionFirstIndex, + lastIndex: sectionLastIndex, + minOffset: sectionMinOffset, + maxOffset: sectionMaxOffset, + headerExtent: headerExtent, + tileExtent: tileExtent, builder: (context, listIndex) { listIndex -= sectionFirstIndex; if (showHeader) { @@ -69,16 +96,16 @@ class CollectionListSliver extends StatelessWidget { }, ), ); - firstIndex += widgetCount; }); - final childCount = firstIndex; + final childCount = currentIndex; - return SliverList( + return SliverKnownExtentList( + sectionLayouts: sectionLayouts, delegate: SliverChildBuilderDelegate( (context, index) { if (index >= childCount) return null; - final sectionLayout = sectionLayouts.firstWhere((section) => section.hasChild(index)); - return sectionLayout.builder(context, index); + final sectionLayout = sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null); + return sectionLayout?.builder(context, index) ?? const SizedBox.shrink(); }, childCount: childCount, addAutomaticKeepAlives: false, @@ -87,19 +114,55 @@ class CollectionListSliver extends StatelessWidget { } } -class _SectionLayout { +class SectionLayout { final dynamic sectionKey; - final int widgetCount; - final int firstIndex; - final int lastIndex; + final int firstIndex, lastIndex; + final double minOffset, maxOffset; + final double headerExtent, tileExtent; final IndexedWidgetBuilder builder; - const _SectionLayout({ + const SectionLayout({ @required this.sectionKey, - @required this.widgetCount, @required this.firstIndex, + @required this.lastIndex, + @required this.minOffset, + @required this.maxOffset, + @required this.headerExtent, + @required this.tileExtent, @required this.builder, - }) : lastIndex = firstIndex + widgetCount - 1; + }); bool hasChild(int index) => firstIndex <= index && index <= lastIndex; + + bool hasChildAtOffset(double scrollOffset) => minOffset <= scrollOffset && scrollOffset <= maxOffset; + + double indexToLayoutOffset(int index) { + return minOffset + (index == firstIndex ? 0 : headerExtent + (index - firstIndex - 1) * tileExtent); + } + + double indexToMaxScrollOffset(int index) { + return minOffset + headerExtent + (index - firstIndex) * tileExtent; + } + + int getMinChildIndexForScrollOffset(double scrollOffset) { + scrollOffset -= minOffset + headerExtent; + return firstIndex + (scrollOffset < 0 ? 0 : (scrollOffset / tileExtent).floor()); + } + + int getMaxChildIndexForScrollOffset(double scrollOffset) { + scrollOffset -= minOffset + headerExtent; + return firstIndex + (scrollOffset < 0 ? 0 : (scrollOffset / tileExtent).ceil() - 1); + } + + // TODO TLAD cache header extent computation? + static double computeHeaderExtent(String text, double scrollableWidth) { + final para = RenderParagraph( + TextSpan( + text: text, + style: Constants.titleTextStyle, + ), + textDirection: TextDirection.ltr, + )..layout(BoxConstraints(maxWidth: scrollableWidth), parentUsesSize: true); + return para.getMaxIntrinsicHeight(scrollableWidth); + } } diff --git a/lib/widgets/album/collection_section.dart b/lib/widgets/album/collection_section.dart index 6a5ddc4a4..1c3732ea3 100644 --- a/lib/widgets/album/collection_section.dart +++ b/lib/widgets/album/collection_section.dart @@ -1,10 +1,8 @@ import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/collection_source.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/widgets/album/sections.dart'; +import 'package:aves/widgets/album/grid/header_album.dart'; import 'package:aves/widgets/album/thumbnail.dart'; import 'package:aves/widgets/album/transparent_material_page_route.dart'; -import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:flutter/material.dart'; @@ -60,67 +58,3 @@ class ThumbnailMetadata { const ThumbnailMetadata(this.index, this.entry); } - -class SectionHeader extends StatelessWidget { - final CollectionLens collection; - final Map> sections; - final dynamic sectionKey; - - const SectionHeader({ - Key key, - @required this.collection, - @required this.sections, - @required this.sectionKey, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - Widget header; - switch (collection.sortFactor) { - case SortFactor.date: - if (collection.sortFactor == SortFactor.date) { - switch (collection.groupFactor) { - case GroupFactor.album: - header = _buildAlbumSectionHeader(context); - break; - case GroupFactor.month: - header = MonthSectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime); - break; - case GroupFactor.day: - header = DaySectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime); - break; - } - } - break; - case SortFactor.size: - break; - case SortFactor.name: - header = _buildAlbumSectionHeader(context); - break; - } - return header != null - ? IgnorePointer( - child: header, - ) - : const SizedBox.shrink(); - } - - Widget _buildAlbumSectionHeader(BuildContext context) { - var albumIcon = IconUtils.getAlbumIcon(context: context, album: sectionKey as String); - if (albumIcon != null) { - albumIcon = Material( - type: MaterialType.circle, - elevation: 3, - color: Colors.transparent, - shadowColor: Colors.black, - child: albumIcon, - ); - } - final title = CollectionSource.getUniqueAlbumName(sectionKey as String, sections.keys.cast()); - return TitleSectionHeader( - key: ValueKey(title), - leading: albumIcon, - title: title, - ); - } -} diff --git a/lib/widgets/album/grid/header_album.dart b/lib/widgets/album/grid/header_album.dart new file mode 100644 index 000000000..b1462fc35 --- /dev/null +++ b/lib/widgets/album/grid/header_album.dart @@ -0,0 +1,31 @@ +import 'package:aves/widgets/album/grid/header_generic.dart'; +import 'package:aves/widgets/common/icons.dart'; +import 'package:flutter/material.dart'; + +class AlbumSectionHeader extends StatelessWidget { + final String folderPath, albumName; + + const AlbumSectionHeader({ + Key key, + @required this.folderPath, + @required this.albumName, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var albumIcon = IconUtils.getAlbumIcon(context: context, album: folderPath); + if (albumIcon != null) { + albumIcon = Material( + type: MaterialType.circle, + elevation: 3, + color: Colors.transparent, + shadowColor: Colors.black, + child: albumIcon, + ); + } + return TitleSectionHeader( + leading: albumIcon, + title: albumName, + ); + } +} diff --git a/lib/widgets/album/sections.dart b/lib/widgets/album/grid/header_date.dart similarity index 56% rename from lib/widgets/album/sections.dart rename to lib/widgets/album/grid/header_date.dart index d90234f88..6fcac92c4 100644 --- a/lib/widgets/album/sections.dart +++ b/lib/widgets/album/grid/header_date.dart @@ -1,6 +1,5 @@ -import 'package:aves/utils/constants.dart'; import 'package:aves/utils/time_utils.dart'; -import 'package:aves/widgets/common/fx/outlined_text.dart'; +import 'package:aves/widgets/album/grid/header_generic.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -48,33 +47,3 @@ class MonthSectionHeader extends StatelessWidget { return TitleSectionHeader(title: text); } } - -class TitleSectionHeader extends StatelessWidget { - final Widget leading; - final String title; - - const TitleSectionHeader({Key key, this.leading, this.title}) : super(key: key); - - static const leadingDimension = 32.0; - static const leadingPadding = EdgeInsets.only(right: 8, bottom: 4); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: OutlinedText( - leadingBuilder: leading != null - ? (context, isShadow) => Container( - padding: leadingPadding, - width: leadingDimension, - height: leadingDimension, - child: isShadow ? null : leading, - ) - : null, - text: title, - style: Constants.titleTextStyle, - outlineWidth: 2, - ), - ); - } -} diff --git a/lib/widgets/album/grid/header_generic.dart b/lib/widgets/album/grid/header_generic.dart new file mode 100644 index 000000000..f6b54f5c5 --- /dev/null +++ b/lib/widgets/album/grid/header_generic.dart @@ -0,0 +1,95 @@ +import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/album/grid/header_album.dart'; +import 'package:aves/widgets/album/grid/header_date.dart'; +import 'package:aves/widgets/common/fx/outlined_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class SectionHeader extends StatelessWidget { + final CollectionLens collection; + final Map> sections; + final dynamic sectionKey; + + const SectionHeader({ + Key key, + @required this.collection, + @required this.sections, + @required this.sectionKey, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget header; + switch (collection.sortFactor) { + case SortFactor.date: + if (collection.sortFactor == SortFactor.date) { + switch (collection.groupFactor) { + case GroupFactor.album: + header = _buildAlbumSectionHeader(); + break; + case GroupFactor.month: + header = MonthSectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime); + break; + case GroupFactor.day: + header = DaySectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime); + break; + } + } + break; + case SortFactor.size: + break; + case SortFactor.name: + header = _buildAlbumSectionHeader(); + break; + } + return header != null + ? IgnorePointer( + child: header, + ) + : const SizedBox.shrink(); + } + + Widget _buildAlbumSectionHeader() { + final folderPath = sectionKey as String; + return AlbumSectionHeader( + key: ValueKey(folderPath), + folderPath: folderPath, + albumName: collection.source.getUniqueAlbumName(folderPath), + ); + } +} + +class TitleSectionHeader extends StatelessWidget { + final Widget leading; + final String title; + + const TitleSectionHeader({Key key, this.leading, this.title}) : super(key: key); + + static const leadingDimension = 32.0; + static const leadingPadding = EdgeInsets.only(right: 8, bottom: 4); + static const padding = EdgeInsets.all(16); + + @override + Widget build(BuildContext context) { + return Container( + alignment: AlignmentDirectional.centerStart, + padding: padding, + constraints: const BoxConstraints(minHeight: leadingDimension), + child: OutlinedText( + leadingBuilder: leading != null + ? (context, isShadow) => Container( + padding: leadingPadding, + width: leadingDimension, + height: leadingDimension, + child: isShadow ? null : leading, + ) + : null, + text: title, + style: Constants.titleTextStyle, + outlineWidth: 2, + ), + ); + } +} diff --git a/lib/widgets/album/collection_scaling.dart b/lib/widgets/album/grid/scaling.dart similarity index 100% rename from lib/widgets/album/collection_scaling.dart rename to lib/widgets/album/grid/scaling.dart diff --git a/lib/widgets/album/search/search_delegate.dart b/lib/widgets/album/search/search_delegate.dart index b3ae3c208..fd942aaac 100644 --- a/lib/widgets/album/search/search_delegate.dart +++ b/lib/widgets/album/search/search_delegate.dart @@ -1,5 +1,4 @@ import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/collection_source.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; @@ -68,7 +67,7 @@ class ImageSearchDelegate extends SearchDelegate { _buildFilterRow( context: context, title: 'Albums', - filters: source.sortedAlbums.where(containQuery).map((s) => AlbumFilter(s, CollectionSource.getUniqueAlbumName(s, source.sortedAlbums))).where((f) => containQuery(f.uniqueName)), + filters: source.sortedAlbums.where(containQuery).map((s) => AlbumFilter(s, source.getUniqueAlbumName(s))).where((f) => containQuery(f.uniqueName)), ), _buildFilterRow( context: context, diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 73b07634a..a81e886d7 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -5,8 +5,8 @@ import 'package:aves/model/mime_types.dart'; import 'package:aves/widgets/album/collection_app_bar.dart'; import 'package:aves/widgets/album/collection_list_sliver.dart'; import 'package:aves/widgets/album/collection_page.dart'; -import 'package:aves/widgets/album/collection_scaling.dart'; import 'package:aves/widgets/album/empty.dart'; +import 'package:aves/widgets/album/grid/scaling.dart'; import 'package:aves/widgets/album/tile_extent_manager.dart'; import 'package:aves/widgets/common/scroll_thumb.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; @@ -71,7 +71,8 @@ class ThumbnailCollection extends StatelessWidget { : CollectionListSliver( collection: collection, showHeader: showHeaders, - columnCount: (mqSize.width / tileExtent).round(), + // TODO TLAD get more precise width, considering MediaQuery padding + scrollableWidth: mqSize.width, tileExtent: tileExtent, ), SliverToBoxAdapter( diff --git a/lib/widgets/fullscreen/info/basic_section.dart b/lib/widgets/fullscreen/info/basic_section.dart index 8aad7f114..b4aaa2d1f 100644 --- a/lib/widgets/fullscreen/info/basic_section.dart +++ b/lib/widgets/fullscreen/info/basic_section.dart @@ -1,5 +1,4 @@ import 'package:aves/model/collection_lens.dart'; -import 'package:aves/model/collection_source.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; @@ -53,7 +52,7 @@ class BasicSection extends StatelessWidget { if (entry.isVideo) MimeFilter(MimeTypes.ANY_VIDEO), if (entry.isGif) MimeFilter(MimeTypes.GIF), if (isFavourite) FavouriteFilter(), - if (album != null) AlbumFilter(album, CollectionSource.getUniqueAlbumName(album, collection?.source?.sortedAlbums)), + if (album != null) AlbumFilter(album, collection?.source?.getUniqueAlbumName(album)), ...tags.map((tag) => TagFilter(tag)), ]..sort(); if (filters.isEmpty) return const SizedBox.shrink();