feat(ui): add bottom inset so scrollable content clears the transparent navbar

This commit is contained in:
zarzet
2026-06-13 20:31:39 +07:00
parent fb5204b0a6
commit c10c2a290c
13 changed files with 50 additions and 3 deletions
+3 -1
View File
@@ -14,6 +14,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/image_cache_utils.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
import 'package:spotiflac_android/widgets/download_service_picker.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
@@ -336,6 +337,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
final colorScheme = Theme.of(context).colorScheme;
final tracks = _tracks ?? [];
final pageBackgroundColor = colorScheme.surface;
final bottomInset = context.navBarBottomInset;
return Scaffold(
backgroundColor: pageBackgroundColor,
@@ -361,7 +363,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
if (!_isLoading && _error == null && tracks.isNotEmpty) ...[
_buildTrackList(context, colorScheme, tracks),
],
const SliverToBoxAdapter(child: SizedBox(height: 32)),
SliverToBoxAdapter(child: SizedBox(height: 32 + bottomInset)),
],
),
);
+3
View File
@@ -15,6 +15,7 @@ import 'package:spotiflac_android/providers/playback_provider.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/screens/album_screen.dart';
import 'package:spotiflac_android/screens/home_tab.dart'
show ExtensionAlbumScreen;
@@ -456,6 +457,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
final albumsOnly = _albumsOnlyBucket;
final singles = _singlesBucket;
final compilations = _compilationsBucket;
final bottomInset = context.navBarBottomInset;
final hasDiscography =
!_isLoadingDiscography && _error == null && albums.isNotEmpty;
@@ -542,6 +544,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
SliverToBoxAdapter(
child: SizedBox(height: _isSelectionMode ? 120 : 32),
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
),
if (_isSelectionMode)
+3
View File
@@ -15,6 +15,7 @@ import 'package:spotiflac_android/utils/audio_conversion_utils.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/image_cache_utils.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/widgets/batch_progress_dialog.dart';
import 'package:spotiflac_android/widgets/batch_convert_sheet.dart';
@@ -358,6 +359,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final bottomPadding = MediaQuery.of(context).padding.bottom;
final bottomInset = context.navBarBottomInset;
final tracksValue = ref.watch(
downloadedAlbumTracksProvider(
@@ -413,6 +415,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
SliverToBoxAdapter(
child: SizedBox(height: _isSelectionMode ? 120 : 32),
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
),
+3
View File
@@ -6,6 +6,7 @@ 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/app_bar_layout.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
class FavoriteArtistsScreen extends ConsumerWidget {
@@ -18,6 +19,7 @@ class FavoriteArtistsScreen extends ConsumerWidget {
);
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
final bottomInset = context.navBarBottomInset;
return Scaffold(
body: CustomScrollView(
@@ -155,6 +157,7 @@ class FavoriteArtistsScreen extends ConsumerWidget {
);
},
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
),
);
+3
View File
@@ -21,6 +21,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/nav_bar_inset.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/screens/playlist_screen.dart';
@@ -1205,6 +1206,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
final mediaQuery = MediaQuery.of(context);
final screenHeight = mediaQuery.size.height;
final topPadding = normalizedHeaderTopPadding(context);
final bottomInset = context.navBarBottomInset;
final hasHistoryItems = ref.watch(
_homeHistoryPreviewProvider.select((items) => items.isNotEmpty),
);
@@ -1540,6 +1542,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
);
},
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
),
),
@@ -10,6 +10,7 @@ import 'package:spotiflac_android/screens/library_tracks_folder_screen.dart';
import 'package:spotiflac_android/services/cover_cache_manager.dart';
import 'package:spotiflac_android/widgets/bottom_sheet_option_tile.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
class LibraryPlaylistsScreen extends ConsumerWidget {
const LibraryPlaylistsScreen({super.key});
@@ -21,6 +22,7 @@ class LibraryPlaylistsScreen extends ConsumerWidget {
);
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
final bottomInset = context.navBarBottomInset;
return Scaffold(
body: CustomScrollView(
@@ -132,6 +134,7 @@ class LibraryPlaylistsScreen extends ConsumerWidget {
);
}, childCount: playlists.length * 2 - 1),
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
),
floatingActionButton: FloatingActionButton.extended(
@@ -14,6 +14,7 @@ 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/services/cover_cache_manager.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
import 'package:spotiflac_android/widgets/download_service_picker.dart';
import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart';
@@ -322,6 +323,7 @@ class _LibraryTracksFolderScreenState
.maybeWhen(data: (keys) => keys, orElse: () => const <String>{});
final bottomPadding = MediaQuery.of(context).padding.bottom;
final bottomInset = context.navBarBottomInset;
return PopScope(
canPop: !_isSelectionMode,
@@ -379,6 +381,7 @@ class _LibraryTracksFolderScreenState
SliverToBoxAdapter(
child: SizedBox(height: _isSelectionMode ? 200 : 32),
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
),
+3
View File
@@ -12,6 +12,7 @@ import 'package:spotiflac_android/utils/audio_conversion_utils.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/image_cache_utils.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:spotiflac_android/services/ffmpeg_service.dart';
import 'package:spotiflac_android/services/replaygain_service.dart';
@@ -252,6 +253,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final bottomPadding = MediaQuery.of(context).padding.bottom;
final bottomInset = context.navBarBottomInset;
final tracks = _sortedTracksCache;
if (tracks.isEmpty) {
@@ -288,6 +290,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
SliverToBoxAdapter(
child: SizedBox(height: _isSelectionMode ? 120 : 32),
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
),
+4 -1
View File
@@ -9,6 +9,7 @@ import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/image_cache_utils.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
import 'package:spotiflac_android/providers/playback_provider.dart';
@@ -253,7 +254,9 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
_buildAppBar(context, colorScheme),
_buildInfoCard(context, colorScheme),
_buildTrackList(context, colorScheme),
const SliverToBoxAdapter(child: SizedBox(height: 32)),
SliverToBoxAdapter(
child: SizedBox(height: 32 + context.navBarBottomInset),
),
],
),
);
+3
View File
@@ -7,6 +7,7 @@ import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
import 'package:spotiflac_android/screens/store/extension_details_screen.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
class RepoTab extends ConsumerStatefulWidget {
const RepoTab({super.key});
@@ -76,6 +77,7 @@ class _RepoTabState extends ConsumerState<RepoTab> {
}
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
final bottomInset = context.navBarBottomInset;
return Scaffold(
body: RefreshIndicator(
@@ -311,6 +313,7 @@ class _RepoTabState extends ConsumerState<RepoTab> {
const SliverToBoxAdapter(child: SizedBox(height: 16)),
],
],
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
),
),
+3
View File
@@ -15,6 +15,7 @@ import 'package:spotiflac_android/screens/settings/cache_management_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/app_bar_layout.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
@@ -25,6 +26,7 @@ class SettingsTab extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
final bottomInset = context.navBarBottomInset;
return CustomScrollView(
slivers: [
@@ -183,6 +185,7 @@ class SettingsTab extends ConsumerWidget {
),
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
const SliverFillRemaining(hasScrollBody: false, child: SizedBox()),
],
);
@@ -4,6 +4,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/store_provider.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
class ExtensionDetailsScreen extends ConsumerStatefulWidget {
final StoreExtension extension;
@@ -69,7 +70,9 @@ class _ExtensionDetailsScreenState
),
_buildCapabilities(context, liveExtension, colorScheme),
const SliverToBoxAdapter(child: SizedBox(height: 32)),
SliverToBoxAdapter(
child: SizedBox(height: 32 + context.navBarBottomInset),
),
],
),
);
+12
View File
@@ -0,0 +1,12 @@
import 'package:flutter/widgets.dart';
/// Bottom inset needed to clear the transparent shell navigation bar.
///
/// The shell Scaffold uses `extendBody: true`, so its body (and any route
/// pushed inside the tab navigators) receives the navbar height plus the system
/// gesture inset as `MediaQuery.padding.bottom`. Scrollable screens add this as
/// trailing padding so their last item can scroll clear of the bar while the
/// content still shows faintly behind it.
extension NavBarInset on BuildContext {
double get navBarBottomInset => MediaQuery.paddingOf(this).bottom;
}