custom SliverList to avoid performing layout on children

This commit is contained in:
Thibault Deckers 2020-04-10 17:10:40 +09:00
parent 56f48f05d0
commit 02c9ac6a8e
13 changed files with 483 additions and 140 deletions

View file

@ -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<SectionLayout> 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<SectionLayout> _sectionLayouts;
List<SectionLayout> get sectionLayouts => _sectionLayouts;
set sectionLayouts(List<SectionLayout> value) {
assert(value != null);
if (_sectionLayouts == value) return;
_sectionLayouts = value;
markNeedsLayout();
}
RenderSliverKnownExtentBoxAdaptor({
@required RenderSliverBoxChildManager childManager,
@required List<SectionLayout> 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();
}
}

View file

@ -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));

View file

@ -8,6 +8,7 @@ import 'package:path/path.dart';
class CollectionSource {
final List<ImageEntry> _rawEntries;
final Set<String> _folderPaths = {};
final EventBus _eventBus = EventBus();
List<String> 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<String> 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;

View file

@ -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<CollectionDrawer> {
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,

View file

@ -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 = <SectionLayout>[];
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);
}
}

View file

@ -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<dynamic, List<ImageEntry>> 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<String>());
return TitleSectionHeader(
key: ValueKey(title),
leading: albumIcon,
title: title,
);
}
}

View file

@ -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,
);
}
}

View file

@ -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,
),
);
}
}

View file

@ -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<dynamic, List<ImageEntry>> 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,
),
);
}
}

View file

@ -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<CollectionFilter> {
_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,

View file

@ -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(

View file

@ -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();