#67 mosaic layout
This commit is contained in:
parent
0bb21b9787
commit
279c2b0f42
49 changed files with 1922 additions and 830 deletions
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- mosaic layout
|
||||||
|
|
||||||
## <a id="v1.7.0"></a>[v1.7.0] - 2022-09-19
|
## <a id="v1.7.0"></a>[v1.7.0] - 2022-09-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -419,6 +419,7 @@
|
||||||
"viewDialogLayoutSectionTitle": "Layout",
|
"viewDialogLayoutSectionTitle": "Layout",
|
||||||
"viewDialogReverseSortOrder": "Reverse sort order",
|
"viewDialogReverseSortOrder": "Reverse sort order",
|
||||||
|
|
||||||
|
"tileLayoutMosaic": "Mosaic",
|
||||||
"tileLayoutGrid": "Grid",
|
"tileLayoutGrid": "Grid",
|
||||||
"tileLayoutList": "List",
|
"tileLayoutList": "List",
|
||||||
|
|
||||||
|
|
|
@ -289,6 +289,7 @@
|
||||||
"viewDialogLayoutSectionTitle": "Vue",
|
"viewDialogLayoutSectionTitle": "Vue",
|
||||||
"viewDialogReverseSortOrder": "Inverser l’ordre",
|
"viewDialogReverseSortOrder": "Inverser l’ordre",
|
||||||
|
|
||||||
|
"tileLayoutMosaic": "Mosaïque",
|
||||||
"tileLayoutGrid": "Grille",
|
"tileLayoutGrid": "Grille",
|
||||||
"tileLayoutList": "Liste",
|
"tileLayoutList": "Liste",
|
||||||
|
|
||||||
|
|
|
@ -289,6 +289,7 @@
|
||||||
"viewDialogLayoutSectionTitle": "배치",
|
"viewDialogLayoutSectionTitle": "배치",
|
||||||
"viewDialogReverseSortOrder": "순서를 뒤바꾸기",
|
"viewDialogReverseSortOrder": "순서를 뒤바꾸기",
|
||||||
|
|
||||||
|
"tileLayoutMosaic": "모자이크",
|
||||||
"tileLayoutGrid": "바둑판",
|
"tileLayoutGrid": "바둑판",
|
||||||
"tileLayoutList": "목록",
|
"tileLayoutList": "목록",
|
||||||
|
|
||||||
|
|
|
@ -8,4 +8,4 @@ enum EntrySortFactor { date, name, rating, size }
|
||||||
|
|
||||||
enum EntryGroupFactor { none, album, month, day }
|
enum EntryGroupFactor { none, album, month, day }
|
||||||
|
|
||||||
enum TileLayout { grid, list }
|
enum TileLayout { mosaic, grid, list }
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/widgets.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) {
|
String getOrderName(BuildContext context, bool reverse) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
switch (this) {
|
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) {
|
String getOrderName(BuildContext context, bool reverse) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
@ -76,6 +103,19 @@ extension ExtraEntryGroupFactor on EntryGroupFactor {
|
||||||
return l10n.collectionGroupNone;
|
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 {
|
extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor {
|
||||||
|
@ -90,16 +130,40 @@ extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor {
|
||||||
return l10n.albumGroupNone;
|
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 {
|
extension ExtraTileLayout on TileLayout {
|
||||||
String getName(BuildContext context) {
|
String getName(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
case TileLayout.mosaic:
|
||||||
|
return l10n.tileLayoutMosaic;
|
||||||
case TileLayout.grid:
|
case TileLayout.grid:
|
||||||
return l10n.tileLayoutGrid;
|
return l10n.tileLayoutGrid;
|
||||||
case TileLayout.list:
|
case TileLayout.list:
|
||||||
return l10n.tileLayoutList;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,12 @@ class Durations {
|
||||||
static const chipDecorationAnimation = Duration(milliseconds: 200);
|
static const chipDecorationAnimation = Duration(milliseconds: 200);
|
||||||
static const highlightScrollAnimationMinMillis = 400;
|
static const highlightScrollAnimationMinMillis = 400;
|
||||||
static const highlightScrollAnimationMaxMillis = 2000;
|
static const highlightScrollAnimationMaxMillis = 2000;
|
||||||
|
static const scalingGridBackgroundAnimation = Duration(milliseconds: 200);
|
||||||
|
static const scalingGridPositionAnimation = Duration(milliseconds: 150);
|
||||||
|
|
||||||
// collection animations
|
// collection animations
|
||||||
static const filterBarRemovalAnimation = Duration(milliseconds: 400);
|
static const filterBarRemovalAnimation = Duration(milliseconds: 400);
|
||||||
static const collectionOpOverlayAnimation = Duration(milliseconds: 300);
|
static const collectionOpOverlayAnimation = Duration(milliseconds: 300);
|
||||||
static const collectionScalingBackgroundAnimation = Duration(milliseconds: 200);
|
|
||||||
static const sectionHeaderAnimation = Duration(milliseconds: 200);
|
static const sectionHeaderAnimation = Duration(milliseconds: 200);
|
||||||
static const thumbnailOverlayAnimation = Duration(milliseconds: 200);
|
static const thumbnailOverlayAnimation = Duration(milliseconds: 200);
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,11 @@ class AIcons {
|
||||||
static const IconData bin = Icons.delete_outlined;
|
static const IconData bin = Icons.delete_outlined;
|
||||||
static const IconData broken = Icons.broken_image_outlined;
|
static const IconData broken = Icons.broken_image_outlined;
|
||||||
static const IconData checked = Icons.done_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 counter = Icons.plus_one_outlined;
|
||||||
static const IconData date = Icons.calendar_today_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 dateRecent = Icons.today_outlined;
|
||||||
static const IconData dateUndated = Icons.event_busy_outlined;
|
static const IconData dateUndated = Icons.event_busy_outlined;
|
||||||
static const IconData description = Icons.description_outlined;
|
static const IconData description = Icons.description_outlined;
|
||||||
|
@ -43,6 +46,7 @@ class AIcons {
|
||||||
static const IconData sensorControlEnabled = Icons.explore_outlined;
|
static const IconData sensorControlEnabled = Icons.explore_outlined;
|
||||||
static const IconData sensorControlDisabled = Icons.explore_off_outlined;
|
static const IconData sensorControlDisabled = Icons.explore_off_outlined;
|
||||||
static const IconData settings = Icons.settings_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 text = Icons.format_quote_outlined;
|
||||||
static const IconData tag = Icons.local_offer_outlined;
|
static const IconData tag = Icons.local_offer_outlined;
|
||||||
static const IconData tagUntagged = MdiIcons.tagOffOutline;
|
static const IconData tagUntagged = MdiIcons.tagOffOutline;
|
||||||
|
@ -50,6 +54,9 @@ class AIcons {
|
||||||
// view
|
// view
|
||||||
static const IconData group = Icons.group_work_outlined;
|
static const IconData group = Icons.group_work_outlined;
|
||||||
static const IconData layout = Icons.grid_view_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 sort = Icons.sort_outlined;
|
||||||
static const IconData sortOrder = Icons.swap_vert_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 show = Icons.visibility_outlined;
|
||||||
static const IconData slideshow = Icons.slideshow_outlined;
|
static const IconData slideshow = Icons.slideshow_outlined;
|
||||||
static const IconData speed = Icons.speed_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 streams = Icons.translate_outlined;
|
||||||
static const IconData streamVideo = Icons.movie_outlined;
|
static const IconData streamVideo = Icons.movie_outlined;
|
||||||
static const IconData streamAudio = Icons.audiotrack_outlined;
|
static const IconData streamAudio = Icons.audiotrack_outlined;
|
||||||
|
|
|
@ -5,6 +5,11 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
class Constants {
|
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
|
// 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
|
// so we give it a `strutStyle` with a slightly larger height
|
||||||
static const overflowStrutStyle = StrutStyle(height: 1.3);
|
static const overflowStrutStyle = StrutStyle(height: 1.3);
|
||||||
|
|
|
@ -83,6 +83,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
];
|
];
|
||||||
|
|
||||||
static const _layoutOptions = [
|
static const _layoutOptions = [
|
||||||
|
TileLayout.mosaic,
|
||||||
TileLayout.grid,
|
TileLayout.grid,
|
||||||
TileLayout.list,
|
TileLayout.list,
|
||||||
];
|
];
|
||||||
|
@ -537,9 +538,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>(
|
return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>(
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
sortOptions: Map.fromEntries(_sortOptions.map((v) => MapEntry(v, v.getName(context)))),
|
sortOptions: _sortOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||||
groupOptions: Map.fromEntries(_groupOptions.map((v) => MapEntry(v, v.getName(context)))),
|
groupOptions: _groupOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||||
layoutOptions: Map.fromEntries(_layoutOptions.map((v) => MapEntry(v, v.getName(context)))),
|
layoutOptions: _layoutOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||||
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
|
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
|
||||||
canGroup: (s, g, l) => s == EntrySortFactor.date,
|
canGroup: (s, g, l) => s == EntrySortFactor.date,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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/draggable_thumb_label.dart';
|
||||||
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
||||||
import 'package:aves/widgets/common/grid/scaling.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/selector.dart';
|
||||||
import 'package:aves/widgets/common/grid/sliver.dart';
|
import 'package:aves/widgets/common/grid/sliver.dart';
|
||||||
import 'package:aves/widgets/common/grid/theme.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/identity/scroll_thumb.dart';
|
||||||
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.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/decorated.dart';
|
||||||
|
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||||
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
|
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
|
@ -50,7 +53,8 @@ class CollectionGrid extends StatefulWidget {
|
||||||
static const int columnCountDefault = 4;
|
static const int columnCountDefault = 4;
|
||||||
static const double extentMin = 46;
|
static const double extentMin = 46;
|
||||||
static const double extentMax = 300;
|
static const double extentMax = 300;
|
||||||
static const double spacing = 2;
|
static const double fixedExtentLayoutSpacing = 2;
|
||||||
|
static const double mosaicLayoutSpacing = 4;
|
||||||
|
|
||||||
const CollectionGrid({
|
const CollectionGrid({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -64,6 +68,8 @@ class CollectionGrid extends StatefulWidget {
|
||||||
class _CollectionGridState extends State<CollectionGrid> {
|
class _CollectionGridState extends State<CollectionGrid> {
|
||||||
TileExtentController? _tileExtentController;
|
TileExtentController? _tileExtentController;
|
||||||
|
|
||||||
|
String get settingsRouteKey => widget.settingsRouteKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tileExtentController?.dispose();
|
_tileExtentController?.dispose();
|
||||||
|
@ -72,14 +78,17 @@ class _CollectionGridState extends State<CollectionGrid> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_tileExtentController ??= TileExtentController(
|
final spacing = context.select<Settings, double>((s) => s.getTileLayout(settingsRouteKey) == TileLayout.mosaic ? CollectionGrid.mosaicLayoutSpacing : CollectionGrid.fixedExtentLayoutSpacing);
|
||||||
settingsRouteKey: widget.settingsRouteKey,
|
if (_tileExtentController?.spacing != spacing) {
|
||||||
columnCountDefault: CollectionGrid.columnCountDefault,
|
_tileExtentController = TileExtentController(
|
||||||
extentMin: CollectionGrid.extentMin,
|
settingsRouteKey: settingsRouteKey,
|
||||||
extentMax: CollectionGrid.extentMax,
|
columnCountDefault: CollectionGrid.columnCountDefault,
|
||||||
spacing: CollectionGrid.spacing,
|
extentMin: CollectionGrid.extentMin,
|
||||||
horizontalPadding: 2,
|
extentMax: CollectionGrid.extentMax,
|
||||||
);
|
spacing: spacing,
|
||||||
|
horizontalPadding: 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
return TileExtentControllerProvider(
|
return TileExtentControllerProvider(
|
||||||
controller: _tileExtentController!,
|
controller: _tileExtentController!,
|
||||||
child: _CollectionGridContent(),
|
child: _CollectionGridContent(),
|
||||||
|
@ -260,12 +269,13 @@ class _CollectionScaler extends StatelessWidget {
|
||||||
final metrics = context.select<TileExtentController, Tuple2<double, double>>((v) => Tuple2(v.spacing, v.horizontalPadding));
|
final metrics = context.select<TileExtentController, Tuple2<double, double>>((v) => Tuple2(v.spacing, v.horizontalPadding));
|
||||||
final tileSpacing = metrics.item1;
|
final tileSpacing = metrics.item1;
|
||||||
final horizontalPadding = metrics.item2;
|
final horizontalPadding = metrics.item2;
|
||||||
|
final brightness = Theme.of(context).brightness;
|
||||||
return GridScaleGestureDetector<AvesEntry>(
|
return GridScaleGestureDetector<AvesEntry>(
|
||||||
scrollableKey: scrollableKey,
|
scrollableKey: scrollableKey,
|
||||||
tileLayout: tileLayout,
|
tileLayout: tileLayout,
|
||||||
heightForWidth: (width) => width,
|
heightForWidth: (width) => width,
|
||||||
gridBuilder: (center, tileSize, child) => CustomPaint(
|
gridBuilder: (center, tileSize, child) => CustomPaint(
|
||||||
painter: GridPainter(
|
painter: FixedExtentGridPainter(
|
||||||
tileLayout: tileLayout,
|
tileLayout: tileLayout,
|
||||||
tileCenter: center,
|
tileCenter: center,
|
||||||
tileSize: tileSize,
|
tileSize: tileSize,
|
||||||
|
@ -278,7 +288,7 @@ class _CollectionScaler extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
scaledBuilder: (entry, tileSize) => EntryListDetailsTheme(
|
scaledItemBuilder: (entry, tileSize) => EntryListDetailsTheme(
|
||||||
extent: tileSize.height,
|
extent: tileSize.height,
|
||||||
child: Tile(
|
child: Tile(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
|
@ -286,6 +296,15 @@ class _CollectionScaler extends StatelessWidget {
|
||||||
tileLayout: tileLayout,
|
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,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import 'package:aves/model/source/enums/enums.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.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/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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,14 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/section_keys.dart';
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/widgets/collection/grid/headers/any.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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesEntry> {
|
class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesEntry> {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final bool selectable;
|
final bool selectable;
|
||||||
|
|
||||||
const SectionedEntryListLayoutProvider({
|
SectionedEntryListLayoutProvider({
|
||||||
super.key,
|
super.key,
|
||||||
required this.collection,
|
required this.collection,
|
||||||
required this.selectable,
|
required this.selectable,
|
||||||
|
@ -25,6 +25,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
|
||||||
}) : super(
|
}) : super(
|
||||||
tileWidth: tileExtent,
|
tileWidth: tileExtent,
|
||||||
tileHeight: tileExtent,
|
tileHeight: tileExtent,
|
||||||
|
coverRatioResolver: (item) => item.displayAspectRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -122,6 +122,7 @@ class Tile extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
switch (tileLayout) {
|
switch (tileLayout) {
|
||||||
|
case TileLayout.mosaic:
|
||||||
case TileLayout.grid:
|
case TileLayout.grid:
|
||||||
return _buildThumbnail();
|
return _buildThumbnail();
|
||||||
case TileLayout.list:
|
case TileLayout.list:
|
||||||
|
@ -145,6 +146,7 @@ class Tile extends StatelessWidget {
|
||||||
Widget _buildThumbnail() => DecoratedThumbnail(
|
Widget _buildThumbnail() => DecoratedThumbnail(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
tileExtent: thumbnailExtent,
|
tileExtent: thumbnailExtent,
|
||||||
|
isMosaic: tileLayout == TileLayout.mosaic,
|
||||||
// when the user is scrolling faster than we can retrieve the thumbnails,
|
// when the user is scrolling faster than we can retrieve the thumbnails,
|
||||||
// the retrieval task queue can pile up for thumbnails that got disposed
|
// 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
|
// in this case we pause the image retrieval task to get it out of the queue
|
||||||
|
|
|
@ -5,6 +5,7 @@ class TextDropdownButton<T> extends DropdownButton<T> {
|
||||||
super.key,
|
super.key,
|
||||||
required List<T> values,
|
required List<T> values,
|
||||||
required String Function(T value) valueText,
|
required String Function(T value) valueText,
|
||||||
|
IconData Function(T value)? valueIcon,
|
||||||
super.value,
|
super.value,
|
||||||
super.hint,
|
super.hint,
|
||||||
super.disabledHint,
|
super.disabledHint,
|
||||||
|
@ -32,21 +33,51 @@ class TextDropdownButton<T> extends DropdownButton<T> {
|
||||||
items: values
|
items: values
|
||||||
.map((v) => DropdownMenuItem<T>(
|
.map((v) => DropdownMenuItem<T>(
|
||||||
value: v,
|
value: v,
|
||||||
child: Text(valueText(v)),
|
child: _buildItem(valueText(v), valueIcon?.call(v), selected: false),
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
selectedItemBuilder: (context) => values
|
selectedItemBuilder: (context) => values
|
||||||
.map((v) => DropdownMenuItem<T>(
|
.map((v) => DropdownMenuItem<T>(
|
||||||
value: v,
|
value: v,
|
||||||
child: Align(
|
child: _buildItem(valueText(v), valueIcon?.call(v), selected: true),
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
child: Text(
|
|
||||||
valueText(v),
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static Widget _buildItem<T>(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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:aves/theme/format.dart';
|
import 'package:aves/theme/format.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.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/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -41,10 +41,7 @@ class DraggableThumbLabel<T> extends StatelessWidget {
|
||||||
final sectionLayout = sll.getSectionAt(offsetY);
|
final sectionLayout = sll.getSectionAt(offsetY);
|
||||||
if (sectionLayout == null) return const SizedBox();
|
if (sectionLayout == null) return const SizedBox();
|
||||||
|
|
||||||
final section = sll.sections[sectionLayout.sectionKey]!;
|
final item = sll.getItemAt(Offset(0, offsetY)) ?? sll.sections[sectionLayout.sectionKey]!.first;
|
||||||
final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent);
|
|
||||||
final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileHeight + sll.spacing)) * sll.columnCount;
|
|
||||||
final item = section[itemIndex];
|
|
||||||
|
|
||||||
final lines = lineBuilder(context, item);
|
final lines = lineBuilder(context, item);
|
||||||
if (lines.isEmpty) return const SizedBox();
|
if (lines.isEmpty) return const SizedBox();
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.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/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'dart:math';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/source/enums/enums.dart';
|
import 'package:aves/model/source/enums/enums.dart';
|
||||||
import 'package:aves/theme/durations.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:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/source/enums/enums.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/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/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:aves/widgets/common/tile_extent_controller.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
|
@ -26,7 +23,8 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||||
final TileLayout tileLayout;
|
final TileLayout tileLayout;
|
||||||
final double Function(double width) heightForWidth;
|
final double Function(double width) heightForWidth;
|
||||||
final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
|
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 Object Function(T item)? highlightItem;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
|
@ -36,7 +34,8 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||||
required this.tileLayout,
|
required this.tileLayout,
|
||||||
required this.heightForWidth,
|
required this.heightForWidth,
|
||||||
required this.gridBuilder,
|
required this.gridBuilder,
|
||||||
required this.scaledBuilder,
|
required this.scaledItemBuilder,
|
||||||
|
required this.mosaicItemBuilder,
|
||||||
this.highlightItem,
|
this.highlightItem,
|
||||||
required this.child,
|
required this.child,
|
||||||
});
|
});
|
||||||
|
@ -53,6 +52,8 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
OverlayEntry? _overlayEntry;
|
OverlayEntry? _overlayEntry;
|
||||||
ScalerMetadata<T>? _metadata;
|
ScalerMetadata<T>? _metadata;
|
||||||
|
|
||||||
|
TileLayout get tileLayout => widget.tileLayout;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final gestureSettings = context.select<MediaQueryData, DeviceGestureSettings>((mq) => mq.gestureSettings);
|
final gestureSettings = context.select<MediaQueryData, DeviceGestureSettings>((mq) => mq.gestureSettings);
|
||||||
|
@ -108,59 +109,66 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
// abort if we cannot find an image to show on overlay
|
// abort if we cannot find an image to show on overlay
|
||||||
if (renderMetaData == null) return;
|
if (renderMetaData == null) return;
|
||||||
_metadata = renderMetaData.metaData;
|
_metadata = renderMetaData.metaData;
|
||||||
_startSize = renderMetaData.size;
|
switch (tileLayout) {
|
||||||
|
case TileLayout.mosaic:
|
||||||
|
_startSize = Size.square(tileExtentController.extentNotifier.value);
|
||||||
|
break;
|
||||||
|
case TileLayout.grid:
|
||||||
|
case TileLayout.list:
|
||||||
|
_startSize = renderMetaData.size;
|
||||||
|
break;
|
||||||
|
}
|
||||||
_scaledSizeNotifier = ValueNotifier(_startSize!);
|
_scaledSizeNotifier = ValueNotifier(_startSize!);
|
||||||
|
|
||||||
// not the same as `MediaQuery` metrics, because of screen insets/padding
|
// not the same as `MediaQuery` metrics, because of screen insets/padding
|
||||||
final scrollViewWidth = scrollableBox.size.width;
|
final scrollViewRect = scrollableBox.localToGlobal(Offset.zero) & scrollableBox.size;
|
||||||
final scrollViewXMin = scrollableBox.localToGlobal(Offset.zero).dx;
|
final contentRect = scrollViewRect.deflate(tileExtentController.horizontalPadding);
|
||||||
final scrollViewXMax = scrollableBox.localToGlobal(Offset(scrollViewWidth, 0)).dx;
|
|
||||||
|
|
||||||
final horizontalPadding = tileExtentController.horizontalPadding;
|
|
||||||
final xMin = scrollViewXMin + horizontalPadding;
|
|
||||||
final xMax = scrollViewXMax - horizontalPadding;
|
|
||||||
|
|
||||||
_extentMin = tileExtentController.effectiveExtentMin;
|
_extentMin = tileExtentController.effectiveExtentMin;
|
||||||
_extentMax = tileExtentController.effectiveExtentMax;
|
_extentMax = tileExtentController.effectiveExtentMax;
|
||||||
|
|
||||||
final halfSize = _startSize! / 2;
|
final halfSize = _startSize! / 2;
|
||||||
final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
|
switch (tileLayout) {
|
||||||
final tileLayout = widget.tileLayout;
|
case TileLayout.mosaic:
|
||||||
_overlayEntry = OverlayEntry(
|
_overlayEntry = OverlayEntry(
|
||||||
builder: (context) => _ScaleOverlay(
|
builder: (context) => MosaicScaleOverlay(
|
||||||
builder: (scaledTileSize) {
|
contentRect: contentRect,
|
||||||
late final double themeExtent;
|
spacing: tileExtentController.spacing,
|
||||||
switch (tileLayout) {
|
extentMax: _extentMax!,
|
||||||
case TileLayout.grid:
|
scaledSizeNotifier: _scaledSizeNotifier!,
|
||||||
themeExtent = scaledTileSize.width;
|
itemBuilder: widget.mosaicItemBuilder,
|
||||||
break;
|
),
|
||||||
case TileLayout.list:
|
);
|
||||||
themeExtent = scaledTileSize.height;
|
break;
|
||||||
break;
|
case TileLayout.grid:
|
||||||
}
|
case TileLayout.list:
|
||||||
return SizedBox.fromSize(
|
final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
|
||||||
size: scaledTileSize,
|
_overlayEntry = OverlayEntry(
|
||||||
child: GridTheme(
|
builder: (context) => FixedExtentScaleOverlay(
|
||||||
extent: themeExtent,
|
tileLayout: tileLayout,
|
||||||
child: widget.scaledBuilder(_metadata!.item, scaledTileSize),
|
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,
|
break;
|
||||||
center: tileCenter,
|
}
|
||||||
xMin: xMin,
|
|
||||||
xMax: xMax,
|
|
||||||
gridBuilder: widget.gridBuilder,
|
|
||||||
scaledSizeNotifier: _scaledSizeNotifier!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Overlay.of(scrollableContext)!.insert(_overlayEntry!);
|
Overlay.of(scrollableContext)!.insert(_overlayEntry!);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||||
if (_scaledSizeNotifier == null) return;
|
if (_scaledSizeNotifier == null) return;
|
||||||
final s = details.scale;
|
final s = details.scale;
|
||||||
switch (widget.tileLayout) {
|
switch (tileLayout) {
|
||||||
|
case TileLayout.mosaic:
|
||||||
case TileLayout.grid:
|
case TileLayout.grid:
|
||||||
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
|
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
|
||||||
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
|
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
|
||||||
|
@ -184,7 +192,8 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
final oldExtent = tileExtentController.extentNotifier.value;
|
final oldExtent = tileExtentController.extentNotifier.value;
|
||||||
// sanitize and update grid layout if necessary
|
// sanitize and update grid layout if necessary
|
||||||
late final double preferredExtent;
|
late final double preferredExtent;
|
||||||
switch (widget.tileLayout) {
|
switch (tileLayout) {
|
||||||
|
case TileLayout.mosaic:
|
||||||
case TileLayout.grid:
|
case TileLayout.grid:
|
||||||
preferredExtent = _scaledSizeNotifier!.value.width;
|
preferredExtent = _scaledSizeNotifier!.value.width;
|
||||||
break;
|
break;
|
||||||
|
@ -226,241 +235,3 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ScaleOverlay extends StatefulWidget {
|
|
||||||
final Widget Function(Size scaledTileSize) builder;
|
|
||||||
final TileLayout tileLayout;
|
|
||||||
final Offset center;
|
|
||||||
final double xMin, xMax;
|
|
||||||
final ValueNotifier<Size> 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<Size>(
|
|
||||||
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<MediaQueryData, Size>((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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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<T> 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<SectionedListLayout<T>>(
|
|
||||||
update: (context, _) => _updateLayouts(context),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SectionedListLayout<T> _updateLayouts(BuildContext context) {
|
|
||||||
final _showHeaders = showHeaders;
|
|
||||||
final _sections = sections;
|
|
||||||
final sectionKeys = _sections.keys.toList();
|
|
||||||
final animate = tileAnimationDelay > Duration.zero;
|
|
||||||
|
|
||||||
final sectionLayouts = <SectionLayout>[];
|
|
||||||
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<T>(
|
|
||||||
sections: _sections,
|
|
||||||
showHeaders: _showHeaders,
|
|
||||||
columnCount: columnCount,
|
|
||||||
tileWidth: tileWidth,
|
|
||||||
tileHeight: tileHeight,
|
|
||||||
spacing: spacing,
|
|
||||||
horizontalPadding: horizontalPadding,
|
|
||||||
sectionLayouts: sectionLayouts,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInSection(
|
|
||||||
BuildContext context,
|
|
||||||
List<T> 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 = <Widget>[];
|
|
||||||
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<DurationsData>();
|
|
||||||
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<SectionKey, List<T>> 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<bool>('showHeaders', showHeaders));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SectionedListLayout<T> {
|
|
||||||
final Map<SectionKey, List<T>> sections;
|
|
||||||
final bool showHeaders;
|
|
||||||
final int columnCount;
|
|
||||||
final double tileWidth, tileHeight, spacing, horizontalPadding;
|
|
||||||
final List<SectionLayout> 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<SectionKey?, List<T>>? 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<Object?> get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, tileHeight, spacing];
|
|
||||||
|
|
||||||
const SectionLayout({
|
|
||||||
required this.sectionKey,
|
|
||||||
required this.firstIndex,
|
|
||||||
required this.lastIndex,
|
|
||||||
required this.minOffset,
|
|
||||||
required this.maxOffset,
|
|
||||||
required this.headerExtent,
|
|
||||||
required this.tileHeight,
|
|
||||||
required this.spacing,
|
|
||||||
required this.builder,
|
|
||||||
}) : bodyFirstIndex = firstIndex + 1,
|
|
||||||
bodyMinOffset = minOffset + headerExtent,
|
|
||||||
mainAxisStride = tileHeight + spacing;
|
|
||||||
|
|
||||||
bool hasChild(int index) => firstIndex <= index && index <= lastIndex;
|
|
||||||
|
|
||||||
bool hasChildAtOffset(double scrollOffset) => minOffset <= scrollOffset && scrollOffset <= maxOffset;
|
|
||||||
|
|
||||||
double indexToLayoutOffset(int index) {
|
|
||||||
index -= bodyFirstIndex;
|
|
||||||
if (index < 0) return minOffset;
|
|
||||||
return bodyMinOffset + index * mainAxisStride;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getMinChildIndexForScrollOffset(double scrollOffset) {
|
|
||||||
scrollOffset -= bodyMinOffset;
|
|
||||||
if (scrollOffset < 0) return firstIndex;
|
|
||||||
return bodyFirstIndex + scrollOffset ~/ mainAxisStride;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getMaxChildIndexForScrollOffset(double scrollOffset) {
|
|
||||||
scrollOffset -= bodyMinOffset;
|
|
||||||
if (scrollOffset < 0) return firstIndex;
|
|
||||||
return bodyFirstIndex + (scrollOffset / mainAxisStride).ceil() - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GridRow extends MultiChildRenderObjectWidget {
|
|
||||||
final double width, height, spacing;
|
|
||||||
final TextDirection textDirection;
|
|
||||||
|
|
||||||
_GridRow({
|
|
||||||
required this.width,
|
|
||||||
required this.height,
|
|
||||||
required this.spacing,
|
|
||||||
required this.textDirection,
|
|
||||||
required List<Widget> children,
|
|
||||||
}) : super(children: children);
|
|
||||||
|
|
||||||
@override
|
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
|
||||||
return _RenderGridRow(
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
spacing: spacing,
|
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateRenderObject(BuildContext context, _RenderGridRow renderObject) {
|
|
||||||
renderObject.width = width;
|
|
||||||
renderObject.height = height;
|
|
||||||
renderObject.spacing = spacing;
|
|
||||||
renderObject.textDirection = textDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties.add(DoubleProperty('width', width));
|
|
||||||
properties.add(DoubleProperty('height', height));
|
|
||||||
properties.add(DoubleProperty('spacing', spacing));
|
|
||||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GridRowParentData extends ContainerBoxParentData<RenderBox> {}
|
|
||||||
|
|
||||||
class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox, _GridRowParentData>, RenderBoxContainerDefaultsMixin<RenderBox, _GridRowParentData> {
|
|
||||||
_RenderGridRow({
|
|
||||||
List<RenderBox>? children,
|
|
||||||
required double width,
|
|
||||||
required double height,
|
|
||||||
required double spacing,
|
|
||||||
required TextDirection textDirection,
|
|
||||||
}) : _width = width,
|
|
||||||
_height = height,
|
|
||||||
_spacing = spacing,
|
|
||||||
_textDirection = textDirection {
|
|
||||||
addAll(children);
|
|
||||||
}
|
|
||||||
|
|
||||||
double get width => _width;
|
|
||||||
double _width;
|
|
||||||
|
|
||||||
set width(double value) {
|
|
||||||
if (_width == value) return;
|
|
||||||
_width = value;
|
|
||||||
markNeedsLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
double get height => _height;
|
|
||||||
double _height;
|
|
||||||
|
|
||||||
set height(double value) {
|
|
||||||
if (_height == value) return;
|
|
||||||
_height = value;
|
|
||||||
markNeedsLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
double get spacing => _spacing;
|
|
||||||
double _spacing;
|
|
||||||
|
|
||||||
set spacing(double value) {
|
|
||||||
if (_spacing == value) return;
|
|
||||||
_spacing = value;
|
|
||||||
markNeedsLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
TextDirection get textDirection => _textDirection;
|
|
||||||
TextDirection _textDirection;
|
|
||||||
|
|
||||||
set textDirection(TextDirection value) {
|
|
||||||
if (_textDirection == value) return;
|
|
||||||
_textDirection = value;
|
|
||||||
markNeedsLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setupParentData(RenderBox child) {
|
|
||||||
if (child.parentData is! _GridRowParentData) {
|
|
||||||
child.parentData = _GridRowParentData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double get intrinsicWidth => width * childCount + spacing * (childCount - 1);
|
|
||||||
|
|
||||||
@override
|
|
||||||
double computeMinIntrinsicWidth(double height) => intrinsicWidth;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double computeMaxIntrinsicWidth(double height) => intrinsicWidth;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double computeMinIntrinsicHeight(double width) => height;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double computeMaxIntrinsicHeight(double width) => height;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void performLayout() {
|
|
||||||
var child = firstChild;
|
|
||||||
if (child == null) {
|
|
||||||
size = constraints.smallest;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
size = Size(constraints.maxWidth, height);
|
|
||||||
final childConstraints = BoxConstraints.tight(Size(width, height));
|
|
||||||
final flipMainAxis = textDirection == TextDirection.rtl;
|
|
||||||
var offset = Offset(flipMainAxis ? size.width - width : 0, 0);
|
|
||||||
final dx = (flipMainAxis ? -1 : 1) * (width + spacing);
|
|
||||||
while (child != null) {
|
|
||||||
child.layout(childConstraints, parentUsesSize: false);
|
|
||||||
final childParentData = child.parentData! as _GridRowParentData;
|
|
||||||
childParentData.offset = offset;
|
|
||||||
offset += Offset(dx, 0);
|
|
||||||
child = childParentData.nextSibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double? computeDistanceToActualBaseline(TextBaseline baseline) {
|
|
||||||
return defaultComputeDistanceToHighestActualBaseline(baseline);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
|
||||||
return defaultHitTestChildren(result, position: position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(PaintingContext context, Offset offset) {
|
|
||||||
defaultPaint(context, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties.add(DoubleProperty('width', width));
|
|
||||||
properties.add(DoubleProperty('height', height));
|
|
||||||
properties.add(DoubleProperty('spacing', spacing));
|
|
||||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
|
||||||
}
|
|
||||||
}
|
|
62
lib/widgets/common/grid/sections/fixed/list_layout.dart
Normal file
62
lib/widgets/common/grid/sections/fixed/list_layout.dart
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class FixedExtentSectionedListLayout<T> extends SectionedListLayout<T> {
|
||||||
|
final int columnCount;
|
||||||
|
final double tileWidth, tileHeight;
|
||||||
|
|
||||||
|
const FixedExtentSectionedListLayout({
|
||||||
|
required super.sections,
|
||||||
|
required super.showHeaders,
|
||||||
|
required this.columnCount,
|
||||||
|
required this.tileWidth,
|
||||||
|
required this.tileHeight,
|
||||||
|
required super.spacing,
|
||||||
|
required super.horizontalPadding,
|
||||||
|
required super.sectionLayouts,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Rect? getTileRect(T item) {
|
||||||
|
final MapEntry<SectionKey?, List<T>>? 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
162
lib/widgets/common/grid/sections/fixed/row.dart
Normal file
162
lib/widgets/common/grid/sections/fixed/row.dart
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class FixedExtentGridRow extends MultiChildRenderObjectWidget {
|
||||||
|
final double width, height, spacing;
|
||||||
|
final TextDirection textDirection;
|
||||||
|
|
||||||
|
FixedExtentGridRow({
|
||||||
|
super.key,
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
required this.spacing,
|
||||||
|
required this.textDirection,
|
||||||
|
required List<Widget> children,
|
||||||
|
}) : super(children: children);
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
return RenderFixedExtentGridRow(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
spacing: spacing,
|
||||||
|
textDirection: textDirection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(BuildContext context, RenderFixedExtentGridRow renderObject) {
|
||||||
|
renderObject.width = width;
|
||||||
|
renderObject.height = height;
|
||||||
|
renderObject.spacing = spacing;
|
||||||
|
renderObject.textDirection = textDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DoubleProperty('width', width));
|
||||||
|
properties.add(DoubleProperty('height', height));
|
||||||
|
properties.add(DoubleProperty('spacing', spacing));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GridRowParentData extends ContainerBoxParentData<RenderBox> {}
|
||||||
|
|
||||||
|
class RenderFixedExtentGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox, _GridRowParentData>, RenderBoxContainerDefaultsMixin<RenderBox, _GridRowParentData> {
|
||||||
|
RenderFixedExtentGridRow({
|
||||||
|
List<RenderBox>? children,
|
||||||
|
required double width,
|
||||||
|
required double height,
|
||||||
|
required double spacing,
|
||||||
|
required TextDirection textDirection,
|
||||||
|
}) : _width = width,
|
||||||
|
_height = height,
|
||||||
|
_spacing = spacing,
|
||||||
|
_textDirection = textDirection {
|
||||||
|
addAll(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
double get width => _width;
|
||||||
|
double _width;
|
||||||
|
|
||||||
|
set width(double value) {
|
||||||
|
if (_width == value) return;
|
||||||
|
_width = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get height => _height;
|
||||||
|
double _height;
|
||||||
|
|
||||||
|
set height(double value) {
|
||||||
|
if (_height == value) return;
|
||||||
|
_height = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get spacing => _spacing;
|
||||||
|
double _spacing;
|
||||||
|
|
||||||
|
set spacing(double value) {
|
||||||
|
if (_spacing == value) return;
|
||||||
|
_spacing = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextDirection get textDirection => _textDirection;
|
||||||
|
TextDirection _textDirection;
|
||||||
|
|
||||||
|
set textDirection(TextDirection value) {
|
||||||
|
if (_textDirection == value) return;
|
||||||
|
_textDirection = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setupParentData(RenderBox child) {
|
||||||
|
if (child.parentData is! _GridRowParentData) {
|
||||||
|
child.parentData = _GridRowParentData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double get intrinsicWidth => width * childCount + spacing * (childCount - 1);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicWidth(double height) => intrinsicWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicWidth(double height) => intrinsicWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicHeight(double width) => height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicHeight(double width) => height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
var child = firstChild;
|
||||||
|
if (child == null) {
|
||||||
|
size = constraints.smallest;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size = Size(constraints.maxWidth, height);
|
||||||
|
final childConstraints = BoxConstraints.tight(Size(width, height));
|
||||||
|
final flipMainAxis = textDirection == TextDirection.rtl;
|
||||||
|
var offset = Offset(flipMainAxis ? size.width - width : 0, 0);
|
||||||
|
final dx = (flipMainAxis ? -1 : 1) * (width + spacing);
|
||||||
|
while (child != null) {
|
||||||
|
child.layout(childConstraints, parentUsesSize: false);
|
||||||
|
final childParentData = child.parentData! as _GridRowParentData;
|
||||||
|
childParentData.offset = offset;
|
||||||
|
offset += Offset(dx, 0);
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? computeDistanceToActualBaseline(TextBaseline baseline) {
|
||||||
|
return defaultComputeDistanceToHighestActualBaseline(baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||||
|
return defaultHitTestChildren(result, position: position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
defaultPaint(context, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DoubleProperty('width', width));
|
||||||
|
properties.add(DoubleProperty('height', height));
|
||||||
|
properties.add(DoubleProperty('spacing', spacing));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
||||||
|
}
|
||||||
|
}
|
113
lib/widgets/common/grid/sections/fixed/scale_grid.dart
Normal file
113
lib/widgets/common/grid/sections/fixed/scale_grid.dart
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:aves/model/source/enums/enums.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FixedExtentGridPainter 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 FixedExtentGridPainter({
|
||||||
|
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.mosaic:
|
||||||
|
return;
|
||||||
|
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;
|
||||||
|
}
|
136
lib/widgets/common/grid/sections/fixed/scale_overlay.dart
Normal file
136
lib/widgets/common/grid/sections/fixed/scale_overlay.dart
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import 'package:aves/model/source/enums/enums.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class FixedExtentScaleOverlay extends StatelessWidget {
|
||||||
|
final TileLayout tileLayout;
|
||||||
|
final Offset tileCenter;
|
||||||
|
final double xMin, xMax;
|
||||||
|
final ValueNotifier<Size> scaledSizeNotifier;
|
||||||
|
final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
|
||||||
|
final Widget Function(Size scaledTileSize) builder;
|
||||||
|
|
||||||
|
FixedExtentScaleOverlay({
|
||||||
|
super.key,
|
||||||
|
required this.tileLayout,
|
||||||
|
required this.tileCenter,
|
||||||
|
required Rect contentRect,
|
||||||
|
required this.scaledSizeNotifier,
|
||||||
|
required this.gridBuilder,
|
||||||
|
required this.builder,
|
||||||
|
}) : xMin = contentRect.left,
|
||||||
|
xMax = contentRect.right;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MediaQueryDataProvider(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: _OverlayBackground(
|
||||||
|
gradientCenter: tileLayout == TileLayout.grid ? tileCenter : Offset(context.isRtl ? xMax : xMin, tileCenter.dy),
|
||||||
|
child: ValueListenableBuilder<Size>(
|
||||||
|
valueListenable: scaledSizeNotifier,
|
||||||
|
builder: (context, scaledSize, child) {
|
||||||
|
final width = scaledSize.width;
|
||||||
|
final height = scaledSize.height;
|
||||||
|
// keep scaled thumbnail within the screen
|
||||||
|
var dx = .0;
|
||||||
|
if (tileCenter.dx - width / 2 < xMin) {
|
||||||
|
dx = xMin - (tileCenter.dx - width / 2);
|
||||||
|
} else if (tileCenter.dx + width / 2 > xMax) {
|
||||||
|
dx = xMax - (tileCenter.dx + width / 2);
|
||||||
|
}
|
||||||
|
final clampedCenter = tileCenter.translate(dx, 0);
|
||||||
|
|
||||||
|
var child = builder(scaledSize);
|
||||||
|
child = Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: clampedCenter.dx - width / 2,
|
||||||
|
top: clampedCenter.dy - height / 2,
|
||||||
|
child: DefaultTextStyle(
|
||||||
|
style: const TextStyle(),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
child = gridBuilder(clampedCenter, scaledSize, child);
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverlayBackground extends StatefulWidget {
|
||||||
|
final Offset gradientCenter;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _OverlayBackground({
|
||||||
|
required this.gradientCenter,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_OverlayBackground> createState() => _OverlayBackgroundState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverlayBackgroundState extends State<_OverlayBackground> {
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _initialized = true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedContainer(
|
||||||
|
decoration: _buildBackgroundDecoration(context),
|
||||||
|
duration: Durations.scalingGridBackgroundAnimation,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxDecoration _buildBackgroundDecoration(BuildContext context) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final gradientCenter = widget.gradientCenter;
|
||||||
|
return _initialized
|
||||||
|
? BoxDecoration(
|
||||||
|
gradient: RadialGradient(
|
||||||
|
center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select<MediaQueryData, Size>((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 [
|
||||||
|
Constants.transparentBlack,
|
||||||
|
Constants.transparentBlack,
|
||||||
|
]
|
||||||
|
: const [
|
||||||
|
Constants.transparentWhite,
|
||||||
|
Constants.transparentWhite,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
41
lib/widgets/common/grid/sections/fixed/section_layout.dart
Normal file
41
lib/widgets/common/grid/sections/fixed/section_layout.dart
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import 'package:aves/widgets/common/grid/sections/section_layout.dart';
|
||||||
|
|
||||||
|
class FixedExtentSectionLayout extends SectionLayout {
|
||||||
|
final double tileHeight, mainAxisStride;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, tileHeight, spacing];
|
||||||
|
|
||||||
|
const FixedExtentSectionLayout({
|
||||||
|
required super.sectionKey,
|
||||||
|
required super.firstIndex,
|
||||||
|
required super.lastIndex,
|
||||||
|
required super.minOffset,
|
||||||
|
required super.maxOffset,
|
||||||
|
required super.headerExtent,
|
||||||
|
required this.tileHeight,
|
||||||
|
required super.spacing,
|
||||||
|
required super.builder,
|
||||||
|
}) : mainAxisStride = tileHeight + spacing;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double indexToLayoutOffset(int index) {
|
||||||
|
index -= bodyFirstIndex;
|
||||||
|
if (index < 0) return minOffset;
|
||||||
|
return bodyMinOffset + index * mainAxisStride;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int getMinChildIndexForScrollOffset(double scrollOffset) {
|
||||||
|
scrollOffset -= bodyMinOffset;
|
||||||
|
if (scrollOffset < 0) return firstIndex;
|
||||||
|
return bodyFirstIndex + scrollOffset ~/ mainAxisStride;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int getMaxChildIndexForScrollOffset(double scrollOffset) {
|
||||||
|
scrollOffset -= bodyMinOffset;
|
||||||
|
if (scrollOffset < 0) return firstIndex;
|
||||||
|
return bodyFirstIndex + (scrollOffset / mainAxisStride).ceil() - 1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/fixed/list_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/fixed/row.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/fixed/section_layout.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/sections/section_layout_builder.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
class FixedExtentSectionLayoutBuilder<T> extends SectionLayoutBuilder<T> {
|
||||||
|
int _currentIndex = 0;
|
||||||
|
double _currentOffset = 0;
|
||||||
|
|
||||||
|
FixedExtentSectionLayoutBuilder({
|
||||||
|
required super.sections,
|
||||||
|
required super.showHeaders,
|
||||||
|
required super.getHeaderExtent,
|
||||||
|
required super.buildHeader,
|
||||||
|
required super.scrollableWidth,
|
||||||
|
required super.tileLayout,
|
||||||
|
required super.columnCount,
|
||||||
|
required super.spacing,
|
||||||
|
required super.horizontalPadding,
|
||||||
|
required super.tileWidth,
|
||||||
|
required super.tileHeight,
|
||||||
|
required super.tileBuilder,
|
||||||
|
required super.tileAnimationDelay,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
SectionedListLayout<T> updateLayouts(BuildContext context) {
|
||||||
|
final sectionLayouts = sections.keys
|
||||||
|
.map((sectionKey) => buildSectionLayout(
|
||||||
|
headerExtent: showHeaders ? getHeaderExtent(context, sectionKey) : 0.0,
|
||||||
|
sectionKey: sectionKey,
|
||||||
|
section: sections[sectionKey]!,
|
||||||
|
animate: animate,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return FixedExtentSectionedListLayout<T>(
|
||||||
|
sections: sections,
|
||||||
|
showHeaders: showHeaders,
|
||||||
|
columnCount: columnCount,
|
||||||
|
tileWidth: tileWidth,
|
||||||
|
tileHeight: tileHeight,
|
||||||
|
spacing: spacing,
|
||||||
|
horizontalPadding: horizontalPadding,
|
||||||
|
sectionLayouts: sectionLayouts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SectionLayout buildSectionLayout({
|
||||||
|
required double headerExtent,
|
||||||
|
required SectionKey sectionKey,
|
||||||
|
required List<T> section,
|
||||||
|
required bool animate,
|
||||||
|
}) {
|
||||||
|
final sectionItemCount = section.length;
|
||||||
|
final rowCount = (sectionItemCount / columnCount).ceil();
|
||||||
|
final sectionChildCount = 1 + rowCount;
|
||||||
|
|
||||||
|
final sectionFirstIndex = _currentIndex;
|
||||||
|
_currentIndex += sectionChildCount;
|
||||||
|
final sectionLastIndex = _currentIndex - 1;
|
||||||
|
|
||||||
|
final sectionMinOffset = _currentOffset;
|
||||||
|
_currentOffset += headerExtent + tileHeight * rowCount + spacing * (rowCount - 1);
|
||||||
|
final sectionMaxOffset = _currentOffset;
|
||||||
|
|
||||||
|
return FixedExtentSectionLayout(
|
||||||
|
sectionKey: sectionKey,
|
||||||
|
firstIndex: sectionFirstIndex,
|
||||||
|
lastIndex: sectionLastIndex,
|
||||||
|
minOffset: sectionMinOffset,
|
||||||
|
maxOffset: sectionMaxOffset,
|
||||||
|
headerExtent: headerExtent,
|
||||||
|
tileHeight: tileHeight,
|
||||||
|
spacing: spacing,
|
||||||
|
builder: (context, listIndex) {
|
||||||
|
final textDirection = Directionality.of(context);
|
||||||
|
final sectionChildIndex = listIndex - sectionFirstIndex;
|
||||||
|
return buildSectionWidget(
|
||||||
|
context: context,
|
||||||
|
section: section,
|
||||||
|
sectionGridIndex: listIndex * columnCount,
|
||||||
|
sectionChildIndex: sectionChildIndex,
|
||||||
|
itemIndexRange: () => Tuple2(
|
||||||
|
(sectionChildIndex - 1) * columnCount,
|
||||||
|
sectionChildIndex * columnCount,
|
||||||
|
),
|
||||||
|
sectionKey: sectionKey,
|
||||||
|
headerExtent: headerExtent,
|
||||||
|
animate: animate,
|
||||||
|
buildGridRow: (children) => FixedExtentGridRow(
|
||||||
|
width: tileWidth,
|
||||||
|
height: tileHeight,
|
||||||
|
spacing: spacing,
|
||||||
|
textDirection: textDirection,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
28
lib/widgets/common/grid/sections/list_layout.dart
Normal file
28
lib/widgets/common/grid/sections/list_layout.dart
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/section_layout.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
abstract class SectionedListLayout<T> {
|
||||||
|
final Map<SectionKey, List<T>> sections;
|
||||||
|
final bool showHeaders;
|
||||||
|
final double spacing, horizontalPadding;
|
||||||
|
final List<SectionLayout> sectionLayouts;
|
||||||
|
|
||||||
|
const SectionedListLayout({
|
||||||
|
required this.sections,
|
||||||
|
required this.showHeaders,
|
||||||
|
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);
|
||||||
|
|
||||||
|
SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset);
|
||||||
|
|
||||||
|
// `position` in layout space, i.e. x=0 is start
|
||||||
|
T? getItemAt(Offset position);
|
||||||
|
}
|
78
lib/widgets/common/grid/sections/mosaic/list_layout.dart
Normal file
78
lib/widgets/common/grid/sections/mosaic/list_layout.dart
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/mosaic/section_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/section_layout.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class MosaicSectionedListLayout<T> extends SectionedListLayout<T> {
|
||||||
|
const MosaicSectionedListLayout({
|
||||||
|
required super.sections,
|
||||||
|
required super.showHeaders,
|
||||||
|
required super.spacing,
|
||||||
|
required super.horizontalPadding,
|
||||||
|
required super.sectionLayouts,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<MosaicRowLayout> _rowsFor(SectionLayout sectionLayout) => (sectionLayout as MosaicSectionLayout).rows;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Rect? getTileRect(T item) {
|
||||||
|
final MapEntry<SectionKey?, List<T>>? 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 row = _rowsFor(sectionLayout).firstWhereOrNull((row) => sectionItemIndex <= row.lastIndex);
|
||||||
|
if (row == null) return null;
|
||||||
|
|
||||||
|
final rowItemIndex = sectionItemIndex - row.firstIndex;
|
||||||
|
final tileWidth = row.itemWidths[rowItemIndex];
|
||||||
|
final tileHeight = row.height - spacing;
|
||||||
|
|
||||||
|
var left = horizontalPadding;
|
||||||
|
row.itemWidths.forEachIndexedWhile((i, width) {
|
||||||
|
if (i == rowItemIndex) return true;
|
||||||
|
|
||||||
|
left += width + spacing;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
final listIndex = sectionLayout.firstIndex + 1 + _rowsFor(sectionLayout).indexOf(row);
|
||||||
|
|
||||||
|
final top = sectionLayout.indexToLayoutOffset(listIndex);
|
||||||
|
return Rect.fromLTWH(left, top, tileWidth, tileHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
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 = _rowsFor(sectionLayout).firstWhereOrNull((v) => dy < v.maxOffset);
|
||||||
|
if (row == null) return null;
|
||||||
|
|
||||||
|
var dx = position.dx - horizontalPadding;
|
||||||
|
var index = -1;
|
||||||
|
row.itemWidths.forEachIndexedWhile((i, width) {
|
||||||
|
dx -= width + spacing;
|
||||||
|
if (dx > 0) return true;
|
||||||
|
|
||||||
|
index = row.firstIndex + i;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index < 0 || index >= section.length) return null;
|
||||||
|
return section[index];
|
||||||
|
}
|
||||||
|
}
|
154
lib/widgets/common/grid/sections/mosaic/row.dart
Normal file
154
lib/widgets/common/grid/sections/mosaic/row.dart
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import 'package:aves/widgets/common/grid/sections/mosaic/section_layout.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class MosaicGridRow extends MultiChildRenderObjectWidget {
|
||||||
|
final MosaicRowLayout rowLayout;
|
||||||
|
final double spacing;
|
||||||
|
final TextDirection textDirection;
|
||||||
|
|
||||||
|
MosaicGridRow({
|
||||||
|
super.key,
|
||||||
|
required this.rowLayout,
|
||||||
|
required this.spacing,
|
||||||
|
required this.textDirection,
|
||||||
|
required List<Widget> children,
|
||||||
|
}) : super(children: children);
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
return RenderMosaicGridRow(
|
||||||
|
rowLayout: rowLayout,
|
||||||
|
spacing: spacing,
|
||||||
|
textDirection: textDirection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(BuildContext context, RenderMosaicGridRow renderObject) {
|
||||||
|
renderObject.rowLayout = rowLayout;
|
||||||
|
renderObject.spacing = spacing;
|
||||||
|
renderObject.textDirection = textDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DiagnosticsProperty<MosaicRowLayout>('rowLayout', rowLayout));
|
||||||
|
properties.add(DoubleProperty('spacing', spacing));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GridRowParentData extends ContainerBoxParentData<RenderBox> {}
|
||||||
|
|
||||||
|
class RenderMosaicGridRow extends RenderBox with ContainerRenderObjectMixin<RenderBox, _GridRowParentData>, RenderBoxContainerDefaultsMixin<RenderBox, _GridRowParentData> {
|
||||||
|
RenderMosaicGridRow({
|
||||||
|
List<RenderBox>? children,
|
||||||
|
required MosaicRowLayout rowLayout,
|
||||||
|
required double spacing,
|
||||||
|
required TextDirection textDirection,
|
||||||
|
}) : _rowLayout = rowLayout,
|
||||||
|
_spacing = spacing,
|
||||||
|
_textDirection = textDirection {
|
||||||
|
addAll(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
MosaicRowLayout get rowLayout => _rowLayout;
|
||||||
|
MosaicRowLayout _rowLayout;
|
||||||
|
|
||||||
|
set rowLayout(MosaicRowLayout value) {
|
||||||
|
if (_rowLayout == value) return;
|
||||||
|
_rowLayout = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get spacing => _spacing;
|
||||||
|
double _spacing;
|
||||||
|
|
||||||
|
set spacing(double value) {
|
||||||
|
if (_spacing == value) return;
|
||||||
|
_spacing = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextDirection get textDirection => _textDirection;
|
||||||
|
TextDirection _textDirection;
|
||||||
|
|
||||||
|
set textDirection(TextDirection value) {
|
||||||
|
if (_textDirection == value) return;
|
||||||
|
_textDirection = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setupParentData(RenderBox child) {
|
||||||
|
if (child.parentData is! _GridRowParentData) {
|
||||||
|
child.parentData = _GridRowParentData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double get intrinsicWidth => rowLayout.itemWidths.sum + spacing * (childCount - 1);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicWidth(double height) => intrinsicWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicWidth(double height) => intrinsicWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicHeight(double width) => rowLayout.height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicHeight(double width) => rowLayout.height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
var child = firstChild;
|
||||||
|
if (child == null) {
|
||||||
|
size = constraints.smallest;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final thumbnailHeight = rowLayout.height - spacing;
|
||||||
|
size = Size(constraints.maxWidth, rowLayout.height);
|
||||||
|
final flipMainAxis = textDirection == TextDirection.rtl;
|
||||||
|
final sign = (flipMainAxis ? -1.0 : 1.0);
|
||||||
|
var i = 0;
|
||||||
|
var offset = Offset(flipMainAxis ? size.width - rowLayout.itemWidths[i] : 0, 0);
|
||||||
|
while (child != null) {
|
||||||
|
final thumbnailWidth = rowLayout.itemWidths[i];
|
||||||
|
final childConstraints = BoxConstraints.tight(Size(thumbnailWidth, thumbnailHeight));
|
||||||
|
child.layout(childConstraints, parentUsesSize: false);
|
||||||
|
final childParentData = child.parentData! as _GridRowParentData;
|
||||||
|
childParentData.offset = offset;
|
||||||
|
final dx = sign * (thumbnailWidth + spacing);
|
||||||
|
offset += Offset(dx, 0);
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? computeDistanceToActualBaseline(TextBaseline baseline) {
|
||||||
|
return defaultComputeDistanceToHighestActualBaseline(baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||||
|
return defaultHitTestChildren(result, position: position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
defaultPaint(context, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DiagnosticsProperty<MosaicRowLayout>('rowLayout', rowLayout));
|
||||||
|
properties.add(DoubleProperty('spacing', spacing));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
||||||
|
}
|
||||||
|
}
|
74
lib/widgets/common/grid/sections/mosaic/scale_grid.dart
Normal file
74
lib/widgets/common/grid/sections/mosaic/scale_grid.dart
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/mosaic/scale_overlay.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/mosaic/section_layout_builder.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MosaicGrid extends StatelessWidget {
|
||||||
|
final Rect contentRect;
|
||||||
|
final Size tileSize;
|
||||||
|
final double spacing;
|
||||||
|
final MosaicItemBuilder builder;
|
||||||
|
|
||||||
|
static const _itemRatios = <double>[
|
||||||
|
3 / 4,
|
||||||
|
16 / 9,
|
||||||
|
9 / 16,
|
||||||
|
3 / 4,
|
||||||
|
4 / 3,
|
||||||
|
4 / 3,
|
||||||
|
3 / 4,
|
||||||
|
4 / 3,
|
||||||
|
4 / 3,
|
||||||
|
4 / 3,
|
||||||
|
];
|
||||||
|
|
||||||
|
const MosaicGrid({
|
||||||
|
super.key,
|
||||||
|
required this.contentRect,
|
||||||
|
required this.tileSize,
|
||||||
|
required this.spacing,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final children = <Widget>[];
|
||||||
|
|
||||||
|
final targetExtent = tileSize.width;
|
||||||
|
final rows = MosaicSectionLayoutBuilder.computeMosaicRows(
|
||||||
|
section: List.generate(5, (i) => _itemRatios).expand((v) => v).toList(),
|
||||||
|
availableWidthFor: (itemCount) => contentRect.width - (itemCount - 1) * spacing,
|
||||||
|
heightMax: targetExtent * MosaicSectionLayoutBuilder.heightMaxFactor,
|
||||||
|
targetExtent: targetExtent,
|
||||||
|
spacing: spacing,
|
||||||
|
bottom: tileSize.height - tileSize.width,
|
||||||
|
coverRatioResolver: (item) => item,
|
||||||
|
);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
var dy = contentRect.top;
|
||||||
|
rows.forEach((row) {
|
||||||
|
var dx = contentRect.left;
|
||||||
|
final itemHeight = row.height - spacing;
|
||||||
|
row.itemWidths.forEach((itemWidth) {
|
||||||
|
children.add(
|
||||||
|
AnimatedPositioned(
|
||||||
|
left: dx,
|
||||||
|
top: dy,
|
||||||
|
width: itemWidth,
|
||||||
|
height: itemHeight,
|
||||||
|
duration: Durations.scalingGridPositionAnimation,
|
||||||
|
child: builder(i, targetExtent),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
dx += itemWidth + spacing;
|
||||||
|
i++;
|
||||||
|
});
|
||||||
|
dy += row.height;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
116
lib/widgets/common/grid/sections/mosaic/scale_overlay.dart
Normal file
116
lib/widgets/common/grid/sections/mosaic/scale_overlay.dart
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/mosaic/scale_grid.dart';
|
||||||
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
typedef MosaicItemBuilder = Widget Function(int index, double targetExtent);
|
||||||
|
|
||||||
|
class MosaicScaleOverlay extends StatelessWidget {
|
||||||
|
final Rect contentRect;
|
||||||
|
final double spacing, extentMax;
|
||||||
|
final ValueNotifier<Size> scaledSizeNotifier;
|
||||||
|
final MosaicItemBuilder itemBuilder;
|
||||||
|
|
||||||
|
const MosaicScaleOverlay({
|
||||||
|
super.key,
|
||||||
|
required this.contentRect,
|
||||||
|
required this.spacing,
|
||||||
|
required this.extentMax,
|
||||||
|
required this.scaledSizeNotifier,
|
||||||
|
required this.itemBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MediaQueryDataProvider(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: _OverlayBackground(
|
||||||
|
child: ValueListenableBuilder<Size>(
|
||||||
|
valueListenable: scaledSizeNotifier,
|
||||||
|
builder: (context, scaledSize, child) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
Widget _buildBar(double width, Color color) => ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
child: Container(
|
||||||
|
color: color,
|
||||||
|
width: width,
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return SafeArea(
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
bottom: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
_buildBar(extentMax, colorScheme.onPrimary.withOpacity(.1)),
|
||||||
|
_buildBar(scaledSize.width, colorScheme.secondary),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: MosaicGrid(
|
||||||
|
contentRect: contentRect,
|
||||||
|
tileSize: scaledSize,
|
||||||
|
spacing: spacing,
|
||||||
|
builder: itemBuilder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverlayBackground extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _OverlayBackground({
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_OverlayBackground> createState() => _OverlayBackgroundState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverlayBackgroundState extends State<_OverlayBackground> {
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _initialized = true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedContainer(
|
||||||
|
decoration: _buildBackgroundDecoration(context),
|
||||||
|
duration: Durations.scalingGridBackgroundAnimation,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxDecoration _buildBackgroundDecoration(BuildContext context) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
return _initialized
|
||||||
|
? BoxDecoration(
|
||||||
|
color: isDark ? Colors.black87 : const Color(0xDDFFFFFF),
|
||||||
|
)
|
||||||
|
: BoxDecoration(
|
||||||
|
color: isDark ? Constants.transparentBlack : Constants.transparentWhite,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
61
lib/widgets/common/grid/sections/mosaic/section_layout.dart
Normal file
61
lib/widgets/common/grid/sections/mosaic/section_layout.dart
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import 'package:aves/widgets/common/grid/sections/section_layout.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class MosaicSectionLayout extends SectionLayout {
|
||||||
|
final List<MosaicRowLayout> rows;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, rows, spacing];
|
||||||
|
|
||||||
|
const MosaicSectionLayout({
|
||||||
|
required super.sectionKey,
|
||||||
|
required super.firstIndex,
|
||||||
|
required super.lastIndex,
|
||||||
|
required super.minOffset,
|
||||||
|
required super.maxOffset,
|
||||||
|
required super.headerExtent,
|
||||||
|
required this.rows,
|
||||||
|
required super.spacing,
|
||||||
|
required super.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
double indexToLayoutOffset(int index) {
|
||||||
|
index -= bodyFirstIndex;
|
||||||
|
if (index < 0) return minOffset;
|
||||||
|
return bodyMinOffset + (index < rows.length ? rows[index].minOffset : rows.lastOrNull?.maxOffset ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int getMinChildIndexForScrollOffset(double scrollOffset) {
|
||||||
|
scrollOffset -= bodyMinOffset;
|
||||||
|
if (scrollOffset < 0) return firstIndex;
|
||||||
|
return bodyFirstIndex + rows.indexWhere((v) => scrollOffset < v.maxOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int getMaxChildIndexForScrollOffset(double scrollOffset) {
|
||||||
|
scrollOffset -= bodyMinOffset;
|
||||||
|
if (scrollOffset < 0) return firstIndex;
|
||||||
|
final rowIndex = rows.indexWhere((v) => scrollOffset < v.maxOffset);
|
||||||
|
return bodyFirstIndex + (rowIndex == -1 ? rows.length - 1 : rowIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MosaicRowLayout extends Equatable {
|
||||||
|
final int firstIndex, lastIndex;
|
||||||
|
final double minOffset, maxOffset, height;
|
||||||
|
final List<double> itemWidths;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [firstIndex, lastIndex, minOffset, maxOffset, height, itemWidths];
|
||||||
|
|
||||||
|
const MosaicRowLayout({
|
||||||
|
required this.firstIndex,
|
||||||
|
required this.lastIndex,
|
||||||
|
required this.minOffset,
|
||||||
|
required this.height,
|
||||||
|
required this.itemWidths,
|
||||||
|
}) : maxOffset = minOffset + height;
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/mosaic/list_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/mosaic/row.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/mosaic/section_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/provider.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/section_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/section_layout_builder.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
class MosaicSectionLayoutBuilder<T> extends SectionLayoutBuilder<T> {
|
||||||
|
int _currentIndex = 0;
|
||||||
|
double _currentOffset = 0;
|
||||||
|
late double Function(int itemCount) rowAvailableWidth;
|
||||||
|
late double rowHeightMax;
|
||||||
|
final CoverRatioResolver<T> coverRatioResolver;
|
||||||
|
|
||||||
|
static const heightMaxFactor = 2.4;
|
||||||
|
|
||||||
|
MosaicSectionLayoutBuilder({
|
||||||
|
required super.sections,
|
||||||
|
required super.showHeaders,
|
||||||
|
required super.getHeaderExtent,
|
||||||
|
required super.buildHeader,
|
||||||
|
required super.scrollableWidth,
|
||||||
|
required super.tileLayout,
|
||||||
|
required super.columnCount,
|
||||||
|
required super.spacing,
|
||||||
|
required super.horizontalPadding,
|
||||||
|
required super.tileWidth,
|
||||||
|
required super.tileHeight,
|
||||||
|
required super.tileBuilder,
|
||||||
|
required super.tileAnimationDelay,
|
||||||
|
required this.coverRatioResolver,
|
||||||
|
}) {
|
||||||
|
final rowWidth = scrollableWidth - horizontalPadding * 2;
|
||||||
|
rowAvailableWidth = (itemCount) => rowWidth - (itemCount - 1) * spacing;
|
||||||
|
rowHeightMax = tileWidth * heightMaxFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SectionedListLayout<T> updateLayouts(BuildContext context) {
|
||||||
|
final sectionLayouts = sections.keys
|
||||||
|
.map((sectionKey) => buildSectionLayout(
|
||||||
|
headerExtent: showHeaders ? getHeaderExtent(context, sectionKey) : 0.0,
|
||||||
|
sectionKey: sectionKey,
|
||||||
|
section: sections[sectionKey]!,
|
||||||
|
animate: animate,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return MosaicSectionedListLayout<T>(
|
||||||
|
sections: sections,
|
||||||
|
showHeaders: showHeaders,
|
||||||
|
spacing: spacing,
|
||||||
|
horizontalPadding: horizontalPadding,
|
||||||
|
sectionLayouts: sectionLayouts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SectionLayout buildSectionLayout({
|
||||||
|
required double headerExtent,
|
||||||
|
required SectionKey sectionKey,
|
||||||
|
required List<T> section,
|
||||||
|
required bool animate,
|
||||||
|
}) {
|
||||||
|
final rows = computeMosaicRows(
|
||||||
|
section: section,
|
||||||
|
availableWidthFor: rowAvailableWidth,
|
||||||
|
heightMax: rowHeightMax,
|
||||||
|
targetExtent: tileWidth,
|
||||||
|
spacing: spacing,
|
||||||
|
bottom: bottom,
|
||||||
|
coverRatioResolver: coverRatioResolver,
|
||||||
|
);
|
||||||
|
final rowCount = rows.length;
|
||||||
|
final sectionChildCount = 1 + rowCount;
|
||||||
|
|
||||||
|
final sectionFirstIndex = _currentIndex;
|
||||||
|
_currentIndex += sectionChildCount;
|
||||||
|
final sectionLastIndex = _currentIndex - 1;
|
||||||
|
|
||||||
|
final sectionMinOffset = _currentOffset;
|
||||||
|
_currentOffset += headerExtent + rows.map((v) => v.height).sum - spacing;
|
||||||
|
final sectionMaxOffset = _currentOffset;
|
||||||
|
|
||||||
|
return MosaicSectionLayout(
|
||||||
|
sectionKey: sectionKey,
|
||||||
|
firstIndex: sectionFirstIndex,
|
||||||
|
lastIndex: sectionLastIndex,
|
||||||
|
minOffset: sectionMinOffset,
|
||||||
|
maxOffset: sectionMaxOffset,
|
||||||
|
headerExtent: headerExtent,
|
||||||
|
rows: rows,
|
||||||
|
spacing: spacing,
|
||||||
|
builder: (context, listIndex) {
|
||||||
|
final textDirection = Directionality.of(context);
|
||||||
|
final sectionChildIndex = listIndex - sectionFirstIndex;
|
||||||
|
final row = sectionChildIndex == 0 ? null : rows[sectionChildIndex - 1];
|
||||||
|
return buildSectionWidget(
|
||||||
|
context: context,
|
||||||
|
section: section,
|
||||||
|
sectionGridIndex: listIndex * columnCount,
|
||||||
|
sectionChildIndex: sectionChildIndex,
|
||||||
|
itemIndexRange: () => row == null ? const Tuple2(0, 0) : Tuple2(row.firstIndex, row.lastIndex + 1),
|
||||||
|
sectionKey: sectionKey,
|
||||||
|
headerExtent: headerExtent,
|
||||||
|
animate: animate,
|
||||||
|
buildGridRow: (children) {
|
||||||
|
return row == null
|
||||||
|
? const SizedBox()
|
||||||
|
: MosaicGridRow(
|
||||||
|
rowLayout: row,
|
||||||
|
spacing: spacing,
|
||||||
|
textDirection: textDirection,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MosaicRowLayout> computeMosaicRows<T>({
|
||||||
|
required List<T> section,
|
||||||
|
required double Function(int itemCount) availableWidthFor,
|
||||||
|
required double heightMax,
|
||||||
|
required double targetExtent,
|
||||||
|
required double spacing,
|
||||||
|
required double bottom,
|
||||||
|
required CoverRatioResolver<T> coverRatioResolver,
|
||||||
|
}) {
|
||||||
|
final rows = <MosaicRowLayout>[];
|
||||||
|
final items = <T>[];
|
||||||
|
double ratioSum = 0, ratioMin = double.infinity;
|
||||||
|
int firstIndex = 0;
|
||||||
|
double minOffset = 0;
|
||||||
|
|
||||||
|
void addRow(int i, {required bool complete}) {
|
||||||
|
if (items.isEmpty) return;
|
||||||
|
|
||||||
|
final availableWidth = availableWidthFor(items.length);
|
||||||
|
var height = availableWidth / ratioSum + spacing;
|
||||||
|
if (height > heightMax + precisionErrorTolerance) {
|
||||||
|
if (!complete) {
|
||||||
|
ratioSum = availableWidth / (heightMax - spacing);
|
||||||
|
addRow(i, complete: complete);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
height += bottom;
|
||||||
|
rows.add(MosaicRowLayout(
|
||||||
|
firstIndex: firstIndex,
|
||||||
|
lastIndex: i - 1,
|
||||||
|
minOffset: minOffset,
|
||||||
|
height: height,
|
||||||
|
itemWidths: items.map((item) => availableWidth * coverRatioResolver(item) / ratioSum).toList(),
|
||||||
|
));
|
||||||
|
firstIndex = i;
|
||||||
|
minOffset += height;
|
||||||
|
ratioMin = double.infinity;
|
||||||
|
ratioSum = 0;
|
||||||
|
items.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
section.forEachIndexed((i, item) {
|
||||||
|
final ratio = coverRatioResolver(item);
|
||||||
|
final nextAvailableWidth = availableWidthFor(items.length + 1);
|
||||||
|
final nextRatioSum = ratio + ratioSum;
|
||||||
|
final nextItemMinWidth = nextAvailableWidth * min(ratio, ratioMin) / nextRatioSum;
|
||||||
|
final nextHeight = nextAvailableWidth / nextRatioSum + spacing;
|
||||||
|
if (nextItemMinWidth < targetExtent || nextHeight < targetExtent) {
|
||||||
|
// add row when appending the next item would make other items too small
|
||||||
|
addRow(i, complete: true);
|
||||||
|
}
|
||||||
|
items.add(item);
|
||||||
|
ratioMin = min(ratio, ratioMin);
|
||||||
|
ratioSum += ratio;
|
||||||
|
});
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
// add last row, possibly incomplete
|
||||||
|
addRow(section.length, complete: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
104
lib/widgets/common/grid/sections/provider.dart
Normal file
104
lib/widgets/common/grid/sections/provider.dart
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import 'package:aves/model/source/enums/enums.dart';
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/fixed/section_layout_builder.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/mosaic/section_layout_builder.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
typedef CoverRatioResolver<T> = double Function(T item);
|
||||||
|
|
||||||
|
abstract class SectionedListLayoutProvider<T> 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 CoverRatioResolver<T> coverRatioResolver;
|
||||||
|
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.coverRatioResolver,
|
||||||
|
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<SectionedListLayout<T>>(
|
||||||
|
update: (context, _) {
|
||||||
|
switch (tileLayout) {
|
||||||
|
case TileLayout.mosaic:
|
||||||
|
return MosaicSectionLayoutBuilder<T>(
|
||||||
|
sections: sections,
|
||||||
|
showHeaders: showHeaders,
|
||||||
|
getHeaderExtent: getHeaderExtent,
|
||||||
|
buildHeader: buildHeader,
|
||||||
|
scrollableWidth: scrollableWidth,
|
||||||
|
tileLayout: tileLayout,
|
||||||
|
columnCount: columnCount,
|
||||||
|
spacing: spacing,
|
||||||
|
horizontalPadding: horizontalPadding,
|
||||||
|
tileWidth: tileWidth,
|
||||||
|
tileHeight: tileHeight,
|
||||||
|
tileBuilder: tileBuilder,
|
||||||
|
tileAnimationDelay: tileAnimationDelay,
|
||||||
|
coverRatioResolver: coverRatioResolver,
|
||||||
|
).updateLayouts(context);
|
||||||
|
case TileLayout.grid:
|
||||||
|
case TileLayout.list:
|
||||||
|
return FixedExtentSectionLayoutBuilder<T>(
|
||||||
|
sections: sections,
|
||||||
|
showHeaders: showHeaders,
|
||||||
|
buildHeader: buildHeader,
|
||||||
|
getHeaderExtent: getHeaderExtent,
|
||||||
|
scrollableWidth: scrollableWidth,
|
||||||
|
tileLayout: tileLayout,
|
||||||
|
columnCount: columnCount,
|
||||||
|
spacing: spacing,
|
||||||
|
horizontalPadding: horizontalPadding,
|
||||||
|
tileWidth: tileWidth,
|
||||||
|
tileHeight: tileHeight,
|
||||||
|
tileBuilder: tileBuilder,
|
||||||
|
tileAnimationDelay: tileAnimationDelay,
|
||||||
|
).updateLayouts(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get showHeaders;
|
||||||
|
|
||||||
|
Map<SectionKey, List<T>> 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(EnumProperty<TileLayout>('tileLayout', tileLayout));
|
||||||
|
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<bool>('showHeaders', showHeaders));
|
||||||
|
}
|
||||||
|
}
|
37
lib/widgets/common/grid/sections/section_layout.dart
Normal file
37
lib/widgets/common/grid/sections/section_layout.dart
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
abstract class SectionLayout extends Equatable {
|
||||||
|
final SectionKey sectionKey;
|
||||||
|
final int firstIndex, lastIndex, bodyFirstIndex;
|
||||||
|
final double minOffset, maxOffset, bodyMinOffset;
|
||||||
|
final double headerExtent, spacing;
|
||||||
|
final IndexedWidgetBuilder builder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, spacing];
|
||||||
|
|
||||||
|
const SectionLayout({
|
||||||
|
required this.sectionKey,
|
||||||
|
required this.firstIndex,
|
||||||
|
required this.lastIndex,
|
||||||
|
required this.minOffset,
|
||||||
|
required this.maxOffset,
|
||||||
|
required this.headerExtent,
|
||||||
|
required this.spacing,
|
||||||
|
required this.builder,
|
||||||
|
}) : bodyFirstIndex = firstIndex + 1,
|
||||||
|
bodyMinOffset = minOffset + headerExtent;
|
||||||
|
|
||||||
|
bool hasChild(int index) => firstIndex <= index && index <= lastIndex;
|
||||||
|
|
||||||
|
bool hasChildAtOffset(double scrollOffset) => minOffset <= scrollOffset && scrollOffset <= maxOffset;
|
||||||
|
|
||||||
|
double indexToLayoutOffset(int index);
|
||||||
|
|
||||||
|
int getMinChildIndexForScrollOffset(double scrollOffset);
|
||||||
|
|
||||||
|
int getMaxChildIndexForScrollOffset(double scrollOffset);
|
||||||
|
}
|
99
lib/widgets/common/grid/sections/section_layout_builder.dart
Normal file
99
lib/widgets/common/grid/sections/section_layout_builder.dart
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import 'package:aves/model/source/enums/enums.dart';
|
||||||
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/section_layout.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
abstract class SectionLayoutBuilder<T> {
|
||||||
|
final Map<SectionKey, List<T>> sections;
|
||||||
|
final bool showHeaders;
|
||||||
|
final double Function(BuildContext context, SectionKey sectionKey) getHeaderExtent;
|
||||||
|
final Widget Function(BuildContext context, SectionKey sectionKey, double headerExtent) buildHeader;
|
||||||
|
final double scrollableWidth;
|
||||||
|
final TileLayout tileLayout;
|
||||||
|
final int columnCount;
|
||||||
|
final double spacing, horizontalPadding, tileWidth, tileHeight, bottom;
|
||||||
|
final Widget Function(T item) tileBuilder;
|
||||||
|
final Duration tileAnimationDelay;
|
||||||
|
final bool animate;
|
||||||
|
|
||||||
|
const SectionLayoutBuilder({
|
||||||
|
required this.sections,
|
||||||
|
required this.showHeaders,
|
||||||
|
required this.getHeaderExtent,
|
||||||
|
required this.buildHeader,
|
||||||
|
required this.scrollableWidth,
|
||||||
|
required this.tileLayout,
|
||||||
|
required this.columnCount,
|
||||||
|
required this.spacing,
|
||||||
|
required this.horizontalPadding,
|
||||||
|
required this.tileWidth,
|
||||||
|
required this.tileHeight,
|
||||||
|
required this.tileBuilder,
|
||||||
|
required this.tileAnimationDelay,
|
||||||
|
}) : animate = tileAnimationDelay > Duration.zero,
|
||||||
|
bottom = tileHeight - tileWidth;
|
||||||
|
|
||||||
|
SectionedListLayout<T> updateLayouts(BuildContext context);
|
||||||
|
|
||||||
|
SectionLayout buildSectionLayout({
|
||||||
|
required double headerExtent,
|
||||||
|
required SectionKey sectionKey,
|
||||||
|
required List<T> section,
|
||||||
|
required bool animate,
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget buildSectionWidget({
|
||||||
|
required BuildContext context,
|
||||||
|
required List<T> section,
|
||||||
|
required int sectionGridIndex,
|
||||||
|
required int sectionChildIndex,
|
||||||
|
required Tuple2<int, int> Function() itemIndexRange,
|
||||||
|
required SectionKey sectionKey,
|
||||||
|
required double headerExtent,
|
||||||
|
required bool animate,
|
||||||
|
required Widget Function(List<Widget> children) buildGridRow,
|
||||||
|
}) {
|
||||||
|
if (sectionChildIndex == 0) {
|
||||||
|
final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : const SizedBox();
|
||||||
|
return animate ? _buildAnimation(context, sectionGridIndex, header) : header;
|
||||||
|
}
|
||||||
|
|
||||||
|
final sectionItemCount = section.length;
|
||||||
|
final itemMinMax = itemIndexRange();
|
||||||
|
final minItemIndex = itemMinMax.item1.clamp(0, sectionItemCount);
|
||||||
|
final maxItemIndex = itemMinMax.item2.clamp(0, sectionItemCount);
|
||||||
|
final children = <Widget>[];
|
||||||
|
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: buildGridRow(children),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAnimation(BuildContext context, int index, Widget child) {
|
||||||
|
final durations = context.watch<DurationsData>();
|
||||||
|
return AnimationConfiguration.staggeredGrid(
|
||||||
|
position: index,
|
||||||
|
columnCount: columnCount,
|
||||||
|
duration: durations.staggeredAnimation,
|
||||||
|
delay: tileAnimationDelay,
|
||||||
|
child: SlideAnimation(
|
||||||
|
verticalOffset: 50.0,
|
||||||
|
child: FadeInAnimation(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.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/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
|
||||||
|
import 'package:aves/widgets/common/grid/sections/section_layout.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -26,7 +27,7 @@ class SectionedListSliver<T> extends StatelessWidget {
|
||||||
(context, index) {
|
(context, index) {
|
||||||
if (index >= childCount) return null;
|
if (index >= childCount) return null;
|
||||||
final sectionLayout = sectionLayouts.firstWhereOrNull((section) => section.hasChild(index));
|
final sectionLayout = sectionLayouts.firstWhereOrNull((section) => section.hasChild(index));
|
||||||
return sectionLayout?.builder(context, index) ?? const SizedBox.shrink();
|
return sectionLayout?.builder(context, index) ?? const SizedBox();
|
||||||
},
|
},
|
||||||
childCount: childCount,
|
childCount: childCount,
|
||||||
addAutomaticKeepAlives: false,
|
addAutomaticKeepAlives: false,
|
||||||
|
|
|
@ -9,7 +9,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final double tileExtent;
|
final double tileExtent;
|
||||||
final ValueNotifier<bool>? cancellableNotifier;
|
final ValueNotifier<bool>? cancellableNotifier;
|
||||||
final bool selectable, highlightable;
|
final bool isMosaic, selectable, highlightable;
|
||||||
final Object? Function()? heroTagger;
|
final Object? Function()? heroTagger;
|
||||||
|
|
||||||
static final Color borderColor = Colors.grey.shade700;
|
static final Color borderColor = Colors.grey.shade700;
|
||||||
|
@ -20,6 +20,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
required this.entry,
|
required this.entry,
|
||||||
required this.tileExtent,
|
required this.tileExtent,
|
||||||
this.cancellableNotifier,
|
this.cancellableNotifier,
|
||||||
|
this.isMosaic = false,
|
||||||
this.selectable = true,
|
this.selectable = true,
|
||||||
this.highlightable = true,
|
this.highlightable = true,
|
||||||
this.heroTagger,
|
this.heroTagger,
|
||||||
|
@ -27,9 +28,13 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final thumbnailWidth = isMosaic ? tileExtent * entry.displayAspectRatio : tileExtent;
|
||||||
|
final thumbnailHeight = tileExtent;
|
||||||
|
|
||||||
Widget child = ThumbnailImage(
|
Widget child = ThumbnailImage(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: tileExtent,
|
extent: tileExtent,
|
||||||
|
isMosaic: isMosaic,
|
||||||
cancellableNotifier: cancellableNotifier,
|
cancellableNotifier: cancellableNotifier,
|
||||||
heroTag: heroTagger?.call(),
|
heroTag: heroTagger?.call(),
|
||||||
);
|
);
|
||||||
|
@ -57,8 +62,8 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
width: borderWidth,
|
width: borderWidth,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
width: tileExtent,
|
width: thumbnailWidth,
|
||||||
height: tileExtent,
|
height: thumbnailHeight,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import 'package:provider/provider.dart';
|
||||||
class ThumbnailImage extends StatefulWidget {
|
class ThumbnailImage extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final double extent;
|
final double extent;
|
||||||
final bool progressive;
|
final bool isMosaic, progressive;
|
||||||
final BoxFit? fit;
|
final BoxFit? fit;
|
||||||
final bool showLoadingBackground;
|
final bool showLoadingBackground;
|
||||||
final ValueNotifier<bool>? cancellableNotifier;
|
final ValueNotifier<bool>? cancellableNotifier;
|
||||||
|
@ -30,6 +30,7 @@ class ThumbnailImage extends StatefulWidget {
|
||||||
required this.entry,
|
required this.entry,
|
||||||
required this.extent,
|
required this.extent,
|
||||||
this.progressive = true,
|
this.progressive = true,
|
||||||
|
this.isMosaic = false,
|
||||||
this.fit,
|
this.fit,
|
||||||
this.showLoadingBackground = true,
|
this.showLoadingBackground = true,
|
||||||
this.cancellableNotifier,
|
this.cancellableNotifier,
|
||||||
|
@ -38,6 +39,14 @@ class ThumbnailImage extends StatefulWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ThumbnailImage> createState() => _ThumbnailImageState();
|
State<ThumbnailImage> createState() => _ThumbnailImageState();
|
||||||
|
|
||||||
|
static Color computeLoadingBackgroundColor(int hashCode, Brightness brightness) {
|
||||||
|
var rgb = 0x30 + hashCode % 0x20;
|
||||||
|
if (brightness == Brightness.light) {
|
||||||
|
rgb = 0xFF - rgb;
|
||||||
|
}
|
||||||
|
return Color.fromARGB(0xFF, rgb, rgb, rgb);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ThumbnailImageState extends State<ThumbnailImage> {
|
class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
|
@ -52,6 +61,8 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
|
|
||||||
double get extent => widget.extent;
|
double get extent => widget.extent;
|
||||||
|
|
||||||
|
bool get isMosaic => widget.isMosaic;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -180,13 +191,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
Color? _loadingBackgroundColor;
|
Color? _loadingBackgroundColor;
|
||||||
|
|
||||||
Color loadingBackgroundColor(BuildContext context) {
|
Color loadingBackgroundColor(BuildContext context) {
|
||||||
if (_loadingBackgroundColor == null) {
|
_loadingBackgroundColor ??= ThumbnailImage.computeLoadingBackgroundColor(entry.uri.hashCode, Theme.of(context).brightness);
|
||||||
var rgb = 0x30 + entry.uri.hashCode % 0x20;
|
|
||||||
if (Theme.of(context).brightness == Brightness.light) {
|
|
||||||
rgb = 0xFF - rgb;
|
|
||||||
}
|
|
||||||
_loadingBackgroundColor = Color.fromARGB(0xFF, rgb, rgb, rgb);
|
|
||||||
}
|
|
||||||
return _loadingBackgroundColor!;
|
return _loadingBackgroundColor!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,13 +205,21 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
// use `RawImage` instead of `Image`, using `ImageInfo` to check dimensions
|
// use `RawImage` instead of `Image`, using `ImageInfo` to check dimensions
|
||||||
// and have more control when chaining image providers
|
// and have more control when chaining image providers
|
||||||
|
|
||||||
final fit = widget.fit ?? (entry.isSvg ? BoxFit.contain : BoxFit.cover);
|
final thumbnailWidth = isMosaic ? extent * entry.displayAspectRatio : extent;
|
||||||
|
final thumbnailHeight = extent;
|
||||||
|
|
||||||
|
final fit = widget.fit ??
|
||||||
|
(entry.isSvg
|
||||||
|
? BoxFit.contain
|
||||||
|
: isMosaic
|
||||||
|
? BoxFit.contain
|
||||||
|
: BoxFit.cover);
|
||||||
final imageInfo = _lastImageInfo;
|
final imageInfo = _lastImageInfo;
|
||||||
Widget image = imageInfo == null
|
Widget image = imageInfo == null
|
||||||
? Container(
|
? Container(
|
||||||
color: widget.showLoadingBackground ? loadingBackgroundColor(context) : Colors.transparent,
|
color: widget.showLoadingBackground ? loadingBackgroundColor(context) : Colors.transparent,
|
||||||
width: extent,
|
width: thumbnailWidth,
|
||||||
height: extent,
|
height: thumbnailHeight,
|
||||||
)
|
)
|
||||||
: Selector<Settings, EntryBackground>(
|
: Selector<Settings, EntryBackground>(
|
||||||
selector: (context, s) => s.imageBackground,
|
selector: (context, s) => s.imageBackground,
|
||||||
|
@ -240,8 +253,8 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
return RawImage(
|
return RawImage(
|
||||||
image: imageInfo.image,
|
image: imageInfo.image,
|
||||||
debugImageLabel: imageInfo.debugLabel,
|
debugImageLabel: imageInfo.debugLabel,
|
||||||
width: extent,
|
width: thumbnailWidth,
|
||||||
height: extent,
|
height: thumbnailHeight,
|
||||||
scale: imageInfo.scale,
|
scale: imageInfo.scale,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
colorBlendMode: BlendMode.dstOver,
|
colorBlendMode: BlendMode.dstOver,
|
||||||
|
|
|
@ -172,7 +172,7 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const SizedBox(
|
separatorBuilder: (context, index) => const SizedBox(
|
||||||
height: CollectionGrid.spacing,
|
height: CollectionGrid.fixedExtentLayoutSpacing,
|
||||||
),
|
),
|
||||||
itemCount: min(entryCount, previewMax),
|
itemCount: min(entryCount, previewMax),
|
||||||
),
|
),
|
||||||
|
|
|
@ -12,18 +12,18 @@ import 'aves_dialog.dart';
|
||||||
|
|
||||||
class TileViewDialog<S, G, L> extends StatefulWidget {
|
class TileViewDialog<S, G, L> extends StatefulWidget {
|
||||||
final Tuple4<S?, G?, L?, bool> initialValue;
|
final Tuple4<S?, G?, L?, bool> initialValue;
|
||||||
final Map<S, String> sortOptions;
|
final List<TileViewDialogOption<S>> sortOptions;
|
||||||
final Map<G, String> groupOptions;
|
final List<TileViewDialogOption<G>> groupOptions;
|
||||||
final Map<L, String> layoutOptions;
|
final List<TileViewDialogOption<L>> layoutOptions;
|
||||||
final String Function(S sort, bool reverse) sortOrder;
|
final String Function(S sort, bool reverse) sortOrder;
|
||||||
final bool Function(S? sort, G? group, L? layout)? canGroup;
|
final bool Function(S? sort, G? group, L? layout)? canGroup;
|
||||||
|
|
||||||
const TileViewDialog({
|
const TileViewDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.initialValue,
|
required this.initialValue,
|
||||||
this.sortOptions = const {},
|
this.sortOptions = const [],
|
||||||
this.groupOptions = const {},
|
this.groupOptions = const [],
|
||||||
this.layoutOptions = const {},
|
this.layoutOptions = const [],
|
||||||
required this.sortOrder,
|
required this.sortOrder,
|
||||||
this.canGroup,
|
this.canGroup,
|
||||||
});
|
});
|
||||||
|
@ -38,11 +38,11 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
||||||
late L? _selectedLayout;
|
late L? _selectedLayout;
|
||||||
late bool _reverseSort;
|
late bool _reverseSort;
|
||||||
|
|
||||||
Map<S, String> get sortOptions => widget.sortOptions;
|
List<TileViewDialogOption<S>> get sortOptions => widget.sortOptions;
|
||||||
|
|
||||||
Map<G, String> get groupOptions => widget.groupOptions;
|
List<TileViewDialogOption<G>> get groupOptions => widget.groupOptions;
|
||||||
|
|
||||||
Map<L, String> get layoutOptions => widget.layoutOptions;
|
List<TileViewDialogOption<L>> get layoutOptions => widget.layoutOptions;
|
||||||
|
|
||||||
bool get canGroup => (widget.canGroup ?? (s, g, l) => true).call(_selectedSort, _selectedGroup, _selectedLayout);
|
bool get canGroup => (widget.canGroup ?? (s, g, l) => true).call(_selectedSort, _selectedGroup, _selectedLayout);
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
Widget? trailing,
|
Widget? trailing,
|
||||||
required Map<T, String> options,
|
required List<TileViewDialogOption<T>> options,
|
||||||
required T value,
|
required T value,
|
||||||
required ValueChanged<T?> onChanged,
|
required ValueChanged<T?> onChanged,
|
||||||
Widget? bottom,
|
Widget? bottom,
|
||||||
|
@ -171,8 +171,9 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsetsDirectional.only(start: iconSize + 16, end: 12),
|
padding: EdgeInsetsDirectional.only(start: iconSize + 16, end: 12),
|
||||||
child: TextDropdownButton<T>(
|
child: TextDropdownButton<T>(
|
||||||
values: options.keys.toList(),
|
values: options.map((v) => v.value).toList(),
|
||||||
valueText: (v) => options[v] ?? v.toString(),
|
valueText: (v) => options.firstWhere((option) => option.value == v).title,
|
||||||
|
valueIcon: (v) => options.firstWhere((option) => option.value == v).icon,
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: (v) => setState(() => onChanged(v)),
|
onChanged: (v) => setState(() => onChanged(v)),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
|
@ -190,3 +191,16 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class TileViewDialogOption<T> {
|
||||||
|
final T value;
|
||||||
|
final String title;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
const TileViewDialogOption({
|
||||||
|
required this.value,
|
||||||
|
required this.title,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -149,9 +149,9 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return TileViewDialog<ChipSortFactor, AlbumChipGroupFactor, TileLayout>(
|
return TileViewDialog<ChipSortFactor, AlbumChipGroupFactor, TileLayout>(
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
sortOptions: Map.fromEntries(ChipSetActionDelegate.sortOptions.map((v) => MapEntry(v, v.getName(context)))),
|
sortOptions: ChipSetActionDelegate.sortOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||||
groupOptions: Map.fromEntries(_groupOptions.map((v) => MapEntry(v, v.getName(context)))),
|
groupOptions: _groupOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||||
layoutOptions: Map.fromEntries(ChipSetActionDelegate.layoutOptions.map((v) => MapEntry(v, v.getName(context)))),
|
layoutOptions: ChipSetActionDelegate.layoutOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||||
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
|
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,6 +54,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
||||||
];
|
];
|
||||||
|
|
||||||
static const layoutOptions = [
|
static const layoutOptions = [
|
||||||
|
TileLayout.mosaic,
|
||||||
TileLayout.grid,
|
TileLayout.grid,
|
||||||
TileLayout.list,
|
TileLayout.list,
|
||||||
];
|
];
|
||||||
|
@ -222,8 +223,8 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return TileViewDialog<ChipSortFactor, void, TileLayout>(
|
return TileViewDialog<ChipSortFactor, void, TileLayout>(
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
sortOptions: Map.fromEntries(sortOptions.map((v) => MapEntry(v, v.getName(context)))),
|
sortOptions: sortOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||||
layoutOptions: Map.fromEntries(layoutOptions.map((v) => MapEntry(v, v.getName(context)))),
|
layoutOptions: layoutOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||||
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
|
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
|
@ -23,7 +22,6 @@ import 'package:provider/provider.dart';
|
||||||
class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
||||||
final T filter;
|
final T filter;
|
||||||
final double extent, thumbnailExtent;
|
final double extent, thumbnailExtent;
|
||||||
final AvesEntry? coverEntry;
|
|
||||||
final bool showText, pinned;
|
final bool showText, pinned;
|
||||||
final String? banner;
|
final String? banner;
|
||||||
final FilterCallback? onTap;
|
final FilterCallback? onTap;
|
||||||
|
@ -34,7 +32,6 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
||||||
required this.filter,
|
required this.filter,
|
||||||
required this.extent,
|
required this.extent,
|
||||||
double? thumbnailExtent,
|
double? thumbnailExtent,
|
||||||
this.coverEntry,
|
|
||||||
this.showText = true,
|
this.showText = true,
|
||||||
this.pinned = false,
|
this.pinned = false,
|
||||||
this.banner,
|
this.banner,
|
||||||
|
@ -101,7 +98,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChip(BuildContext context, CollectionSource source) {
|
Widget _buildChip(BuildContext context, CollectionSource source) {
|
||||||
final entry = coverEntry ?? source.coverEntry(filter);
|
final entry = source.coverEntry(filter);
|
||||||
final titlePadding = min<double>(4.0, extent / 32);
|
final titlePadding = min<double>(4.0, extent / 32);
|
||||||
Key? chipKey;
|
Key? chipKey;
|
||||||
if (filter is AlbumFilter) {
|
if (filter is AlbumFilter) {
|
||||||
|
|
|
@ -6,7 +6,9 @@ import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/query.dart';
|
import 'package:aves/model/query.dart';
|
||||||
import 'package:aves/model/selection.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums/enums.dart';
|
import 'package:aves/model/source/enums/enums.dart';
|
||||||
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
|
@ -15,7 +17,8 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
||||||
import 'package:aves/widgets/common/grid/scaling.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/selector.dart';
|
import 'package:aves/widgets/common/grid/selector.dart';
|
||||||
import 'package:aves/widgets/common/grid/sliver.dart';
|
import 'package:aves/widgets/common/grid/sliver.dart';
|
||||||
import 'package:aves/widgets/common/grid/theme.dart';
|
import 'package:aves/widgets/common/grid/theme.dart';
|
||||||
|
@ -24,6 +27,7 @@ import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/query_provider.dart';
|
import 'package:aves/widgets/common/providers/query_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
|
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
|
||||||
|
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart';
|
import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart';
|
import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart';
|
||||||
|
@ -244,6 +248,7 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final source = context.read<CollectionSource>();
|
||||||
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
|
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
|
||||||
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey));
|
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey));
|
||||||
return Selector<Query, bool>(
|
return Selector<Query, bool>(
|
||||||
|
@ -284,7 +289,7 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
final tileHeight = CoveredFilterChip.tileHeight(
|
final tileHeight = CoveredFilterChip.tileHeight(
|
||||||
extent: thumbnailExtent,
|
extent: thumbnailExtent,
|
||||||
textScaleFactor: textScaleFactor,
|
textScaleFactor: textScaleFactor,
|
||||||
showText: tileLayout == TileLayout.grid,
|
showText: tileLayout != TileLayout.list,
|
||||||
);
|
);
|
||||||
return GridTheme(
|
return GridTheme(
|
||||||
extent: thumbnailExtent,
|
extent: thumbnailExtent,
|
||||||
|
@ -312,6 +317,10 @@ class _FilterGridContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
tileAnimationDelay: tileAnimationDelay,
|
tileAnimationDelay: tileAnimationDelay,
|
||||||
|
coverRatioResolver: (item) {
|
||||||
|
final coverEntry = source.coverEntry(item.filter) ?? item.entry;
|
||||||
|
return coverEntry?.displayAspectRatio ?? 1;
|
||||||
|
},
|
||||||
child: child!,
|
child: child!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -469,12 +478,13 @@ class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
|
||||||
final metrics = context.select<TileExtentController, Tuple2<double, double>>((v) => Tuple2(v.spacing, v.horizontalPadding));
|
final metrics = context.select<TileExtentController, Tuple2<double, double>>((v) => Tuple2(v.spacing, v.horizontalPadding));
|
||||||
final tileSpacing = metrics.item1;
|
final tileSpacing = metrics.item1;
|
||||||
final horizontalPadding = metrics.item2;
|
final horizontalPadding = metrics.item2;
|
||||||
|
final brightness = Theme.of(context).brightness;
|
||||||
return GridScaleGestureDetector<FilterGridItem<T>>(
|
return GridScaleGestureDetector<FilterGridItem<T>>(
|
||||||
scrollableKey: scrollableKey,
|
scrollableKey: scrollableKey,
|
||||||
tileLayout: tileLayout,
|
tileLayout: tileLayout,
|
||||||
heightForWidth: (width) => CoveredFilterChip.tileHeight(extent: width, textScaleFactor: textScaleFactor, showText: true),
|
heightForWidth: (width) => CoveredFilterChip.tileHeight(extent: width, textScaleFactor: textScaleFactor, showText: true),
|
||||||
gridBuilder: (center, tileSize, child) => CustomPaint(
|
gridBuilder: (center, tileSize, child) => CustomPaint(
|
||||||
painter: GridPainter(
|
painter: FixedExtentGridPainter(
|
||||||
tileLayout: tileLayout,
|
tileLayout: tileLayout,
|
||||||
tileCenter: center,
|
tileCenter: center,
|
||||||
tileSize: tileSize,
|
tileSize: tileSize,
|
||||||
|
@ -487,7 +497,7 @@ class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
scaledBuilder: (item, tileSize) => FilterListDetailsTheme(
|
scaledItemBuilder: (item, tileSize) => FilterListDetailsTheme(
|
||||||
extent: tileSize.height,
|
extent: tileSize.height,
|
||||||
child: FilterTile(
|
child: FilterTile(
|
||||||
gridItem: item,
|
gridItem: item,
|
||||||
|
@ -497,6 +507,16 @@ class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
|
||||||
banner: bannerBuilder(context, item.filter),
|
banner: bannerBuilder(context, item.filter),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
mosaicItemBuilder: (index, targetExtent) => DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ThumbnailImage.computeLoadingBackgroundColor(index * 10, brightness).withOpacity(.9),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.read<AvesColorsData>().neutral,
|
||||||
|
width: AvesFilterChip.outlineWidth,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.all(CoveredFilterChip.radius(targetExtent)),
|
||||||
|
),
|
||||||
|
),
|
||||||
highlightItem: (item) => item.filter,
|
highlightItem: (item) => item.filter,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
|
@ -139,6 +139,7 @@ class FilterTile<T extends CollectionFilter> extends StatelessWidget {
|
||||||
final onChipTap = onTap != null ? (filter) => onTap?.call() : null;
|
final onChipTap = onTap != null ? (filter) => onTap?.call() : null;
|
||||||
|
|
||||||
switch (tileLayout) {
|
switch (tileLayout) {
|
||||||
|
case TileLayout.mosaic:
|
||||||
case TileLayout.grid:
|
case TileLayout.grid:
|
||||||
return FilterChipGridDecorator<T, FilterGridItem<T>>(
|
return FilterChipGridDecorator<T, FilterGridItem<T>>(
|
||||||
gridItem: gridItem,
|
gridItem: gridItem,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/source/section_keys.dart';
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
import 'package:aves/widgets/common/grid/sections/provider.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/section_header.dart';
|
import 'package:aves/widgets/filter_grids/common/section_header.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -22,6 +22,7 @@ class SectionedFilterListLayoutProvider<T extends CollectionFilter> extends Sect
|
||||||
required super.tileHeight,
|
required super.tileHeight,
|
||||||
required super.tileBuilder,
|
required super.tileBuilder,
|
||||||
required super.tileAnimationDelay,
|
required super.tileAnimationDelay,
|
||||||
|
required super.coverRatioResolver,
|
||||||
required super.child,
|
required super.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,10 @@
|
||||||
{
|
{
|
||||||
"de": [
|
"de": [
|
||||||
"entryInfoActionEditTitleDescription",
|
"tileLayoutMosaic"
|
||||||
"filterNoDateLabel",
|
],
|
||||||
"filterNoTitleLabel",
|
|
||||||
"viewDialogReverseSortOrder",
|
"el": [
|
||||||
"sortOrderNewestFirst",
|
"tileLayoutMosaic"
|
||||||
"sortOrderOldestFirst",
|
|
||||||
"sortOrderAtoZ",
|
|
||||||
"sortOrderZtoA",
|
|
||||||
"sortOrderHighestFirst",
|
|
||||||
"sortOrderLowestFirst",
|
|
||||||
"sortOrderLargestFirst",
|
|
||||||
"sortOrderSmallestFirst",
|
|
||||||
"searchMetadataSectionTitle"
|
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
|
@ -21,6 +13,7 @@
|
||||||
"filterNoTitleLabel",
|
"filterNoTitleLabel",
|
||||||
"filterRecentlyAddedLabel",
|
"filterRecentlyAddedLabel",
|
||||||
"viewDialogReverseSortOrder",
|
"viewDialogReverseSortOrder",
|
||||||
|
"tileLayoutMosaic",
|
||||||
"sortOrderNewestFirst",
|
"sortOrderNewestFirst",
|
||||||
"sortOrderOldestFirst",
|
"sortOrderOldestFirst",
|
||||||
"sortOrderAtoZ",
|
"sortOrderAtoZ",
|
||||||
|
@ -34,12 +27,21 @@
|
||||||
"viewerInfoLabelDescription"
|
"viewerInfoLabelDescription"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"id": [
|
||||||
|
"tileLayoutMosaic"
|
||||||
|
],
|
||||||
|
|
||||||
|
"it": [
|
||||||
|
"tileLayoutMosaic"
|
||||||
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
"entryInfoActionEditTitleDescription",
|
"entryInfoActionEditTitleDescription",
|
||||||
"filterNoDateLabel",
|
"filterNoDateLabel",
|
||||||
"filterNoTitleLabel",
|
"filterNoTitleLabel",
|
||||||
"filterRecentlyAddedLabel",
|
"filterRecentlyAddedLabel",
|
||||||
"viewDialogReverseSortOrder",
|
"viewDialogReverseSortOrder",
|
||||||
|
"tileLayoutMosaic",
|
||||||
"sortOrderNewestFirst",
|
"sortOrderNewestFirst",
|
||||||
"sortOrderOldestFirst",
|
"sortOrderOldestFirst",
|
||||||
"sortOrderAtoZ",
|
"sortOrderAtoZ",
|
||||||
|
@ -54,11 +56,16 @@
|
||||||
"viewerInfoLabelDescription"
|
"viewerInfoLabelDescription"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"nl": [
|
||||||
|
"tileLayoutMosaic"
|
||||||
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
"entryInfoActionEditTitleDescription",
|
"entryInfoActionEditTitleDescription",
|
||||||
"filterNoDateLabel",
|
"filterNoDateLabel",
|
||||||
"filterNoTitleLabel",
|
"filterNoTitleLabel",
|
||||||
"viewDialogReverseSortOrder",
|
"viewDialogReverseSortOrder",
|
||||||
|
"tileLayoutMosaic",
|
||||||
"sortOrderNewestFirst",
|
"sortOrderNewestFirst",
|
||||||
"sortOrderOldestFirst",
|
"sortOrderOldestFirst",
|
||||||
"sortOrderAtoZ",
|
"sortOrderAtoZ",
|
||||||
|
@ -70,6 +77,10 @@
|
||||||
"searchMetadataSectionTitle"
|
"searchMetadataSectionTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"ru": [
|
||||||
|
"tileLayoutMosaic"
|
||||||
|
],
|
||||||
|
|
||||||
"tr": [
|
"tr": [
|
||||||
"slideshowActionResume",
|
"slideshowActionResume",
|
||||||
"slideshowActionShowInCollection",
|
"slideshowActionShowInCollection",
|
||||||
|
@ -90,6 +101,7 @@
|
||||||
"wallpaperTargetHomeLock",
|
"wallpaperTargetHomeLock",
|
||||||
"menuActionSlideshow",
|
"menuActionSlideshow",
|
||||||
"viewDialogReverseSortOrder",
|
"viewDialogReverseSortOrder",
|
||||||
|
"tileLayoutMosaic",
|
||||||
"sortOrderNewestFirst",
|
"sortOrderNewestFirst",
|
||||||
"sortOrderOldestFirst",
|
"sortOrderOldestFirst",
|
||||||
"sortOrderAtoZ",
|
"sortOrderAtoZ",
|
||||||
|
@ -116,5 +128,9 @@
|
||||||
"settingsWidgetShowOutline",
|
"settingsWidgetShowOutline",
|
||||||
"viewerSetWallpaperButtonLabel",
|
"viewerSetWallpaperButtonLabel",
|
||||||
"viewerInfoLabelDescription"
|
"viewerInfoLabelDescription"
|
||||||
|
],
|
||||||
|
|
||||||
|
"zh": [
|
||||||
|
"tileLayoutMosaic"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue