diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index d1f8c00b..64fbac66 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -14,6 +14,7 @@ import 'package:spotiflac_android/utils/cover_art_utils.dart'; import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart'; import 'package:spotiflac_android/widgets/error_card.dart'; import 'package:spotiflac_android/widgets/album_detail_header.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/utils/provider_resource_ids.dart'; import 'package:spotiflac_android/utils/ttl_cache.dart'; @@ -536,37 +537,40 @@ class _AlbumScreenState extends ConsumerState ), ) .maybeWhen(data: (keys) => keys, orElse: () => const {}); - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - final track = tracks[index]; - final isInHistory = existingHistoryKeys.contains( - historyLookups[index].lookupKey, - ); - return KeyedSubtree( - key: ValueKey(track.id), - child: StaggeredListItem( - index: index, - child: TrackListTile( - track: track, - isInHistory: isInHistory, - onDownload: () => _downloadTrack(context, track), - clickableArtist: true, - leading: SizedBox( - width: 32, - child: Center( - child: Text( - '${track.trackNumber ?? 0}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, + return SliverPadding( + padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final track = tracks[index]; + final isInHistory = existingHistoryKeys.contains( + historyLookups[index].lookupKey, + ); + return KeyedSubtree( + key: ValueKey(track.id), + child: StaggeredListItem( + index: index, + child: TrackListTile( + track: track, + isInHistory: isInHistory, + onDownload: () => _downloadTrack(context, track), + clickableArtist: true, + leading: SizedBox( + width: 32, + child: Center( + child: Text( + '${track.trackNumber ?? 0}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), ), ), ), ), ), - ), - ); - }, childCount: tracks.length), + ); + }, childCount: tracks.length), + ), ); } diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index acf97e04..362a715f 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -24,6 +24,7 @@ import 'package:spotiflac_android/services/csv_import_service.dart'; import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/app_bar_layout.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; @@ -3168,7 +3169,10 @@ class _HomeTabState extends ConsumerState return [ SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), + // Aligned with the clamped result list below on wide screens. + padding: + EdgeInsets.fromLTRB(16, 8, 8, 8) + + EdgeInsets.symmetric(horizontal: wideListInset(context)), child: Row( children: [ Expanded( @@ -3212,29 +3216,32 @@ class _HomeTabState extends ConsumerState ), ), ), - SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - final isFirst = index == 0; - final isLast = index == itemCount - 1; - return StaggeredListItem( - index: index, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: sectionColor, - borderRadius: BorderRadius.vertical( - top: isFirst ? const Radius.circular(20) : Radius.zero, - bottom: isLast ? const Radius.circular(20) : Radius.zero, + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final isFirst = index == 0; + final isLast = index == itemCount - 1; + return StaggeredListItem( + index: index, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: sectionColor, + borderRadius: BorderRadius.vertical( + top: isFirst ? const Radius.circular(20) : Radius.zero, + bottom: isLast ? const Radius.circular(20) : Radius.zero, + ), + ), + clipBehavior: Clip.antiAlias, + child: Material( + color: Colors.transparent, + child: itemBuilder(index, !isLast), ), ), - clipBehavior: Clip.antiAlias, - child: Material( - color: Colors.transparent, - child: itemBuilder(index, !isLast), - ), - ), - ); - }, childCount: itemCount), + ); + }, childCount: itemCount), + ), ), ]; } diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index e7c70184..7145479b 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -13,6 +13,7 @@ import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/providers/playback_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/utils/cover_art_utils.dart'; import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart'; @@ -260,35 +261,40 @@ class _LibraryTracksFolderScreenState ), ) else - SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - final entry = entries[index]; - final isSelected = selectedIds.contains(entry.key); - final isInHistory = existingHistoryKeys.contains( - historyLookups[index].lookupKey, - ); - return KeyedSubtree( - key: ValueKey(entry.key), - child: StaggeredListItem( - index: index, - child: _CollectionTrackTile( - entry: entry, - mode: widget.mode, - playlistId: widget.playlistId, - folderTracks: folderTracks, - isInHistory: isInHistory, - isSelectionMode: isSelectionMode, - isSelected: isSelected, - onTap: isSelectionMode - ? () => toggleSelection(entry.key) - : null, - onLongPress: isSelectionMode - ? null - : () => enterSelectionMode(entry.key), + SliverPadding( + padding: EdgeInsets.symmetric( + horizontal: wideListInset(context), + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final entry = entries[index]; + final isSelected = selectedIds.contains(entry.key); + final isInHistory = existingHistoryKeys.contains( + historyLookups[index].lookupKey, + ); + return KeyedSubtree( + key: ValueKey(entry.key), + child: StaggeredListItem( + index: index, + child: _CollectionTrackTile( + entry: entry, + mode: widget.mode, + playlistId: widget.playlistId, + folderTracks: folderTracks, + isInHistory: isInHistory, + isSelectionMode: isSelectionMode, + isSelected: isSelected, + onTap: isSelectionMode + ? () => toggleSelection(entry.key) + : null, + onLongPress: isSelectionMode + ? null + : () => enterSelectionMode(entry.key), + ), ), - ), - ); - }, childCount: entries.length), + ); + }, childCount: entries.length), + ), ), SliverToBoxAdapter( child: SizedBox(height: isSelectionMode ? 200 : 32), diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 4b62b2cf..1eb53a00 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -584,6 +584,39 @@ class _MainShellState extends ConsumerState }); } + // Material breakpoint: rail navigation on tablet/landscape widths, the + // bottom NavigationBar on phones. + final useNavigationRail = MediaQuery.sizeOf(context).width >= 600; + + final pageView = AnimatedBuilder( + animation: _tabJumpTransitionController, + child: PageView.builder( + controller: _pageController, + itemCount: tabs.length, + onPageChanged: _onPageChanged, + physics: const NeverScrollableScrollPhysics(), + // TickerMode mutes animations and lets visibility-aware widgets + // (e.g. MotionHeaderBanner) pause when their tab is hidden — + // kept-alive pages otherwise keep running offscreen. + itemBuilder: (context, index) => _KeepAliveTabPage( + key: ValueKey('page-$index'), + child: TickerMode( + enabled: index == _currentIndex, + child: tabs[index], + ), + ), + ), + builder: (context, child) { + final t = Curves.easeOutCubic.transform( + _tabJumpTransitionController.value, + ); + return Opacity( + opacity: t, + child: Transform.scale(scale: 0.985 + (0.015 * t), child: child), + ); + }, + ); + return BackButtonListener( onBackButtonPressed: () async { await _handleBackPress(); @@ -591,34 +624,34 @@ class _MainShellState extends ConsumerState }, child: Scaffold( extendBody: true, - body: AnimatedBuilder( - animation: _tabJumpTransitionController, - child: PageView.builder( - controller: _pageController, - itemCount: tabs.length, - onPageChanged: _onPageChanged, - physics: const NeverScrollableScrollPhysics(), - // TickerMode mutes animations and lets visibility-aware widgets - // (e.g. MotionHeaderBanner) pause when their tab is hidden — - // kept-alive pages otherwise keep running offscreen. - itemBuilder: (context, index) => _KeepAliveTabPage( - key: ValueKey('page-$index'), - child: TickerMode( - enabled: index == _currentIndex, - child: tabs[index], - ), - ), - ), - builder: (context, child) { - final t = Curves.easeOutCubic.transform( - _tabJumpTransitionController.value, - ); - return Opacity( - opacity: t, - child: Transform.scale(scale: 0.985 + (0.015 * t), child: child), - ); - }, - ), + body: useNavigationRail + ? Row( + children: [ + SafeArea( + right: false, + bottom: false, + child: NavigationRail( + selectedIndex: _currentIndex.clamp(0, maxIndex), + onDestinationSelected: _onNavTap, + labelType: NavigationRailLabelType.all, + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer, + destinations: [ + for (final destination in destinations) + NavigationRailDestination( + icon: destination.icon, + selectedIcon: destination.selectedIcon, + label: Text(destination.label), + ), + ], + ), + ), + const VerticalDivider(width: 1), + Expanded(child: pageView), + ], + ) + : pageView, bottomNavigationBar: ClipRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18), @@ -626,29 +659,30 @@ class _MainShellState extends ConsumerState mainAxisSize: MainAxisSize.min, children: [ const MiniPlayer(), - DecoratedBox( - position: DecorationPosition.foreground, - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: Theme.of( - context, - ).colorScheme.outlineVariant.withValues(alpha: 0.5), + if (!useNavigationRail) + DecoratedBox( + position: DecorationPosition.foreground, + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of( + context, + ).colorScheme.outlineVariant.withValues(alpha: 0.5), + ), ), ), + child: NavigationBar( + selectedIndex: _currentIndex.clamp(0, maxIndex), + onDestinationSelected: _onNavTap, + animationDuration: const Duration(milliseconds: 500), + elevation: 0, + height: 64, + backgroundColor: settingsGroupColor( + context, + ).withValues(alpha: 0.72), + destinations: destinations, + ), ), - child: NavigationBar( - selectedIndex: _currentIndex.clamp(0, maxIndex), - onDestinationSelected: _onNavTap, - animationDuration: const Duration(milliseconds: 500), - elevation: 0, - height: 64, - backgroundColor: settingsGroupColor( - context, - ).withValues(alpha: 0.72), - destinations: destinations, - ), - ), ], ), ), diff --git a/lib/screens/now_playing_screen.dart b/lib/screens/now_playing_screen.dart index 5478021c..8126def5 100644 --- a/lib/screens/now_playing_screen.dart +++ b/lib/screens/now_playing_screen.dart @@ -417,6 +417,66 @@ class _NowPlayingScreenState extends ConsumerState { return LayoutBuilder( builder: (context, constraints) { + Widget artworkAt(double artSize) => Center( + child: _artworkDragRegion( + context, + Hero( + tag: kNowPlayingArtworkHeroTag, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SizedBox( + width: artSize, + height: artSize, + child: PlayerArtwork( + artUri: mediaItem.artUri?.toString(), + colorScheme: colorScheme, + cacheWidth: + (artSize * MediaQuery.devicePixelRatioOf(context)) + .round(), + ), + ), + ), + ), + ), + ); + + // Tablet/landscape: artwork pane left, metadata and controls right, + // instead of one narrow column in a sea of empty space. + final twoPane = + constraints.maxWidth >= 720 && + constraints.maxWidth > constraints.maxHeight; + if (twoPane) { + final artSize = (constraints.maxHeight - 96).clamp(0.0, 420.0); + return Row( + children: [ + Expanded(child: artworkAt(artSize)), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 16), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 32, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: _metadataAndControls( + mediaItem, + controller, + colorScheme, + isPlaying: isPlaying, + position: position, + duration: duration, + posMs: posMs, + maxMs: maxMs, + ), + ), + ), + ), + ), + ], + ); + } + final artSize = (constraints.maxWidth - 64).clamp(0.0, 360.0); return SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 16), @@ -425,149 +485,17 @@ class _NowPlayingScreenState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Center( - child: _artworkDragRegion( - context, - Hero( - tag: kNowPlayingArtworkHeroTag, - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: SizedBox( - width: artSize, - height: artSize, - child: PlayerArtwork( - artUri: mediaItem.artUri?.toString(), - colorScheme: colorScheme, - cacheWidth: - (artSize * - MediaQuery.devicePixelRatioOf(context)) - .round(), - ), - ), - ), - ), - ), - ), + artworkAt(artSize), const SizedBox(height: 32), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: Column( - children: [ - Text( - mediaItem.title, - style: Theme.of(context).textTheme.headlineSmall - ?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 6), - Text( - mediaItem.artist ?? '', - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(color: colorScheme.onSurfaceVariant), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - SliderTheme( - data: SliderThemeData( - trackHeight: 4, - activeTrackColor: colorScheme.primary, - inactiveTrackColor: colorScheme.onSurface.withValues( - alpha: 0.18, - ), - thumbColor: colorScheme.primary, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 7, - ), - overlayShape: const RoundSliderOverlayShape( - overlayRadius: 16, - ), - ), - child: Slider( - value: posMs.clamp(0, maxMs), - max: maxMs, - onChanged: duration.inMilliseconds > 0 - ? (value) => controller.seek( - Duration(milliseconds: value.round()), - ) - : null, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - Text( - formatClock(position.inSeconds), - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - Expanded( - child: Center( - child: _QualityBadge( - label: _qualityLabel(), - colorScheme: colorScheme, - ), - ), - ), - Text( - formatClock(duration.inSeconds), - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - iconSize: 44, - icon: const Icon(Icons.skip_previous), - onPressed: controller.previous, - ), - const SizedBox(width: 20), - Container( - decoration: BoxDecoration( - color: colorScheme.primary, - shape: BoxShape.circle, - ), - child: IconButton( - iconSize: 44, - padding: const EdgeInsets.all(12), - color: colorScheme.onPrimary, - icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow), - onPressed: () => controller.togglePlayPause(isPlaying), - ), - ), - const SizedBox(width: 20), - IconButton( - iconSize: 44, - icon: const Icon(Icons.skip_next), - onPressed: controller.next, - ), - ], + ..._metadataAndControls( + mediaItem, + controller, + colorScheme, + isPlaying: isPlaying, + position: position, + duration: duration, + posMs: posMs, + maxMs: maxMs, ), ], ), @@ -577,6 +505,135 @@ class _NowPlayingScreenState extends ConsumerState { ); } + /// Title/artist, seek slider, and transport buttons — shared by the + /// portrait column and the landscape right pane. + List _metadataAndControls( + MediaItem mediaItem, + MusicPlayerController controller, + ColorScheme colorScheme, { + required bool isPlaying, + required Duration position, + required Duration duration, + required double posMs, + required double maxMs, + }) { + return [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Column( + children: [ + Text( + mediaItem.title, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + mediaItem.artist ?? '', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + SliderTheme( + data: SliderThemeData( + trackHeight: 4, + activeTrackColor: colorScheme.primary, + inactiveTrackColor: colorScheme.onSurface.withValues( + alpha: 0.18, + ), + thumbColor: colorScheme.primary, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 16), + ), + child: Slider( + value: posMs.clamp(0, maxMs), + max: maxMs, + onChanged: duration.inMilliseconds > 0 + ? (value) => + controller.seek(Duration(milliseconds: value.round())) + : null, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Text( + formatClock(position.inSeconds), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + Expanded( + child: Center( + child: _QualityBadge( + label: _qualityLabel(), + colorScheme: colorScheme, + ), + ), + ), + Text( + formatClock(duration.inSeconds), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + iconSize: 44, + icon: const Icon(Icons.skip_previous), + onPressed: controller.previous, + ), + const SizedBox(width: 20), + Container( + decoration: BoxDecoration( + color: colorScheme.primary, + shape: BoxShape.circle, + ), + child: IconButton( + iconSize: 44, + padding: const EdgeInsets.all(12), + color: colorScheme.onPrimary, + icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow), + onPressed: () => controller.togglePlayPause(isPlaying), + ), + ), + const SizedBox(width: 20), + IconButton( + iconSize: 44, + icon: const Icon(Icons.skip_next), + onPressed: controller.next, + ), + ], + ), + ]; + } + Widget _lyricsSection(ColorScheme colorScheme) { if (_loadingMeta) { return const Center(child: CircularProgressIndicator()); diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 7bc247cd..9438fd38 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -9,6 +9,7 @@ import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/utils/image_cache_utils.dart'; import 'package:spotiflac_android/utils/cover_art_utils.dart'; +import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; import 'package:spotiflac_android/utils/provider_resource_ids.dart'; import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart'; @@ -307,46 +308,49 @@ class _PlaylistScreenState extends ConsumerState ), ) .maybeWhen(data: (keys) => keys, orElse: () => const {}); - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - final track = _tracks[index]; - final isInHistory = existingHistoryKeys.contains( - historyLookups[index].lookupKey, - ); - return KeyedSubtree( - key: ValueKey(track.id), - child: StaggeredListItem( - index: index, - child: TrackListTile( - track: track, - isInHistory: isInHistory, - onDownload: () => - _downloadTrack(context, track, playlistPosition: index + 1), - leading: track.coverUrl != null - ? CachedCoverImage( - imageUrl: track.coverUrl!, - width: 48, - height: 48, - borderRadius: BorderRadius.circular(8), - ) - : Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, + return SliverPadding( + padding: EdgeInsets.symmetric(horizontal: wideListInset(context)), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final track = _tracks[index]; + final isInHistory = existingHistoryKeys.contains( + historyLookups[index].lookupKey, + ); + return KeyedSubtree( + key: ValueKey(track.id), + child: StaggeredListItem( + index: index, + child: TrackListTile( + track: track, + isInHistory: isInHistory, + onDownload: () => + _downloadTrack(context, track, playlistPosition: index + 1), + leading: track.coverUrl != null + ? CachedCoverImage( + imageUrl: track.coverUrl!, + width: 48, + height: 48, borderRadius: BorderRadius.circular(8), + ) + : Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.music_note, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), - child: Icon( - Icons.music_note, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), + ), ), - ), - ); - }, childCount: _tracks.length), + ); + }, childCount: _tracks.length), + ), ); } diff --git a/lib/utils/adaptive_layout.dart b/lib/utils/adaptive_layout.dart new file mode 100644 index 00000000..2dc061ad --- /dev/null +++ b/lib/utils/adaptive_layout.dart @@ -0,0 +1,9 @@ +import 'package:flutter/widgets.dart'; + +/// 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; +}