custom SliverList to avoid performing layout on children
This commit is contained in:
parent
56f48f05d0
commit
02c9ac6a8e
13 changed files with 483 additions and 140 deletions
252
lib/labs/sliver_known_extent_list.dart
Normal file
252
lib/labs/sliver_known_extent_list.dart
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
31
lib/widgets/album/grid/header_album.dart
Normal file
31
lib/widgets/album/grid/header_album.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
95
lib/widgets/album/grid/header_generic.dart
Normal file
95
lib/widgets/album/grid/header_generic.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue