From 1893f3faa8541b3ed40a1d2994ef79d9f751c341 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 14 Jul 2026 08:15:02 +0700 Subject: [PATCH] feat(tablet): clamp remaining screens - settings, queue lists, wizard - SettingsGroup centers itself at 720dp via its incoming constraint (not screen width, so groups inside clamped sheets are never over-inset; unbounded-width contexts guarded); priority scaffold, bespoke hero cards, and donate page get the same inset - all 18+ settings pages clamp from shared widgets - queue list-mode, local/downloaded album track lists, favorite artists, home recent-access, and extension detail sections adopt wideListInset like the screens fixed earlier - track metadata content clamped at 720dp, which also tames the width-derived spectrogram height - announcement dialog capped at 480dp wide; setup wizard and tutorial content at 560dp with a 400dp action button --- lib/screens/downloaded_album_screen.dart | 41 +++-- lib/screens/favorite_artists_screen.dart | 116 ++++++------- lib/screens/home_tab.dart | 4 +- lib/screens/local_album_screen.dart | 6 +- lib/screens/queue_tab.dart | 1 + lib/screens/queue_tab_filter_widgets.dart | 152 +++++++++-------- .../repo/extension_details_screen.dart | 25 ++- lib/screens/settings/about_page.dart | 8 +- .../settings/appearance_settings_page.dart | 5 +- lib/screens/settings/donate_page.dart | 8 +- .../download_fallback_extensions_page.dart | 20 ++- .../settings/library_settings_page.dart | 6 +- .../lyrics_provider_priority_page.dart | 9 +- .../metadata_provider_priority_page.dart | 5 +- .../settings/provider_priority_page.dart | 5 +- lib/screens/settings/settings_tab.dart | 26 ++- lib/screens/setup_screen.dart | 157 ++++++++++-------- lib/screens/track_metadata_screen.dart | 7 +- lib/screens/tutorial_screen.dart | 140 +++++++++------- lib/utils/adaptive_layout.dart | 16 +- lib/widgets/app_announcement_dialog.dart | 2 +- lib/widgets/priority_settings_scaffold.dart | 13 +- lib/widgets/settings_group.dart | 48 ++++-- 23 files changed, 499 insertions(+), 321 deletions(-) diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index e837d3c5..d35f5e72 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -9,6 +9,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/services/batch_track_actions.dart'; import 'package:spotiflac_android/models/unified_library_item.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/confirm_and_delete_tracks.dart'; import 'package:spotiflac_android/utils/cover_art_utils.dart'; import 'package:spotiflac_android/utils/file_access.dart'; @@ -515,23 +516,26 @@ class _DownloadedAlbumScreenState extends ConsumerState final discMap = _getDiscGroups(tracks); if (discMap.length <= 1) { - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - final track = tracks[index]; - return KeyedSubtree( - key: ValueKey(track.id), - child: StaggeredListItem( - index: index, - child: _buildTrackItem( - context, - colorScheme, - track, - tracks, - index, + return SliverPadding( + padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final track = tracks[index]; + return KeyedSubtree( + key: ValueKey(track.id), + child: StaggeredListItem( + index: index, + child: _buildTrackItem( + context, + colorScheme, + track, + tracks, + index, + ), ), - ), - ); - }, childCount: tracks.length), + ); + }, childCount: tracks.length), + ), ); } @@ -565,7 +569,10 @@ class _DownloadedAlbumScreenState extends ConsumerState } } - return SliverList(delegate: SliverChildListDelegate(children)); + return SliverPadding( + padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), + sliver: SliverList(delegate: SliverChildListDelegate(children)), + ); } Widget _buildTrackItem( diff --git a/lib/screens/favorite_artists_screen.dart b/lib/screens/favorite_artists_screen.dart index 377ccfb3..2076f9c9 100644 --- a/lib/screens/favorite_artists_screen.dart +++ b/lib/screens/favorite_artists_screen.dart @@ -5,6 +5,7 @@ import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/screens/artist_screen.dart'; import 'package:spotiflac_android/services/cover_cache_manager.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/widgets/animation_utils.dart'; import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart'; @@ -58,67 +59,70 @@ class FavoriteArtistsScreen extends ConsumerWidget { ), ) else - SliverList.separated( - itemCount: artists.length, - separatorBuilder: (_, _) => const Divider(height: 1), - itemBuilder: (context, index) { - final artist = artists[index]; - return ListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), - leading: _ArtistThumbnail(artist: artist), - title: Text( - artist.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: - artist.providerId == null || artist.providerId!.isEmpty - ? null - : Text( - artist.providerId!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: IconButton( - tooltip: context.l10n.artistOptionRemoveFromFavorites, - icon: Icon(Icons.favorite, color: colorScheme.error), - onPressed: () async { - await ref - .read(libraryCollectionsProvider.notifier) - .removeFavoriteArtist(artist.key); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.collectionRemovedFromFavoriteArtists( - artist.name, + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), + sliver: SliverList.separated( + itemCount: artists.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final artist = artists[index]; + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + leading: _ArtistThumbnail(artist: artist), + title: Text( + artist.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: + artist.providerId == null || artist.providerId!.isEmpty + ? null + : Text( + artist.providerId!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: IconButton( + tooltip: context.l10n.artistOptionRemoveFromFavorites, + icon: Icon(Icons.favorite, color: colorScheme.error), + onPressed: () async { + await ref + .read(libraryCollectionsProvider.notifier) + .removeFavoriteArtist(artist.key); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.collectionRemovedFromFavoriteArtists( + artist.name, + ), ), ), + ); + }, + ), + onTap: () { + Navigator.of(context).push( + slidePageRoute( + page: ArtistScreen( + artistId: artist.artistId, + artistName: artist.name, + coverUrl: artist.imageUrl, + extensionId: + artist.providerId != null && + artist.providerId!.isNotEmpty + ? artist.providerId + : null, + ), ), ); }, - ), - onTap: () { - Navigator.of(context).push( - slidePageRoute( - page: ArtistScreen( - artistId: artist.artistId, - artistName: artist.name, - coverUrl: artist.imageUrl, - extensionId: - artist.providerId != null && - artist.providerId!.isNotEmpty - ? artist.providerId - : null, - ), - ), - ); - }, - ); - }, + ); + }, + ), ), SliverToBoxAdapter(child: SizedBox(height: bottomInset)), ], diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 386e5041..0a8f3a70 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -2209,7 +2209,9 @@ class _HomeTabState extends ConsumerState final hasHiddenDownloads = view.hasHiddenDownloads; return Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + padding: + const EdgeInsets.fromLTRB(16, 8, 16, 8) + + EdgeInsets.symmetric(horizontal: wideListInset(context)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index a7e681a9..e6f828b2 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -7,6 +7,7 @@ import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/confirm_and_delete_tracks.dart'; import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/image_cache_utils.dart'; @@ -372,7 +373,10 @@ class _LocalAlbumScreenState extends ConsumerState ); } - return SliverMainAxisGroup(slivers: slivers); + return SliverPadding( + padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), + sliver: SliverMainAxisGroup(slivers: slivers), + ); } Widget _buildTrackItem( diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 59f7e468..a746c96f 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -8,6 +8,7 @@ import 'package:share_plus/share_plus.dart'; import 'package:spotiflac_android/services/ffmpeg_service.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/app_bar_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; diff --git a/lib/screens/queue_tab_filter_widgets.dart b/lib/screens/queue_tab_filter_widgets.dart index 870014fc..906e3a5c 100644 --- a/lib/screens/queue_tab_filter_widgets.dart +++ b/lib/screens/queue_tab_filter_widgets.dart @@ -469,36 +469,52 @@ extension _QueueTabFilterWidgets on _QueueTabState { ), ) else - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index < collectionCount) { - return _buildAllTabListCollectionItem( - context: context, - colorScheme: colorScheme, - entry: collectionEntries[index], - collectionState: collectionState, - filteredUnifiedItems: filteredUnifiedItems, - ); - } - final afterCollections = index - collectionCount; - if (afterCollections < leadCount) { - return leadListCell(afterCollections); - } - final trackIndex = afterCollections - leadCount; - if (trackIndex < filteredUnifiedItems.length) { - final item = filteredUnifiedItems[trackIndex]; - return KeyedSubtree( - key: ValueKey(item.id), - child: LongPressDraggable( - data: item, - feedback: _buildDragFeedback( - context, - item, - colorScheme, - ), - childWhenDragging: Opacity( - opacity: 0.4, + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index < collectionCount) { + return _buildAllTabListCollectionItem( + context: context, + colorScheme: colorScheme, + entry: collectionEntries[index], + collectionState: collectionState, + filteredUnifiedItems: filteredUnifiedItems, + ); + } + final afterCollections = index - collectionCount; + if (afterCollections < leadCount) { + return leadListCell(afterCollections); + } + final trackIndex = afterCollections - leadCount; + if (trackIndex < filteredUnifiedItems.length) { + final item = filteredUnifiedItems[trackIndex]; + return KeyedSubtree( + key: ValueKey(item.id), + child: LongPressDraggable( + data: item, + feedback: _buildDragFeedback( + context, + item, + colorScheme, + ), + childWhenDragging: Opacity( + opacity: 0.4, + child: _buildUnifiedLibraryItem( + context, + item, + colorScheme, + downloadedNavigationItems: + downloadedNavigationItems, + downloadedNavigationIndex: + downloadedNavigationIndexByUnifiedId[item.id], + localNavigationItems: localNavigationItems, + localNavigationIndex: + localNavigationIndexByUnifiedId[item.id], + libraryItems: filteredUnifiedItems, + ), + ), child: _buildUnifiedLibraryItem( context, item, @@ -513,25 +529,13 @@ extension _QueueTabFilterWidgets on _QueueTabState { libraryItems: filteredUnifiedItems, ), ), - child: _buildUnifiedLibraryItem( - context, - item, - colorScheme, - downloadedNavigationItems: downloadedNavigationItems, - downloadedNavigationIndex: - downloadedNavigationIndexByUnifiedId[item.id], - localNavigationItems: localNavigationItems, - localNavigationIndex: - localNavigationIndexByUnifiedId[item.id], - libraryItems: filteredUnifiedItems, - ), - ), - ); - } - return const SizedBox.shrink(); - }, - childCount: - leadCount + collectionCount + filteredUnifiedItems.length, + ); + } + return const SizedBox.shrink(); + }, + childCount: + leadCount + collectionCount + filteredUnifiedItems.length, + ), ), ), ], @@ -605,28 +609,33 @@ extension _QueueTabFilterWidgets on _QueueTabState { }, childCount: leadCount + filteredUnifiedItems.length), ), ) - : SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - if (index < leadCount) { - return leadListCell(index); - } - final item = filteredUnifiedItems[index - leadCount]; - return KeyedSubtree( - key: ValueKey(item.id), - child: _buildUnifiedLibraryItem( - context, - item, - colorScheme, - downloadedNavigationItems: downloadedNavigationItems, - downloadedNavigationIndex: - downloadedNavigationIndexByUnifiedId[item.id], - localNavigationItems: localNavigationItems, - localNavigationIndex: - localNavigationIndexByUnifiedId[item.id], - libraryItems: filteredUnifiedItems, - ), - ); - }, childCount: leadCount + filteredUnifiedItems.length), + : SliverPadding( + padding: EdgeInsets.symmetric( + horizontal: wideListInset(context), + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + if (index < leadCount) { + return leadListCell(index); + } + final item = filteredUnifiedItems[index - leadCount]; + return KeyedSubtree( + key: ValueKey(item.id), + child: _buildUnifiedLibraryItem( + context, + item, + colorScheme, + downloadedNavigationItems: downloadedNavigationItems, + downloadedNavigationIndex: + downloadedNavigationIndexByUnifiedId[item.id], + localNavigationItems: localNavigationItems, + localNavigationIndex: + localNavigationIndexByUnifiedId[item.id], + libraryItems: filteredUnifiedItems, + ), + ); + }, childCount: leadCount + filteredUnifiedItems.length), + ), ), if (!hasQueueItems && @@ -960,5 +969,4 @@ extension _QueueTabFilterWidgets on _QueueTabState { return selectedItems.isNotEmpty && selectedItems.every((item) => item.localItem != null); } - } diff --git a/lib/screens/repo/extension_details_screen.dart b/lib/screens/repo/extension_details_screen.dart index a1f76af8..21702615 100644 --- a/lib/screens/repo/extension_details_screen.dart +++ b/lib/screens/repo/extension_details_screen.dart @@ -4,6 +4,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/repo_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; class ExtensionDetailsScreen extends ConsumerStatefulWidget { @@ -153,7 +154,9 @@ class _ExtensionDetailsScreenState ) { return SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(16), + padding: + const EdgeInsets.all(16) + + EdgeInsets.symmetric(horizontal: wideListInset(context)), child: Card( elevation: 0, color: colorScheme.surfaceContainerLow, @@ -295,7 +298,9 @@ class _ExtensionDetailsScreenState ) { return SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), + padding: + const EdgeInsets.fromLTRB(20, 8, 20, 8) + + EdgeInsets.symmetric(horizontal: wideListInset(context)), child: Row( children: [ Icon(icon, size: 20, color: colorScheme.primary), @@ -320,7 +325,9 @@ class _ExtensionDetailsScreenState ) { return SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 8) + + EdgeInsets.symmetric(horizontal: wideListInset(context)), child: Text( ext.description, style: Theme.of(context).textTheme.bodyLarge?.copyWith( @@ -339,7 +346,9 @@ class _ExtensionDetailsScreenState ) { return SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 8) + + EdgeInsets.symmetric(horizontal: wideListInset(context)), child: Wrap( spacing: 8, runSpacing: 8, @@ -369,7 +378,9 @@ class _ExtensionDetailsScreenState ColorScheme colorScheme, ) { return SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8) + + EdgeInsets.symmetric(horizontal: wideListInset(context)), sliver: SliverToBoxAdapter( child: Card( elevation: 0, @@ -416,7 +427,9 @@ class _ExtensionDetailsScreenState final isUtility = ext.category == 'utility'; return SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8) + + EdgeInsets.symmetric(horizontal: wideListInset(context)), sliver: SliverToBoxAdapter( child: Card( elevation: 0, diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 14b268cf..352502a3 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:spotiflac_android/services/cover_cache_manager.dart'; import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart'; @@ -23,7 +24,12 @@ class AboutPage extends StatelessWidget { SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + padding: EdgeInsets.fromLTRB( + 16 + wideListInset(context), + 16, + 16 + wideListInset(context), + 8, + ), child: _AppHeaderCard(), ), ), diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index fa194ec2..48d7969f 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -4,6 +4,7 @@ import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/l10n/supported_locales.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/theme_provider.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart'; @@ -24,8 +25,8 @@ class AppearanceSettingsPage extends ConsumerWidget { SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, + padding: EdgeInsets.symmetric( + horizontal: 16 + wideListInset(context), vertical: 8, ), child: _ThemePreviewCard(), diff --git a/lib/screens/settings/donate_page.dart b/lib/screens/settings/donate_page.dart index b79deb5e..9a042631 100644 --- a/lib/screens/settings/donate_page.dart +++ b/lib/screens/settings/donate_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:spotiflac_android/services/app_remote_config_service.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/donate_icons.dart'; import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart'; @@ -63,7 +64,12 @@ class _DonatePageState extends State { const SettingsSliverAppBar(title: 'Donate'), SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.fromLTRB( + 16 + wideListInset(context), + 16, + 16 + wideListInset(context), + 16, + ), child: Column( children: [ _DonateLinksCard(colorScheme: colorScheme, config: _config), diff --git a/lib/screens/settings/download_fallback_extensions_page.dart b/lib/screens/settings/download_fallback_extensions_page.dart index 07599084..b62ca449 100644 --- a/lib/screens/settings/download_fallback_extensions_page.dart +++ b/lib/screens/settings/download_fallback_extensions_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/discard_changes_dialog.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart'; @@ -53,6 +54,7 @@ class _DownloadFallbackExtensionsPageState @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; + final wideInset = wideListInset(context); return PopScope( canPop: !_hasChanges, @@ -92,7 +94,12 @@ class _DownloadFallbackExtensionsPageState ), SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.fromLTRB( + 16 + wideInset, + 16, + 16 + wideInset, + 16, + ), child: Text( context.l10n.providerPriorityFallbackExtensionsDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( @@ -104,7 +111,7 @@ class _DownloadFallbackExtensionsPageState if (_extensions.isEmpty) SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.symmetric(horizontal: 16 + wideInset), child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -124,7 +131,7 @@ class _DownloadFallbackExtensionsPageState ), if (_extensions.isNotEmpty) SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.symmetric(horizontal: 16 + wideInset), sliver: SliverToBoxAdapter( child: SettingsGroup( margin: EdgeInsets.zero, @@ -157,7 +164,12 @@ class _DownloadFallbackExtensionsPageState if (_extensions.isNotEmpty) SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + padding: EdgeInsets.fromLTRB( + 16 + wideInset, + 8, + 16 + wideInset, + 0, + ), child: Text( context.l10n.providerPriorityFallbackExtensionsHint, style: Theme.of(context).textTheme.bodySmall?.copyWith( diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index 788b1607..59c6491a 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -8,6 +8,7 @@ import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart'; @@ -715,7 +716,10 @@ class _LibraryHeroCard extends StatelessWidget { : itemCount + excludedDownloadedCount; return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + margin: EdgeInsets.symmetric( + horizontal: 16 + wideListInset(context), + vertical: 8, + ), constraints: const BoxConstraints(minHeight: 220), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, diff --git a/lib/screens/settings/lyrics_provider_priority_page.dart b/lib/screens/settings/lyrics_provider_priority_page.dart index b662ff40..0d06ade0 100644 --- a/lib/screens/settings/lyrics_provider_priority_page.dart +++ b/lib/screens/settings/lyrics_provider_priority_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/discard_changes_dialog.dart'; import 'package:spotiflac_android/widgets/priority_settings_scaffold.dart'; import 'package:spotiflac_android/widgets/reorderable_priority_item.dart'; @@ -83,7 +84,9 @@ class _LyricsProviderPriorityPageState ), if (_enabledProviders.isNotEmpty) SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.symmetric( + horizontal: 16 + wideListInset(context), + ), sliver: SliverReorderableList( itemCount: _enabledProviders.length, itemBuilder: (context, index) { @@ -127,7 +130,9 @@ class _LyricsProviderPriorityPageState ), if (disabled.isNotEmpty) SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.symmetric( + horizontal: 16 + wideListInset(context), + ), sliver: SliverList( delegate: SliverChildBuilderDelegate((context, index) { final id = disabled[index]; diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index 1ff26822..386bb1ca 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/discard_changes_dialog.dart'; import 'package:spotiflac_android/widgets/priority_settings_scaffold.dart'; import 'package:spotiflac_android/widgets/reorderable_priority_item.dart'; @@ -57,7 +58,9 @@ class _MetadataProviderPriorityPageState onConfirmDiscard: showDiscardChangesDialog, slivers: [ SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.symmetric( + horizontal: 16 + wideListInset(context), + ), sliver: SliverReorderableList( itemCount: _providers.length, itemBuilder: (context, index) { diff --git a/lib/screens/settings/provider_priority_page.dart b/lib/screens/settings/provider_priority_page.dart index c9425974..9617ed0e 100644 --- a/lib/screens/settings/provider_priority_page.dart +++ b/lib/screens/settings/provider_priority_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/discard_changes_dialog.dart'; import 'package:spotiflac_android/widgets/priority_settings_scaffold.dart'; import 'package:spotiflac_android/widgets/reorderable_priority_item.dart'; @@ -55,7 +56,9 @@ class _ProviderPriorityPageState extends ConsumerState { onConfirmDiscard: showDiscardChangesDialog, slivers: [ SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.symmetric( + horizontal: 16 + wideListInset(context), + ), sliver: SliverReorderableList( itemCount: _providers.length, itemBuilder: (context, index) { diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index c2703a72..879bb4d0 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -15,6 +15,7 @@ import 'package:spotiflac_android/screens/settings/cache_management_page.dart'; import 'package:spotiflac_android/screens/settings/backup_restore_page.dart'; import 'package:spotiflac_android/screens/settings/donate_page.dart'; import 'package:spotiflac_android/screens/settings/log_screen.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/app_bar_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -28,6 +29,7 @@ class SettingsTab extends ConsumerWidget { final colorScheme = Theme.of(context).colorScheme; final topPadding = normalizedHeaderTopPadding(context); final bottomInset = context.navBarBottomInset; + final wideInset = wideListInset(context); return CustomScrollView( slivers: [ @@ -69,7 +71,12 @@ class SettingsTab extends ConsumerWidget { builder: (context) { final l10n = context.l10n; return SettingsGroup( - margin: const EdgeInsets.fromLTRB(16, 16, 16, 4), + margin: EdgeInsets.fromLTRB( + 16 + wideInset, + 16, + 16 + wideInset, + 4, + ), children: [ SettingsItem( icon: Icons.palette_outlined, @@ -103,7 +110,12 @@ class SettingsTab extends ConsumerWidget { builder: (context) { final l10n = context.l10n; return SettingsGroup( - margin: const EdgeInsets.fromLTRB(16, 4, 16, 4), + margin: EdgeInsets.fromLTRB( + 16 + wideInset, + 4, + 16 + wideInset, + 4, + ), children: [ SettingsItem( icon: Icons.download_outlined, @@ -145,7 +157,12 @@ class SettingsTab extends ConsumerWidget { builder: (context) { final l10n = context.l10n; return SettingsGroup( - margin: const EdgeInsets.fromLTRB(16, 4, 16, 4), + margin: EdgeInsets.fromLTRB( + 16 + wideInset, + 4, + 16 + wideInset, + 4, + ), children: [ SettingsItem( icon: Icons.storage_outlined, @@ -158,8 +175,7 @@ class SettingsTab extends ConsumerWidget { icon: Icons.tune_outlined, title: l10n.settingsApp, subtitle: l10n.settingsAppSubtitle, - onTap: () => - _navigateTo(context, const AppSettingsPage()), + onTap: () => _navigateTo(context, const AppSettingsPage()), ), SettingsItem( icon: Icons.settings_backup_restore, diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index d9ab6e9e..94e8c1d7 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -9,6 +9,7 @@ import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/l10n/supported_locales.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/logger.dart'; @@ -653,41 +654,46 @@ class _SetupScreenState extends ConsumerState { return SingleChildScrollView( padding: const EdgeInsets.all(24), - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: minContentHeight), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/logo-transparent.png', - width: logoSize, - height: logoSize, - color: colorScheme.primary, - fit: BoxFit.contain, - ), - SizedBox(height: titleGap), - Text( - context.l10n.appName, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.displaySmall?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - fontSize: - (Theme.of(context).textTheme.displaySmall?.fontSize ?? - 36) * - (1 + ((textScale - 1) * 0.18)), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: minContentHeight, + maxWidth: 560, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/logo-transparent.png', + width: logoSize, + height: logoSize, + color: colorScheme.primary, + fit: BoxFit.contain, ), - ), - SizedBox(height: subtitleGap), - Text( - context.l10n.setupDownloadInFlac, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - height: 1.5, + SizedBox(height: titleGap), + Text( + context.l10n.appName, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + fontSize: + (Theme.of(context).textTheme.displaySmall?.fontSize ?? + 36) * + (1 + ((textScale - 1) * 0.18)), + ), ), - ), - ], + SizedBox(height: subtitleGap), + Text( + context.l10n.setupDownloadInFlac, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurfaceVariant, + height: 1.5, + ), + ), + ], + ), ), ), ); @@ -743,7 +749,12 @@ class _SetupScreenState extends ConsumerState { return Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 0), + padding: EdgeInsets.fromLTRB( + 24 + wideListInset(context, contentMaxWidth: 560), + 24, + 24 + wideListInset(context, contentMaxWidth: 560), + 0, + ), child: Column( children: [ Container( @@ -782,7 +793,12 @@ class _SetupScreenState extends ConsumerState { ), Expanded( child: ListView.builder( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 80), + padding: EdgeInsets.fromLTRB( + 24 + wideListInset(context, contentMaxWidth: 560), + 0, + 24 + wideListInset(context, contentMaxWidth: 560), + 80, + ), itemCount: languages.length, itemBuilder: (context, index) { final lang = languages[index]; @@ -984,40 +1000,49 @@ class _StepLayout extends StatelessWidget { return SingleChildScrollView( padding: const EdgeInsets.all(24), - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: minContentHeight), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.all(iconPadding), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - shape: BoxShape.circle, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: minContentHeight, + maxWidth: 560, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(iconPadding), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + shape: BoxShape.circle, + ), + child: Icon( + icon, + size: iconSize, + color: colorScheme.primary, + ), ), - child: Icon(icon, size: iconSize, color: colorScheme.primary), - ), - SizedBox(height: titleGap), - Text( - title, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, + SizedBox(height: titleGap), + Text( + title, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - SizedBox(height: descriptionGap), - Text( - description, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - height: 1.5, + SizedBox(height: descriptionGap), + Text( + description, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurfaceVariant, + height: 1.5, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - SizedBox(height: actionGap), - child, - ], + SizedBox(height: actionGap), + child, + ], + ), ), ), ); diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 55e03abb..e0cdd414 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -1181,7 +1181,12 @@ class _TrackMetadataScreenState extends ConsumerState ), SliverToBoxAdapter( - child: _buildAnimatedTrackContent(context, ref, colorScheme), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 720), + child: _buildAnimatedTrackContent(context, ref, colorScheme), + ), + ), ), SliverToBoxAdapter(child: SizedBox(height: bottomInset)), ], diff --git a/lib/screens/tutorial_screen.dart b/lib/screens/tutorial_screen.dart index 265d5137..00e34f96 100644 --- a/lib/screens/tutorial_screen.dart +++ b/lib/screens/tutorial_screen.dart @@ -244,21 +244,26 @@ class _TutorialScreenState extends ConsumerState { }), ), SizedBox(height: bottomGap), - SizedBox( - width: double.infinity, - height: actionButtonHeight, - child: FilledButton( - onPressed: _nextPage, - style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), - ), - ), - child: Text( - isLastPage ? l10n.setupGetStarted : l10n.setupNext, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: SizedBox( + width: double.infinity, + height: actionButtonHeight, + child: FilledButton( + onPressed: _nextPage, + style: FilledButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + ), + child: Text( + isLastPage ? l10n.setupGetStarted : l10n.setupNext, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), ), ), ), @@ -712,58 +717,65 @@ class _TutorialPage extends StatelessWidget { return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24), physics: const BouncingScrollPhysics(), - child: Column( - children: [ - SizedBox(height: topGap), - AnimatedContainer( - duration: const Duration(milliseconds: 500), - curve: Curves.easeOutBack, - transform: Matrix4.translationValues(0, isActive ? 0 : -20, 0), - padding: EdgeInsets.all(iconPadding), - decoration: BoxDecoration( - color: (iconColor ?? colorScheme.primary).withValues(alpha: 0.15), - shape: BoxShape.circle, - ), - child: Icon( - icon, - size: iconSize, - color: iconColor ?? colorScheme.primary, - ), - ), - SizedBox(height: iconTextGap), - AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: isActive ? 1.0 : 0.0, - curve: Curves.easeOut, - child: Text( - title, - style: Theme.of(context).textTheme.headlineLarge?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - letterSpacing: -0.5, + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: Column( + children: [ + SizedBox(height: topGap), + AnimatedContainer( + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutBack, + transform: Matrix4.translationValues(0, isActive ? 0 : -20, 0), + padding: EdgeInsets.all(iconPadding), + decoration: BoxDecoration( + color: (iconColor ?? colorScheme.primary).withValues( + alpha: 0.15, + ), + shape: BoxShape.circle, + ), + child: Icon( + icon, + size: iconSize, + color: iconColor ?? colorScheme.primary, + ), ), - textAlign: TextAlign.center, - ), - ), - SizedBox(height: descriptionGap), - AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: isActive ? 1.0 : 0.0, - curve: Curves.easeOut, - child: Text( - description, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - height: 1.5, - fontSize: 16 * (1 + ((textScale - 1) * 0.1)), + SizedBox(height: iconTextGap), + AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: isActive ? 1.0 : 0.0, + curve: Curves.easeOut, + child: Text( + title, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + letterSpacing: -0.5, + ), + textAlign: TextAlign.center, + ), ), - textAlign: TextAlign.center, - ), + SizedBox(height: descriptionGap), + AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: isActive ? 1.0 : 0.0, + curve: Curves.easeOut, + child: Text( + description, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurfaceVariant, + height: 1.5, + fontSize: 16 * (1 + ((textScale - 1) * 0.1)), + ), + textAlign: TextAlign.center, + ), + ), + SizedBox(height: contentGap), + content, // The content itself now handles its own internal animations + SizedBox(height: bottomGap), + ], ), - SizedBox(height: contentGap), - content, // The content itself now handles its own internal animations - SizedBox(height: bottomGap), - ], + ), ), ); } diff --git a/lib/utils/adaptive_layout.dart b/lib/utils/adaptive_layout.dart index 2dc061ad..763060d1 100644 --- a/lib/utils/adaptive_layout.dart +++ b/lib/utils/adaptive_layout.dart @@ -1,9 +1,17 @@ import 'package:flutter/widgets.dart'; +/// Horizontal inset that centers content of [maxWidth] at [contentMaxWidth]; +/// zero once it fits, so rows stop stretching across a wide surface. Prefer +/// this constraint-based form when the widget may live inside an already +/// clamped box (e.g. a bottom sheet), where screen width would over-inset. +double wideInsetForWidth(double maxWidth, {double contentMaxWidth = 720}) => + maxWidth > contentMaxWidth ? (maxWidth - contentMaxWidth) / 2 : 0; + /// Horizontal inset that centers full-width list content at [contentMaxWidth] /// on tablets/landscape; zero on phones, so rows stop stretching across the /// whole screen. -double wideListInset(BuildContext context, {double contentMaxWidth = 720}) { - final width = MediaQuery.sizeOf(context).width; - return width > contentMaxWidth ? (width - contentMaxWidth) / 2 : 0; -} +double wideListInset(BuildContext context, {double contentMaxWidth = 720}) => + wideInsetForWidth( + MediaQuery.sizeOf(context).width, + contentMaxWidth: contentMaxWidth, + ); diff --git a/lib/widgets/app_announcement_dialog.dart b/lib/widgets/app_announcement_dialog.dart index 58f94e25..6fb9756c 100644 --- a/lib/widgets/app_announcement_dialog.dart +++ b/lib/widgets/app_announcement_dialog.dart @@ -101,7 +101,7 @@ class AppAnnouncementDialog extends StatelessWidget { ], ), content: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 260), + constraints: const BoxConstraints(maxHeight: 260, maxWidth: 480), child: SingleChildScrollView( child: Text( announcement.message, diff --git a/lib/widgets/priority_settings_scaffold.dart b/lib/widgets/priority_settings_scaffold.dart index 56ab99bf..cff18713 100644 --- a/lib/widgets/priority_settings_scaffold.dart +++ b/lib/widgets/priority_settings_scaffold.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart'; class PrioritySettingsScaffold extends StatelessWidget { @@ -40,6 +41,7 @@ class PrioritySettingsScaffold extends StatelessWidget { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; + final wideInset = wideListInset(context); return PopScope( canPop: !hasChanges, @@ -70,7 +72,9 @@ class PrioritySettingsScaffold extends StatelessWidget { ), SliverToBoxAdapter( child: Padding( - padding: descriptionPadding, + padding: descriptionPadding.add( + EdgeInsets.symmetric(horizontal: wideInset), + ), child: Text( description, style: Theme.of(context).textTheme.bodyMedium?.copyWith( @@ -82,7 +86,12 @@ class PrioritySettingsScaffold extends StatelessWidget { ...slivers, SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.fromLTRB( + 16 + wideInset, + 16, + 16 + wideInset, + 16, + ), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( diff --git a/lib/widgets/settings_group.dart b/lib/widgets/settings_group.dart index 212020ab..cc8b7409 100644 --- a/lib/widgets/settings_group.dart +++ b/lib/widgets/settings_group.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; /// Background fill for grouped cards, matching the Settings group look. Blends a /// translucent overlay over the surface so it stays visible on AMOLED (pure @@ -28,19 +29,42 @@ class SettingsGroup extends StatelessWidget { final cardColor = settingsGroupColor(context); final colorScheme = Theme.of(context).colorScheme; - return Container( - margin: margin ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - decoration: BoxDecoration( - color: cardColor, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: colorScheme.outlineVariant.withValues(alpha: 0.5), - ), + final decoration = BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: colorScheme.outlineVariant.withValues(alpha: 0.5), ), - clipBehavior: Clip.antiAlias, - child: Material( - color: Colors.transparent, - child: Column(mainAxisSize: MainAxisSize.min, children: children), + ); + final child = Material( + color: Colors.transparent, + child: Column(mainAxisSize: MainAxisSize.min, children: children), + ); + + // Explicit caller margin wins as-is. Otherwise center on wide surfaces + // using the incoming constraint (not screen width) so groups nested in an + // already clamped box, e.g. a bottom sheet, are not over-inset. + if (margin != null) { + return Container( + margin: margin, + decoration: decoration, + clipBehavior: Clip.antiAlias, + child: child, + ); + } + return LayoutBuilder( + builder: (context, constraints) => Container( + margin: EdgeInsets.symmetric( + horizontal: + 16 + + (constraints.hasBoundedWidth + ? wideInsetForWidth(constraints.maxWidth) + : 0), + vertical: 4, + ), + decoration: decoration, + clipBehavior: Clip.antiAlias, + child: child, ), ); }