diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a258bf65..6b9692100 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+### Added
+
+- mosaic layout
+
## [v1.7.0] - 2022-09-19
### Added
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index b563fe5f1..67854960a 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -419,6 +419,7 @@
"viewDialogLayoutSectionTitle": "Layout",
"viewDialogReverseSortOrder": "Reverse sort order",
+ "tileLayoutMosaic": "Mosaic",
"tileLayoutGrid": "Grid",
"tileLayoutList": "List",
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index 67fff4117..fe7253960 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -289,6 +289,7 @@
"viewDialogLayoutSectionTitle": "Vue",
"viewDialogReverseSortOrder": "Inverser l’ordre",
+ "tileLayoutMosaic": "Mosaïque",
"tileLayoutGrid": "Grille",
"tileLayoutList": "Liste",
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index ea9379b64..d7f102915 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -289,6 +289,7 @@
"viewDialogLayoutSectionTitle": "배치",
"viewDialogReverseSortOrder": "순서를 뒤바꾸기",
+ "tileLayoutMosaic": "모자이크",
"tileLayoutGrid": "바둑판",
"tileLayoutList": "목록",
diff --git a/lib/model/source/enums/enums.dart b/lib/model/source/enums/enums.dart
index ae59b22f3..0ad4b5b2f 100644
--- a/lib/model/source/enums/enums.dart
+++ b/lib/model/source/enums/enums.dart
@@ -8,4 +8,4 @@ enum EntrySortFactor { date, name, rating, size }
enum EntryGroupFactor { none, album, month, day }
-enum TileLayout { grid, list }
+enum TileLayout { mosaic, grid, list }
diff --git a/lib/model/source/enums/view.dart b/lib/model/source/enums/view.dart
index a80c116c9..5ed3c8a13 100644
--- a/lib/model/source/enums/view.dart
+++ b/lib/model/source/enums/view.dart
@@ -1,3 +1,4 @@
+import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
@@ -18,6 +19,19 @@ extension ExtraEntrySortFactor on EntrySortFactor {
}
}
+ IconData get icon {
+ switch (this) {
+ case EntrySortFactor.date:
+ return AIcons.date;
+ case EntrySortFactor.name:
+ return AIcons.name;
+ case EntrySortFactor.rating:
+ return AIcons.rating;
+ case EntrySortFactor.size:
+ return AIcons.size;
+ }
+ }
+
String getOrderName(BuildContext context, bool reverse) {
final l10n = context.l10n;
switch (this) {
@@ -48,6 +62,19 @@ extension ExtraChipSortFactor on ChipSortFactor {
}
}
+ IconData get icon {
+ switch (this) {
+ case ChipSortFactor.date:
+ return AIcons.date;
+ case ChipSortFactor.name:
+ return AIcons.name;
+ case ChipSortFactor.count:
+ return AIcons.count;
+ case ChipSortFactor.size:
+ return AIcons.size;
+ }
+ }
+
String getOrderName(BuildContext context, bool reverse) {
final l10n = context.l10n;
switch (this) {
@@ -76,6 +103,19 @@ extension ExtraEntryGroupFactor on EntryGroupFactor {
return l10n.collectionGroupNone;
}
}
+
+ IconData get icon {
+ switch (this) {
+ case EntryGroupFactor.album:
+ return AIcons.album;
+ case EntryGroupFactor.month:
+ return AIcons.dateByMonth;
+ case EntryGroupFactor.day:
+ return AIcons.dateByDay;
+ case EntryGroupFactor.none:
+ return AIcons.clear;
+ }
+ }
}
extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor {
@@ -90,16 +130,40 @@ extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor {
return l10n.albumGroupNone;
}
}
+
+ IconData get icon {
+ switch (this) {
+ case AlbumChipGroupFactor.importance:
+ return AIcons.important;
+ case AlbumChipGroupFactor.volume:
+ return AIcons.removableStorage;
+ case AlbumChipGroupFactor.none:
+ return AIcons.clear;
+ }
+ }
}
extension ExtraTileLayout on TileLayout {
String getName(BuildContext context) {
final l10n = context.l10n;
switch (this) {
+ case TileLayout.mosaic:
+ return l10n.tileLayoutMosaic;
case TileLayout.grid:
return l10n.tileLayoutGrid;
case TileLayout.list:
return l10n.tileLayoutList;
}
}
+
+ IconData get icon {
+ switch (this) {
+ case TileLayout.mosaic:
+ return AIcons.layoutMosaic;
+ case TileLayout.grid:
+ return AIcons.layoutGrid;
+ case TileLayout.list:
+ return AIcons.layoutList;
+ }
+ }
}
diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart
index 50e81aab5..34bbf9fe8 100644
--- a/lib/theme/durations.dart
+++ b/lib/theme/durations.dart
@@ -24,11 +24,12 @@ class Durations {
static const chipDecorationAnimation = Duration(milliseconds: 200);
static const highlightScrollAnimationMinMillis = 400;
static const highlightScrollAnimationMaxMillis = 2000;
+ static const scalingGridBackgroundAnimation = Duration(milliseconds: 200);
+ static const scalingGridPositionAnimation = Duration(milliseconds: 150);
// collection animations
static const filterBarRemovalAnimation = Duration(milliseconds: 400);
static const collectionOpOverlayAnimation = Duration(milliseconds: 300);
- static const collectionScalingBackgroundAnimation = Duration(milliseconds: 200);
static const sectionHeaderAnimation = Duration(milliseconds: 200);
static const thumbnailOverlayAnimation = Duration(milliseconds: 200);
diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart
index 6dcdc8a03..7246282ee 100644
--- a/lib/theme/icons.dart
+++ b/lib/theme/icons.dart
@@ -14,8 +14,11 @@ class AIcons {
static const IconData bin = Icons.delete_outlined;
static const IconData broken = Icons.broken_image_outlined;
static const IconData checked = Icons.done_outlined;
+ static const IconData count = MdiIcons.counter;
static const IconData counter = Icons.plus_one_outlined;
static const IconData date = Icons.calendar_today_outlined;
+ static const IconData dateByDay = Icons.today_outlined;
+ static const IconData dateByMonth = Icons.calendar_month_outlined;
static const IconData dateRecent = Icons.today_outlined;
static const IconData dateUndated = Icons.event_busy_outlined;
static const IconData description = Icons.description_outlined;
@@ -43,6 +46,7 @@ class AIcons {
static const IconData sensorControlEnabled = Icons.explore_outlined;
static const IconData sensorControlDisabled = Icons.explore_off_outlined;
static const IconData settings = Icons.settings_outlined;
+ static const IconData size = Icons.data_usage_outlined;
static const IconData text = Icons.format_quote_outlined;
static const IconData tag = Icons.local_offer_outlined;
static const IconData tagUntagged = MdiIcons.tagOffOutline;
@@ -50,6 +54,9 @@ class AIcons {
// view
static const IconData group = Icons.group_work_outlined;
static const IconData layout = Icons.grid_view_outlined;
+ static const IconData layoutMosaic = Icons.view_compact_outlined;
+ static const IconData layoutGrid = Icons.view_comfy_outlined;
+ static const IconData layoutList = Icons.list_outlined;
static const IconData sort = Icons.sort_outlined;
static const IconData sortOrder = Icons.swap_vert_outlined;
@@ -111,7 +118,7 @@ class AIcons {
static const IconData show = Icons.visibility_outlined;
static const IconData slideshow = Icons.slideshow_outlined;
static const IconData speed = Icons.speed_outlined;
- static const IconData stats = Icons.pie_chart_outline_outlined;
+ static const IconData stats = Icons.donut_small_outlined;
static const IconData streams = Icons.translate_outlined;
static const IconData streamVideo = Icons.movie_outlined;
static const IconData streamAudio = Icons.audiotrack_outlined;
diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart
index 89aa6b2f4..13b362449 100644
--- a/lib/utils/constants.dart
+++ b/lib/utils/constants.dart
@@ -5,6 +5,11 @@ import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
class Constants {
+ // `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`)
+ // when used in gradients or lerping to it
+ static const transparentWhite = Color(0x00FFFFFF);
+ static const transparentBlack = Colors.transparent;
+
// as of Flutter v2.8.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped
// so we give it a `strutStyle` with a slightly larger height
static const overflowStrutStyle = StrutStyle(height: 1.3);
diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart
index 4a2d6ccdf..13ee3c64b 100644
--- a/lib/widgets/collection/app_bar.dart
+++ b/lib/widgets/collection/app_bar.dart
@@ -83,6 +83,7 @@ class _CollectionAppBarState extends State with SingleTickerPr
];
static const _layoutOptions = [
+ TileLayout.mosaic,
TileLayout.grid,
TileLayout.list,
];
@@ -537,9 +538,9 @@ class _CollectionAppBarState extends State with SingleTickerPr
builder: (context) {
return TileViewDialog(
initialValue: initialValue,
- sortOptions: Map.fromEntries(_sortOptions.map((v) => MapEntry(v, v.getName(context)))),
- groupOptions: Map.fromEntries(_groupOptions.map((v) => MapEntry(v, v.getName(context)))),
- layoutOptions: Map.fromEntries(_layoutOptions.map((v) => MapEntry(v, v.getName(context)))),
+ sortOptions: _sortOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
+ groupOptions: _groupOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
+ layoutOptions: _layoutOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
canGroup: (s, g, l) => s == EntrySortFactor.date,
);
diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart
index 39b161ac0..8a8dc9a46 100644
--- a/lib/widgets/collection/collection_grid.dart
+++ b/lib/widgets/collection/collection_grid.dart
@@ -25,7 +25,9 @@ import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';
import 'package:aves/widgets/common/grid/item_tracker.dart';
import 'package:aves/widgets/common/grid/scaling.dart';
-import 'package:aves/widgets/common/grid/section_layout.dart';
+import 'package:aves/widgets/common/grid/sections/fixed/scale_grid.dart';
+import 'package:aves/widgets/common/grid/sections/list_layout.dart';
+import 'package:aves/widgets/common/grid/sections/section_layout.dart';
import 'package:aves/widgets/common/grid/selector.dart';
import 'package:aves/widgets/common/grid/sliver.dart';
import 'package:aves/widgets/common/grid/theme.dart';
@@ -34,6 +36,7 @@ import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
+import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
import 'package:flutter/gestures.dart';
@@ -50,7 +53,8 @@ class CollectionGrid extends StatefulWidget {
static const int columnCountDefault = 4;
static const double extentMin = 46;
static const double extentMax = 300;
- static const double spacing = 2;
+ static const double fixedExtentLayoutSpacing = 2;
+ static const double mosaicLayoutSpacing = 4;
const CollectionGrid({
super.key,
@@ -64,6 +68,8 @@ class CollectionGrid extends StatefulWidget {
class _CollectionGridState extends State {
TileExtentController? _tileExtentController;
+ String get settingsRouteKey => widget.settingsRouteKey;
+
@override
void dispose() {
_tileExtentController?.dispose();
@@ -72,14 +78,17 @@ class _CollectionGridState extends State {
@override
Widget build(BuildContext context) {
- _tileExtentController ??= TileExtentController(
- settingsRouteKey: widget.settingsRouteKey,
- columnCountDefault: CollectionGrid.columnCountDefault,
- extentMin: CollectionGrid.extentMin,
- extentMax: CollectionGrid.extentMax,
- spacing: CollectionGrid.spacing,
- horizontalPadding: 2,
- );
+ final spacing = context.select((s) => s.getTileLayout(settingsRouteKey) == TileLayout.mosaic ? CollectionGrid.mosaicLayoutSpacing : CollectionGrid.fixedExtentLayoutSpacing);
+ if (_tileExtentController?.spacing != spacing) {
+ _tileExtentController = TileExtentController(
+ settingsRouteKey: settingsRouteKey,
+ columnCountDefault: CollectionGrid.columnCountDefault,
+ extentMin: CollectionGrid.extentMin,
+ extentMax: CollectionGrid.extentMax,
+ spacing: spacing,
+ horizontalPadding: 2,
+ );
+ }
return TileExtentControllerProvider(
controller: _tileExtentController!,
child: _CollectionGridContent(),
@@ -260,12 +269,13 @@ class _CollectionScaler extends StatelessWidget {
final metrics = context.select>((v) => Tuple2(v.spacing, v.horizontalPadding));
final tileSpacing = metrics.item1;
final horizontalPadding = metrics.item2;
+ final brightness = Theme.of(context).brightness;
return GridScaleGestureDetector(
scrollableKey: scrollableKey,
tileLayout: tileLayout,
heightForWidth: (width) => width,
gridBuilder: (center, tileSize, child) => CustomPaint(
- painter: GridPainter(
+ painter: FixedExtentGridPainter(
tileLayout: tileLayout,
tileCenter: center,
tileSize: tileSize,
@@ -278,7 +288,7 @@ class _CollectionScaler extends StatelessWidget {
),
child: child,
),
- scaledBuilder: (entry, tileSize) => EntryListDetailsTheme(
+ scaledItemBuilder: (entry, tileSize) => EntryListDetailsTheme(
extent: tileSize.height,
child: Tile(
entry: entry,
@@ -286,6 +296,15 @@ class _CollectionScaler extends StatelessWidget {
tileLayout: tileLayout,
),
),
+ mosaicItemBuilder: (index, targetExtent) => DecoratedBox(
+ decoration: BoxDecoration(
+ color: ThumbnailImage.computeLoadingBackgroundColor(index * 10, brightness).withOpacity(.9),
+ border: Border.all(
+ color: DecoratedThumbnail.borderColor,
+ width: DecoratedThumbnail.borderWidth,
+ ),
+ ),
+ ),
child: child,
);
}
diff --git a/lib/widgets/collection/draggable_thumb_label.dart b/lib/widgets/collection/draggable_thumb_label.dart
index 145a00607..d04670818 100644
--- a/lib/widgets/collection/draggable_thumb_label.dart
+++ b/lib/widgets/collection/draggable_thumb_label.dart
@@ -6,7 +6,7 @@ import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';
-import 'package:aves/widgets/common/grid/section_layout.dart';
+import 'package:aves/widgets/common/grid/sections/list_layout.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
diff --git a/lib/widgets/collection/grid/section_layout.dart b/lib/widgets/collection/grid/section_layout.dart
index 6ca41975c..276861e32 100644
--- a/lib/widgets/collection/grid/section_layout.dart
+++ b/lib/widgets/collection/grid/section_layout.dart
@@ -2,14 +2,14 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/section_keys.dart';
import 'package:aves/widgets/collection/grid/headers/any.dart';
-import 'package:aves/widgets/common/grid/section_layout.dart';
+import 'package:aves/widgets/common/grid/sections/provider.dart';
import 'package:flutter/material.dart';
class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider {
final CollectionLens collection;
final bool selectable;
- const SectionedEntryListLayoutProvider({
+ SectionedEntryListLayoutProvider({
super.key,
required this.collection,
required this.selectable,
@@ -25,6 +25,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider item.displayAspectRatio,
);
@override
diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart
index 3b4ed5225..c37693b3b 100644
--- a/lib/widgets/collection/grid/tile.dart
+++ b/lib/widgets/collection/grid/tile.dart
@@ -122,6 +122,7 @@ class Tile extends StatelessWidget {
@override
Widget build(BuildContext context) {
switch (tileLayout) {
+ case TileLayout.mosaic:
case TileLayout.grid:
return _buildThumbnail();
case TileLayout.list:
@@ -145,6 +146,7 @@ class Tile extends StatelessWidget {
Widget _buildThumbnail() => DecoratedThumbnail(
entry: entry,
tileExtent: thumbnailExtent,
+ isMosaic: tileLayout == TileLayout.mosaic,
// when the user is scrolling faster than we can retrieve the thumbnails,
// the retrieval task queue can pile up for thumbnails that got disposed
// in this case we pause the image retrieval task to get it out of the queue
diff --git a/lib/widgets/common/basic/text_dropdown_button.dart b/lib/widgets/common/basic/text_dropdown_button.dart
index 4f60ef164..8efc50028 100644
--- a/lib/widgets/common/basic/text_dropdown_button.dart
+++ b/lib/widgets/common/basic/text_dropdown_button.dart
@@ -5,6 +5,7 @@ class TextDropdownButton extends DropdownButton {
super.key,
required List values,
required String Function(T value) valueText,
+ IconData Function(T value)? valueIcon,
super.value,
super.hint,
super.disabledHint,
@@ -32,21 +33,51 @@ class TextDropdownButton extends DropdownButton {
items: values
.map((v) => DropdownMenuItem(
value: v,
- child: Text(valueText(v)),
+ child: _buildItem(valueText(v), valueIcon?.call(v), selected: false),
))
.toList(),
selectedItemBuilder: (context) => values
.map((v) => DropdownMenuItem(
value: v,
- child: Align(
- alignment: AlignmentDirectional.centerStart,
- child: Text(
- valueText(v),
- softWrap: false,
- overflow: TextOverflow.fade,
- ),
- ),
+ child: _buildItem(valueText(v), valueIcon?.call(v), selected: true),
))
.toList(),
);
+
+ static Widget _buildItem(String text, IconData? icon, {required bool selected}) {
+ final softWrap = selected ? false : null;
+ final overflow = selected ? TextOverflow.fade : null;
+
+ Widget child = icon != null
+ ? Text.rich(
+ TextSpan(
+ children: [
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: Padding(
+ padding: const EdgeInsetsDirectional.only(end: 8, bottom: 2),
+ child: Icon(icon),
+ ),
+ ),
+ TextSpan(text: text),
+ ],
+ ),
+ softWrap: softWrap,
+ overflow: overflow,
+ )
+ : Text(
+ text,
+ softWrap: softWrap,
+ overflow: overflow,
+ );
+
+ if (selected) {
+ child = Align(
+ alignment: AlignmentDirectional.centerStart,
+ child: child,
+ );
+ }
+
+ return child;
+ }
}
diff --git a/lib/widgets/common/grid/draggable_thumb_label.dart b/lib/widgets/common/grid/draggable_thumb_label.dart
index af6888457..eb516f34c 100644
--- a/lib/widgets/common/grid/draggable_thumb_label.dart
+++ b/lib/widgets/common/grid/draggable_thumb_label.dart
@@ -1,6 +1,6 @@
import 'package:aves/theme/format.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
-import 'package:aves/widgets/common/grid/section_layout.dart';
+import 'package:aves/widgets/common/grid/sections/list_layout.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
@@ -41,10 +41,7 @@ class DraggableThumbLabel extends StatelessWidget {
final sectionLayout = sll.getSectionAt(offsetY);
if (sectionLayout == null) return const SizedBox();
- final section = sll.sections[sectionLayout.sectionKey]!;
- final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent);
- final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileHeight + sll.spacing)) * sll.columnCount;
- final item = section[itemIndex];
+ final item = sll.getItemAt(Offset(0, offsetY)) ?? sll.sections[sectionLayout.sectionKey]!.first;
final lines = lineBuilder(context, item);
if (lines.isEmpty) return const SizedBox();
diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart
index 43137f3e2..14bcf5433 100644
--- a/lib/widgets/common/grid/header.dart
+++ b/lib/widgets/common/grid/header.dart
@@ -4,7 +4,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
-import 'package:aves/widgets/common/grid/section_layout.dart';
+import 'package:aves/widgets/common/grid/sections/list_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart
index abaa6e063..33e3d7472 100644
--- a/lib/widgets/common/grid/item_tracker.dart
+++ b/lib/widgets/common/grid/item_tracker.dart
@@ -4,7 +4,7 @@ import 'dart:math';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/theme/durations.dart';
-import 'package:aves/widgets/common/grid/section_layout.dart';
+import 'package:aves/widgets/common/grid/sections/list_layout.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart
index 96988cbff..f0e783126 100644
--- a/lib/widgets/common/grid/scaling.dart
+++ b/lib/widgets/common/grid/scaling.dart
@@ -1,12 +1,9 @@
-import 'dart:ui' as ui;
-
import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums/enums.dart';
-import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart';
-import 'package:aves/widgets/common/extensions/build_context.dart';
+import 'package:aves/widgets/common/grid/sections/fixed/scale_overlay.dart';
+import 'package:aves/widgets/common/grid/sections/mosaic/scale_overlay.dart';
import 'package:aves/widgets/common/grid/theme.dart';
-import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
@@ -26,7 +23,8 @@ class GridScaleGestureDetector extends StatefulWidget {
final TileLayout tileLayout;
final double Function(double width) heightForWidth;
final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
- final Widget Function(T item, Size tileSize) scaledBuilder;
+ final Widget Function(T item, Size tileSize) scaledItemBuilder;
+ final MosaicItemBuilder mosaicItemBuilder;
final Object Function(T item)? highlightItem;
final Widget child;
@@ -36,7 +34,8 @@ class GridScaleGestureDetector extends StatefulWidget {
required this.tileLayout,
required this.heightForWidth,
required this.gridBuilder,
- required this.scaledBuilder,
+ required this.scaledItemBuilder,
+ required this.mosaicItemBuilder,
this.highlightItem,
required this.child,
});
@@ -53,6 +52,8 @@ class _GridScaleGestureDetectorState extends State? _metadata;
+ TileLayout get tileLayout => widget.tileLayout;
+
@override
Widget build(BuildContext context) {
final gestureSettings = context.select((mq) => mq.gestureSettings);
@@ -108,59 +109,66 @@ class _GridScaleGestureDetectorState extends State _ScaleOverlay(
- builder: (scaledTileSize) {
- late final double themeExtent;
- switch (tileLayout) {
- case TileLayout.grid:
- themeExtent = scaledTileSize.width;
- break;
- case TileLayout.list:
- themeExtent = scaledTileSize.height;
- break;
- }
- return SizedBox.fromSize(
- size: scaledTileSize,
- child: GridTheme(
- extent: themeExtent,
- child: widget.scaledBuilder(_metadata!.item, scaledTileSize),
+ switch (tileLayout) {
+ case TileLayout.mosaic:
+ _overlayEntry = OverlayEntry(
+ builder: (context) => MosaicScaleOverlay(
+ contentRect: contentRect,
+ spacing: tileExtentController.spacing,
+ extentMax: _extentMax!,
+ scaledSizeNotifier: _scaledSizeNotifier!,
+ itemBuilder: widget.mosaicItemBuilder,
+ ),
+ );
+ break;
+ case TileLayout.grid:
+ case TileLayout.list:
+ final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
+ _overlayEntry = OverlayEntry(
+ builder: (context) => FixedExtentScaleOverlay(
+ tileLayout: tileLayout,
+ tileCenter: tileCenter,
+ contentRect: contentRect,
+ scaledSizeNotifier: _scaledSizeNotifier!,
+ gridBuilder: widget.gridBuilder,
+ builder: (scaledTileSize) => SizedBox.fromSize(
+ size: scaledTileSize,
+ child: GridTheme(
+ extent: tileLayout == TileLayout.grid ? scaledTileSize.width : scaledTileSize.height,
+ child: widget.scaledItemBuilder(_metadata!.item, scaledTileSize),
+ ),
),
- );
- },
- tileLayout: tileLayout,
- center: tileCenter,
- xMin: xMin,
- xMax: xMax,
- gridBuilder: widget.gridBuilder,
- scaledSizeNotifier: _scaledSizeNotifier!,
- ),
- );
+ ),
+ );
+ break;
+ }
Overlay.of(scrollableContext)!.insert(_overlayEntry!);
}
void _onScaleUpdate(ScaleUpdateDetails details) {
if (_scaledSizeNotifier == null) return;
final s = details.scale;
- switch (widget.tileLayout) {
+ switch (tileLayout) {
+ case TileLayout.mosaic:
case TileLayout.grid:
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
@@ -184,7 +192,8 @@ class _GridScaleGestureDetectorState extends State extends State scaledSizeNotifier;
- final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
-
- const _ScaleOverlay({
- required this.builder,
- required this.tileLayout,
- required this.center,
- required this.xMin,
- required this.xMax,
- required this.scaledSizeNotifier,
- required this.gridBuilder,
- });
-
- @override
- State<_ScaleOverlay> createState() => _ScaleOverlayState();
-}
-
-class _ScaleOverlayState extends State<_ScaleOverlay> {
- bool _init = false;
-
- Offset get center => widget.center;
-
- double get xMin => widget.xMin;
-
- double get xMax => widget.xMax;
-
- // `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`)
- // when used in gradients or lerping to it
- static const transparentWhite = Color(0x00FFFFFF);
-
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _init = true));
- }
-
- @override
- Widget build(BuildContext context) {
- return MediaQueryDataProvider(
- child: Builder(
- builder: (context) => IgnorePointer(
- child: AnimatedContainer(
- decoration: _buildBackgroundDecoration(context),
- duration: Durations.collectionScalingBackgroundAnimation,
- child: ValueListenableBuilder(
- valueListenable: widget.scaledSizeNotifier,
- builder: (context, scaledSize, child) {
- final width = scaledSize.width;
- final height = scaledSize.height;
- // keep scaled thumbnail within the screen
- var dx = .0;
- if (center.dx - width / 2 < xMin) {
- dx = xMin - (center.dx - width / 2);
- } else if (center.dx + width / 2 > xMax) {
- dx = xMax - (center.dx + width / 2);
- }
- final clampedCenter = center.translate(dx, 0);
-
- var child = widget.builder(scaledSize);
- child = Stack(
- children: [
- Positioned(
- left: clampedCenter.dx - width / 2,
- top: clampedCenter.dy - height / 2,
- child: DefaultTextStyle(
- style: const TextStyle(),
- child: child,
- ),
- ),
- ],
- );
- child = widget.gridBuilder(clampedCenter, scaledSize, child);
- return child;
- },
- ),
- ),
- ),
- ),
- );
- }
-
- BoxDecoration _buildBackgroundDecoration(BuildContext context) {
- late final Offset gradientCenter;
- switch (widget.tileLayout) {
- case TileLayout.grid:
- gradientCenter = center;
- break;
- case TileLayout.list:
- gradientCenter = Offset(context.isRtl ? xMax : xMin, center.dy);
- break;
- }
-
- final isDark = Theme.of(context).brightness == Brightness.dark;
- return _init
- ? BoxDecoration(
- gradient: RadialGradient(
- center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select((mq) => mq.size)),
- radius: 1,
- colors: isDark
- ? const [
- Colors.black,
- Colors.black54,
- ]
- : const [
- Colors.white,
- Colors.white38,
- ],
- ),
- )
- : BoxDecoration(
- // provide dummy gradient to lerp to the other one during animation
- gradient: RadialGradient(
- colors: isDark
- ? const [
- Colors.transparent,
- Colors.transparent,
- ]
- : const [
- transparentWhite,
- transparentWhite,
- ],
- ),
- );
- }
-}
-
-class GridPainter extends CustomPainter {
- final TileLayout tileLayout;
- final Offset tileCenter;
- final Size tileSize;
- final double spacing, horizontalPadding, borderWidth;
- final Radius borderRadius;
- final Color color;
- final TextDirection textDirection;
-
- const GridPainter({
- required this.tileLayout,
- required this.tileCenter,
- required this.tileSize,
- required this.spacing,
- required this.horizontalPadding,
- required this.borderWidth,
- required this.borderRadius,
- required this.color,
- required this.textDirection,
- });
-
- @override
- void paint(Canvas canvas, Size size) {
- late final Offset chipCenter;
- late final Size chipSize;
- late final int deltaColumn;
- late final Shader strokeShader;
- switch (tileLayout) {
- case TileLayout.grid:
- chipCenter = tileCenter;
- chipSize = tileSize;
- deltaColumn = 2;
- strokeShader = ui.Gradient.radial(
- tileCenter,
- chipSize.shortestSide * 2,
- [
- color,
- Colors.transparent,
- ],
- [
- .8,
- 1,
- ],
- );
- break;
- case TileLayout.list:
- chipSize = Size.square(tileSize.shortestSide);
- final chipCenterToEdge = chipSize.width / 2;
- chipCenter = Offset(textDirection == TextDirection.rtl ? size.width - (chipCenterToEdge + horizontalPadding) : chipCenterToEdge + horizontalPadding, tileCenter.dy);
- deltaColumn = 0;
- strokeShader = ui.Gradient.linear(
- tileCenter - Offset(0, chipSize.shortestSide * 3),
- tileCenter + Offset(0, chipSize.shortestSide * 3),
- [
- Colors.transparent,
- color,
- color,
- Colors.transparent,
- ],
- [
- 0,
- .2,
- .8,
- 1,
- ],
- );
- break;
- }
- final strokePaint = Paint()
- ..style = PaintingStyle.stroke
- ..strokeWidth = borderWidth
- ..shader = strokeShader;
- final fillPaint = Paint()
- ..style = PaintingStyle.fill
- ..color = color.withOpacity(.25);
-
- final chipWidth = chipSize.width;
- final chipHeight = chipSize.height;
-
- final deltaX = tileSize.width + spacing;
- final deltaY = tileSize.height + spacing;
- for (var i = -deltaColumn; i <= deltaColumn; i++) {
- final dx = deltaX * i;
- for (var j = -2; j <= 2; j++) {
- if (i == 0 && j == 0) continue;
- final dy = deltaY * j;
- final rect = RRect.fromRectAndRadius(
- Rect.fromCenter(
- center: chipCenter + Offset(dx, dy),
- width: chipWidth - borderWidth,
- height: chipHeight - borderWidth,
- ),
- borderRadius,
- );
-
- if ((i.abs() == 1 && j == 0) || (j.abs() == 1 && i == 0)) {
- canvas.drawRRect(rect, fillPaint);
- }
- canvas.drawRRect(rect, strokePaint);
- }
- }
- }
-
- @override
- bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
-}
diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart
deleted file mode 100644
index 040304d67..000000000
--- a/lib/widgets/common/grid/section_layout.dart
+++ /dev/null
@@ -1,450 +0,0 @@
-import 'dart:math';
-
-import 'package:aves/model/source/enums/enums.dart';
-import 'package:aves/model/source/section_keys.dart';
-import 'package:aves/theme/durations.dart';
-import 'package:collection/collection.dart';
-import 'package:equatable/equatable.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
-import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
-import 'package:provider/provider.dart';
-
-abstract class SectionedListLayoutProvider extends StatelessWidget {
- final double scrollableWidth;
- final TileLayout tileLayout;
- final int columnCount;
- final double spacing, horizontalPadding, tileWidth, tileHeight;
- final Widget Function(T item) tileBuilder;
- final Duration tileAnimationDelay;
- final Widget child;
-
- const SectionedListLayoutProvider({
- super.key,
- required this.scrollableWidth,
- required this.tileLayout,
- required int columnCount,
- required this.spacing,
- required this.horizontalPadding,
- required double tileWidth,
- required this.tileHeight,
- required this.tileBuilder,
- required this.tileAnimationDelay,
- required this.child,
- }) : assert(scrollableWidth != 0),
- columnCount = tileLayout == TileLayout.list ? 1 : columnCount,
- tileWidth = tileLayout == TileLayout.list ? scrollableWidth - (horizontalPadding * 2) : tileWidth;
-
- @override
- Widget build(BuildContext context) {
- return ProxyProvider0>(
- update: (context, _) => _updateLayouts(context),
- child: child,
- );
- }
-
- SectionedListLayout _updateLayouts(BuildContext context) {
- final _showHeaders = showHeaders;
- final _sections = sections;
- final sectionKeys = _sections.keys.toList();
- final animate = tileAnimationDelay > Duration.zero;
-
- final sectionLayouts = [];
- var currentIndex = 0;
- var currentOffset = 0.0;
- sectionKeys.forEach((sectionKey) {
- final section = _sections[sectionKey]!;
- final sectionItemCount = section.length;
- final rowCount = (sectionItemCount / columnCount).ceil();
- final sectionChildCount = 1 + rowCount;
-
- final headerExtent = _showHeaders ? getHeaderExtent(context, sectionKey) : 0.0;
-
- final sectionFirstIndex = currentIndex;
- currentIndex += sectionChildCount;
- final sectionLastIndex = currentIndex - 1;
-
- final sectionMinOffset = currentOffset;
- currentOffset += headerExtent + tileHeight * rowCount + spacing * (rowCount - 1);
- final sectionMaxOffset = currentOffset;
-
- sectionLayouts.add(
- SectionLayout(
- sectionKey: sectionKey,
- firstIndex: sectionFirstIndex,
- lastIndex: sectionLastIndex,
- minOffset: sectionMinOffset,
- maxOffset: sectionMaxOffset,
- headerExtent: headerExtent,
- tileHeight: tileHeight,
- spacing: spacing,
- builder: (context, listIndex) => _buildInSection(
- context,
- section,
- listIndex * columnCount,
- listIndex - sectionFirstIndex,
- sectionKey,
- headerExtent,
- animate,
- ),
- ),
- );
- });
- return SectionedListLayout(
- sections: _sections,
- showHeaders: _showHeaders,
- columnCount: columnCount,
- tileWidth: tileWidth,
- tileHeight: tileHeight,
- spacing: spacing,
- horizontalPadding: horizontalPadding,
- sectionLayouts: sectionLayouts,
- );
- }
-
- Widget _buildInSection(
- BuildContext context,
- List section,
- int sectionGridIndex,
- int sectionChildIndex,
- SectionKey sectionKey,
- double headerExtent,
- bool animate,
- ) {
- if (sectionChildIndex == 0) {
- final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : const SizedBox.shrink();
- return animate ? _buildAnimation(context, sectionGridIndex, header) : header;
- }
- sectionChildIndex--;
-
- final sectionItemCount = section.length;
-
- final minItemIndex = sectionChildIndex * columnCount;
- final maxItemIndex = min(sectionItemCount, minItemIndex + columnCount);
- final children = [];
- for (var i = minItemIndex; i < maxItemIndex; i++) {
- final itemGridIndex = sectionGridIndex + i - minItemIndex;
- final item = RepaintBoundary(
- child: tileBuilder(section[i]),
- );
- children.add(animate ? _buildAnimation(context, itemGridIndex, item) : item);
- }
- return Padding(
- padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
- child: _GridRow(
- width: tileWidth,
- height: tileHeight,
- spacing: spacing,
- textDirection: Directionality.of(context),
- children: children,
- ),
- );
- }
-
- Widget _buildAnimation(BuildContext context, int index, Widget child) {
- final durations = context.watch();
- return AnimationConfiguration.staggeredGrid(
- position: index,
- columnCount: columnCount,
- duration: durations.staggeredAnimation,
- delay: tileAnimationDelay,
- child: SlideAnimation(
- verticalOffset: 50.0,
- child: FadeInAnimation(
- child: child,
- ),
- ),
- );
- }
-
- bool get showHeaders;
-
- Map> get sections;
-
- double getHeaderExtent(BuildContext context, SectionKey sectionKey);
-
- Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent);
-
- @override
- void debugFillProperties(DiagnosticPropertiesBuilder properties) {
- super.debugFillProperties(properties);
- properties.add(DoubleProperty('scrollableWidth', scrollableWidth));
- properties.add(IntProperty('columnCount', columnCount));
- properties.add(DoubleProperty('spacing', spacing));
- properties.add(DoubleProperty('horizontalPadding', horizontalPadding));
- properties.add(DoubleProperty('tileWidth', tileWidth));
- properties.add(DoubleProperty('tileHeight', tileHeight));
- properties.add(DiagnosticsProperty('showHeaders', showHeaders));
- }
-}
-
-class SectionedListLayout {
- final Map> sections;
- final bool showHeaders;
- final int columnCount;
- final double tileWidth, tileHeight, spacing, horizontalPadding;
- final List sectionLayouts;
-
- const SectionedListLayout({
- required this.sections,
- required this.showHeaders,
- required this.columnCount,
- required this.tileWidth,
- required this.tileHeight,
- required this.spacing,
- required this.horizontalPadding,
- required this.sectionLayouts,
- });
-
- // return tile rectangle in layout space, i.e. x=0 is start
- Rect? getTileRect(T item) {
- final MapEntry>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item));
- if (section == null) return null;
-
- final sectionKey = section.key;
- final sectionLayout = sectionLayouts.firstWhereOrNull((sl) => sl.sectionKey == sectionKey);
- if (sectionLayout == null) return null;
-
- final sectionItemIndex = section.value.indexOf(item);
- final column = sectionItemIndex % columnCount;
- final row = (sectionItemIndex / columnCount).floor();
- final listIndex = sectionLayout.firstIndex + 1 + row;
-
- final left = horizontalPadding + tileWidth * column + spacing * (column - 1);
- final top = sectionLayout.indexToLayoutOffset(listIndex);
- return Rect.fromLTWH(left, top, tileWidth, tileHeight);
- }
-
- SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset);
-
- // `position` in layout space, i.e. x=0 is start
- T? getItemAt(Offset position) {
- var dy = position.dy;
- final sectionLayout = getSectionAt(dy);
- if (sectionLayout == null) return null;
-
- final section = sections[sectionLayout.sectionKey];
- if (section == null) return null;
-
- dy -= sectionLayout.minOffset + sectionLayout.headerExtent;
- if (dy < 0) return null;
-
- final row = dy ~/ (tileHeight + spacing);
- final column = max(0, position.dx - horizontalPadding) ~/ (tileWidth + spacing);
- final index = row * columnCount + column;
- if (index >= section.length) return null;
-
- return section[index];
- }
-
- @override
- String toString() => '$runtimeType#${shortHash(this)}{sectionCount=${sections.length} columnCount=$columnCount, tileWidth=$tileWidth, tileHeight=$tileHeight}';
-}
-
-@immutable
-class SectionLayout extends Equatable {
- final SectionKey sectionKey;
- final int firstIndex, lastIndex, bodyFirstIndex;
- final double minOffset, maxOffset, bodyMinOffset;
- final double headerExtent, tileHeight, spacing, mainAxisStride;
- final IndexedWidgetBuilder builder;
-
- @override
- List