import 'dart:math'; import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_source.dart'; import 'package:aves/utils/android_file_utils.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:aves/widgets/common/icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; class SectionHeader extends StatelessWidget { final CollectionLens collection; final dynamic sectionKey; const SectionHeader({ Key key, @required this.collection, @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 ?? const SizedBox.shrink(); } Widget _buildAlbumSectionHeader() { final folderPath = sectionKey as String; return AlbumSectionHeader( key: ValueKey(folderPath), folderPath: folderPath, albumName: collection.source.getUniqueAlbumName(folderPath), ); } // TODO TLAD cache header extent computation? static double computeHeaderHeight(CollectionSource source, dynamic sectionKey, double scrollableWidth) { var headerExtent = 0.0; if (sectionKey is String) { // only compute height for album headers, as they're the only likely ones to split on multiple lines final hasLeading = androidFileUtils.getAlbumType(sectionKey) != AlbumType.Default; final hasTrailing = androidFileUtils.isOnSD(sectionKey); final text = source.getUniqueAlbumName(sectionKey); final maxWidth = scrollableWidth - TitleSectionHeader.padding.horizontal; final para = RenderParagraph( TextSpan( children: [ if (hasLeading) // `RenderParagraph` fails to lay out `WidgetSpan` offscreen as of Flutter v1.17.0 // so we use a hair space times a magic number to match leading width TextSpan(text: '\u200A' * 23), // 23 hair spaces match a width of 40.0 if (hasTrailing) TextSpan(text: '\u200A' * 17), TextSpan( text: text, style: Constants.titleTextStyle, ), ], ), textDirection: TextDirection.ltr, )..layout(BoxConstraints(maxWidth: maxWidth), parentUsesSize: true); headerExtent = para.getMaxIntrinsicHeight(maxWidth); } headerExtent = max(headerExtent, TitleSectionHeader.leadingDimension) + TitleSectionHeader.padding.vertical; return headerExtent; } } class TitleSectionHeader extends StatelessWidget { final dynamic sectionKey; final Widget leading, trailing; final String title; const TitleSectionHeader({ Key key, @required this.sectionKey, this.leading, @required this.title, this.trailing, }) : super(key: key); static const leadingDimension = 32.0; static const leadingPadding = EdgeInsets.only(right: 8, bottom: 4); static const trailingPadding = EdgeInsets.only(left: 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: (context, isShadow) => SectionSelectableLeading( sectionKey: sectionKey, browsingBuilder: leading != null ? (context) => Container( padding: leadingPadding, width: leadingDimension, height: leadingDimension, child: isShadow ? null : leading, ) : null, ), text: title, trailingBuilder: trailing != null ? (context, isShadow) => Container( padding: trailingPadding, child: isShadow ? null : trailing, ) : null, style: Constants.titleTextStyle, outlineWidth: 2, ), ); } } class SectionSelectableLeading extends StatelessWidget { final dynamic sectionKey; final WidgetBuilder browsingBuilder; static final WidgetBuilder _defaultBrowsingBuilder = (context) => const SizedBox.shrink(); SectionSelectableLeading({ Key key, @required this.sectionKey, WidgetBuilder browsingBuilder, }) : browsingBuilder = browsingBuilder ?? _defaultBrowsingBuilder, super(key: key); @override Widget build(BuildContext context) { final collection = Provider.of(context); return ValueListenableBuilder( valueListenable: collection.activityNotifier, builder: (context, activity, child) { final child = collection.isSelecting ? AnimatedBuilder( animation: collection.selectionChangeNotifier, builder: (context, child) { final sectionEntries = collection.sections[sectionKey]; final selected = collection.isSelected(sectionEntries); final child = IconButton( key: ValueKey(selected), iconSize: 26, padding: const EdgeInsets.only(top: 1), alignment: Alignment.topLeft, icon: Icon(selected ? AIcons.selected : AIcons.unselected), onPressed: () { if (selected) { collection.removeFromSelection(sectionEntries); } else { collection.addToSelection(sectionEntries); } }, tooltip: selected ? 'Deselect section' : 'Select section', constraints: const BoxConstraints( minHeight: TitleSectionHeader.leadingDimension, minWidth: TitleSectionHeader.leadingDimension, ), ); return AnimatedSwitcher( duration: Duration(milliseconds: (300 * timeDilation).toInt()), switchInCurve: Curves.easeOutBack, switchOutCurve: Curves.easeOutBack, transitionBuilder: (child, animation) => ScaleTransition( child: child, scale: animation, ), child: child, ); }, ) : browsingBuilder(context); return AnimatedSwitcher( duration: Duration(milliseconds: (300 * timeDilation).toInt()), switchInCurve: Curves.easeOutBack, switchOutCurve: Curves.easeOutBack, transitionBuilder: (child, animation) => ScaleTransition( child: child, scale: animation, ), child: child, ); }, ); } }