refactor(library): move queue download labels, dialogs and sliver grid out of queue_tab.dart

Pure moves: download label helpers and the download dialogs become parts of
the queue_tab library, and the animated sliver grid becomes a public widget
in lib/widgets since it uses no private state.
This commit is contained in:
zarzet committed 2026-10-18 22:26:54 +07:00
1 parent 23c6621552
commit 96568f720a
5 files changed
+367 -358

No files matched your search

+3 -354
View File
@@ -84,6 +84,7 @@ import 'package:spotiflac_android/screens/queue_library_refresh_policy.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/widgets/download_service_picker.dart';
import 'package:spotiflac_android/widgets/animated_library_sliver_grid.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
import 'package:spotiflac_android/widgets/smoothed_progress.dart';
@@ -99,110 +100,8 @@ part 'queue_tab_filter_widgets.dart';
part 'queue_tab_sections.dart';
part 'queue_tab_batch_actions.dart';
part 'queue_tab_item_widgets.dart';
String _formatDownloadSizeMB(num bytes) => '${formatMegabytes(bytes)} MB';
String _formatDownloadProgressLabel(
BuildContext context,
DownloadItem item, {
double? visualProgress,
}) {
final progress = (visualProgress ?? item.progress).clamp(0.0, 1.0);
final speedSuffix = item.speedMBps > 0
? ' • ${item.speedMBps.toStringAsFixed(1)} MB/s'
: '';
if (item.bytesTotal > 0) {
final received = visualProgress != null
? item.bytesTotal * progress
: item.bytesReceived > 0
? item.bytesReceived
: item.bytesTotal * progress;
final percent = (progress * 100).toStringAsFixed(0);
return '${_formatDownloadSizeMB(received)} / ${_formatDownloadSizeMB(item.bytesTotal)} • $percent%$speedSuffix';
}
if (item.bytesReceived > 0) {
final canEstimateTotal = progress > 0.01 && progress < 0.995;
if (canEstimateTotal) {
final estimatedTotal = item.bytesReceived / progress;
if (estimatedTotal > item.bytesReceived) {
return '${_formatDownloadSizeMB(item.bytesReceived)} / ~${_formatDownloadSizeMB(estimatedTotal)}$speedSuffix';
}
}
return '${_formatDownloadSizeMB(item.bytesReceived)}$speedSuffix';
}
if (progress > 0) {
final percent = (progress * 100).toStringAsFixed(0);
return '$percent%$speedSuffix';
}
if (item.speedMBps > 0) {
return context.l10n.queueDownloadSpeedStatus(
item.speedMBps.toStringAsFixed(1),
);
}
if (item.error == 'Waiting for verification') {
return context.l10n.queueWaitingForVerification;
}
switch (item.preparationStage) {
case 'checking_session':
return context.l10n.queueCheckingDownloadSession;
case 'resolving_metadata':
return context.l10n.queueResolvingDownloadMetadata;
case 'resolving_stream':
return context.l10n.queueResolvingDownloadStream;
}
if (item.error == 'Retrying after verification') {
return context.l10n.queueResumingAfterVerification;
}
return context.l10n.queueDownloadStarting;
}
String _formatDownloadStatusLine(
BuildContext context,
DownloadItem item, {
double? visualProgress,
}) {
final base = _formatDownloadProgressLabel(
context,
item,
visualProgress: visualProgress,
);
final eta = _formatDownloadEta(item, visualProgress: visualProgress);
return eta == null ? base : '$base • $eta';
}
String? _formatDownloadEta(DownloadItem item, {double? visualProgress}) {
if (item.speedMBps <= 0 || item.bytesTotal <= 0) return null;
final progress = (visualProgress ?? item.progress).clamp(0.0, 1.0);
final received = visualProgress != null
? (item.bytesTotal * progress).round()
: item.bytesReceived > 0
? item.bytesReceived
: (item.bytesTotal * progress).round();
final remaining = item.bytesTotal - received;
if (remaining <= 0) return null;
final seconds = remaining / (item.speedMBps * 1024 * 1024);
if (!seconds.isFinite || seconds > 3600) return null;
if (seconds < 60) return '~${seconds.round()}s';
final minutes = (seconds / 60).floor();
final secs = (seconds % 60).round();
return '~${minutes}m${secs.toString().padLeft(2, '0')}s';
}
bool _shouldAnimateDownloadProgress(BuildContext context, DownloadItem item) {
final progress = item.progress.clamp(0.0, 1.0);
final animationsDisabled =
MediaQuery.maybeDisableAnimationsOf(context) ?? false;
return item.status == DownloadStatus.downloading &&
progress > 0 &&
progress < 1 &&
!animationsDisabled;
}
part 'queue_tab_download_labels.dart';
part 'queue_tab_download_dialogs.dart';
DownloadHistoryItem? _historyItemForCompletionBridge(
DownloadItem item,
@@ -1848,254 +1747,4 @@ class _QueueTabState extends ConsumerState<QueueTab> {
},
);
}
Future<void> _confirmCancelDownload(
BuildContext context,
DownloadItem item,
) async {
final confirmed = await showAppDialog<bool>(
context: context,
builder: (ctx) => AppAlertDialog(
title: Text(context.l10n.cancelDownloadTitle),
content: Text(context.l10n.cancelDownloadContent(item.track.name)),
actions: [
AppDialogAction(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(context.l10n.cancelDownloadKeep),
),
AppDialogAction(
isDestructive: true,
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(context.l10n.dialogCancel),
),
],
),
);
if (confirmed == true) {
// cancelItem (not dismissItem): the item stays in the queue as
// cancelled so it can be retried without re-searching the track.
ref.read(downloadQueueProvider.notifier).cancelItem(item.id);
}
}
Future<void> _showDownloadErrorDialog(
BuildContext context,
DownloadItem item,
) async {
final colorScheme = Theme.of(context).colorScheme;
final isRateLimit = item.errorType == DownloadErrorType.rateLimit;
final isCancelled = item.status == DownloadStatus.skipped;
final isFolderAccessLost =
item.errorMessage == safPermissionLostErrorMessage ||
item.errorMessage == downloadFolderAccessLostErrorMessage;
final title = isCancelled
? context.l10n.queueCancelledTitle
: isRateLimit
? context.l10n.queueRateLimitTitle
: context.l10n.updateDownloadFailed;
final message = isCancelled
? context.l10n.queueCancelledMessage
: isRateLimit
? context.l10n.queueRateLimitMessage
: (item.errorMessage.trim().isNotEmpty
? _localizedDownloadError(context, item.errorMessage)
: context.l10n.updateDownloadFailed);
final action = await showAppDialog<String>(
context: context,
builder: (ctx) => AppAlertDialog(
title: Text(title),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.track.name,
style: Theme.of(
ctx,
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
SelectableText(
message,
style: Theme.of(ctx).textTheme.bodyMedium,
),
],
),
),
actions: [
AppDialogAction(
isDestructive: true,
onPressed: () => Navigator.of(ctx).pop('remove'),
style: TextButton.styleFrom(foregroundColor: colorScheme.error),
child: Text(context.l10n.dialogRemove),
),
AppDialogAction(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(context.l10n.dialogCancel),
),
if (isFolderAccessLost)
AppDialogAction(
filled: true,
onPressed: () => Navigator.of(ctx).pop('reselect'),
child: Text(context.l10n.downloadFolderReselect),
)
else
AppDialogAction(
filled: true,
onPressed: () => Navigator.of(ctx).pop('retry'),
child: Text(context.l10n.dialogRetry),
),
],
),
);
if (!mounted) return;
if (action == 'retry') {
ref.read(downloadQueueProvider.notifier).retryItem(item.id);
} else if (action == 'remove') {
ref.read(downloadQueueProvider.notifier).removeItem(item.id);
} else if (action == 'reselect') {
final reselected = await _reselectDownloadFolder();
if (reselected && mounted) {
ref.read(downloadQueueProvider.notifier).retryItem(item.id);
}
}
}
/// Reopen the platform folder picker to restore download folder access,
/// then persist the new location. Returns true when a folder was saved.
Future<bool> _reselectDownloadFolder() async {
if (Platform.isAndroid) {
final result = await PlatformBridge.pickSafTree();
if (result == null) return false;
final treeUri = result['tree_uri'] as String? ?? '';
final displayName = result['display_name'] as String? ?? '';
if (treeUri.isEmpty) return false;
final notifier = ref.read(settingsProvider.notifier);
notifier.setStorageMode('saf');
notifier.setDownloadTreeUri(
treeUri,
displayName: displayName.isNotEmpty ? displayName : treeUri,
);
return true;
}
if (Platform.isIOS) {
IosPickedDirectory? picked;
try {
picked = await PlatformBridge.pickIosDirectory();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.snackbarFolderPickerFailed(
context.friendlyError(e),
),
),
),
);
}
return false;
}
if (picked == null || !mounted) return false;
final validation = validateIosPath(
picked.path,
bookmark: picked.bookmark,
);
if (!validation.isValid) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
validation.errorReason ?? context.l10n.setupIcloudNotSupported,
),
),
);
}
return false;
}
ref
.read(settingsProvider.notifier)
.setDownloadDirectory(picked.path, iosBookmark: picked.bookmark);
return true;
}
return false;
}
}
class _AnimatedLibrarySliverGrid extends StatefulWidget {
final double maxCrossAxisExtent;
final double mainAxisSpacing;
final double crossAxisSpacing;
final double childAspectRatio;
final SliverChildDelegate delegate;
const _AnimatedLibrarySliverGrid({
required this.maxCrossAxisExtent,
required this.mainAxisSpacing,
required this.crossAxisSpacing,
required this.childAspectRatio,
required this.delegate,
});
@override
State<_AnimatedLibrarySliverGrid> createState() =>
_AnimatedLibrarySliverGridState();
}
class _AnimatedLibrarySliverGridState extends State<_AnimatedLibrarySliverGrid>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final CurvedAnimation _curve;
late double _beginExtent;
late double _endExtent;
@override
void initState() {
super.initState();
_beginExtent = widget.maxCrossAxisExtent;
_endExtent = widget.maxCrossAxisExtent;
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 130),
)..value = 1;
_curve = CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic);
}
@override
void didUpdateWidget(covariant _AnimatedLibrarySliverGrid oldWidget) {
super.didUpdateWidget(oldWidget);
if ((widget.maxCrossAxisExtent - _endExtent).abs() < 0.1) return;
_beginExtent = _currentExtent;
_endExtent = widget.maxCrossAxisExtent;
_controller.forward(from: 0);
}
double get _currentExtent =>
_beginExtent + ((_endExtent - _beginExtent) * _curve.value);
@override
void dispose() {
_curve.dispose();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: _currentExtent,
mainAxisSpacing: widget.mainAxisSpacing,
crossAxisSpacing: widget.crossAxisSpacing,
childAspectRatio: widget.childAspectRatio,
),
delegate: widget.delegate,
);
},
);
}
}
+176
View File
@@ -0,0 +1,176 @@
part of 'queue_tab.dart';
extension _QueueTabDownloadDialogs on _QueueTabState {
Future<void> _confirmCancelDownload(
BuildContext context,
DownloadItem item,
) async {
final confirmed = await showAppDialog<bool>(
context: context,
builder: (ctx) => AppAlertDialog(
title: Text(context.l10n.cancelDownloadTitle),
content: Text(context.l10n.cancelDownloadContent(item.track.name)),
actions: [
AppDialogAction(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(context.l10n.cancelDownloadKeep),
),
AppDialogAction(
isDestructive: true,
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(context.l10n.dialogCancel),
),
],
),
);
if (confirmed == true) {
// cancelItem (not dismissItem): the item stays in the queue as
// cancelled so it can be retried without re-searching the track.
ref.read(downloadQueueProvider.notifier).cancelItem(item.id);
}
}
Future<void> _showDownloadErrorDialog(
BuildContext context,
DownloadItem item,
) async {
final colorScheme = Theme.of(context).colorScheme;
final isRateLimit = item.errorType == DownloadErrorType.rateLimit;
final isCancelled = item.status == DownloadStatus.skipped;
final isFolderAccessLost =
item.errorMessage == safPermissionLostErrorMessage ||
item.errorMessage == downloadFolderAccessLostErrorMessage;
final title = isCancelled
? context.l10n.queueCancelledTitle
: isRateLimit
? context.l10n.queueRateLimitTitle
: context.l10n.updateDownloadFailed;
final message = isCancelled
? context.l10n.queueCancelledMessage
: isRateLimit
? context.l10n.queueRateLimitMessage
: (item.errorMessage.trim().isNotEmpty
? _localizedDownloadError(context, item.errorMessage)
: context.l10n.updateDownloadFailed);
final action = await showAppDialog<String>(
context: context,
builder: (ctx) => AppAlertDialog(
title: Text(title),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.track.name,
style: Theme.of(
ctx,
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
SelectableText(
message,
style: Theme.of(ctx).textTheme.bodyMedium,
),
],
),
),
actions: [
AppDialogAction(
isDestructive: true,
onPressed: () => Navigator.of(ctx).pop('remove'),
style: TextButton.styleFrom(foregroundColor: colorScheme.error),
child: Text(context.l10n.dialogRemove),
),
AppDialogAction(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(context.l10n.dialogCancel),
),
if (isFolderAccessLost)
AppDialogAction(
filled: true,
onPressed: () => Navigator.of(ctx).pop('reselect'),
child: Text(context.l10n.downloadFolderReselect),
)
else
AppDialogAction(
filled: true,
onPressed: () => Navigator.of(ctx).pop('retry'),
child: Text(context.l10n.dialogRetry),
),
],
),
);
if (!mounted) return;
if (action == 'retry') {
ref.read(downloadQueueProvider.notifier).retryItem(item.id);
} else if (action == 'remove') {
ref.read(downloadQueueProvider.notifier).removeItem(item.id);
} else if (action == 'reselect') {
final reselected = await _reselectDownloadFolder();
if (reselected && mounted) {
ref.read(downloadQueueProvider.notifier).retryItem(item.id);
}
}
}
/// Reopen the platform folder picker to restore download folder access,
/// then persist the new location. Returns true when a folder was saved.
Future<bool> _reselectDownloadFolder() async {
if (Platform.isAndroid) {
final result = await PlatformBridge.pickSafTree();
if (result == null) return false;
final treeUri = result['tree_uri'] as String? ?? '';
final displayName = result['display_name'] as String? ?? '';
if (treeUri.isEmpty) return false;
final notifier = ref.read(settingsProvider.notifier);
notifier.setStorageMode('saf');
notifier.setDownloadTreeUri(
treeUri,
displayName: displayName.isNotEmpty ? displayName : treeUri,
);
return true;
}
if (Platform.isIOS) {
IosPickedDirectory? picked;
try {
picked = await PlatformBridge.pickIosDirectory();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.snackbarFolderPickerFailed(
context.friendlyError(e),
),
),
),
);
}
return false;
}
if (picked == null || !mounted) return false;
final validation = validateIosPath(
picked.path,
bookmark: picked.bookmark,
);
if (!validation.isValid) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
validation.errorReason ?? context.l10n.setupIcloudNotSupported,
),
),
);
}
return false;
}
ref
.read(settingsProvider.notifier)
.setDownloadDirectory(picked.path, iosBookmark: picked.bookmark);
return true;
}
return false;
}
}
+105
View File
@@ -0,0 +1,105 @@
part of 'queue_tab.dart';
String _formatDownloadSizeMB(num bytes) => '${formatMegabytes(bytes)} MB';
String _formatDownloadProgressLabel(
BuildContext context,
DownloadItem item, {
double? visualProgress,
}) {
final progress = (visualProgress ?? item.progress).clamp(0.0, 1.0);
final speedSuffix = item.speedMBps > 0
? ' • ${item.speedMBps.toStringAsFixed(1)} MB/s'
: '';
if (item.bytesTotal > 0) {
final received = visualProgress != null
? item.bytesTotal * progress
: item.bytesReceived > 0
? item.bytesReceived
: item.bytesTotal * progress;
final percent = (progress * 100).toStringAsFixed(0);
return '${_formatDownloadSizeMB(received)} / ${_formatDownloadSizeMB(item.bytesTotal)} • $percent%$speedSuffix';
}
if (item.bytesReceived > 0) {
final canEstimateTotal = progress > 0.01 && progress < 0.995;
if (canEstimateTotal) {
final estimatedTotal = item.bytesReceived / progress;
if (estimatedTotal > item.bytesReceived) {
return '${_formatDownloadSizeMB(item.bytesReceived)} / ~${_formatDownloadSizeMB(estimatedTotal)}$speedSuffix';
}
}
return '${_formatDownloadSizeMB(item.bytesReceived)}$speedSuffix';
}
if (progress > 0) {
final percent = (progress * 100).toStringAsFixed(0);
return '$percent%$speedSuffix';
}
if (item.speedMBps > 0) {
return context.l10n.queueDownloadSpeedStatus(
item.speedMBps.toStringAsFixed(1),
);
}
if (item.error == 'Waiting for verification') {
return context.l10n.queueWaitingForVerification;
}
switch (item.preparationStage) {
case 'checking_session':
return context.l10n.queueCheckingDownloadSession;
case 'resolving_metadata':
return context.l10n.queueResolvingDownloadMetadata;
case 'resolving_stream':
return context.l10n.queueResolvingDownloadStream;
}
if (item.error == 'Retrying after verification') {
return context.l10n.queueResumingAfterVerification;
}
return context.l10n.queueDownloadStarting;
}
String _formatDownloadStatusLine(
BuildContext context,
DownloadItem item, {
double? visualProgress,
}) {
final base = _formatDownloadProgressLabel(
context,
item,
visualProgress: visualProgress,
);
final eta = _formatDownloadEta(item, visualProgress: visualProgress);
return eta == null ? base : '$base • $eta';
}
String? _formatDownloadEta(DownloadItem item, {double? visualProgress}) {
if (item.speedMBps <= 0 || item.bytesTotal <= 0) return null;
final progress = (visualProgress ?? item.progress).clamp(0.0, 1.0);
final received = visualProgress != null
? (item.bytesTotal * progress).round()
: item.bytesReceived > 0
? item.bytesReceived
: (item.bytesTotal * progress).round();
final remaining = item.bytesTotal - received;
if (remaining <= 0) return null;
final seconds = remaining / (item.speedMBps * 1024 * 1024);
if (!seconds.isFinite || seconds > 3600) return null;
if (seconds < 60) return '~${seconds.round()}s';
final minutes = (seconds / 60).floor();
final secs = (seconds % 60).round();
return '~${minutes}m${secs.toString().padLeft(2, '0')}s';
}
bool _shouldAnimateDownloadProgress(BuildContext context, DownloadItem item) {
final progress = item.progress.clamp(0.0, 1.0);
final animationsDisabled =
MediaQuery.maybeDisableAnimationsOf(context) ?? false;
return item.status == DownloadStatus.downloading &&
progress > 0 &&
progress < 1 &&
!animationsDisabled;
}
+4 -4
View File
@@ -466,7 +466,7 @@ extension _QueueTabFilterWidgets on _QueueTabState {
filteredGroupedLocalAlbums.isNotEmpty))
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: edge),
sliver: _AnimatedLibrarySliverGrid(
sliver: AnimatedLibrarySliverGrid(
maxCrossAxisExtent: _libraryAlbumGridExtent,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
@@ -521,7 +521,7 @@ extension _QueueTabFilterWidgets on _QueueTabState {
),
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: edge),
sliver: _AnimatedLibrarySliverGrid(
sliver: AnimatedLibrarySliverGrid(
maxCrossAxisExtent: _libraryAlbumGridExtent,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
@@ -562,7 +562,7 @@ extension _QueueTabFilterWidgets on _QueueTabState {
if (historyViewMode == 'grid')
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: edge),
sliver: _AnimatedLibrarySliverGrid(
sliver: AnimatedLibrarySliverGrid(
maxCrossAxisExtent: _libraryGridExtent,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
@@ -746,7 +746,7 @@ extension _QueueTabFilterWidgets on _QueueTabState {
historyViewMode == 'grid'
? SliverPadding(
padding: EdgeInsets.symmetric(horizontal: edge),
sliver: _AnimatedLibrarySliverGrid(
sliver: AnimatedLibrarySliverGrid(
maxCrossAxisExtent: _libraryGridExtent,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
class AnimatedLibrarySliverGrid extends StatefulWidget {
final double maxCrossAxisExtent;
final double mainAxisSpacing;
final double crossAxisSpacing;
final double childAspectRatio;
final SliverChildDelegate delegate;
const AnimatedLibrarySliverGrid({
super.key,
required this.maxCrossAxisExtent,
required this.mainAxisSpacing,
required this.crossAxisSpacing,
required this.childAspectRatio,
required this.delegate,
});
@override
State<AnimatedLibrarySliverGrid> createState() =>
_AnimatedLibrarySliverGridState();
}
class _AnimatedLibrarySliverGridState extends State<AnimatedLibrarySliverGrid>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final CurvedAnimation _curve;
late double _beginExtent;
late double _endExtent;
@override
void initState() {
super.initState();
_beginExtent = widget.maxCrossAxisExtent;
_endExtent = widget.maxCrossAxisExtent;
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 130),
)..value = 1;
_curve = CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic);
}
@override
void didUpdateWidget(covariant AnimatedLibrarySliverGrid oldWidget) {
super.didUpdateWidget(oldWidget);
if ((widget.maxCrossAxisExtent - _endExtent).abs() < 0.1) return;
_beginExtent = _currentExtent;
_endExtent = widget.maxCrossAxisExtent;
_controller.forward(from: 0);
}
double get _currentExtent =>
_beginExtent + ((_endExtent - _beginExtent) * _curve.value);
@override
void dispose() {
_curve.dispose();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: _currentExtent,
mainAxisSpacing: widget.mainAxisSpacing,
crossAxisSpacing: widget.crossAxisSpacing,
childAspectRatio: widget.childAspectRatio,
),
delegate: widget.delegate,
);
},
);
}
}