From d845180e6039a60bc5ef174fad8285328171cc20 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 28 Dec 2024 21:18:35 +0600 Subject: [PATCH] fix: mobile keyboard overflow --- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 1 + .../horizontal_playbutton_card_view.dart | 3 +- .../playbutton_view/playbutton_tile.dart | 8 +- .../presentation_modifiers.dart | 15 +- .../track_presentation.dart | 30 +++- lib/main.dart | 6 + lib/modules/library/user_albums.dart | 6 +- lib/modules/library/user_artists.dart | 6 +- lib/modules/library/user_playlists.dart | 6 +- lib/pages/lastfm_login/lastfm_login.dart | 140 +++++++++--------- lib/pages/root/root_app.dart | 18 ++- lib/pages/search/search.dart | 136 ++++++++--------- 13 files changed, 213 insertions(+), 164 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 63871a3d..bbfc1404 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 70; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d3..c53e2b31 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart index d37bc8a0..47fb0f33 100644 --- a/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart +++ b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart @@ -38,6 +38,7 @@ class HorizontalPlaybuttonCardView extends HookWidget { Widget build(BuildContext context) { final scrollController = useScrollController(); final isArtist = items.every((s) => s is Artist); + final scale = context.theme.scaling; return Padding( padding: const EdgeInsets.all(8.0), @@ -92,7 +93,7 @@ class HorizontalPlaybuttonCardView extends HookWidget { ), isLoading: isLoadingNextPage, hasReachedMax: !hasNextPage, - separatorBuilder: (context, index) => const Gap(8.0), + separatorBuilder: (context, index) => Gap(12 * scale), itemBuilder: (context, index) { final item = items[index]; diff --git a/lib/components/playbutton_view/playbutton_tile.dart b/lib/components/playbutton_view/playbutton_tile.dart index e06f3689..3daaf75c 100644 --- a/lib/components/playbutton_view/playbutton_tile.dart +++ b/lib/components/playbutton_view/playbutton_tile.dart @@ -35,7 +35,7 @@ class PlaybuttonTile extends StatelessWidget { final cleanDescription = description?.unescapeHtml().cleanHtml() ?? ""; final scale = context.theme.scaling; - return Button.ghost( + return Button( leading: Container( width: 50 * scale, height: 50 * scale, @@ -47,6 +47,12 @@ class PlaybuttonTile extends StatelessWidget { ), ), ), + style: ButtonVariance.ghost.copyWith( + padding: (context, states, value) { + return (ButtonVariance.ghost.padding(context, states) as EdgeInsets) + .copyWith(right: 0, left: 0); + }, + ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/components/track_presentation/presentation_modifiers.dart b/lib/components/track_presentation/presentation_modifiers.dart index d1678e17..4d781d24 100644 --- a/lib/components/track_presentation/presentation_modifiers.dart +++ b/lib/components/track_presentation/presentation_modifiers.dart @@ -11,7 +11,11 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; class TrackPresentationModifiersSection extends HookConsumerWidget { - const TrackPresentationModifiersSection({super.key}); + final FocusNode? focusNode; + const TrackPresentationModifiersSection({ + super.key, + this.focusNode, + }); @override Widget build(BuildContext context, ref) { @@ -22,11 +26,12 @@ class TrackPresentationModifiersSection extends HookConsumerWidget { ); final controller = useTextEditingController(); + final scale = context.theme.scaling; return LayoutBuilder(builder: (context, constrains) { return Padding( padding: EdgeInsets.symmetric( - horizontal: constrains.mdAndUp ? 16 : 8, + horizontal: (constrains.mdAndUp ? 16 : 8) * scale, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -55,11 +60,13 @@ class TrackPresentationModifiersSection extends HookConsumerWidget { children: [ Flexible( child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 320, + constraints: BoxConstraints( + maxWidth: 320 * scale, + maxHeight: 38 * scale, ), child: TextField( controller: controller, + focusNode: focusNode, leading: Icon( SpotubeIcons.search, color: context.theme.colorScheme.mutedForeground, diff --git a/lib/components/track_presentation/track_presentation.dart b/lib/components/track_presentation/track_presentation.dart index 8bc1c6df..e81a2e1e 100644 --- a/lib/components/track_presentation/track_presentation.dart +++ b/lib/components/track_presentation/track_presentation.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart' show ListTile; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; @@ -9,6 +10,7 @@ import 'package:spotube/components/track_presentation/presentation_top.dart'; import 'package:spotube/components/track_presentation/presentation_modifiers.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/utils/platform.dart'; class TrackPresentation extends HookConsumerWidget { final TrackPresentationOptions options; @@ -22,6 +24,29 @@ class TrackPresentation extends HookConsumerWidget { final headerTextStyle = context.theme.typography.small.copyWith( color: context.theme.colorScheme.mutedForeground, ); + final scrollController = useScrollController(); + final focusNode = useFocusNode(); + final scale = context.theme.scaling; + + useEffect(() { + if (!kIsMobile) return null; + void listener() { + if (!scrollController.hasClients) return; + + if (focusNode.hasFocus) { + scrollController.animateTo( + 300 * scale, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + focusNode.addListener(listener); + return () { + focusNode.removeListener(listener); + }; + }, [focusNode, scrollController, scale]); return Data.inherit( data: options, @@ -29,6 +54,7 @@ class TrackPresentation extends HookConsumerWidget { child: Scaffold( headers: const [TitleBar()], child: CustomScrollView( + controller: scrollController, slivers: [ const TrackPresentationTopSection(), const SliverGap(16), @@ -36,7 +62,9 @@ class TrackPresentation extends HookConsumerWidget { builder: (context, constrains) { return SliverList.list( children: [ - const TrackPresentationModifiersSection(), + TrackPresentationModifiersSection( + focusNode: focusNode, + ), ListTile( titleTextStyle: headerTextStyle, subtitleTextStyle: headerTextStyle, diff --git a/lib/main.dart b/lib/main.dart index ecf2cc37..2f93ea87 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -228,6 +228,12 @@ class Spotube extends HookConsumerWidget { ), materialTheme: material.ThemeData( splashFactory: material.NoSplash.splashFactory, + appBarTheme: const material.AppBarTheme( + surfaceTintColor: Colors.transparent, + scrolledUnderElevation: 0, + shadowColor: Colors.transparent, + elevation: 0, + ), ), themeMode: themeMode, shortcuts: { diff --git a/lib/modules/library/user_albums.dart b/lib/modules/library/user_albums.dart index a388c0ad..b460f22e 100644 --- a/lib/modules/library/user_albums.dart +++ b/lib/modules/library/user_albums.dart @@ -50,9 +50,9 @@ class UserAlbums extends HookConsumerWidget { return SafeArea( child: Scaffold( child: RefreshTrigger( - onRefresh: () async { - ref.invalidate(favoriteAlbumsProvider); - }, + // onRefresh: () async { + // ref.invalidate(favoriteAlbumsProvider); + // }, child: InterScrollbar( controller: controller, child: CustomScrollView( diff --git a/lib/modules/library/user_artists.dart b/lib/modules/library/user_artists.dart index 83a321fc..eaf5afb3 100644 --- a/lib/modules/library/user_artists.dart +++ b/lib/modules/library/user_artists.dart @@ -55,9 +55,9 @@ class UserArtists extends HookConsumerWidget { return SafeArea( child: Scaffold( child: RefreshTrigger( - onRefresh: () async { - ref.invalidate(followedArtistsProvider); - }, + // onRefresh: () async { + // ref.invalidate(followedArtistsProvider); + // }, child: InterScrollbar( controller: controller, child: Padding( diff --git a/lib/modules/library/user_playlists.dart b/lib/modules/library/user_playlists.dart index 2a2d65e0..1b1bf110 100644 --- a/lib/modules/library/user_playlists.dart +++ b/lib/modules/library/user_playlists.dart @@ -78,9 +78,9 @@ class UserPlaylists extends HookConsumerWidget { } return RefreshTrigger( - onRefresh: () async { - ref.invalidate(favoritePlaylistsProvider); - }, + // onRefresh: () async { + // ref.invalidate(favoritePlaylistsProvider); + // }, child: SafeArea( child: InterScrollbar( controller: controller, diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index 2611d771..d5466544 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:form_validator/form_validator.dart'; + import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; @@ -15,31 +16,59 @@ class LastFMLoginPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final theme = Theme.of(context); final router = GoRouter.of(context); final scrobblerNotifier = ref.read(scrobblerProvider.notifier); - final formKey = useMemoized(() => GlobalKey(), []); - final username = useTextEditingController(); - final password = useTextEditingController(); + final usernameKey = + useMemoized(() => const FormKey("username"), []); + final passwordKey = + useMemoized(() => const FormKey("password"), []); + final passwordVisible = useState(false); final isLoading = useState(false); return Scaffold( - appBar: const TitleBar(leading: [BackButton()]), - body: Center( - child: ConstrainedBox( + headers: const [ + SafeArea( + child: TitleBar( + leading: [BackButton()], + ), + ), + ], + child: SingleChildScrollView( + child: Container( constraints: const BoxConstraints(maxWidth: 400), + alignment: Alignment.center, + padding: const EdgeInsets.all(16), child: Card( - margin: const EdgeInsets.all(8.0), child: Padding( padding: const EdgeInsets.all(16.0).copyWith(top: 8), child: Form( - key: formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, + onSubmit: (context, values) async { + try { + isLoading.value = true; + await scrobblerNotifier.login( + values[usernameKey].trim(), + values[passwordKey], + ); + router.pop(); + } catch (e) { + if (context.mounted) { + showPromptDialog( + context: context, + title: context.l10n.error("Authentication failed"), + message: e.toString(), + cancelText: null, + ); + } + } finally { + isLoading.value = false; + } + }, child: Column( mainAxisSize: MainAxisSize.min, + spacing: 10, children: [ Container( decoration: BoxDecoration( @@ -53,38 +82,35 @@ class LastFMLoginPage extends HookConsumerWidget { size: 60, ), ), - Text( - "last.fm", - style: theme.textTheme.titleLarge, - ), - const SizedBox(height: 10), + const Text("last.fm").h3(), Text(context.l10n.login_with_your_lastfm), - const SizedBox(height: 10), AutofillGroup( child: Column( + spacing: 10, children: [ - TextFormField( - autofillHints: const [ - AutofillHints.username, - AutofillHints.email, - ], - controller: username, - validator: ValidationBuilder().required().build(), - decoration: InputDecoration( - labelText: context.l10n.username, + FormField( + label: Text(context.l10n.username), + key: usernameKey, + validator: const NotEmptyValidator(), + child: TextField( + autofillHints: const [ + AutofillHints.username, + AutofillHints.email, + ], + placeholder: Text(context.l10n.username), ), ), - const SizedBox(height: 10), - TextFormField( - autofillHints: const [ - AutofillHints.password, - ], - controller: password, - validator: ValidationBuilder().required().build(), - obscureText: !passwordVisible.value, - decoration: InputDecoration( - labelText: context.l10n.password, - suffixIcon: IconButton( + FormField( + key: passwordKey, + validator: const NotEmptyValidator(), + label: Text(context.l10n.password), + child: TextField( + autofillHints: const [ + AutofillHints.password, + ], + obscureText: !passwordVisible.value, + placeholder: Text(context.l10n.password), + trailing: IconButton.ghost( icon: Icon( passwordVisible.value ? SpotubeIcons.eye @@ -98,37 +124,13 @@ class LastFMLoginPage extends HookConsumerWidget { ], ), ), - const SizedBox(height: 10), - FilledButton( - onPressed: isLoading.value - ? null - : () async { - try { - isLoading.value = true; - if (formKey.currentState?.validate() != true) { - return; - } - await scrobblerNotifier.login( - username.text.trim(), - password.text, - ); - router.pop(); - } catch (e) { - if (context.mounted) { - showPromptDialog( - context: context, - title: context.l10n - .error("Authentication failed"), - message: e.toString(), - cancelText: null, - ); - } - } finally { - isLoading.value = false; - } - }, - child: Text(context.l10n.login), - ), + FormErrorBuilder(builder: (context, errors, child) { + return Button.primary( + onPressed: () => context.submitForm(), + enabled: errors.isEmpty && !isLoading.value, + child: Text(context.l10n.login), + ); + }), ], ), ), diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 606bba34..cdb56910 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -179,13 +179,17 @@ class RootApp extends HookConsumerWidget { return getSidebarTileList(context.l10n).map((s) => s.name).toList(); }, []); - final scaffold = Scaffold( - footers: const [ - BottomPlayer(), - SpotubeNavigationBar(), - ], - floatingFooter: true, - child: Sidebar(child: child), + final scaffold = MediaQuery.removeViewInsets( + context: context, + removeBottom: true, + child: Scaffold( + footers: const [ + BottomPlayer(), + SpotubeNavigationBar(), + ], + floatingFooter: true, + child: Sidebar(child: child), + ), ); if (!kIsAndroid) { diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 5c096a32..9e2bf20c 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -29,13 +29,15 @@ class SearchPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final theme = Theme.of(context); - final searchTerm = ref.watch(searchTermStateProvider); + final mediaQuery = MediaQuery.sizeOf(context); + + final scrollController = useScrollController(); final controller = useSearchController(); final focusNode = useFocusNode(); final auth = ref.watch(authenticationProvider); - final mediaQuery = MediaQuery.of(context); + final searchTerm = ref.watch(searchTermStateProvider); final searchTrack = ref.watch(searchProvider(SearchType.track)); final searchAlbum = ref.watch(searchProvider(SearchType.album)); final searchPlaylist = ref.watch(searchProvider(SearchType.playlist)); @@ -51,35 +53,6 @@ class SearchPage extends HookConsumerWidget { return null; }, []); - final resultWidget = HookBuilder( - builder: (context) { - final controller = useScrollController(); - - return InterScrollbar( - controller: controller, - child: SingleChildScrollView( - controller: controller, - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SearchTracksSection(), - SearchPlaylistsSection(), - Gap(20), - SearchArtistsSection(), - Gap(20), - SearchAlbumsSection(), - ], - ), - ), - ), - ), - ); - }, - ); - void onSubmitted(String value) { ref.read(searchTermStateProvider.notifier).state = value; if (value.trim().isEmpty) { @@ -182,59 +155,80 @@ class SearchPage extends HookConsumerWidget { Expanded( child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: searchTerm.isEmpty - ? Column( - children: [ - SizedBox( - height: mediaQuery.size.height * 0.2, - ), - Icon( - SpotubeIcons.web, - size: 120, + child: switch ((searchTerm.isEmpty, isFetching)) { + (true, false) => Column( + children: [ + SizedBox( + height: mediaQuery.height * 0.2, + ), + Icon( + SpotubeIcons.web, + size: 120, + color: theme.colorScheme.foreground + .withOpacity(0.7), + ), + const SizedBox(height: 20), + Text( + context.l10n.search_to_get_results, + style: theme.typography.h3.copyWith( + fontWeight: FontWeight.w900, color: theme.colorScheme.foreground - .withOpacity(0.7), + .withOpacity(0.5), ), - const SizedBox(height: 20), + ), + ], + ), + (false, true) => Container( + constraints: BoxConstraints( + maxWidth: mediaQuery.lgAndUp + ? mediaQuery.width * 0.5 + : mediaQuery.width, + ), + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ Text( - context.l10n.search_to_get_results, - style: theme.typography.h3.copyWith( + context.l10n.crunching_results, + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.w900, color: theme.colorScheme.foreground - .withOpacity(0.5), + .withOpacity(0.7), ), ), + const SizedBox(height: 20), + const LinearProgressIndicator(), ], - ) - : isFetching - ? Container( - constraints: BoxConstraints( - maxWidth: mediaQuery.lgAndUp - ? mediaQuery.size.width * 0.5 - : mediaQuery.size.width, - ), - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), + ), + ), + _ => InterScrollbar( + controller: scrollController, + child: SingleChildScrollView( + controller: scrollController, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: SafeArea( child: Column( - mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: - CrossAxisAlignment.center, + CrossAxisAlignment.start, children: [ - Text( - context.l10n.crunching_results, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w900, - color: theme.colorScheme.foreground - .withOpacity(0.7), - ), - ), - const SizedBox(height: 20), - const LinearProgressIndicator(), + SearchTracksSection(), + SearchPlaylistsSection(), + Gap(20), + SearchArtistsSection(), + Gap(20), + SearchAlbumsSection(), ], ), - ) - : resultWidget, + ), + ), + ), + ), + }, ), ), ],