From d4504722d82374f4c21f59372af122d703b106fd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 31 Jan 2025 23:07:37 +0600 Subject: [PATCH] fix(android): back button and safe area issues --- lib/components/titlebar/titlebar.dart | 13 +- lib/pages/connect/connect.dart | 114 +-- lib/pages/connect/control/control.dart | 1 - lib/pages/home/feed/feed_section.dart | 123 +-- lib/pages/home/genres/genre_playlists.dart | 165 ++-- lib/pages/home/genres/genres.dart | 1 - lib/pages/lastfm_login/lastfm_login.dart | 183 ++-- .../playlist_generate/playlist_generate.dart | 824 +++++++++--------- .../playlist_generate_result.dart | 403 ++++----- lib/pages/mobile_login/mobile_login.dart | 89 +- lib/pages/profile/profile.dart | 1 - lib/pages/settings/about.dart | 301 +++---- lib/pages/settings/blacklist.dart | 88 +- lib/pages/settings/settings.dart | 1 - lib/pages/stats/albums/albums.dart | 52 +- lib/pages/stats/artists/artists.dart | 52 +- lib/pages/stats/fees/fees.dart | 148 ++-- lib/pages/stats/minutes/minutes.dart | 58 +- lib/pages/stats/playlists/playlists.dart | 57 +- lib/pages/stats/streams/streams.dart | 58 +- lib/pages/track/track.dart | 343 ++++---- 21 files changed, 1559 insertions(+), 1516 deletions(-) diff --git a/lib/components/titlebar/titlebar.dart b/lib/components/titlebar/titlebar.dart index 5b86f6ad..778f0b09 100644 --- a/lib/components/titlebar/titlebar.dart +++ b/lib/components/titlebar/titlebar.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -73,6 +74,10 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget { final hasFullscreen = MediaQuery.sizeOf(context).width == constraints.maxWidth; + final canPop = leading.isEmpty && + automaticallyImplyLeading && + (Navigator.canPop(context) || context.watchRouter.canPop()); + return GestureDetector( onHorizontalDragStart: (_) => onDrag(ref), onVerticalDragStart: (_) => onDrag(ref), @@ -94,13 +99,7 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget { } }, child: AppBar( - leading: leading.isEmpty && - automaticallyImplyLeading && - Navigator.canPop(context) - ? [ - const BackButton(), - ] - : leading, + leading: canPop ? [const BackButton()] : leading, trailing: [ ...trailing, Align( diff --git a/lib/pages/connect/connect.dart b/lib/pages/connect/connect.dart index d394ba16..bb8bbfae 100644 --- a/lib/pages/connect/connect.dart +++ b/lib/pages/connect/connect.dart @@ -23,65 +23,65 @@ class ConnectPage extends HookConsumerWidget { final connectClientsNotifier = ref.read(connectClientsProvider.notifier); final discoveredDevices = connectClients.asData?.value.services; - return Scaffold( - headers: [ - TitleBar( - automaticallyImplyLeading: true, - title: Text(context.l10n.devices), - ) - ], - child: Padding( - padding: const EdgeInsets.all(10.0), - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - sliver: SliverToBoxAdapter( - child: Text( - context.l10n.remote, - style: typography.bold, + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar(title: Text(context.l10n.devices)), + ], + child: Padding( + padding: const EdgeInsets.all(10.0), + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + sliver: SliverToBoxAdapter( + child: Text( + context.l10n.remote, + style: typography.bold, + ), ), ), - ), - const SliverGap(10), - SliverList.separated( - itemCount: discoveredDevices?.length ?? 0, - separatorBuilder: (context, index) => const Gap(10), - itemBuilder: (context, index) { - final device = discoveredDevices![index]; - final selected = - connectClients.asData?.value.resolvedService?.name == - device.name; - return ButtonTile( - selected: selected, - leading: const Icon(SpotubeIcons.monitor), - title: Text(device.name), - subtitle: selected - ? Text( - "${connectClients.asData?.value.resolvedService?.host}" - ":${connectClients.asData?.value.resolvedService?.port}", - ) - : null, - trailing: selected - ? IconButton.outline( - icon: const Icon(SpotubeIcons.power), - size: ButtonSize.small, - onPressed: () => - connectClientsNotifier.clearResolvedService(), - ) - : null, - onPressed: () { - if (selected) { - context.navigateTo(const ConnectControlRoute()); - } else { - connectClientsNotifier.resolveService(device); - } - }, - ); - }, - ), - const ConnectPageLocalDevices(), - ], + const SliverGap(10), + SliverList.separated( + itemCount: discoveredDevices?.length ?? 0, + separatorBuilder: (context, index) => const Gap(10), + itemBuilder: (context, index) { + final device = discoveredDevices![index]; + final selected = + connectClients.asData?.value.resolvedService?.name == + device.name; + return ButtonTile( + selected: selected, + leading: const Icon(SpotubeIcons.monitor), + title: Text(device.name), + subtitle: selected + ? Text( + "${connectClients.asData?.value.resolvedService?.host}" + ":${connectClients.asData?.value.resolvedService?.port}", + ) + : null, + trailing: selected + ? IconButton.outline( + icon: const Icon(SpotubeIcons.power), + size: ButtonSize.small, + onPressed: () => + connectClientsNotifier.clearResolvedService(), + ) + : null, + onPressed: () { + if (selected) { + context.navigateTo(const ConnectControlRoute()); + } else { + connectClientsNotifier.resolveService(device); + } + }, + ); + }, + ), + const ConnectPageLocalDevices(), + ], + ), ), ), ); diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index d0b423a6..2511809c 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -75,7 +75,6 @@ class ConnectControlPage extends HookConsumerWidget { headers: [ TitleBar( title: Text(resolvedService!.name), - automaticallyImplyLeading: true, ) ], child: LayoutBuilder(builder: (context, constrains) { diff --git a/lib/pages/home/feed/feed_section.dart b/lib/pages/home/feed/feed_section.dart index eff70808..2b38d0ed 100644 --- a/lib/pages/home/feed/feed_section.dart +++ b/lib/pages/home/feed/feed_section.dart @@ -28,68 +28,71 @@ class HomeFeedSectionPage extends HookConsumerWidget { final controller = useScrollController(); final isArtist = section.items.every((item) => item.artist != null); - return Skeletonizer( - enabled: homeFeedSection.isLoading, - child: Scaffold( - headers: [ - TitleBar( - title: Text(section.title ?? ""), - automaticallyImplyLeading: true, - ) - ], - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: CustomScrollView( - controller: controller, - slivers: [ - if (isArtist) - SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisExtent: 250, - crossAxisSpacing: 8, - mainAxisSpacing: 8, + return SafeArea( + bottom: false, + child: Skeletonizer( + enabled: homeFeedSection.isLoading, + child: Scaffold( + headers: [ + TitleBar( + title: Text(section.title ?? ""), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: CustomScrollView( + controller: controller, + slivers: [ + if (isArtist) + SliverGrid.builder( + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: 250, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: section.items.length, + itemBuilder: (context, index) { + final item = section.items[index]; + return ArtistCard(item.artist!.asArtist); + }, + ) + else + PlaybuttonView( + controller: controller, + itemCount: section.items.length, + hasMore: false, + isLoading: false, + onRequestMore: () => {}, + listItemBuilder: (context, index) { + final item = section.items[index]; + if (item.album != null) { + return AlbumCard.tile(item.album!.asAlbum); + } + if (item.playlist != null) { + return PlaylistCard.tile(item.playlist!.asPlaylist); + } + return const SizedBox.shrink(); + }, + gridItemBuilder: (context, index) { + final item = section.items[index]; + if (item.album != null) { + return AlbumCard(item.album!.asAlbum); + } + if (item.playlist != null) { + return PlaylistCard(item.playlist!.asPlaylist); + } + return const SizedBox.shrink(); + }, + ), + const SliverToBoxAdapter( + child: SafeArea( + child: SizedBox(), ), - itemCount: section.items.length, - itemBuilder: (context, index) { - final item = section.items[index]; - return ArtistCard(item.artist!.asArtist); - }, - ) - else - PlaybuttonView( - controller: controller, - itemCount: section.items.length, - hasMore: false, - isLoading: false, - onRequestMore: () => {}, - listItemBuilder: (context, index) { - final item = section.items[index]; - if (item.album != null) { - return AlbumCard.tile(item.album!.asAlbum); - } - if (item.playlist != null) { - return PlaylistCard.tile(item.playlist!.asPlaylist); - } - return const SizedBox.shrink(); - }, - gridItemBuilder: (context, index) { - final item = section.items[index]; - if (item.album != null) { - return AlbumCard(item.album!.asAlbum); - } - if (item.playlist != null) { - return PlaylistCard(item.playlist!.asPlaylist); - } - return const SizedBox.shrink(); - }, ), - const SliverToBoxAdapter( - child: SafeArea( - child: SizedBox(), - ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart index a3e38309..ea421cb4 100644 --- a/lib/pages/home/genres/genre_playlists.dart +++ b/lib/pages/home/genres/genre_playlists.dart @@ -45,93 +45,98 @@ class GenrePlaylistsPage extends HookConsumerWidget { automaticSystemUiAdjustment: false, ); - return Scaffold( - headers: [ - if (kIsDesktop) - const TitleBar( - leading: [ - BackButton(), - ], - backgroundColor: Colors.transparent, - surfaceOpacity: 0, - surfaceBlur: 0, - ) - ], - floatingHeader: true, - child: DecoratedBox( - decoration: BoxDecoration( - image: DecorationImage( - image: UniversalImage.imageProvider(category.icons!.first.url!), - alignment: Alignment.topCenter, - fit: BoxFit.cover, - repeat: ImageRepeat.noRepeat, - matchTextDirection: true, + return SafeArea( + child: Scaffold( + headers: [ + if (kIsDesktop) + const TitleBar( + leading: [ + BackButton(), + ], + backgroundColor: Colors.transparent, + surfaceOpacity: 0, + surfaceBlur: 0, + ) + ], + floatingHeader: true, + child: DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: UniversalImage.imageProvider(category.icons!.first.url!), + alignment: Alignment.topCenter, + fit: BoxFit.cover, + repeat: ImageRepeat.noRepeat, + matchTextDirection: true, + ), ), - ), - child: SurfaceCard( - borderRadius: BorderRadius.zero, - padding: EdgeInsets.zero, - child: CustomScrollView( - controller: scrollController, - slivers: [ - SliverAppBar( - automaticallyImplyLeading: false, - leading: kIsMobile ? const BackButton() : null, - expandedHeight: mediaQuery.mdAndDown ? 200 : 150, - title: const Text(""), - backgroundColor: Colors.transparent, - flexibleSpace: FlexibleSpaceBar( - centerTitle: kIsDesktop, - title: Text( - category.name!, - style: context.theme.typography.h3.copyWith( - color: Colors.white, - letterSpacing: 3, - shadows: [ - Shadow( - offset: const Offset(-1.5, -1.5), - color: Colors.black.withAlpha(138), + child: SurfaceCard( + borderRadius: BorderRadius.zero, + padding: EdgeInsets.zero, + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverSafeArea( + bottom: false, + sliver: SliverAppBar( + automaticallyImplyLeading: false, + leading: kIsMobile ? const BackButton() : null, + expandedHeight: mediaQuery.mdAndDown ? 200 : 150, + title: const Text(""), + backgroundColor: Colors.transparent, + flexibleSpace: FlexibleSpaceBar( + centerTitle: kIsDesktop, + title: Text( + category.name!, + style: context.theme.typography.h3.copyWith( + color: Colors.white, + letterSpacing: 3, + shadows: [ + Shadow( + offset: const Offset(-1.5, -1.5), + color: Colors.black.withAlpha(138), + ), + Shadow( + offset: const Offset(1.5, -1.5), + color: Colors.black.withAlpha(138), + ), + Shadow( + offset: const Offset(1.5, 1.5), + color: Colors.black.withAlpha(138), + ), + Shadow( + offset: const Offset(-1.5, 1.5), + color: Colors.black.withAlpha(138), + ), + ], ), - Shadow( - offset: const Offset(1.5, -1.5), - color: Colors.black.withAlpha(138), - ), - Shadow( - offset: const Offset(1.5, 1.5), - color: Colors.black.withAlpha(138), - ), - Shadow( - offset: const Offset(-1.5, 1.5), - color: Colors.black.withAlpha(138), - ), - ], + ), + collapseMode: CollapseMode.parallax, ), ), - collapseMode: CollapseMode.parallax, ), - ), - const SliverGap(20), - SliverSafeArea( - top: false, - sliver: SliverPadding( - padding: EdgeInsets.symmetric( - horizontal: mediaQuery.mdAndDown ? 12 : 24, - ), - sliver: PlaybuttonView( - controller: scrollController, - itemCount: playlists.asData?.value.items.length ?? 0, - isLoading: playlists.isLoading, - hasMore: playlists.asData?.value.hasMore == true, - onRequestMore: playlistsNotifier.fetchMore, - listItemBuilder: (context, index) => - PlaylistCard.tile(playlists.asData!.value.items[index]), - gridItemBuilder: (context, index) => - PlaylistCard(playlists.asData!.value.items[index]), + const SliverGap(20), + SliverSafeArea( + top: false, + sliver: SliverPadding( + padding: EdgeInsets.symmetric( + horizontal: mediaQuery.mdAndDown ? 12 : 24, + ), + sliver: PlaybuttonView( + controller: scrollController, + itemCount: playlists.asData?.value.items.length ?? 0, + isLoading: playlists.isLoading, + hasMore: playlists.asData?.value.hasMore == true, + onRequestMore: playlistsNotifier.fetchMore, + listItemBuilder: (context, index) => PlaylistCard.tile( + playlists.asData!.value.items[index]), + gridItemBuilder: (context, index) => + PlaylistCard(playlists.asData!.value.items[index]), + ), ), ), - ), - const SliverGap(20), - ], + const SliverGap(20), + ], + ), ), ), ), diff --git a/lib/pages/home/genres/genres.dart b/lib/pages/home/genres/genres.dart index eaddeae6..38d110db 100644 --- a/lib/pages/home/genres/genres.dart +++ b/lib/pages/home/genres/genres.dart @@ -32,7 +32,6 @@ class GenrePage extends HookConsumerWidget { headers: [ TitleBar( title: Text(context.l10n.explore_genres), - automaticallyImplyLeading: true, ) ], child: GridView.builder( diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index 6b741f4d..41042a1b 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -31,6 +31,7 @@ class LastFMLoginPage extends HookConsumerWidget { return Scaffold( headers: const [ SafeArea( + bottom: false, child: TitleBar( leading: [BackButton()], ), @@ -39,102 +40,104 @@ class LastFMLoginPage extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - constraints: const BoxConstraints(maxWidth: 400), - alignment: Alignment.center, - padding: const EdgeInsets.all(16), - child: Card( - padding: const EdgeInsets.all(16.0), - child: Form( - onSubmit: (context, values) async { - try { - isLoading.value = true; - await scrobblerNotifier.login( - values[usernameKey].trim(), - values[passwordKey], - ); - if (context.mounted) { - context.back(); - } - } catch (e) { - if (context.mounted) { - showPromptDialog( - context: context, - title: context.l10n.error("Authentication failed"), - message: e.toString(), - cancelText: null, + Flexible( + child: Container( + constraints: const BoxConstraints(maxWidth: 400), + alignment: Alignment.center, + padding: const EdgeInsets.all(16), + child: Card( + padding: const EdgeInsets.all(16.0), + child: Form( + onSubmit: (context, values) async { + try { + isLoading.value = true; + await scrobblerNotifier.login( + values[usernameKey].trim(), + values[passwordKey], ); + if (context.mounted) { + context.back(); + } + } catch (e) { + if (context.mounted) { + showPromptDialog( + context: context, + title: context.l10n.error("Authentication failed"), + message: e.toString(), + cancelText: null, + ); + } + } finally { + isLoading.value = false; } - } finally { - isLoading.value = false; - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: 10, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: const Color.fromARGB(255, 186, 0, 0), + }, + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 10, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: const Color.fromARGB(255, 186, 0, 0), + ), + padding: const EdgeInsets.all(12), + child: const Icon( + SpotubeIcons.lastFm, + color: Colors.white, + size: 60, + ), ), - padding: const EdgeInsets.all(12), - child: const Icon( - SpotubeIcons.lastFm, - color: Colors.white, - size: 60, - ), - ), - const Text("last.fm").h3(), - Text(context.l10n.login_with_your_lastfm), - AutofillGroup( - child: Column( - spacing: 10, - children: [ - FormField( - label: Text(context.l10n.username), - key: usernameKey, - validator: const NotEmptyValidator(), - child: TextField( - autofillHints: const [ - AutofillHints.username, - AutofillHints.email, - ], - placeholder: Text(context.l10n.username), - ), - ), - 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 - : SpotubeIcons.noEye, - ), - onPressed: () => passwordVisible.value = - !passwordVisible.value, + const Text("last.fm").h3(), + Text(context.l10n.login_with_your_lastfm), + AutofillGroup( + child: Column( + spacing: 10, + children: [ + FormField( + label: Text(context.l10n.username), + key: usernameKey, + validator: const NotEmptyValidator(), + child: TextField( + autofillHints: const [ + AutofillHints.username, + AutofillHints.email, + ], + placeholder: Text(context.l10n.username), ), ), - ), - ], + 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 + : SpotubeIcons.noEye, + ), + onPressed: () => passwordVisible.value = + !passwordVisible.value, + ), + ), + ), + ], + ), ), - ), - FormErrorBuilder(builder: (context, errors, child) { - return Button.primary( - onPressed: () => context.submitForm(), - enabled: errors.isEmpty && !isLoading.value, - 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/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index 1bc96a29..573d502c 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -256,426 +256,430 @@ class PlaylistGeneratorPage extends HookConsumerWidget { final controller = useScrollController(); - return Scaffold( - headers: [ - TitleBar( - leading: const [BackButton()], - title: Text(context.l10n.generate), - ) - ], - child: Scrollbar( - controller: controller, - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: Breakpoints.lg), - child: SafeArea( - child: LayoutBuilder(builder: (context, constrains) { - return ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith(scrollbars: false), - child: ListView( - controller: controller, - padding: const EdgeInsets.all(16), - children: [ - ValueListenableBuilder( - valueListenable: limit, - builder: (context, value, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.number_of_tracks_generate, - style: typography.semiBold, - ), - Row( - spacing: 5, - children: [ - Container( - width: 40, - height: 40, - alignment: Alignment.center, - decoration: BoxDecoration( - color: theme.colorScheme.primary - .withAlpha(25), - shape: BoxShape.circle, - ), - child: Text( - value.round().toString(), - style: typography.large.copyWith( - color: theme.colorScheme.primary, + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar( + leading: const [BackButton()], + title: Text(context.l10n.generate), + ) + ], + child: Scrollbar( + controller: controller, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: Breakpoints.lg), + child: SafeArea( + child: LayoutBuilder(builder: (context, constrains) { + return ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(scrollbars: false), + child: ListView( + controller: controller, + padding: const EdgeInsets.all(16), + children: [ + ValueListenableBuilder( + valueListenable: limit, + builder: (context, value, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.number_of_tracks_generate, + style: typography.semiBold, + ), + Row( + spacing: 5, + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: theme.colorScheme.primary + .withAlpha(25), + shape: BoxShape.circle, + ), + child: Text( + value.round().toString(), + style: typography.large.copyWith( + color: theme.colorScheme.primary, + ), ), ), - ), - Expanded( - child: Slider( - value: - SliderValue.single(value.toDouble()), - min: 10, - max: 100, - divisions: 9, - onChanged: (value) { - limit.value = value.value.round(); - }, - ), - ) - ], - ) + Expanded( + child: Slider( + value: SliderValue.single( + value.toDouble()), + min: 10, + max: 100, + divisions: 9, + onChanged: (value) { + limit.value = value.value.round(); + }, + ), + ) + ], + ) + ], + ); + }, + ), + const SizedBox(height: 16), + if (constrains.mdAndUp) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: countrySelector, + ), + const SizedBox(width: 16), + Expanded( + child: genreSelector, + ), ], - ); - }, - ), - const SizedBox(height: 16), - if (constrains.mdAndUp) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: countrySelector, - ), - const SizedBox(width: 16), - Expanded( - child: genreSelector, - ), - ], - ) - else ...[ - countrySelector, + ) + else ...[ + countrySelector, + const SizedBox(height: 16), + genreSelector, + ], const SizedBox(height: 16), - genreSelector, - ], - const SizedBox(height: 16), - if (constrains.mdAndUp) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: artistAutoComplete, - ), - const SizedBox(width: 16), - Expanded( - child: tracksAutocomplete, - ), - ], - ) - else ...[ - artistAutoComplete, + if (constrains.mdAndUp) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: artistAutoComplete, + ), + const SizedBox(width: 16), + Expanded( + child: tracksAutocomplete, + ), + ], + ) + else ...[ + artistAutoComplete, + const SizedBox(height: 16), + tracksAutocomplete, + ], const SizedBox(height: 16), - tracksAutocomplete, - ], - const SizedBox(height: 16), - RecommendationAttributeDials( - title: Text(context.l10n.acousticness), - values: ( - target: target.value.acousticness?.toDouble() ?? 0, - min: min.value.acousticness?.toDouble() ?? 0, - max: max.value.acousticness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - acousticness: value.target, - ); - min.value = min.value.copyWith( - acousticness: value.min, - ); - max.value = max.value.copyWith( - acousticness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.danceability), - values: ( - target: target.value.danceability?.toDouble() ?? 0, - min: min.value.danceability?.toDouble() ?? 0, - max: max.value.danceability?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - danceability: value.target, - ); - min.value = min.value.copyWith( - danceability: value.min, - ); - max.value = max.value.copyWith( - danceability: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.energy), - values: ( - target: target.value.energy?.toDouble() ?? 0, - min: min.value.energy?.toDouble() ?? 0, - max: max.value.energy?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - energy: value.target, - ); - min.value = min.value.copyWith( - energy: value.min, - ); - max.value = max.value.copyWith( - energy: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.instrumentalness), - values: ( - target: - target.value.instrumentalness?.toDouble() ?? 0, - min: min.value.instrumentalness?.toDouble() ?? 0, - max: max.value.instrumentalness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - instrumentalness: value.target, - ); - min.value = min.value.copyWith( - instrumentalness: value.min, - ); - max.value = max.value.copyWith( - instrumentalness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.liveness), - values: ( - target: target.value.liveness?.toDouble() ?? 0, - min: min.value.liveness?.toDouble() ?? 0, - max: max.value.liveness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - liveness: value.target, - ); - min.value = min.value.copyWith( - liveness: value.min, - ); - max.value = max.value.copyWith( - liveness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.loudness), - values: ( - target: target.value.loudness?.toDouble() ?? 0, - min: min.value.loudness?.toDouble() ?? 0, - max: max.value.loudness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - loudness: value.target, - ); - min.value = min.value.copyWith( - loudness: value.min, - ); - max.value = max.value.copyWith( - loudness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.speechiness), - values: ( - target: target.value.speechiness?.toDouble() ?? 0, - min: min.value.speechiness?.toDouble() ?? 0, - max: max.value.speechiness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - speechiness: value.target, - ); - min.value = min.value.copyWith( - speechiness: value.min, - ); - max.value = max.value.copyWith( - speechiness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.valence), - values: ( - target: target.value.valence?.toDouble() ?? 0, - min: min.value.valence?.toDouble() ?? 0, - max: max.value.valence?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - valence: value.target, - ); - min.value = min.value.copyWith( - valence: value.min, - ); - max.value = max.value.copyWith( - valence: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.popularity), - base: 100, - values: ( - target: target.value.popularity?.toDouble() ?? 0, - min: min.value.popularity?.toDouble() ?? 0, - max: max.value.popularity?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - popularity: value.target, - ); - min.value = min.value.copyWith( - popularity: value.min, - ); - max.value = max.value.copyWith( - popularity: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.key), - base: 11, - values: ( - target: target.value.key?.toDouble() ?? 0, - min: min.value.key?.toDouble() ?? 0, - max: max.value.key?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - key: value.target, - ); - min.value = min.value.copyWith( - key: value.min, - ); - max.value = max.value.copyWith( - key: value.max, - ); - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.duration), - values: ( - max: (max.value.durationMs ?? 0) / 1000, - target: (target.value.durationMs ?? 0) / 1000, - min: (min.value.durationMs ?? 0) / 1000, - ), - onChanged: (value) { - target.value = target.value.copyWith( - durationMs: (value.target * 1000).toInt(), - ); - min.value = min.value.copyWith( - durationMs: (value.min * 1000).toInt(), - ); - max.value = max.value.copyWith( - durationMs: (value.max * 1000).toInt(), - ); - }, - presets: { - context.l10n.short: (min: 50, target: 90, max: 120), - context.l10n.medium: ( - min: 120, - target: 180, - max: 200 + RecommendationAttributeDials( + title: Text(context.l10n.acousticness), + values: ( + target: target.value.acousticness?.toDouble() ?? 0, + min: min.value.acousticness?.toDouble() ?? 0, + max: max.value.acousticness?.toDouble() ?? 0, ), - context.l10n.long: (min: 480, target: 560, max: 640) - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.tempo), - values: ( - max: max.value.tempo?.toDouble() ?? 0, - target: target.value.tempo?.toDouble() ?? 0, - min: min.value.tempo?.toDouble() ?? 0, + onChanged: (value) { + target.value = target.value.copyWith( + acousticness: value.target, + ); + min.value = min.value.copyWith( + acousticness: value.min, + ); + max.value = max.value.copyWith( + acousticness: value.max, + ); + }, ), - onChanged: (value) { - target.value = target.value.copyWith( - tempo: value.target, - ); - min.value = min.value.copyWith( - tempo: value.min, - ); - max.value = max.value.copyWith( - tempo: value.max, - ); - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.mode), - values: ( - max: max.value.mode?.toDouble() ?? 0, - target: target.value.mode?.toDouble() ?? 0, - min: min.value.mode?.toDouble() ?? 0, + RecommendationAttributeDials( + title: Text(context.l10n.danceability), + values: ( + target: target.value.danceability?.toDouble() ?? 0, + min: min.value.danceability?.toDouble() ?? 0, + max: max.value.danceability?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + danceability: value.target, + ); + min.value = min.value.copyWith( + danceability: value.min, + ); + max.value = max.value.copyWith( + danceability: value.max, + ); + }, ), - onChanged: (value) { - target.value = target.value.copyWith( - mode: value.target, - ); - min.value = min.value.copyWith( - mode: value.min, - ); - max.value = max.value.copyWith( - mode: value.max, - ); - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.time_signature), - values: ( - max: max.value.timeSignature?.toDouble() ?? 0, - target: target.value.timeSignature?.toDouble() ?? 0, - min: min.value.timeSignature?.toDouble() ?? 0, + RecommendationAttributeDials( + title: Text(context.l10n.energy), + values: ( + target: target.value.energy?.toDouble() ?? 0, + min: min.value.energy?.toDouble() ?? 0, + max: max.value.energy?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + energy: value.target, + ); + min.value = min.value.copyWith( + energy: value.min, + ); + max.value = max.value.copyWith( + energy: value.max, + ); + }, ), - onChanged: (value) { - target.value = target.value.copyWith( - timeSignature: value.target, - ); - min.value = min.value.copyWith( - timeSignature: value.min, - ); - max.value = max.value.copyWith( - timeSignature: value.max, - ); - }, - ), - const Gap(20), - Center( - child: Button.primary( - leading: const Icon(SpotubeIcons.magic), - onPressed: artists.value.isEmpty && - tracks.value.isEmpty && - genres.value.isEmpty - ? null - : () { - final routeState = - GeneratePlaylistProviderInput( - seedArtists: artists.value - .map((a) => a.id!) - .toList(), - seedTracks: - tracks.value.map((t) => t.id!).toList(), - seedGenres: genres.value, - limit: limit.value, - max: max.value, - min: min.value, - target: target.value, - ); - context.navigateTo( - PlaylistGenerateResultRoute( - state: routeState, - ), - ); - }, - child: Text(context.l10n.generate), + RecommendationAttributeDials( + title: Text(context.l10n.instrumentalness), + values: ( + target: + target.value.instrumentalness?.toDouble() ?? 0, + min: min.value.instrumentalness?.toDouble() ?? 0, + max: max.value.instrumentalness?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + instrumentalness: value.target, + ); + min.value = min.value.copyWith( + instrumentalness: value.min, + ); + max.value = max.value.copyWith( + instrumentalness: value.max, + ); + }, ), - ), - ], - ), - ); - }), + RecommendationAttributeDials( + title: Text(context.l10n.liveness), + values: ( + target: target.value.liveness?.toDouble() ?? 0, + min: min.value.liveness?.toDouble() ?? 0, + max: max.value.liveness?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + liveness: value.target, + ); + min.value = min.value.copyWith( + liveness: value.min, + ); + max.value = max.value.copyWith( + liveness: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.loudness), + values: ( + target: target.value.loudness?.toDouble() ?? 0, + min: min.value.loudness?.toDouble() ?? 0, + max: max.value.loudness?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + loudness: value.target, + ); + min.value = min.value.copyWith( + loudness: value.min, + ); + max.value = max.value.copyWith( + loudness: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.speechiness), + values: ( + target: target.value.speechiness?.toDouble() ?? 0, + min: min.value.speechiness?.toDouble() ?? 0, + max: max.value.speechiness?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + speechiness: value.target, + ); + min.value = min.value.copyWith( + speechiness: value.min, + ); + max.value = max.value.copyWith( + speechiness: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.valence), + values: ( + target: target.value.valence?.toDouble() ?? 0, + min: min.value.valence?.toDouble() ?? 0, + max: max.value.valence?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + valence: value.target, + ); + min.value = min.value.copyWith( + valence: value.min, + ); + max.value = max.value.copyWith( + valence: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.popularity), + base: 100, + values: ( + target: target.value.popularity?.toDouble() ?? 0, + min: min.value.popularity?.toDouble() ?? 0, + max: max.value.popularity?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + popularity: value.target, + ); + min.value = min.value.copyWith( + popularity: value.min, + ); + max.value = max.value.copyWith( + popularity: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.key), + base: 11, + values: ( + target: target.value.key?.toDouble() ?? 0, + min: min.value.key?.toDouble() ?? 0, + max: max.value.key?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + key: value.target, + ); + min.value = min.value.copyWith( + key: value.min, + ); + max.value = max.value.copyWith( + key: value.max, + ); + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.duration), + values: ( + max: (max.value.durationMs ?? 0) / 1000, + target: (target.value.durationMs ?? 0) / 1000, + min: (min.value.durationMs ?? 0) / 1000, + ), + onChanged: (value) { + target.value = target.value.copyWith( + durationMs: (value.target * 1000).toInt(), + ); + min.value = min.value.copyWith( + durationMs: (value.min * 1000).toInt(), + ); + max.value = max.value.copyWith( + durationMs: (value.max * 1000).toInt(), + ); + }, + presets: { + context.l10n.short: (min: 50, target: 90, max: 120), + context.l10n.medium: ( + min: 120, + target: 180, + max: 200 + ), + context.l10n.long: (min: 480, target: 560, max: 640) + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.tempo), + values: ( + max: max.value.tempo?.toDouble() ?? 0, + target: target.value.tempo?.toDouble() ?? 0, + min: min.value.tempo?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + tempo: value.target, + ); + min.value = min.value.copyWith( + tempo: value.min, + ); + max.value = max.value.copyWith( + tempo: value.max, + ); + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.mode), + values: ( + max: max.value.mode?.toDouble() ?? 0, + target: target.value.mode?.toDouble() ?? 0, + min: min.value.mode?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + mode: value.target, + ); + min.value = min.value.copyWith( + mode: value.min, + ); + max.value = max.value.copyWith( + mode: value.max, + ); + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.time_signature), + values: ( + max: max.value.timeSignature?.toDouble() ?? 0, + target: target.value.timeSignature?.toDouble() ?? 0, + min: min.value.timeSignature?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + timeSignature: value.target, + ); + min.value = min.value.copyWith( + timeSignature: value.min, + ); + max.value = max.value.copyWith( + timeSignature: value.max, + ); + }, + ), + const Gap(20), + Center( + child: Button.primary( + leading: const Icon(SpotubeIcons.magic), + onPressed: artists.value.isEmpty && + tracks.value.isEmpty && + genres.value.isEmpty + ? null + : () { + final routeState = + GeneratePlaylistProviderInput( + seedArtists: artists.value + .map((a) => a.id!) + .toList(), + seedTracks: tracks.value + .map((t) => t.id!) + .toList(), + seedGenres: genres.value, + limit: limit.value, + max: max.value, + min: min.value, + target: target.value, + ); + context.navigateTo( + PlaylistGenerateResultRoute( + state: routeState, + ), + ); + }, + child: Text(context.l10n.generate), + ), + ), + ], + ), + ); + }), + ), ), ), ), diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index 4c350366..9e6f2987 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -48,218 +48,225 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { final isAllTrackSelected = selectedTracks.value.length == (generatedPlaylist.asData?.value.length ?? 0); - return Scaffold( - headers: const [ - TitleBar(leading: [BackButton()]) - ], - child: generatedPlaylist.isLoading - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - Text(context.l10n.generating_playlist), - ], - ), - ) - : Padding( - padding: const EdgeInsets.all(8.0), - child: ListView( - children: [ - GridView( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 8, - mainAxisSpacing: 8, - mainAxisExtent: 32, - ), - shrinkWrap: true, - children: [ - Button.primary( - leading: const Icon(SpotubeIcons.play), - onPressed: selectedTracks.value.isEmpty - ? null - : () async { - await playlistNotifier.load( - generatedPlaylist.asData!.value - .where( - (e) => selectedTracks.value - .contains(e.id!), - ) - .toList(), - autoPlay: true, - ); - }, - child: Text(context.l10n.play), + return SafeArea( + bottom: false, + child: Scaffold( + headers: const [ + TitleBar(leading: [BackButton()]) + ], + child: generatedPlaylist.isLoading + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + Text(context.l10n.generating_playlist), + ], + ), + ) + : Padding( + padding: const EdgeInsets.all(8.0), + child: ListView( + children: [ + GridView( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + mainAxisExtent: 32, ), - Button.primary( - leading: const Icon(SpotubeIcons.queueAdd), - onPressed: selectedTracks.value.isEmpty - ? null - : () async { - await playlistNotifier.addTracks( - generatedPlaylist.asData!.value.where( - (e) => selectedTracks.value.contains(e.id!), - ), - ); - if (context.mounted) { - showToast( - context: context, - location: ToastLocation.topRight, - builder: (context, overlay) { - return SurfaceCard( - child: Text( - context.l10n.add_count_to_queue( - selectedTracks.value.length, - ), - ), - ); - }, - ); - } - }, - child: Text(context.l10n.add_to_queue), - ), - Button.primary( - leading: const Icon(SpotubeIcons.addFilled), - onPressed: selectedTracks.value.isEmpty - ? null - : () async { - final playlist = await showDialog( - context: context, - builder: (context) => PlaylistCreateDialog( - trackIds: selectedTracks.value, - ), - ); - - if (playlist != null && context.mounted) { - context.navigateTo( - PlaylistRoute( - id: playlist.id!, - playlist: playlist, - ), - ); - } - }, - child: Text(context.l10n.create_a_playlist), - ), - Button.primary( - leading: const Icon(SpotubeIcons.playlistAdd), - onPressed: selectedTracks.value.isEmpty - ? null - : () async { - final hasAdded = await showDialog( - context: context, - builder: (context) => PlaylistAddTrackDialog( - openFromPlaylist: null, - tracks: selectedTracks.value - .map( - (e) => generatedPlaylist.asData!.value - .firstWhere( - (element) => element.id == e, - ), + shrinkWrap: true, + children: [ + Button.primary( + leading: const Icon(SpotubeIcons.play), + onPressed: selectedTracks.value.isEmpty + ? null + : () async { + await playlistNotifier.load( + generatedPlaylist.asData!.value + .where( + (e) => selectedTracks.value + .contains(e.id!), ) .toList(), - ), - ); - - if (context.mounted && hasAdded == true) { - showToast( - context: context, - location: ToastLocation.topRight, - builder: (context, overlay) { - return SurfaceCard( - child: Text( - context.l10n.add_count_to_playlist( - selectedTracks.value.length, - ), - ), - ); - }, + autoPlay: true, ); - } - }, - child: Text(context.l10n.add_to_playlist), - ) - ], - ), - const SizedBox(height: 16), - if (generatedPlaylist.asData?.value != null) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - context.l10n.selected_count_tracks( - selectedTracks.value.length, - ), + }, + child: Text(context.l10n.play), ), - Button.secondary( - onPressed: () { - if (isAllTrackSelected) { - selectedTracks.value = []; - } else { - selectedTracks.value = generatedPlaylist - .asData?.value - .map((e) => e.id!) - .toList() ?? - []; - } - }, - leading: const Icon(SpotubeIcons.selectionCheck), - child: Text( - isAllTrackSelected - ? context.l10n.deselect_all - : context.l10n.select_all, - ), + Button.primary( + leading: const Icon(SpotubeIcons.queueAdd), + onPressed: selectedTracks.value.isEmpty + ? null + : () async { + await playlistNotifier.addTracks( + generatedPlaylist.asData!.value.where( + (e) => + selectedTracks.value.contains(e.id!), + ), + ); + if (context.mounted) { + showToast( + context: context, + location: ToastLocation.topRight, + builder: (context, overlay) { + return SurfaceCard( + child: Text( + context.l10n.add_count_to_queue( + selectedTracks.value.length, + ), + ), + ); + }, + ); + } + }, + child: Text(context.l10n.add_to_queue), ), + Button.primary( + leading: const Icon(SpotubeIcons.addFilled), + onPressed: selectedTracks.value.isEmpty + ? null + : () async { + final playlist = await showDialog( + context: context, + builder: (context) => PlaylistCreateDialog( + trackIds: selectedTracks.value, + ), + ); + + if (playlist != null && context.mounted) { + context.navigateTo( + PlaylistRoute( + id: playlist.id!, + playlist: playlist, + ), + ); + } + }, + child: Text(context.l10n.create_a_playlist), + ), + Button.primary( + leading: const Icon(SpotubeIcons.playlistAdd), + onPressed: selectedTracks.value.isEmpty + ? null + : () async { + final hasAdded = await showDialog( + context: context, + builder: (context) => + PlaylistAddTrackDialog( + openFromPlaylist: null, + tracks: selectedTracks.value + .map( + (e) => generatedPlaylist + .asData!.value + .firstWhere( + (element) => element.id == e, + ), + ) + .toList(), + ), + ); + + if (context.mounted && hasAdded == true) { + showToast( + context: context, + location: ToastLocation.topRight, + builder: (context, overlay) { + return SurfaceCard( + child: Text( + context.l10n.add_count_to_playlist( + selectedTracks.value.length, + ), + ), + ); + }, + ); + } + }, + child: Text(context.l10n.add_to_playlist), + ) ], ), - const SizedBox(height: 8), - SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (final track - in generatedPlaylist.asData?.value ?? []) - Row( - spacing: 5, - children: [ - Checkbox( - state: selectedTracks.value.contains(track.id) - ? CheckboxState.checked - : CheckboxState.unchecked, - onChanged: (value) { - if (value == CheckboxState.checked) { - selectedTracks.value.add(track.id!); - } else { - selectedTracks.value.remove(track.id); - } - selectedTracks.value = - selectedTracks.value.toList(); - }, - ), - Expanded( - child: GestureDetector( - onTap: () { - selectedTracks.value.contains(track.id) - ? selectedTracks.value.remove(track.id) - : selectedTracks.value.add(track.id!); + const SizedBox(height: 16), + if (generatedPlaylist.asData?.value != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.l10n.selected_count_tracks( + selectedTracks.value.length, + ), + ), + Button.secondary( + onPressed: () { + if (isAllTrackSelected) { + selectedTracks.value = []; + } else { + selectedTracks.value = generatedPlaylist + .asData?.value + .map((e) => e.id!) + .toList() ?? + []; + } + }, + leading: const Icon(SpotubeIcons.selectionCheck), + child: Text( + isAllTrackSelected + ? context.l10n.deselect_all + : context.l10n.select_all, + ), + ), + ], + ), + const SizedBox(height: 8), + SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final track + in generatedPlaylist.asData?.value ?? []) + Row( + spacing: 5, + children: [ + Checkbox( + state: selectedTracks.value.contains(track.id) + ? CheckboxState.checked + : CheckboxState.unchecked, + onChanged: (value) { + if (value == CheckboxState.checked) { + selectedTracks.value.add(track.id!); + } else { + selectedTracks.value.remove(track.id); + } selectedTracks.value = selectedTracks.value.toList(); }, - child: SimpleTrackTile(track: track), ), - ), - ], - ) - ], + Expanded( + child: GestureDetector( + onTap: () { + selectedTracks.value.contains(track.id) + ? selectedTracks.value + .remove(track.id) + : selectedTracks.value.add(track.id!); + selectedTracks.value = + selectedTracks.value.toList(); + }, + child: SimpleTrackTile(track: track), + ), + ), + ], + ) + ], + ), ), - ), - ], + ], + ), ), - ), + ), ); } } diff --git a/lib/pages/mobile_login/mobile_login.dart b/lib/pages/mobile_login/mobile_login.dart index 4128bfe6..eb50316f 100644 --- a/lib/pages/mobile_login/mobile_login.dart +++ b/lib/pages/mobile_login/mobile_login.dart @@ -27,50 +27,53 @@ class WebViewLoginPage extends HookConsumerWidget { ); } - return Scaffold( - headers: const [ - TitleBar( - leading: [BackButton(color: Colors.white)], - backgroundColor: Colors.transparent, - ), - ], - floatingHeader: true, - child: InAppWebView( - initialSettings: InAppWebViewSettings( - userAgent: - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36", - ), - initialUrlRequest: URLRequest( - url: WebUri("https://accounts.spotify.com/"), - ), - onPermissionRequest: (controller, permissionRequest) async { - return PermissionResponse( - resources: permissionRequest.resources, - action: PermissionResponseAction.GRANT, - ); - }, - onLoadStop: (controller, action) async { - if (action == null) return; - String url = action.toString(); - if (url.endsWith("/")) { - url = url.substring(0, url.length - 1); - } - - final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status"); - - if (exp.hasMatch(url)) { - final cookies = - await CookieManager.instance().getCookies(url: action); - final cookieHeader = - "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}"; - - await authenticationNotifier.login(cookieHeader); - if (context.mounted) { - // ignore: use_build_context_synchronously - context.navigateTo(const HomeRoute()); + return SafeArea( + bottom: false, + child: Scaffold( + headers: const [ + TitleBar( + leading: [BackButton(color: Colors.white)], + backgroundColor: Colors.transparent, + ), + ], + floatingHeader: true, + child: InAppWebView( + initialSettings: InAppWebViewSettings( + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36", + ), + initialUrlRequest: URLRequest( + url: WebUri("https://accounts.spotify.com/"), + ), + onPermissionRequest: (controller, permissionRequest) async { + return PermissionResponse( + resources: permissionRequest.resources, + action: PermissionResponseAction.GRANT, + ); + }, + onLoadStop: (controller, action) async { + if (action == null) return; + String url = action.toString(); + if (url.endsWith("/")) { + url = url.substring(0, url.length - 1); } - } - }, + + final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status"); + + if (exp.hasMatch(url)) { + final cookies = + await CookieManager.instance().getCookies(url: action); + final cookieHeader = + "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}"; + + await authenticationNotifier.login(cookieHeader); + if (context.mounted) { + // ignore: use_build_context_synchronously + context.navigateTo(const HomeRoute()); + } + } + }, + ), ), ); } diff --git a/lib/pages/profile/profile.dart b/lib/pages/profile/profile.dart index a82c80b5..b6c4a2cd 100644 --- a/lib/pages/profile/profile.dart +++ b/lib/pages/profile/profile.dart @@ -44,7 +44,6 @@ class ProfilePage extends HookConsumerWidget { headers: [ TitleBar( title: Text(context.l10n.profile), - automaticallyImplyLeading: true, ) ], child: Skeletonizer( diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 0703d4ef..1837bbec 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -31,163 +31,166 @@ class AboutSpotubePage extends HookConsumerWidget { const colon = TableCell(child: Text(":")); - return Scaffold( - headers: [ - TitleBar( - leading: const [BackButton()], - title: Text(context.l10n.about_spotube), - ) - ], - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - Assets.spotubeLogoPng.image( - height: 200, - width: 200, - ), - Center( - child: Column( - children: [ - Text(context.l10n.spotube_description).semiBold().large(), - const SizedBox(height: 20), - Table( - columnWidths: const { - 0: FixedTableSize(95), - 1: FixedTableSize(10), - 2: IntrinsicTableSize(), - }, - defaultRowHeight: const FixedTableSize(40), - rows: [ - TableRow( - cells: [ - TableCell(child: Text(context.l10n.founder)), - colon, - TableCell( - child: Hyperlink( - context.l10n.kingkor_roy_tirtho, - "https://github.com/KRTirtho", + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar( + leading: const [BackButton()], + title: Text(context.l10n.about_spotube), + ) + ], + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Assets.spotubeLogoPng.image( + height: 200, + width: 200, + ), + Center( + child: Column( + children: [ + Text(context.l10n.spotube_description).semiBold().large(), + const SizedBox(height: 20), + Table( + columnWidths: const { + 0: FixedTableSize(95), + 1: FixedTableSize(10), + 2: IntrinsicTableSize(), + }, + defaultRowHeight: const FixedTableSize(40), + rows: [ + TableRow( + cells: [ + TableCell(child: Text(context.l10n.founder)), + colon, + TableCell( + child: Hyperlink( + context.l10n.kingkor_roy_tirtho, + "https://github.com/KRTirtho", + ), + ) + ], + ), + TableRow( + cells: [ + TableCell(child: Text(context.l10n.version)), + colon, + TableCell(child: Text("v${packageInfo.version}")) + ], + ), + TableRow( + cells: [ + TableCell(child: Text(context.l10n.channel)), + colon, + TableCell(child: Text(Env.releaseChannel.name)) + ], + ), + TableRow( + cells: [ + TableCell(child: Text(context.l10n.build_number)), + colon, + TableCell( + child: Text(packageInfo.buildNumber + .replaceAll(".", " ")), + ) + ], + ), + TableRow( + cells: [ + TableCell(child: Text(context.l10n.repository)), + colon, + const TableCell( + child: Hyperlink( + "github.com/KRTirtho/spotube", + "https://github.com/KRTirtho/spotube", + ), ), - ) - ], - ), - TableRow( - cells: [ - TableCell(child: Text(context.l10n.version)), - colon, - TableCell(child: Text("v${packageInfo.version}")) - ], - ), - TableRow( - cells: [ - TableCell(child: Text(context.l10n.channel)), - colon, - TableCell(child: Text(Env.releaseChannel.name)) - ], - ), - TableRow( - cells: [ - TableCell(child: Text(context.l10n.build_number)), - colon, - TableCell( - child: Text( - packageInfo.buildNumber.replaceAll(".", " ")), - ) - ], - ), - TableRow( - cells: [ - TableCell(child: Text(context.l10n.repository)), - colon, - const TableCell( - child: Hyperlink( - "github.com/KRTirtho/spotube", - "https://github.com/KRTirtho/spotube", + ], + ), + TableRow( + cells: [ + TableCell(child: Text(context.l10n.license)), + colon, + const TableCell( + child: Hyperlink( + "BSD-4-Clause", + "https://raw.githubusercontent.com/KRTirtho/spotube/master/LICENSE", + ), ), - ), - ], - ), - TableRow( - cells: [ - TableCell(child: Text(context.l10n.license)), - colon, - const TableCell( - child: Hyperlink( - "BSD-4-Clause", - "https://raw.githubusercontent.com/KRTirtho/spotube/master/LICENSE", + ], + ), + TableRow( + cells: [ + TableCell(child: Text(context.l10n.bug_issues)), + colon, + const TableCell( + child: Hyperlink( + "github.com/KRTirtho/spotube/issues", + "https://github.com/KRTirtho/spotube/issues", + ), ), - ), - ], - ), - TableRow( - cells: [ - TableCell(child: Text(context.l10n.bug_issues)), - colon, - const TableCell( - child: Hyperlink( - "github.com/KRTirtho/spotube/issues", - "https://github.com/KRTirtho/spotube/issues", - ), - ), - ], - ), - ], + ], + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => launchUrl( + Uri.parse("https://discord.gg/uJ94vxB6vg"), + mode: LaunchMode.externalApplication, + ), + child: const UniversalImage( + path: + "https://discord.com/api/guilds/1012234096237350943/widget.png?style=banner2", ), - ], - ), - ), - const SizedBox(height: 20), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => launchUrl( - Uri.parse("https://discord.gg/uJ94vxB6vg"), - mode: LaunchMode.externalApplication, - ), - child: const UniversalImage( - path: - "https://discord.com/api/guilds/1012234096237350943/widget.png?style=banner2", ), ), - ), - const SizedBox(height: 20), - Text( - context.l10n.made_with, - textAlign: TextAlign.center, - style: theme.typography.small, - ), - Text( - context.l10n.copyright(DateTime.now().year), - textAlign: TextAlign.center, - style: theme.typography.small, - ), - const SizedBox(height: 20), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 750), - child: SafeArea( - child: license.when( - data: (data) { - return Text( - data, - style: theme.typography.small, - ); - }, - loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, - error: (e, s) { - return Text( - e.toString(), - style: theme.typography.small, - ); - }, + const SizedBox(height: 20), + Text( + context.l10n.made_with, + textAlign: TextAlign.center, + style: theme.typography.small, + ), + Text( + context.l10n.copyright(DateTime.now().year), + textAlign: TextAlign.center, + style: theme.typography.small, + ), + const SizedBox(height: 20), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 750), + child: SafeArea( + child: license.when( + data: (data) { + return Text( + data, + style: theme.typography.small, + ); + }, + loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, + error: (e, s) { + return Text( + e.toString(), + style: theme.typography.small, + ); + }, + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index 377a6079..8ac2c1b9 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -47,50 +47,52 @@ class BlackListPage extends HookConsumerWidget { [blacklist, searchText.value], ); - return Scaffold( - headers: [ - TitleBar( - title: Text(context.l10n.blacklist), - leading: const [BackButton()], - ) - ], - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - onChanged: (value) => searchText.value = value, - placeholder: Text(context.l10n.search), - leading: const Icon(SpotubeIcons.search), - ), - ), - InterScrollbar( - controller: controller, - child: ListView.builder( - controller: controller, - shrinkWrap: true, - itemCount: filteredBlacklist.length, - itemBuilder: (context, index) { - final item = filteredBlacklist.elementAt(index); - return ButtonTile( - style: ButtonVariance.ghost, - leading: Text("${index + 1}."), - title: Text("${item.name} (${item.elementType.name})"), - subtitle: Text(item.elementId), - trailing: IconButton.ghost( - icon: Icon(SpotubeIcons.trash, color: Colors.red[400]), - onPressed: () { - ref - .read(blacklistProvider.notifier) - .remove(filteredBlacklist.elementAt(index).elementId); - }, - ), - ); - }, - ), - ), + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar( + title: Text(context.l10n.blacklist), + leading: const [BackButton()], + ) ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + onChanged: (value) => searchText.value = value, + placeholder: Text(context.l10n.search), + leading: const Icon(SpotubeIcons.search), + ), + ), + InterScrollbar( + controller: controller, + child: ListView.builder( + controller: controller, + shrinkWrap: true, + itemCount: filteredBlacklist.length, + itemBuilder: (context, index) { + final item = filteredBlacklist.elementAt(index); + return ButtonTile( + style: ButtonVariance.ghost, + leading: Text("${index + 1}."), + title: Text("${item.name} (${item.elementType.name})"), + subtitle: Text(item.elementId), + trailing: IconButton.ghost( + icon: Icon(SpotubeIcons.trash, color: Colors.red[400]), + onPressed: () { + ref.read(blacklistProvider.notifier).remove( + filteredBlacklist.elementAt(index).elementId); + }, + ), + ); + }, + ), + ), + ], + ), ), ); } diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 53610ae4..0948bdeb 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -34,7 +34,6 @@ class SettingsPage extends HookConsumerWidget { headers: [ TitleBar( title: Text(context.l10n.settings), - automaticallyImplyLeading: true, ) ], child: Scrollbar( diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index 807b8049..834837af 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -26,31 +26,33 @@ class StatsAlbumsPage extends HookConsumerWidget { final albumsData = topAlbums.asData?.value.items ?? []; - return Scaffold( - headers: [ - TitleBar( - automaticallyImplyLeading: true, - title: Text(context.l10n.albums), - ) - ], - child: Skeletonizer( - enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage, - child: InfiniteList( - onFetchData: () async { - await topAlbumsNotifier.fetchMore(); - }, - hasError: topAlbums.hasError, - isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage, - hasReachedMax: topAlbums.asData?.value.hasMore ?? true, - itemCount: albumsData.length, - itemBuilder: (context, index) { - final album = albumsData[index]; - return StatsAlbumItem( - album: album.album, - info: Text(context.l10n - .count_plays(compactNumberFormatter.format(album.count))), - ); - }, + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar( + title: Text(context.l10n.albums), + ) + ], + child: Skeletonizer( + enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + child: InfiniteList( + onFetchData: () async { + await topAlbumsNotifier.fetchMore(); + }, + hasError: topAlbums.hasError, + isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + hasReachedMax: topAlbums.asData?.value.hasMore ?? true, + itemCount: albumsData.length, + itemBuilder: (context, index) { + final album = albumsData[index]; + return StatsAlbumItem( + album: album.album, + info: Text(context.l10n + .count_plays(compactNumberFormatter.format(album.count))), + ); + }, + ), ), ), ); diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index 311faa0c..f3d2f0dd 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -29,31 +29,33 @@ class StatsArtistsPage extends HookConsumerWidget { final artistsData = useMemoized( () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); - return Scaffold( - headers: [ - TitleBar( - automaticallyImplyLeading: true, - title: Text(context.l10n.artists), - ) - ], - child: Skeletonizer( - enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, - child: InfiniteList( - onFetchData: () async { - await topTracksNotifier.fetchMore(); - }, - hasError: topTracks.hasError, - isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, - hasReachedMax: topTracks.asData?.value.hasMore ?? true, - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text(context.l10n - .count_plays(compactNumberFormatter.format(artist.count))), - ); - }, + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar( + title: Text(context.l10n.artists), + ) + ], + child: Skeletonizer( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: InfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text(context.l10n + .count_plays(compactNumberFormatter.format(artist.count))), + ); + }, + ), ), ), ); diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index 8a20758d..6df911ce 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -50,79 +50,83 @@ class StatsStreamFeesPage extends HookConsumerWidget { HistoryDuration.allTime: context.l10n.all_time, }; - return Scaffold( - headers: [ - TitleBar( - automaticallyImplyLeading: true, - title: Text(context.l10n.streaming_fees_hypothetical), - ) - ], - child: CustomScrollView( - slivers: [ - SliverCrossAxisConstrained( - maxCrossAxisExtent: 600, - alignment: -1, - child: SliverPadding( - padding: const EdgeInsets.all(16.0), - sliver: SliverToBoxAdapter( - child: Text( - context.l10n.spotify_hipotetical_calculation, - ).small().muted(), - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - context.l10n.total_money(usdFormatter.format(total)), - ).semiBold().large(), - Select( - value: duration.value, - onChanged: (value) { - if (value == null) return; - duration.value = value; - }, - itemBuilder: (context, value) => Text(translations[value]!), - constraints: const BoxConstraints(maxWidth: 150), - popupWidthConstraint: PopoverConstraint.anchorMaxSize, - children: [ - for (final entry in translations.entries) - SelectItemButton( - value: entry.key, - child: Text(entry.value), - ), - ], - ), - ], - ), - ), - ), - SliverSafeArea( - sliver: Skeletonizer.sliver( - enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, - child: SliverInfiniteList( - onFetchData: () async { - await topTracksNotifier.fetchMore(); - }, - hasError: topTracks.hasError, - isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, - hasReachedMax: topTracks.asData?.value.hasMore ?? true, - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text(usdFormatter.format(artist.count * 0.005)), - ); - }, - ), - ), - ), + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar( + title: Text(context.l10n.streaming_fees_hypothetical), + ) ], + child: CustomScrollView( + slivers: [ + SliverCrossAxisConstrained( + maxCrossAxisExtent: 600, + alignment: -1, + child: SliverPadding( + padding: const EdgeInsets.all(16.0), + sliver: SliverToBoxAdapter( + child: Text( + context.l10n.spotify_hipotetical_calculation, + ).small().muted(), + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.l10n.total_money(usdFormatter.format(total)), + ).semiBold().large(), + Select( + value: duration.value, + onChanged: (value) { + if (value == null) return; + duration.value = value; + }, + itemBuilder: (context, value) => + Text(translations[value]!), + constraints: const BoxConstraints(maxWidth: 150), + popupWidthConstraint: PopoverConstraint.anchorMaxSize, + children: [ + for (final entry in translations.entries) + SelectItemButton( + value: entry.key, + child: Text(entry.value), + ), + ], + ), + ], + ), + ), + ), + SliverSafeArea( + sliver: Skeletonizer.sliver( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: + topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text(usdFormatter.format(artist.count * 0.005)), + ); + }, + ), + ), + ), + ], + ), ), ); } diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index df7f5983..2ee4c8d7 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -28,34 +28,36 @@ class StatsMinutesPage extends HookConsumerWidget { final tracksData = topTracks.asData?.value.items ?? []; - return Scaffold( - headers: [ - TitleBar( - title: Text(context.l10n.minutes_listened), - automaticallyImplyLeading: true, - ) - ], - child: Skeletonizer( - enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, - child: InfiniteList( - separatorBuilder: (context, index) => const Gap(8), - onFetchData: () async { - await topTracksNotifier.fetchMore(); - }, - hasError: topTracks.hasError, - isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, - hasReachedMax: topTracks.asData?.value.hasMore ?? true, - itemCount: tracksData.length, - itemBuilder: (context, index) { - final track = tracksData[index]; - return StatsTrackItem( - track: track.track, - info: Text( - context.l10n.count_mins(compactNumberFormatter - .format(track.count * track.track.duration!.inMinutes)), - ), - ); - }, + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar( + title: Text(context.l10n.minutes_listened), + ) + ], + child: Skeletonizer( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: InfiniteList( + separatorBuilder: (context, index) => const Gap(8), + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: tracksData.length, + itemBuilder: (context, index) { + final track = tracksData[index]; + return StatsTrackItem( + track: track.track, + info: Text( + context.l10n.count_mins(compactNumberFormatter + .format(track.count * track.track.duration!.inMinutes)), + ), + ); + }, + ), ), ), ); diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index 78c3cd24..03ea5126 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -27,33 +27,36 @@ class StatsPlaylistsPage extends HookConsumerWidget { final playlistsData = topPlaylists.asData?.value.items ?? []; - return Scaffold( - headers: [ - TitleBar( - automaticallyImplyLeading: true, - title: Text(context.l10n.playlists), - ) - ], - child: Skeletonizer( - enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, - child: InfiniteList( - onFetchData: () async { - await topPlaylistsNotifier.fetchMore(); - }, - hasError: topPlaylists.hasError, - isLoading: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, - hasReachedMax: topPlaylists.asData?.value.hasMore ?? true, - itemCount: playlistsData.length, - itemBuilder: (context, index) { - final playlist = playlistsData[index]; - return StatsPlaylistItem( - playlist: playlist.playlist, - info: Text( - context.l10n - .count_plays(compactNumberFormatter.format(playlist.count)), - ), - ); - }, + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar( + title: Text(context.l10n.playlists), + ) + ], + child: Skeletonizer( + enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, + child: InfiniteList( + onFetchData: () async { + await topPlaylistsNotifier.fetchMore(); + }, + hasError: topPlaylists.hasError, + isLoading: + topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, + hasReachedMax: topPlaylists.asData?.value.hasMore ?? true, + itemCount: playlistsData.length, + itemBuilder: (context, index) { + final playlist = playlistsData[index]; + return StatsPlaylistItem( + playlist: playlist.playlist, + info: Text( + context.l10n.count_plays( + compactNumberFormatter.format(playlist.count)), + ), + ); + }, + ), ), ), ); diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 05e53d7c..0d919a44 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -28,34 +28,36 @@ class StatsStreamsPage extends HookConsumerWidget { final tracksData = topTracks.asData?.value.items ?? []; - return Scaffold( - headers: [ - TitleBar( - title: Text(context.l10n.streamed_songs), - automaticallyImplyLeading: true, - ) - ], - child: Skeletonizer( - enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, - child: InfiniteList( - separatorBuilder: (context, index) => const Gap(8), - onFetchData: () async { - await topTracksNotifier.fetchMore(); - }, - hasError: topTracks.hasError, - isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, - hasReachedMax: topTracks.asData?.value.hasMore ?? true, - itemCount: tracksData.length, - itemBuilder: (context, index) { - final track = tracksData[index]; - return StatsTrackItem( - track: track.track, - info: Text( - context.l10n - .count_plays(compactNumberFormatter.format(track.count)), - ), - ); - }, + return SafeArea( + bottom: false, + child: Scaffold( + headers: [ + TitleBar( + title: Text(context.l10n.streamed_songs), + ) + ], + child: Skeletonizer( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: InfiniteList( + separatorBuilder: (context, index) => const Gap(8), + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: tracksData.length, + itemBuilder: (context, index) { + final track = tracksData[index]; + return StatsTrackItem( + track: track.track, + info: Text( + context.l10n + .count_plays(compactNumberFormatter.format(track.count)), + ), + ); + }, + ), ), ), ); diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index 765969be..0f8dae5d 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -54,197 +54,200 @@ class TrackPage extends HookConsumerWidget { } } - return Scaffold( - headers: const [ - TitleBar( - automaticallyImplyLeading: true, - backgroundColor: Colors.transparent, - surfaceBlur: 0, - ) - ], - floatingHeader: true, - child: Stack( - children: [ - Positioned.fill( - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: UniversalImage.imageProvider( - track.album!.images.asUrlString( - placeholder: ImagePlaceholder.albumArt, + return SafeArea( + bottom: false, + child: Scaffold( + headers: const [ + TitleBar( + backgroundColor: Colors.transparent, + surfaceBlur: 0, + ) + ], + floatingHeader: true, + child: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: UniversalImage.imageProvider( + track.album!.images.asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), ), + fit: BoxFit.cover, + colorFilter: ColorFilter.mode( + colorScheme.background.withOpacity(0.5), + BlendMode.srcOver, + ), + alignment: Alignment.topCenter, ), - fit: BoxFit.cover, - colorFilter: ColorFilter.mode( - colorScheme.background.withOpacity(0.5), - BlendMode.srcOver, - ), - alignment: Alignment.topCenter, ), ), ), - ), - Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Skeletonizer( - enabled: trackQuery.isLoading, - child: Container( - alignment: Alignment.topCenter, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - colorScheme.background, - Colors.transparent, - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: const [0.2, 1], + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Skeletonizer( + enabled: trackQuery.isLoading, + child: Container( + alignment: Alignment.topCenter, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + colorScheme.background, + Colors.transparent, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.2, 1], + ), ), - ), - child: SafeArea( - child: Wrap( - spacing: 20, - runSpacing: 20, - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - path: track.album!.images.asUrlString( - placeholder: ImagePlaceholder.albumArt, + child: SafeArea( + child: Wrap( + spacing: 20, + runSpacing: 20, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: track.album!.images.asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), + height: 200, + width: 200, ), - height: 200, - width: 200, ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: mediaQuery.smAndDown - ? CrossAxisAlignment.center - : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - track.name!, - ).large().semiBold(), - const Gap(10), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(SpotubeIcons.album), - const Gap(5), - Flexible( - child: LinkText( - track.album!.name!, - AlbumRoute( - id: track.album!.id!, - album: track.album!, - ), - push: true, - ), - ), - ], - ), - const Gap(10), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(SpotubeIcons.artist), - const Gap(5), - Flexible( - child: ArtistLink( - artists: track.artists!, - hideOverflowArtist: false, - ), - ), - ], - ), - const Gap(10), - ConstrainedBox( - constraints: - const BoxConstraints(maxWidth: 350), - child: Row( - mainAxisSize: mediaQuery.smAndDown - ? MainAxisSize.max - : MainAxisSize.min, + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: mediaQuery.smAndDown + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + track.name!, + ).large().semiBold(), + const Gap(10), + Row( + mainAxisSize: MainAxisSize.min, children: [ + const Icon(SpotubeIcons.album), const Gap(5), - if (!isActive && - !playlist.tracks - .containsBy(track, (t) => t.id)) - Button.outline( - leading: - const Icon(SpotubeIcons.queueAdd), - child: Text(context.l10n.queue), - onPressed: () { - playlistNotifier.addTrack(track); - }, - ), - const Gap(5), - if (!isActive && - !playlist.tracks - .containsBy(track, (t) => t.id)) - Tooltip( - tooltip: TooltipContainer( - child: Text(context.l10n.play_next), + Flexible( + child: LinkText( + track.album!.name!, + AlbumRoute( + id: track.album!.id!, + album: track.album!, ), - child: IconButton.outline( - icon: const Icon( - SpotubeIcons.lightning), - onPressed: () { - playlistNotifier - .addTracksAtFirst([track]); - }, - ), - ), - const Gap(5), - Tooltip( - tooltip: TooltipContainer( - child: Text( - isActive - ? context.l10n.pause_playback - : context.l10n.play, - ), - ), - child: IconButton.primary( - shape: ButtonShape.circle, - icon: Icon( - isActive - ? SpotubeIcons.pause - : SpotubeIcons.play, - ), - onPressed: onPlay, + push: true, ), ), - const Gap(5), - if (mediaQuery.smAndDown) - const Spacer() - else - const Gap(20), - TrackHeartButton(track: track), - TrackOptions( - track: track, - userPlaylist: false, - ), - const Gap(5), ], ), - ), - ], + const Gap(10), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(SpotubeIcons.artist), + const Gap(5), + Flexible( + child: ArtistLink( + artists: track.artists!, + hideOverflowArtist: false, + ), + ), + ], + ), + const Gap(10), + ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 350), + child: Row( + mainAxisSize: mediaQuery.smAndDown + ? MainAxisSize.max + : MainAxisSize.min, + children: [ + const Gap(5), + if (!isActive && + !playlist.tracks + .containsBy(track, (t) => t.id)) + Button.outline( + leading: + const Icon(SpotubeIcons.queueAdd), + child: Text(context.l10n.queue), + onPressed: () { + playlistNotifier.addTrack(track); + }, + ), + const Gap(5), + if (!isActive && + !playlist.tracks + .containsBy(track, (t) => t.id)) + Tooltip( + tooltip: TooltipContainer( + child: Text(context.l10n.play_next), + ), + child: IconButton.outline( + icon: const Icon( + SpotubeIcons.lightning), + onPressed: () { + playlistNotifier + .addTracksAtFirst([track]); + }, + ), + ), + const Gap(5), + Tooltip( + tooltip: TooltipContainer( + child: Text( + isActive + ? context.l10n.pause_playback + : context.l10n.play, + ), + ), + child: IconButton.primary( + shape: ButtonShape.circle, + icon: Icon( + isActive + ? SpotubeIcons.pause + : SpotubeIcons.play, + ), + onPressed: onPlay, + ), + ), + const Gap(5), + if (mediaQuery.smAndDown) + const Spacer() + else + const Gap(20), + TrackHeartButton(track: track), + TrackOptions( + track: track, + userPlaylist: false, + ), + const Gap(5), + ], + ), + ), + ], + ), ), - ), - ], + ], + ), ), ), ), ), ), - ), - ], + ], + ), ), ); }