fix(android): back button and safe area issues

This commit is contained in:
Kingkor Roy Tirtho 2025-01-31 23:07:37 +06:00
parent 6ddf6b9cce
commit d4504722d8
21 changed files with 1559 additions and 1516 deletions

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
@ -73,6 +74,10 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
final hasFullscreen = final hasFullscreen =
MediaQuery.sizeOf(context).width == constraints.maxWidth; MediaQuery.sizeOf(context).width == constraints.maxWidth;
final canPop = leading.isEmpty &&
automaticallyImplyLeading &&
(Navigator.canPop(context) || context.watchRouter.canPop());
return GestureDetector( return GestureDetector(
onHorizontalDragStart: (_) => onDrag(ref), onHorizontalDragStart: (_) => onDrag(ref),
onVerticalDragStart: (_) => onDrag(ref), onVerticalDragStart: (_) => onDrag(ref),
@ -94,13 +99,7 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
} }
}, },
child: AppBar( child: AppBar(
leading: leading.isEmpty && leading: canPop ? [const BackButton()] : leading,
automaticallyImplyLeading &&
Navigator.canPop(context)
? [
const BackButton(),
]
: leading,
trailing: [ trailing: [
...trailing, ...trailing,
Align( Align(

View File

@ -23,12 +23,11 @@ class ConnectPage extends HookConsumerWidget {
final connectClientsNotifier = ref.read(connectClientsProvider.notifier); final connectClientsNotifier = ref.read(connectClientsProvider.notifier);
final discoveredDevices = connectClients.asData?.value.services; final discoveredDevices = connectClients.asData?.value.services;
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(title: Text(context.l10n.devices)),
automaticallyImplyLeading: true,
title: Text(context.l10n.devices),
)
], ],
child: Padding( child: Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
@ -84,6 +83,7 @@ class ConnectPage extends HookConsumerWidget {
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -75,7 +75,6 @@ class ConnectControlPage extends HookConsumerWidget {
headers: [ headers: [
TitleBar( TitleBar(
title: Text(resolvedService!.name), title: Text(resolvedService!.name),
automaticallyImplyLeading: true,
) )
], ],
child: LayoutBuilder(builder: (context, constrains) { child: LayoutBuilder(builder: (context, constrains) {

View File

@ -28,13 +28,14 @@ class HomeFeedSectionPage extends HookConsumerWidget {
final controller = useScrollController(); final controller = useScrollController();
final isArtist = section.items.every((item) => item.artist != null); final isArtist = section.items.every((item) => item.artist != null);
return Skeletonizer( return SafeArea(
bottom: false,
child: Skeletonizer(
enabled: homeFeedSection.isLoading, enabled: homeFeedSection.isLoading,
child: Scaffold( child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
title: Text(section.title ?? ""), title: Text(section.title ?? ""),
automaticallyImplyLeading: true,
) )
], ],
child: Padding( child: Padding(
@ -44,7 +45,8 @@ class HomeFeedSectionPage extends HookConsumerWidget {
slivers: [ slivers: [
if (isArtist) if (isArtist)
SliverGrid.builder( SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200, maxCrossAxisExtent: 200,
mainAxisExtent: 250, mainAxisExtent: 250,
crossAxisSpacing: 8, crossAxisSpacing: 8,
@ -93,6 +95,7 @@ class HomeFeedSectionPage extends HookConsumerWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -45,7 +45,8 @@ class GenrePlaylistsPage extends HookConsumerWidget {
automaticSystemUiAdjustment: false, automaticSystemUiAdjustment: false,
); );
return Scaffold( return SafeArea(
child: Scaffold(
headers: [ headers: [
if (kIsDesktop) if (kIsDesktop)
const TitleBar( const TitleBar(
@ -74,7 +75,9 @@ class GenrePlaylistsPage extends HookConsumerWidget {
child: CustomScrollView( child: CustomScrollView(
controller: scrollController, controller: scrollController,
slivers: [ slivers: [
SliverAppBar( SliverSafeArea(
bottom: false,
sliver: SliverAppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
leading: kIsMobile ? const BackButton() : null, leading: kIsMobile ? const BackButton() : null,
expandedHeight: mediaQuery.mdAndDown ? 200 : 150, expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
@ -110,6 +113,7 @@ class GenrePlaylistsPage extends HookConsumerWidget {
collapseMode: CollapseMode.parallax, collapseMode: CollapseMode.parallax,
), ),
), ),
),
const SliverGap(20), const SliverGap(20),
SliverSafeArea( SliverSafeArea(
top: false, top: false,
@ -123,8 +127,8 @@ class GenrePlaylistsPage extends HookConsumerWidget {
isLoading: playlists.isLoading, isLoading: playlists.isLoading,
hasMore: playlists.asData?.value.hasMore == true, hasMore: playlists.asData?.value.hasMore == true,
onRequestMore: playlistsNotifier.fetchMore, onRequestMore: playlistsNotifier.fetchMore,
listItemBuilder: (context, index) => listItemBuilder: (context, index) => PlaylistCard.tile(
PlaylistCard.tile(playlists.asData!.value.items[index]), playlists.asData!.value.items[index]),
gridItemBuilder: (context, index) => gridItemBuilder: (context, index) =>
PlaylistCard(playlists.asData!.value.items[index]), PlaylistCard(playlists.asData!.value.items[index]),
), ),
@ -135,6 +139,7 @@ class GenrePlaylistsPage extends HookConsumerWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -32,7 +32,6 @@ class GenrePage extends HookConsumerWidget {
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.explore_genres), title: Text(context.l10n.explore_genres),
automaticallyImplyLeading: true,
) )
], ],
child: GridView.builder( child: GridView.builder(

View File

@ -31,6 +31,7 @@ class LastFMLoginPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
headers: const [ headers: const [
SafeArea( SafeArea(
bottom: false,
child: TitleBar( child: TitleBar(
leading: [BackButton()], leading: [BackButton()],
), ),
@ -39,7 +40,8 @@ class LastFMLoginPage extends HookConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( Flexible(
child: Container(
constraints: const BoxConstraints(maxWidth: 400), constraints: const BoxConstraints(maxWidth: 400),
alignment: Alignment.center, alignment: Alignment.center,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -139,6 +141,7 @@ class LastFMLoginPage extends HookConsumerWidget {
), ),
), ),
), ),
),
], ],
), ),
); );

View File

@ -256,7 +256,9 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final controller = useScrollController(); final controller = useScrollController();
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
leading: const [BackButton()], leading: const [BackButton()],
@ -308,8 +310,8 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
), ),
Expanded( Expanded(
child: Slider( child: Slider(
value: value: SliderValue.single(
SliderValue.single(value.toDouble()), value.toDouble()),
min: 10, min: 10,
max: 100, max: 100,
divisions: 9, divisions: 9,
@ -655,8 +657,9 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
seedArtists: artists.value seedArtists: artists.value
.map((a) => a.id!) .map((a) => a.id!)
.toList(), .toList(),
seedTracks: seedTracks: tracks.value
tracks.value.map((t) => t.id!).toList(), .map((t) => t.id!)
.toList(),
seedGenres: genres.value, seedGenres: genres.value,
limit: limit.value, limit: limit.value,
max: max.value, max: max.value,
@ -680,6 +683,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -48,7 +48,9 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
final isAllTrackSelected = selectedTracks.value.length == final isAllTrackSelected = selectedTracks.value.length ==
(generatedPlaylist.asData?.value.length ?? 0); (generatedPlaylist.asData?.value.length ?? 0);
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: const [ headers: const [
TitleBar(leading: [BackButton()]) TitleBar(leading: [BackButton()])
], ],
@ -101,7 +103,8 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
: () async { : () async {
await playlistNotifier.addTracks( await playlistNotifier.addTracks(
generatedPlaylist.asData!.value.where( generatedPlaylist.asData!.value.where(
(e) => selectedTracks.value.contains(e.id!), (e) =>
selectedTracks.value.contains(e.id!),
), ),
); );
if (context.mounted) { if (context.mounted) {
@ -152,11 +155,13 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
: () async { : () async {
final hasAdded = await showDialog<bool>( final hasAdded = await showDialog<bool>(
context: context, context: context,
builder: (context) => PlaylistAddTrackDialog( builder: (context) =>
PlaylistAddTrackDialog(
openFromPlaylist: null, openFromPlaylist: null,
tracks: selectedTracks.value tracks: selectedTracks.value
.map( .map(
(e) => generatedPlaylist.asData!.value (e) => generatedPlaylist
.asData!.value
.firstWhere( .firstWhere(
(element) => element.id == e, (element) => element.id == e,
), ),
@ -244,7 +249,8 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
selectedTracks.value.contains(track.id) selectedTracks.value.contains(track.id)
? selectedTracks.value.remove(track.id) ? selectedTracks.value
.remove(track.id)
: selectedTracks.value.add(track.id!); : selectedTracks.value.add(track.id!);
selectedTracks.value = selectedTracks.value =
selectedTracks.value.toList(); selectedTracks.value.toList();
@ -260,6 +266,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -27,7 +27,9 @@ class WebViewLoginPage extends HookConsumerWidget {
); );
} }
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: const [ headers: const [
TitleBar( TitleBar(
leading: [BackButton(color: Colors.white)], leading: [BackButton(color: Colors.white)],
@ -72,6 +74,7 @@ class WebViewLoginPage extends HookConsumerWidget {
} }
}, },
), ),
),
); );
} }
} }

View File

@ -44,7 +44,6 @@ class ProfilePage extends HookConsumerWidget {
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.profile), title: Text(context.l10n.profile),
automaticallyImplyLeading: true,
) )
], ],
child: Skeletonizer( child: Skeletonizer(

View File

@ -31,7 +31,9 @@ class AboutSpotubePage extends HookConsumerWidget {
const colon = TableCell(child: Text(":")); const colon = TableCell(child: Text(":"));
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
leading: const [BackButton()], leading: const [BackButton()],
@ -91,8 +93,8 @@ class AboutSpotubePage extends HookConsumerWidget {
TableCell(child: Text(context.l10n.build_number)), TableCell(child: Text(context.l10n.build_number)),
colon, colon,
TableCell( TableCell(
child: Text( child: Text(packageInfo.buildNumber
packageInfo.buildNumber.replaceAll(".", " ")), .replaceAll(".", " ")),
) )
], ],
), ),
@ -191,6 +193,7 @@ class AboutSpotubePage extends HookConsumerWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -47,7 +47,9 @@ class BlackListPage extends HookConsumerWidget {
[blacklist, searchText.value], [blacklist, searchText.value],
); );
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.blacklist), title: Text(context.l10n.blacklist),
@ -81,9 +83,8 @@ class BlackListPage extends HookConsumerWidget {
trailing: IconButton.ghost( trailing: IconButton.ghost(
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]), icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
onPressed: () { onPressed: () {
ref ref.read(blacklistProvider.notifier).remove(
.read(blacklistProvider.notifier) filteredBlacklist.elementAt(index).elementId);
.remove(filteredBlacklist.elementAt(index).elementId);
}, },
), ),
); );
@ -92,6 +93,7 @@ class BlackListPage extends HookConsumerWidget {
), ),
], ],
), ),
),
); );
} }
} }

View File

@ -34,7 +34,6 @@ class SettingsPage extends HookConsumerWidget {
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.settings), title: Text(context.l10n.settings),
automaticallyImplyLeading: true,
) )
], ],
child: Scrollbar( child: Scrollbar(

View File

@ -26,10 +26,11 @@ class StatsAlbumsPage extends HookConsumerWidget {
final albumsData = topAlbums.asData?.value.items ?? []; final albumsData = topAlbums.asData?.value.items ?? [];
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
automaticallyImplyLeading: true,
title: Text(context.l10n.albums), title: Text(context.l10n.albums),
) )
], ],
@ -53,6 +54,7 @@ class StatsAlbumsPage extends HookConsumerWidget {
}, },
), ),
), ),
),
); );
} }
} }

View File

@ -29,10 +29,11 @@ class StatsArtistsPage extends HookConsumerWidget {
final artistsData = useMemoized( final artistsData = useMemoized(
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
automaticallyImplyLeading: true,
title: Text(context.l10n.artists), title: Text(context.l10n.artists),
) )
], ],
@ -56,6 +57,7 @@ class StatsArtistsPage extends HookConsumerWidget {
}, },
), ),
), ),
),
); );
} }
} }

View File

@ -50,10 +50,11 @@ class StatsStreamFeesPage extends HookConsumerWidget {
HistoryDuration.allTime: context.l10n.all_time, HistoryDuration.allTime: context.l10n.all_time,
}; };
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
automaticallyImplyLeading: true,
title: Text(context.l10n.streaming_fees_hypothetical), title: Text(context.l10n.streaming_fees_hypothetical),
) )
], ],
@ -86,7 +87,8 @@ class StatsStreamFeesPage extends HookConsumerWidget {
if (value == null) return; if (value == null) return;
duration.value = value; duration.value = value;
}, },
itemBuilder: (context, value) => Text(translations[value]!), itemBuilder: (context, value) =>
Text(translations[value]!),
constraints: const BoxConstraints(maxWidth: 150), constraints: const BoxConstraints(maxWidth: 150),
popupWidthConstraint: PopoverConstraint.anchorMaxSize, popupWidthConstraint: PopoverConstraint.anchorMaxSize,
children: [ children: [
@ -109,7 +111,8 @@ class StatsStreamFeesPage extends HookConsumerWidget {
await topTracksNotifier.fetchMore(); await topTracksNotifier.fetchMore();
}, },
hasError: topTracks.hasError, hasError: topTracks.hasError,
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, isLoading:
topTracks.isLoading && !topTracks.isLoadingNextPage,
hasReachedMax: topTracks.asData?.value.hasMore ?? true, hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemCount: artistsData.length, itemCount: artistsData.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -124,6 +127,7 @@ class StatsStreamFeesPage extends HookConsumerWidget {
), ),
], ],
), ),
),
); );
} }
} }

View File

@ -28,11 +28,12 @@ class StatsMinutesPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? []; final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.minutes_listened), title: Text(context.l10n.minutes_listened),
automaticallyImplyLeading: true,
) )
], ],
child: Skeletonizer( child: Skeletonizer(
@ -58,6 +59,7 @@ class StatsMinutesPage extends HookConsumerWidget {
}, },
), ),
), ),
),
); );
} }
} }

View File

@ -27,10 +27,11 @@ class StatsPlaylistsPage extends HookConsumerWidget {
final playlistsData = topPlaylists.asData?.value.items ?? []; final playlistsData = topPlaylists.asData?.value.items ?? [];
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
automaticallyImplyLeading: true,
title: Text(context.l10n.playlists), title: Text(context.l10n.playlists),
) )
], ],
@ -41,7 +42,8 @@ class StatsPlaylistsPage extends HookConsumerWidget {
await topPlaylistsNotifier.fetchMore(); await topPlaylistsNotifier.fetchMore();
}, },
hasError: topPlaylists.hasError, hasError: topPlaylists.hasError,
isLoading: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, isLoading:
topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
hasReachedMax: topPlaylists.asData?.value.hasMore ?? true, hasReachedMax: topPlaylists.asData?.value.hasMore ?? true,
itemCount: playlistsData.length, itemCount: playlistsData.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -49,13 +51,14 @@ class StatsPlaylistsPage extends HookConsumerWidget {
return StatsPlaylistItem( return StatsPlaylistItem(
playlist: playlist.playlist, playlist: playlist.playlist,
info: Text( info: Text(
context.l10n context.l10n.count_plays(
.count_plays(compactNumberFormatter.format(playlist.count)), compactNumberFormatter.format(playlist.count)),
), ),
); );
}, },
), ),
), ),
),
); );
} }
} }

View File

@ -28,11 +28,12 @@ class StatsStreamsPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? []; final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.streamed_songs), title: Text(context.l10n.streamed_songs),
automaticallyImplyLeading: true,
) )
], ],
child: Skeletonizer( child: Skeletonizer(
@ -58,6 +59,7 @@ class StatsStreamsPage extends HookConsumerWidget {
}, },
), ),
), ),
),
); );
} }
} }

View File

@ -54,10 +54,11 @@ class TrackPage extends HookConsumerWidget {
} }
} }
return Scaffold( return SafeArea(
bottom: false,
child: Scaffold(
headers: const [ headers: const [
TitleBar( TitleBar(
automaticallyImplyLeading: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
surfaceBlur: 0, surfaceBlur: 0,
) )
@ -121,7 +122,8 @@ class TrackPage extends HookConsumerWidget {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding:
const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
crossAxisAlignment: mediaQuery.smAndDown crossAxisAlignment: mediaQuery.smAndDown
? CrossAxisAlignment.center ? CrossAxisAlignment.center
@ -246,6 +248,7 @@ class TrackPage extends HookConsumerWidget {
), ),
], ],
), ),
),
); );
} }
} }