From 5423a112dc4406520e1800cb70b5aeb31070bede Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Thu, 16 Jan 2025 17:58:41 +0100 Subject: [PATCH 01/10] feat: songs ui --- lib/album/album.dart | 1 + lib/album/models/album_data.dart | 13 +++ lib/album/models/models.dart | 1 + lib/album/view/album_view.dart | 23 +++- lib/app/app_router/routes/albums_routes.dart | 34 ++++++ .../app_router/routes/dashboard_routes.dart | 10 +- lib/{songs => song}/bloc/song_bloc.dart | 2 +- lib/{songs => song}/bloc/song_event.dart | 0 lib/{songs => song}/bloc/song_state.dart | 1 - lib/{songs => song}/song.dart | 1 + lib/song/view/song_page.dart | 45 ++++++++ lib/song/view/song_view.dart | 107 ++++++++++++++++++ lib/song/view/view.dart | 2 + lib/theme/app_theme.dart | 8 +- .../lib/src/swiftify_repository.dart | 6 +- test/songs/bloc/song_bloc_test.dart | 2 +- test/songs/bloc/song_event_test.dart | 2 +- test/songs/bloc/song_state_test.dart | 2 +- 18 files changed, 238 insertions(+), 22 deletions(-) create mode 100644 lib/album/models/album_data.dart create mode 100644 lib/album/models/models.dart create mode 100644 lib/app/app_router/routes/albums_routes.dart rename lib/{songs => song}/bloc/song_bloc.dart (94%) rename lib/{songs => song}/bloc/song_event.dart (100%) rename lib/{songs => song}/bloc/song_state.dart (92%) rename lib/{songs => song}/song.dart (54%) create mode 100644 lib/song/view/song_page.dart create mode 100644 lib/song/view/song_view.dart create mode 100644 lib/song/view/view.dart diff --git a/lib/album/album.dart b/lib/album/album.dart index ed640dc..11f36db 100644 --- a/lib/album/album.dart +++ b/lib/album/album.dart @@ -1,2 +1,3 @@ export 'bloc/album_bloc.dart'; +export 'models/models.dart'; export 'view/view.dart'; diff --git a/lib/album/models/album_data.dart b/lib/album/models/album_data.dart new file mode 100644 index 0000000..13a5666 --- /dev/null +++ b/lib/album/models/album_data.dart @@ -0,0 +1,13 @@ +class AlbumData { + AlbumData({ + required this.albumId, + required this.albumTitle, + required this.coverAlbum, + required this.albumReleaseDate, + }); + + final int albumId; + final String albumTitle; + final String? coverAlbum; + final String albumReleaseDate; +} diff --git a/lib/album/models/models.dart b/lib/album/models/models.dart new file mode 100644 index 0000000..ee55682 --- /dev/null +++ b/lib/album/models/models.dart @@ -0,0 +1 @@ +export 'album_data.dart'; diff --git a/lib/album/view/album_view.dart b/lib/album/view/album_view.dart index 1d04a34..6a8620d 100644 --- a/lib/album/view/album_view.dart +++ b/lib/album/view/album_view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:swiftify/album/bloc/album_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:swiftify/album/album.dart'; +import 'package:swiftify/song/song.dart'; class AlbumView extends StatelessWidget { const AlbumView({super.key}); @@ -79,10 +81,21 @@ class AlbumsContent extends StatelessWidget { itemCount: albums.length, itemBuilder: (context, index) { final album = albums[index]; - return AlbumItem( - title: album.title, - releaseDate: album.releaseDate, - coverAlbum: album.coverAlbum, + return GestureDetector( + onTap: () => context.push( + '${AlbumPage.routeName}/${SongPage.routeName}', + extra: AlbumData( + albumId: album.albumId, + albumTitle: album.title, + coverAlbum: album.coverAlbum, + albumReleaseDate: album.releaseDate, + ), + ), + child: AlbumItem( + title: album.title, + releaseDate: album.releaseDate, + coverAlbum: album.coverAlbum, + ), ); }, ); diff --git a/lib/app/app_router/routes/albums_routes.dart b/lib/app/app_router/routes/albums_routes.dart new file mode 100644 index 0000000..1e809ff --- /dev/null +++ b/lib/app/app_router/routes/albums_routes.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:swiftify/album/album.dart'; +import 'package:swiftify/app/app_router/app_router.dart'; +import 'package:swiftify/song/song.dart'; + +final albumsRoutes = AppRoute( + name: AlbumPage.routeName, + path: AlbumPage.routeName, + builder: (context, state) => const AlbumPage(), + routes: [ + AppRoute( + parentNavigatorKey: rootNavigatorKey, + name: '${AlbumPage.routeName}/${SongPage.routeName}', + path: SongPage.routeName, + pageBuilder: (context, state) => CustomTransitionPage( + child: SongPage.pageBuilder(context, state), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ) + .chain( + CurveTween(curve: Curves.easeInOut), + ) + .animate(animation), + child: child, + ); + }, + ), + ), + ], +); diff --git a/lib/app/app_router/routes/dashboard_routes.dart b/lib/app/app_router/routes/dashboard_routes.dart index 94d515e..a33af11 100644 --- a/lib/app/app_router/routes/dashboard_routes.dart +++ b/lib/app/app_router/routes/dashboard_routes.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:swiftify/album/album.dart'; import 'package:swiftify/app/app_router/app_router.dart'; +import 'package:swiftify/app/app_router/routes/albums_routes.dart'; import 'package:swiftify/app/app_router/scaffold_with_bottom_navigation.dart'; import 'package:swiftify/favorites/favorites.dart'; @@ -16,13 +16,7 @@ final dashBoardRoutes = StatefulShellRoute.indexedStack( branches: [ StatefulShellBranch( navigatorKey: albumNavigatorKey, - routes: [ - AppRoute( - name: AlbumPage.routeName, - path: AlbumPage.routeName, - builder: (context, state) => const AlbumPage(), - ), - ], + routes: [albumsRoutes], ), StatefulShellBranch( navigatorKey: favoritesNavigatorKey, diff --git a/lib/songs/bloc/song_bloc.dart b/lib/song/bloc/song_bloc.dart similarity index 94% rename from lib/songs/bloc/song_bloc.dart rename to lib/song/bloc/song_bloc.dart index c0f6e75..9cb96fe 100644 --- a/lib/songs/bloc/song_bloc.dart +++ b/lib/song/bloc/song_bloc.dart @@ -19,8 +19,8 @@ class SongBloc extends Bloc { SongsRequested event, Emitter emit, ) async { + emit(state.copyWith(status: SongStatus.loading)); try { - emit(state.copyWith(status: SongStatus.loading)); final songs = await _swiftifyRepository.getSongsByAlbum( albumId: event.albumId, ); diff --git a/lib/songs/bloc/song_event.dart b/lib/song/bloc/song_event.dart similarity index 100% rename from lib/songs/bloc/song_event.dart rename to lib/song/bloc/song_event.dart diff --git a/lib/songs/bloc/song_state.dart b/lib/song/bloc/song_state.dart similarity index 92% rename from lib/songs/bloc/song_state.dart rename to lib/song/bloc/song_state.dart index 0236743..718c8c2 100644 --- a/lib/songs/bloc/song_state.dart +++ b/lib/song/bloc/song_state.dart @@ -13,7 +13,6 @@ class SongState extends Equatable { bool get isLoading => status == SongStatus.loading; bool get isSuccess => status == SongStatus.success; - bool get isFailure => status == SongStatus.failure; SongState copyWith({ List? songs, diff --git a/lib/songs/song.dart b/lib/song/song.dart similarity index 54% rename from lib/songs/song.dart rename to lib/song/song.dart index 98152e0..8b2058d 100644 --- a/lib/songs/song.dart +++ b/lib/song/song.dart @@ -1 +1,2 @@ export 'bloc/song_bloc.dart'; +export 'view/view.dart'; diff --git a/lib/song/view/song_page.dart b/lib/song/view/song_page.dart new file mode 100644 index 0000000..28f7f11 --- /dev/null +++ b/lib/song/view/song_page.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:swiftify/album/album.dart'; +import 'package:swiftify/song/song.dart'; +import 'package:swiftify_repository/swiftify_repository.dart'; + +class SongPage extends StatelessWidget { + const SongPage({ + required this.albumData, + super.key, + }); + + factory SongPage.pageBuilder(_, GoRouterState? state) { + final albumData = state!.extra! as AlbumData; + return SongPage(albumData: albumData); + } + + final AlbumData albumData; + + static const routeName = 'songs'; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SongBloc( + swiftifyRepository: context.read(), + )..add( + SongsRequested( + albumId: albumData.albumId, + ), + ), + child: Scaffold( + appBar: AppBar( + title: Text(albumData.albumTitle), + ), + body: SongView( + albumTitle: albumData.albumTitle, + coverAlbum: albumData.coverAlbum, + releaseDate: albumData.albumReleaseDate, + ), + ), + ); + } +} diff --git a/lib/song/view/song_view.dart b/lib/song/view/song_view.dart new file mode 100644 index 0000000..aa4d89d --- /dev/null +++ b/lib/song/view/song_view.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:swiftify/song/song.dart'; +import 'package:swiftify_repository/swiftify_repository.dart'; + +class SongView extends StatelessWidget { + const SongView({ + required this.albumTitle, + required this.coverAlbum, + required this.releaseDate, + super.key, + }); + + final String albumTitle; + final String? coverAlbum; + final String releaseDate; + + @override + Widget build(BuildContext context) { + final songs = context.select((SongBloc bloc) => bloc.state.songs); + + final isLoading = context.select((SongBloc bloc) => bloc.state.isLoading); + + return ListView.builder( + itemCount: songs.length, + itemBuilder: (context, index) { + final song = songs[index]; + return isLoading + ? const Center(child: CircularProgressIndicator()) + : SongItem(coverAlbum: coverAlbum, song: song); + }, + ); + } +} + +class SongItem extends StatelessWidget { + const SongItem({ + required this.coverAlbum, + required this.song, + super.key, + }); + + final String? coverAlbum; + final Song song; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + return Card( + margin: const EdgeInsets.all(8), + child: ListTile( + leading: AlbumSongImage(coverAlbum: coverAlbum), + title: Text( + song.title, + style: textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold), + ), + subtitle: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Taylor Swift'), + Text(song.duration ?? ''), + if (song.genres != null && song.genres!.isNotEmpty) ...[ + Row( + children: song.genres! + .map( + (genre) => Chip( + label: Text(genre, style: textTheme.labelMedium), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + ) + .toList(), + ), + ], + ], + ), + ), + ); + } +} + +class AlbumSongImage extends StatelessWidget { + const AlbumSongImage({ + required this.coverAlbum, + super.key, + }); + + final String? coverAlbum; + + @override + Widget build(BuildContext context) { + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: Image.network( + coverAlbum!, + height: 100, + ), + ); + } +} diff --git a/lib/song/view/view.dart b/lib/song/view/view.dart new file mode 100644 index 0000000..11a7e1a --- /dev/null +++ b/lib/song/view/view.dart @@ -0,0 +1,2 @@ +export 'song_page.dart'; +export 'song_view.dart'; diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 58aa9d9..84b70ed 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -231,7 +231,7 @@ class AppTheme { bodyColor: colorScheme.onSurface, displayColor: colorScheme.onSurface, ), - scaffoldBackgroundColor: colorScheme.surfaceContainer, + scaffoldBackgroundColor: colorScheme.surface, canvasColor: colorScheme.surface, appBarTheme: AppBarTheme( backgroundColor: colorScheme.primary, @@ -241,7 +241,11 @@ class AppTheme { backgroundColor: colorScheme.surface, selectedItemColor: colorScheme.primary, unselectedItemColor: colorScheme.onSurfaceVariant, - elevation: 4, + ), + cardTheme: CardTheme( + color: colorScheme.surfaceContainer, + shadowColor: colorScheme.shadow, + elevation: colorScheme.brightness == Brightness.dark ? 4 : 2, ), ); } diff --git a/packages/swiftify_repository/lib/src/swiftify_repository.dart b/packages/swiftify_repository/lib/src/swiftify_repository.dart index c6f2ba4..93adc73 100644 --- a/packages/swiftify_repository/lib/src/swiftify_repository.dart +++ b/packages/swiftify_repository/lib/src/swiftify_repository.dart @@ -35,8 +35,10 @@ class SwiftifyRepository { }) async { try { final songsResponse = - await apiClient.get>>('albums/$albumId'); - return songsResponse.map(Song.fromJson).toList(); + await apiClient.get>('albums/$albumId'); + return songsResponse + .map((e) => Song.fromJson(e as Map)) + .toList(); } catch (e, st) { Error.throwWithStackTrace(GetSongsException(e), st); } diff --git a/test/songs/bloc/song_bloc_test.dart b/test/songs/bloc/song_bloc_test.dart index d3d65ed..76f65bc 100644 --- a/test/songs/bloc/song_bloc_test.dart +++ b/test/songs/bloc/song_bloc_test.dart @@ -3,7 +3,7 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:swiftify/songs/song.dart'; +import 'package:swiftify/song/song.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; class _MockSwiftifyRepository extends Mock implements SwiftifyRepository {} diff --git a/test/songs/bloc/song_event_test.dart b/test/songs/bloc/song_event_test.dart index 1a3a32c..5b45d94 100644 --- a/test/songs/bloc/song_event_test.dart +++ b/test/songs/bloc/song_event_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:swiftify/songs/song.dart'; +import 'package:swiftify/song/song.dart'; void main() { group('SongsEvent', () { diff --git a/test/songs/bloc/song_state_test.dart b/test/songs/bloc/song_state_test.dart index 5c98450..f94d385 100644 --- a/test/songs/bloc/song_state_test.dart +++ b/test/songs/bloc/song_state_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:swiftify/songs/song.dart'; +import 'package:swiftify/song/song.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; void main() { From b9d4c9fd29ac43680dc407c11d4b11300d1cc8f7 Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Fri, 17 Jan 2025 11:07:11 +0100 Subject: [PATCH 02/10] feat: ui --- lib/song/view/song_view.dart | 101 +++++++++++++++++++++++++---------- lib/theme/app_theme.dart | 7 ++- 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/lib/song/view/song_view.dart b/lib/song/view/song_view.dart index aa4d89d..5d83603 100644 --- a/lib/song/view/song_view.dart +++ b/lib/song/view/song_view.dart @@ -27,7 +27,10 @@ class SongView extends StatelessWidget { final song = songs[index]; return isLoading ? const Center(child: CircularProgressIndicator()) - : SongItem(coverAlbum: coverAlbum, song: song); + : SongItem( + coverAlbum: coverAlbum, + song: song, + ); }, ); } @@ -50,34 +53,34 @@ class SongItem extends StatelessWidget { return Card( margin: const EdgeInsets.all(8), - child: ListTile( - leading: AlbumSongImage(coverAlbum: coverAlbum), - title: Text( - song.title, - style: textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold), - ), - subtitle: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Taylor Swift'), - Text(song.duration ?? ''), - if (song.genres != null && song.genres!.isNotEmpty) ...[ - Row( - children: song.genres! - .map( - (genre) => Chip( - label: Text(genre, style: textTheme.labelMedium), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - ), - ) - .toList(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AlbumSongImage(coverAlbum: coverAlbum), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + song.title, + style: textTheme.bodyLarge + ?.copyWith(fontWeight: FontWeight.bold), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Text('Taylor Swift'), + Text(song.duration ?? ''), + if (song.genres != null && song.genres!.isNotEmpty) ...[ + SongGenres(genres: song.genres), + ], + ], ), - ], - ], - ), + ), + ), + ], ), ); } @@ -93,6 +96,8 @@ class AlbumSongImage extends StatelessWidget { @override Widget build(BuildContext context) { + final height = MediaQuery.sizeOf(context).height * 0.13; + return Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -100,8 +105,46 @@ class AlbumSongImage extends StatelessWidget { ), child: Image.network( coverAlbum!, - height: 100, + fit: BoxFit.cover, + height: height, ), ); } } + +class SongGenres extends StatelessWidget { + const SongGenres({ + required this.genres, + super.key, + }); + + final List? genres; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + return Row( + children: genres! + .map( + (genre) => Padding( + padding: const EdgeInsets.only( + left: 4, + right: 4, + ), + child: Chip( + label: Text( + genre, + style: textTheme.labelMedium, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + ), + ) + .toList(), + ); + } +} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 84b70ed..056f03e 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -242,10 +242,9 @@ class AppTheme { selectedItemColor: colorScheme.primary, unselectedItemColor: colorScheme.onSurfaceVariant, ), - cardTheme: CardTheme( - color: colorScheme.surfaceContainer, - shadowColor: colorScheme.shadow, - elevation: colorScheme.brightness == Brightness.dark ? 4 : 2, + chipTheme: ChipThemeData( + backgroundColor: colorScheme.secondaryContainer, + brightness: colorScheme.brightness, ), ); } From b2e2938ea305c47adf2d8b5dd18dafc2f6de4dad Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Fri, 17 Jan 2025 12:34:31 +0100 Subject: [PATCH 03/10] feat: update ui --- lib/song/view/song_view.dart | 138 +++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 55 deletions(-) diff --git a/lib/song/view/song_view.dart b/lib/song/view/song_view.dart index 5d83603..9601e76 100644 --- a/lib/song/view/song_view.dart +++ b/lib/song/view/song_view.dart @@ -27,7 +27,7 @@ class SongView extends StatelessWidget { final song = songs[index]; return isLoading ? const Center(child: CircularProgressIndicator()) - : SongItem( + : SongCard( coverAlbum: coverAlbum, song: song, ); @@ -36,8 +36,8 @@ class SongView extends StatelessWidget { } } -class SongItem extends StatelessWidget { - const SongItem({ +class SongCard extends StatelessWidget { + const SongCard({ required this.coverAlbum, required this.song, super.key, @@ -48,37 +48,16 @@ class SongItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - return Card( margin: const EdgeInsets.all(8), child: Row( mainAxisSize: MainAxisSize.min, children: [ AlbumSongImage(coverAlbum: coverAlbum), - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - song.title, - style: textTheme.bodyLarge - ?.copyWith(fontWeight: FontWeight.bold), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const Text('Taylor Swift'), - Text(song.duration ?? ''), - if (song.genres != null && song.genres!.isNotEmpty) ...[ - SongGenres(genres: song.genres), - ], - ], - ), - ), + SongInformation( + title: song.title, + duration: song.duration, + genres: song.genres, ), ], ), @@ -96,17 +75,64 @@ class AlbumSongImage extends StatelessWidget { @override Widget build(BuildContext context) { - final height = MediaQuery.sizeOf(context).height * 0.13; - - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + return Expanded( + child: Container( + constraints: const BoxConstraints( + maxWidth: 80, + ), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: Image.network( + coverAlbum!, + fit: BoxFit.cover, + ), ), - child: Image.network( - coverAlbum!, - fit: BoxFit.cover, - height: height, + ); + } +} + +class SongInformation extends StatelessWidget { + const SongInformation({ + required this.title, + required this.duration, + required this.genres, + super.key, + }); + + final String title; + final String? duration; + final List? genres; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + return Flexible( + flex: 3, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.visible, + ), + Text(duration ?? '', style: textTheme.labelLarge), + if (genres != null && genres!.isNotEmpty) ...[ + const SizedBox(height: 4), + SongGenres(genres: genres), + ], + ], + ), ), ); } @@ -124,27 +150,29 @@ class SongGenres extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; return Row( - children: genres! - .map( - (genre) => Padding( - padding: const EdgeInsets.only( - left: 4, - right: 4, - ), - child: Chip( - label: Text( - genre, - style: textTheme.labelMedium, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), + children: [ + for (final genre in genres!) + Row( + children: [ + Text( + genre, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.primary, ), ), - ), - ) - .toList(), + if (genre != genres!.last) + Text( + ' • ', + style: textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + ), + ], + ), + ], ); } } From c89586f810451f371984fd81d0f1915e9e14e270 Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Fri, 17 Jan 2025 12:35:54 +0100 Subject: [PATCH 04/10] feat: move circular progress indicator --- lib/song/view/song_view.dart | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/song/view/song_view.dart b/lib/song/view/song_view.dart index 9601e76..d5facec 100644 --- a/lib/song/view/song_view.dart +++ b/lib/song/view/song_view.dart @@ -18,21 +18,22 @@ class SongView extends StatelessWidget { @override Widget build(BuildContext context) { final songs = context.select((SongBloc bloc) => bloc.state.songs); - final isLoading = context.select((SongBloc bloc) => bloc.state.isLoading); - return ListView.builder( - itemCount: songs.length, - itemBuilder: (context, index) { - final song = songs[index]; - return isLoading - ? const Center(child: CircularProgressIndicator()) - : SongCard( + return isLoading + ? const Center( + child: CircularProgressIndicator(), + ) + : ListView.builder( + itemCount: songs.length, + itemBuilder: (context, index) { + final song = songs[index]; + return SongCard( coverAlbum: coverAlbum, song: song, ); - }, - ); + }, + ); } } @@ -53,7 +54,7 @@ class SongCard extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - AlbumSongImage(coverAlbum: coverAlbum), + AlbumCoverImage(coverAlbum: coverAlbum), SongInformation( title: song.title, duration: song.duration, @@ -65,8 +66,8 @@ class SongCard extends StatelessWidget { } } -class AlbumSongImage extends StatelessWidget { - const AlbumSongImage({ +class AlbumCoverImage extends StatelessWidget { + const AlbumCoverImage({ required this.coverAlbum, super.key, }); From b3cb5a283c06cab10e4d264269eee2702705110f Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Mon, 20 Jan 2025 17:26:19 +0100 Subject: [PATCH 05/10] test: remove test --- test/songs/bloc/song_state_test.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/songs/bloc/song_state_test.dart b/test/songs/bloc/song_state_test.dart index f94d385..90e127b 100644 --- a/test/songs/bloc/song_state_test.dart +++ b/test/songs/bloc/song_state_test.dart @@ -82,11 +82,6 @@ void main() { expect(state.isSuccess, isTrue); }); - test('isFailure', () { - final state = SongState(status: SongStatus.failure); - expect(state.isFailure, isTrue); - }); - test('isLoading', () { final state = SongState(status: SongStatus.loading); expect(state.isLoading, isTrue); From cabed7fe2197c806df8162a6acd5f868f0923a2d Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Tue, 21 Jan 2025 15:37:18 +0100 Subject: [PATCH 06/10] refactor: change to typed go route (#16) * refactor: change go router * refactor: add new routes * test: test * test: test --- .github/workflows/main.yaml | 1 + lib/album/album.dart | 1 - lib/album/models/album_data.dart | 13 -- lib/album/models/models.dart | 1 - lib/album/view/album_page.dart | 2 - lib/album/view/album_view.dart | 18 +- lib/app/app_router/app_router.dart | 32 +--- lib/app/app_router/router.dart | 4 + lib/app/app_router/routes/albums_routes.dart | 34 ---- .../app_router/routes/dashboard_routes.dart | 32 ---- lib/app/app_router/routes/routes.dart | 148 +++++++++++++++++ lib/app/app_router/routes/routes.g.dart | 122 ++++++++++++++ lib/app/app_router/routes/theme_route.dart | 12 -- .../{ => widgets}/modal_pop_up_page.dart | 0 .../scaffold_with_bottom_navigation.dart | 37 +++-- lib/app/app_router/widgets/widgets.dart | 2 + lib/favorites/view/favorites_page.dart | 2 - lib/song/view/song_page.dart | 31 ++-- lib/theme/view/theme_bottom_sheet.dart | 60 +++---- lib/widgets/bottom_sheet_base.dart | 7 +- pubspec.lock | 154 +++++++++++++++++- pubspec.yaml | 2 + test/album/view/album_view_test.dart | 29 ++++ test/app/app_router/app_router_test.dart | 59 ++++++- 24 files changed, 585 insertions(+), 218 deletions(-) delete mode 100644 lib/album/models/album_data.dart delete mode 100644 lib/album/models/models.dart create mode 100644 lib/app/app_router/router.dart delete mode 100644 lib/app/app_router/routes/albums_routes.dart delete mode 100644 lib/app/app_router/routes/dashboard_routes.dart create mode 100644 lib/app/app_router/routes/routes.dart create mode 100644 lib/app/app_router/routes/routes.g.dart delete mode 100644 lib/app/app_router/routes/theme_route.dart rename lib/app/app_router/{ => widgets}/modal_pop_up_page.dart (100%) rename lib/app/app_router/{ => widgets}/scaffold_with_bottom_navigation.dart (58%) create mode 100644 lib/app/app_router/widgets/widgets.dart diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d7cbbcf..b0f625f 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -20,6 +20,7 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: flutter_channel: stable + coverage_excludes: "**/*.g.dart" spell-check: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 diff --git a/lib/album/album.dart b/lib/album/album.dart index 11f36db..ed640dc 100644 --- a/lib/album/album.dart +++ b/lib/album/album.dart @@ -1,3 +1,2 @@ export 'bloc/album_bloc.dart'; -export 'models/models.dart'; export 'view/view.dart'; diff --git a/lib/album/models/album_data.dart b/lib/album/models/album_data.dart deleted file mode 100644 index 13a5666..0000000 --- a/lib/album/models/album_data.dart +++ /dev/null @@ -1,13 +0,0 @@ -class AlbumData { - AlbumData({ - required this.albumId, - required this.albumTitle, - required this.coverAlbum, - required this.albumReleaseDate, - }); - - final int albumId; - final String albumTitle; - final String? coverAlbum; - final String albumReleaseDate; -} diff --git a/lib/album/models/models.dart b/lib/album/models/models.dart deleted file mode 100644 index ee55682..0000000 --- a/lib/album/models/models.dart +++ /dev/null @@ -1 +0,0 @@ -export 'album_data.dart'; diff --git a/lib/album/view/album_page.dart b/lib/album/view/album_page.dart index 9ce1462..b8711c2 100644 --- a/lib/album/view/album_page.dart +++ b/lib/album/view/album_page.dart @@ -6,8 +6,6 @@ import 'package:swiftify_repository/swiftify_repository.dart'; class AlbumPage extends StatelessWidget { const AlbumPage({super.key}); - static const routeName = '/album'; - @override Widget build(BuildContext context) { return BlocProvider( diff --git a/lib/album/view/album_view.dart b/lib/album/view/album_view.dart index 6a8620d..ad1ff8a 100644 --- a/lib/album/view/album_view.dart +++ b/lib/album/view/album_view.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; import 'package:swiftify/album/album.dart'; -import 'package:swiftify/song/song.dart'; +import 'package:swiftify/app/app_router/router.dart'; class AlbumView extends StatelessWidget { const AlbumView({super.key}); @@ -82,15 +81,12 @@ class AlbumsContent extends StatelessWidget { itemBuilder: (context, index) { final album = albums[index]; return GestureDetector( - onTap: () => context.push( - '${AlbumPage.routeName}/${SongPage.routeName}', - extra: AlbumData( - albumId: album.albumId, - albumTitle: album.title, - coverAlbum: album.coverAlbum, - albumReleaseDate: album.releaseDate, - ), - ), + onTap: () => SongPageRoute( + id: album.albumId, + albumTitle: album.title, + coverAlbum: album.coverAlbum, + albumReleaseDate: album.releaseDate, + ).go(context), child: AlbumItem( title: album.title, releaseDate: album.releaseDate, diff --git a/lib/app/app_router/app_router.dart b/lib/app/app_router/app_router.dart index bcbfd18..1e5621c 100644 --- a/lib/app/app_router/app_router.dart +++ b/lib/app/app_router/app_router.dart @@ -1,39 +1,17 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:swiftify/album/album.dart'; -import 'package:swiftify/app/app_router/routes/dashboard_routes.dart'; -import 'package:swiftify/app/app_router/routes/theme_route.dart'; +import 'package:swiftify/app/app_router/routes/routes.dart'; export 'go_router_observer.dart'; -final rootNavigatorKey = GlobalKey(); - -class AppRoute extends GoRoute { - AppRoute({ - required super.path, - super.name, - super.builder, - super.pageBuilder, - super.parentNavigatorKey, - super.routes = const [], - }); - - @override - GoRouterRedirect get redirect => (context, state) { - return null; - }; -} - class AppRouter { AppRouter({ - String? initialLocation = AlbumPage.routeName, List navigatorObservers = const [], GoRouter? goRouter, }) { _goRouter = goRouter ?? _routes( - initialLocation, navigatorObservers, ); } @@ -42,18 +20,14 @@ class AppRouter { GoRouter get routes => _goRouter; GoRouter _routes( - String? initialLocation, List navigatorObservers, ) { return GoRouter( - initialLocation: initialLocation, + initialLocation: const AlbumPageRoute().location, observers: navigatorObservers, debugLogDiagnostics: kDebugMode, navigatorKey: rootNavigatorKey, - routes: [ - dashBoardRoutes, - themeRoute, - ], + routes: $appRoutes, ); } } diff --git a/lib/app/app_router/router.dart b/lib/app/app_router/router.dart new file mode 100644 index 0000000..97bae07 --- /dev/null +++ b/lib/app/app_router/router.dart @@ -0,0 +1,4 @@ +export 'app_router.dart'; +export 'go_router_observer.dart'; +export 'routes/routes.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/app/app_router/routes/albums_routes.dart b/lib/app/app_router/routes/albums_routes.dart deleted file mode 100644 index 1e809ff..0000000 --- a/lib/app/app_router/routes/albums_routes.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:swiftify/album/album.dart'; -import 'package:swiftify/app/app_router/app_router.dart'; -import 'package:swiftify/song/song.dart'; - -final albumsRoutes = AppRoute( - name: AlbumPage.routeName, - path: AlbumPage.routeName, - builder: (context, state) => const AlbumPage(), - routes: [ - AppRoute( - parentNavigatorKey: rootNavigatorKey, - name: '${AlbumPage.routeName}/${SongPage.routeName}', - path: SongPage.routeName, - pageBuilder: (context, state) => CustomTransitionPage( - child: SongPage.pageBuilder(context, state), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return SlideTransition( - position: Tween( - begin: const Offset(1, 0), - end: Offset.zero, - ) - .chain( - CurveTween(curve: Curves.easeInOut), - ) - .animate(animation), - child: child, - ); - }, - ), - ), - ], -); diff --git a/lib/app/app_router/routes/dashboard_routes.dart b/lib/app/app_router/routes/dashboard_routes.dart deleted file mode 100644 index a33af11..0000000 --- a/lib/app/app_router/routes/dashboard_routes.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:swiftify/app/app_router/app_router.dart'; -import 'package:swiftify/app/app_router/routes/albums_routes.dart'; -import 'package:swiftify/app/app_router/scaffold_with_bottom_navigation.dart'; -import 'package:swiftify/favorites/favorites.dart'; - -final albumNavigatorKey = GlobalKey(debugLabel: 'albums'); -final favoritesNavigatorKey = - GlobalKey(debugLabel: 'favorites'); - -final dashBoardRoutes = StatefulShellRoute.indexedStack( - builder: (context, state, navigationShell) => ScaffoldWithBottomNavigation( - navigationShell: navigationShell, - ), - branches: [ - StatefulShellBranch( - navigatorKey: albumNavigatorKey, - routes: [albumsRoutes], - ), - StatefulShellBranch( - navigatorKey: favoritesNavigatorKey, - routes: [ - AppRoute( - name: FavoritesPage.routeName, - path: FavoritesPage.routeName, - builder: (context, state) => const FavoritesPage(), - ), - ], - ), - ], -); diff --git a/lib/app/app_router/routes/routes.dart b/lib/app/app_router/routes/routes.dart new file mode 100644 index 0000000..4600a23 --- /dev/null +++ b/lib/app/app_router/routes/routes.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:swiftify/album/album.dart'; +import 'package:swiftify/app/app_router/router.dart'; +import 'package:swiftify/favorites/favorites.dart'; +import 'package:swiftify/song/song.dart'; +import 'package:swiftify/theme/theme.dart'; + +part 'routes.g.dart'; + +final shellNavigatorKey = GlobalKey(); +final rootNavigatorKey = GlobalKey(); + +@TypedShellRoute( + routes: >[ + TypedGoRoute( + path: AlbumPageRoute.path, + routes: [ + TypedGoRoute( + path: SongPageRoute.path, + ), + ], + ), + TypedGoRoute( + path: FavoritesPageRoute.path, + ), + TypedGoRoute( + path: ThemePageRoute.path, + ), + ], +) +class AppShellRoute extends ShellRouteData { + const AppShellRoute(); + + /// The navigator key for the shell navigator. + static final GlobalKey $navigatorKey = shellNavigatorKey; + + @override + Page pageBuilder( + BuildContext context, + GoRouterState state, + Widget navigator, + ) { + return CustomTransitionPage( + child: ScaffoldWithBottomNavigation(child: navigator), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + ); + } +} + +@immutable +class ThemePageRoute extends GoRouteData { + const ThemePageRoute(); + + static const path = '/theme'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return ModalPopupPage( + builder: (context) => const ThemeBottomSheet(), + ); + } +} + +@immutable +class FavoritesPageRoute extends GoRouteData { + const FavoritesPageRoute(); + + static const path = '/favorites'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return CustomTransitionPage( + child: const FavoritesPage(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + ); + } +} + +@immutable +class AlbumPageRoute extends GoRouteData { + const AlbumPageRoute(); + static const path = '/'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return CustomTransitionPage( + child: const AlbumPage(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + ); + } +} + +@immutable +class SongPageRoute extends GoRouteData { + const SongPageRoute({ + required this.id, + this.albumTitle, + this.coverAlbum, + this.albumReleaseDate, + }); + + /// The id of the album to show + /// This is passed in the path as a parameter + final int id; + + /// The title of the album + /// This is passed in as a query parameter. + /// It is optional and can be null. + final String? albumTitle; + + /// The cover of the album + /// This is passed in as a query parameter. + /// It is optional and can be null. + final String? coverAlbum; + + /// The release date of the album + /// This is passed in as a query parameter. + /// It is optional and can be null. + final String? albumReleaseDate; + + static const path = 'songs/:id'; + + /// The parent navigator key. + /// This is used to push the page on the parent navigator + /// when the page is a nested route + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return CustomTransitionPage( + child: SongPage( + albumId: id, + albumTitle: albumTitle, + coverAlbum: coverAlbum, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + ); + } +} diff --git a/lib/app/app_router/routes/routes.g.dart b/lib/app/app_router/routes/routes.g.dart new file mode 100644 index 0000000..438a892 --- /dev/null +++ b/lib/app/app_router/routes/routes.g.dart @@ -0,0 +1,122 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'routes.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [ + $appShellRoute, + ]; + +RouteBase get $appShellRoute => ShellRouteData.$route( + navigatorKey: AppShellRoute.$navigatorKey, + factory: $AppShellRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: '/', + factory: $AlbumPageRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'songs/:id', + parentNavigatorKey: SongPageRoute.$parentNavigatorKey, + factory: $SongPageRouteExtension._fromState, + ), + ], + ), + GoRouteData.$route( + path: '/favorites', + factory: $FavoritesPageRouteExtension._fromState, + ), + GoRouteData.$route( + path: '/theme', + factory: $ThemePageRouteExtension._fromState, + ), + ], + ); + +extension $AppShellRouteExtension on AppShellRoute { + static AppShellRoute _fromState(GoRouterState state) => const AppShellRoute(); +} + +extension $AlbumPageRouteExtension on AlbumPageRoute { + static AlbumPageRoute _fromState(GoRouterState state) => + const AlbumPageRoute(); + + String get location => GoRouteData.$location( + '/', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $SongPageRouteExtension on SongPageRoute { + static SongPageRoute _fromState(GoRouterState state) => SongPageRoute( + id: int.parse(state.pathParameters['id']!), + albumTitle: state.uri.queryParameters['album-title'], + coverAlbum: state.uri.queryParameters['cover-album'], + albumReleaseDate: state.uri.queryParameters['album-release-date'], + ); + + String get location => GoRouteData.$location( + '/songs/${Uri.encodeComponent(id.toString())}', + queryParams: { + if (albumTitle != null) 'album-title': albumTitle, + if (coverAlbum != null) 'cover-album': coverAlbum, + if (albumReleaseDate != null) 'album-release-date': albumReleaseDate, + }, + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $FavoritesPageRouteExtension on FavoritesPageRoute { + static FavoritesPageRoute _fromState(GoRouterState state) => + const FavoritesPageRoute(); + + String get location => GoRouteData.$location( + '/favorites', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $ThemePageRouteExtension on ThemePageRoute { + static ThemePageRoute _fromState(GoRouterState state) => + const ThemePageRoute(); + + String get location => GoRouteData.$location( + '/theme', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} diff --git a/lib/app/app_router/routes/theme_route.dart b/lib/app/app_router/routes/theme_route.dart deleted file mode 100644 index 0ee8474..0000000 --- a/lib/app/app_router/routes/theme_route.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:swiftify/app/app_router/app_router.dart'; -import 'package:swiftify/app/app_router/modal_pop_up_page.dart'; -import 'package:swiftify/theme/view/theme_bottom_sheet.dart'; - -final themeRoute = AppRoute( - parentNavigatorKey: rootNavigatorKey, - name: ThemeBottomSheet.routeName, - path: ThemeBottomSheet.routeName, - pageBuilder: (context, state) => ModalPopupPage( - builder: (context) => ThemeBottomSheet.pageBuilder(context, state), - ), -); diff --git a/lib/app/app_router/modal_pop_up_page.dart b/lib/app/app_router/widgets/modal_pop_up_page.dart similarity index 100% rename from lib/app/app_router/modal_pop_up_page.dart rename to lib/app/app_router/widgets/modal_pop_up_page.dart diff --git a/lib/app/app_router/scaffold_with_bottom_navigation.dart b/lib/app/app_router/widgets/scaffold_with_bottom_navigation.dart similarity index 58% rename from lib/app/app_router/scaffold_with_bottom_navigation.dart rename to lib/app/app_router/widgets/scaffold_with_bottom_navigation.dart index 961a57a..a27765a 100644 --- a/lib/app/app_router/scaffold_with_bottom_navigation.dart +++ b/lib/app/app_router/widgets/scaffold_with_bottom_navigation.dart @@ -1,28 +1,39 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:swiftify/app/app_router/router.dart'; import 'package:swiftify/l10n/l10n.dart'; -import 'package:swiftify/theme/view/theme_bottom_sheet.dart'; class ScaffoldWithBottomNavigation extends StatelessWidget { const ScaffoldWithBottomNavigation({ - required this.navigationShell, + required this.child, super.key, }); - final StatefulNavigationShell navigationShell; + final Widget child; - void _goBranch(int index) { - navigationShell.goBranch( - index, - initialLocation: index == navigationShell.currentIndex, - ); + int getCurrentIndex(BuildContext context) { + final path = GoRouterState.of(context).uri.path; + if (path == AlbumPageRoute.path) { + return 0; + } else { + return 1; + } + } + + void _onTap(int index, BuildContext context) { + switch (index) { + case 0: + const AlbumPageRoute().go(context); + case 1: + const FavoritesPageRoute().go(context); + } } @override Widget build(BuildContext context) { final l10n = context.l10n; - final currentIndex = navigationShell.currentIndex; + final currentIndex = getCurrentIndex(context); return Scaffold( appBar: AppBar( @@ -32,14 +43,14 @@ class ScaffoldWithBottomNavigation extends StatelessWidget { actions: [ IconButton( icon: const Icon(Icons.lightbulb_outline), - onPressed: () => context.push(ThemeBottomSheet.routeName), + onPressed: () => const ThemePageRoute().push(context), ), ], ), - body: navigationShell, + body: child, bottomNavigationBar: BottomNavigationBar( - currentIndex: navigationShell.currentIndex, - onTap: _goBranch, + currentIndex: currentIndex, + onTap: (index) => _onTap(index, context), items: [ BottomNavigationBarItem( icon: const Icon(Icons.album_outlined), diff --git a/lib/app/app_router/widgets/widgets.dart b/lib/app/app_router/widgets/widgets.dart new file mode 100644 index 0000000..276ad0a --- /dev/null +++ b/lib/app/app_router/widgets/widgets.dart @@ -0,0 +1,2 @@ +export 'modal_pop_up_page.dart'; +export 'scaffold_with_bottom_navigation.dart'; diff --git a/lib/favorites/view/favorites_page.dart b/lib/favorites/view/favorites_page.dart index 21f1eb4..2c9dbbd 100644 --- a/lib/favorites/view/favorites_page.dart +++ b/lib/favorites/view/favorites_page.dart @@ -3,8 +3,6 @@ import 'package:flutter/widgets.dart'; class FavoritesPage extends StatelessWidget { const FavoritesPage({super.key}); - static const routeName = '/favorites'; - @override Widget build(BuildContext context) { return const Center( diff --git a/lib/song/view/song_page.dart b/lib/song/view/song_page.dart index 28f7f11..c5d87d4 100644 --- a/lib/song/view/song_page.dart +++ b/lib/song/view/song_page.dart @@ -1,24 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -import 'package:swiftify/album/album.dart'; import 'package:swiftify/song/song.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; class SongPage extends StatelessWidget { const SongPage({ - required this.albumData, + required this.albumId, + this.albumTitle, + this.coverAlbum, + this.albumReleaseDate, super.key, }); - factory SongPage.pageBuilder(_, GoRouterState? state) { - final albumData = state!.extra! as AlbumData; - return SongPage(albumData: albumData); - } - - final AlbumData albumData; - - static const routeName = 'songs'; + final int albumId; + final String? albumTitle; + final String? coverAlbum; + final String? albumReleaseDate; @override Widget build(BuildContext context) { @@ -26,18 +23,16 @@ class SongPage extends StatelessWidget { create: (context) => SongBloc( swiftifyRepository: context.read(), )..add( - SongsRequested( - albumId: albumData.albumId, - ), + SongsRequested(albumId: albumId), ), child: Scaffold( appBar: AppBar( - title: Text(albumData.albumTitle), + title: Text(albumTitle ?? ''), ), body: SongView( - albumTitle: albumData.albumTitle, - coverAlbum: albumData.coverAlbum, - releaseDate: albumData.albumReleaseDate, + albumTitle: albumTitle ?? '', + coverAlbum: coverAlbum, + releaseDate: albumReleaseDate ?? '', ), ), ); diff --git a/lib/theme/view/theme_bottom_sheet.dart b/lib/theme/view/theme_bottom_sheet.dart index 44a0c0e..0cb7104 100644 --- a/lib/theme/view/theme_bottom_sheet.dart +++ b/lib/theme/view/theme_bottom_sheet.dart @@ -7,46 +7,36 @@ import 'package:swiftify/widgets/bottom_sheet_base.dart'; class ThemeBottomSheet extends StatelessWidget { const ThemeBottomSheet({super.key}); - factory ThemeBottomSheet.pageBuilder(_, __) { - return const ThemeBottomSheet(); - } - - static const routeName = '/theme'; - @override Widget build(BuildContext context) { final l10n = context.l10n; return BottomSheetBase( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: GridView.count( - crossAxisCount: 3, - mainAxisSpacing: 8, - children: [ - CardItem( - title: l10n.system, - iconData: Icons.brightness_auto, - onTap: () => context - .read() - .add(const ThemeModeChanged(ThemeMode.system)), - ), - CardItem( - title: l10n.light, - iconData: Icons.sunny, - onTap: () => context - .read() - .add(const ThemeModeChanged(ThemeMode.light)), - ), - CardItem( - title: l10n.dark, - iconData: Icons.nightlight_round, - onTap: () => context - .read() - .add(const ThemeModeChanged(ThemeMode.dark)), - ), - ], - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + CardItem( + title: l10n.system, + iconData: Icons.brightness_auto, + onTap: () => context + .read() + .add(const ThemeModeChanged(ThemeMode.system)), + ), + CardItem( + title: l10n.light, + iconData: Icons.sunny, + onTap: () => context + .read() + .add(const ThemeModeChanged(ThemeMode.light)), + ), + CardItem( + title: l10n.dark, + iconData: Icons.nightlight_round, + onTap: () => context + .read() + .add(const ThemeModeChanged(ThemeMode.dark)), + ), + ], ), ); } diff --git a/lib/widgets/bottom_sheet_base.dart b/lib/widgets/bottom_sheet_base.dart index dcbed2f..093765e 100644 --- a/lib/widgets/bottom_sheet_base.dart +++ b/lib/widgets/bottom_sheet_base.dart @@ -14,7 +14,7 @@ class BottomSheetBase extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return SizedBox( - height: height ?? MediaQuery.of(context).size.height * 0.3, + height: height ?? MediaQuery.of(context).size.height * 0.2, child: DecoratedBox( decoration: BoxDecoration( color: colorScheme.surfaceTint, @@ -22,10 +22,7 @@ class BottomSheetBase extends StatelessWidget { top: Radius.circular(12), ), ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: child, - ), + child: child, ), ); } diff --git a/pubspec.lock b/pubspec.lock index f8f847a..7a64ab1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -71,6 +71,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" + url: "https://pub.dev" + source: hosted + version: "4.0.3" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + url: "https://pub.dev" + source: hosted + version: "2.4.14" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + url: "https://pub.dev" + source: hosted + version: "8.9.3" characters: dependency: transitive description: @@ -79,6 +143,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" clock: dependency: transitive description: @@ -87,6 +159,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" collection: dependency: transitive description: @@ -119,6 +199,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + url: "https://pub.dev" + source: hosted + version: "3.0.1" diff_match_patch: dependency: transitive description: @@ -159,6 +247,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -211,6 +307,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.6.3" + go_router_builder: + dependency: "direct dev" + description: + name: go_router_builder + sha256: "293e366566c209f70a05508bb5fdd6ce536fa3f75e58ccba1f83341a7943a6be" + url: "https://pub.dev" + source: hosted + version: "2.7.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" hive_ce: dependency: transitive description: @@ -483,6 +595,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.5" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" shelf: dependency: transitive description: @@ -520,6 +640,22 @@ packages: description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" source_map_stack_trace: dependency: transitive description: @@ -560,6 +696,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -633,6 +777,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.5" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" typed_data: dependency: transitive description: @@ -723,4 +875,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index a44ba81..ef00e28 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,8 +25,10 @@ dependencies: dev_dependencies: bloc_test: ^10.0.0 + build_runner: ^2.4.14 flutter_test: sdk: flutter + go_router_builder: ^2.7.3 mocktail: ^1.0.4 very_good_analysis: ^7.0.0 diff --git a/test/album/view/album_view_test.dart b/test/album/view/album_view_test.dart index a9de108..3efff32 100644 --- a/test/album/view/album_view_test.dart +++ b/test/album/view/album_view_test.dart @@ -4,6 +4,7 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'package:mocktail_image_network/mocktail_image_network.dart'; import 'package:swiftify/album/album.dart'; @@ -14,8 +15,11 @@ import '../../helpers/helpers.dart'; class _MockAlbumBloc extends MockBloc implements AlbumBloc {} +class _MockGoRouter extends Mock implements GoRouter {} + void main() { late AlbumBloc albumBloc; + late GoRouter goRouter; final albums = List.generate( 3, (index) => Album( @@ -28,6 +32,7 @@ void main() { setUp(() { albumBloc = _MockAlbumBloc(); + goRouter = _MockGoRouter(); }); testWidgets('renders a CircularProgessIndicator when isLoading', @@ -81,4 +86,28 @@ void main() { expect(find.text('Failed to fetch albums'), findsOneWidget); }); + + testWidgets('navigate to SongsPage when click on a song', (tester) async { + when(() => goRouter.go(any())).thenAnswer((_) async => true); + when(() => albumBloc.state).thenReturn( + AlbumState( + status: AlbumStatus.success, + albums: albums, + ), + ); + + await mockNetworkImages( + () async => tester.pumpApp( + BlocProvider.value( + value: albumBloc, + child: AlbumView(), + ), + goRouter: goRouter, + ), + ); + + await tester.tap(find.text('title 0')); + await tester.pumpAndSettle(); + verify(() => goRouter.go(any())).called(1); + }); } diff --git a/test/app/app_router/app_router_test.dart b/test/app/app_router/app_router_test.dart index 2ffd0a6..3441846 100644 --- a/test/app/app_router/app_router_test.dart +++ b/test/app/app_router/app_router_test.dart @@ -1,10 +1,14 @@ +// ignore_for_file: prefer_const_constructors + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'package:swiftify/album/album.dart'; import 'package:swiftify/app/app_router/app_router.dart'; +import 'package:swiftify/app/app_router/routes/routes.dart'; import 'package:swiftify/favorites/favorites.dart'; +import 'package:swiftify/song/song.dart'; import 'package:swiftify/theme/theme.dart'; import '../../helpers/helpers.dart'; @@ -59,7 +63,7 @@ void main() { appRouter: appRouter, ); - appRouter.routes.go(AlbumPage.routeName); + appRouter.routes.go(AlbumPageRoute.path); await tester.pumpAndSettle(); expect(find.byType(AlbumPage), findsOneWidget); @@ -70,12 +74,7 @@ void main() { appRouter: appRouter, ); - final bottomNav = tester.widget( - find.byType(BottomNavigationBar), - ); - - bottomNav.onTap!(1); - appRouter.routes.go(FavoritesPage.routeName); + appRouter.routes.go(FavoritesPageRoute.path); await tester.pumpAndSettle(); expect(find.byType(FavoritesPage), findsOneWidget); @@ -85,17 +84,59 @@ void main() { await tester.pumpRoutes( appRouter: appRouter, ); - final iconButton = tester.widget( find.byType(IconButton), ); iconButton.onPressed!(); - appRouter.routes.go(ThemeBottomSheet.routeName); + + appRouter.routes.go(ThemePageRoute.path); await tester.pumpAndSettle(); expect(find.byType(ThemeBottomSheet), findsOneWidget); }); + + testWidgets('to SongPage', (tester) async { + await tester.pumpRoutes( + appRouter: appRouter, + ); + + appRouter.routes.go('/songs/1'); + await tester.pumpAndSettle(); + expect(find.byType(SongPage), findsOneWidget); + }); + + group('navigates with BottomNavigationBar', () { + testWidgets('to AlbumPage', (tester) async { + await tester.pumpRoutes( + appRouter: appRouter, + ); + final bottomNav = tester.widget( + find.byType(BottomNavigationBar), + ); + + bottomNav.onTap!(0); + appRouter.routes.go(AlbumPageRoute.path); + + await tester.pumpAndSettle(); + expect(find.byType(AlbumPage), findsOneWidget); + }); + + testWidgets('to FavoritesPage', (tester) async { + await tester.pumpRoutes( + appRouter: appRouter, + ); + final bottomNav = tester.widget( + find.byType(BottomNavigationBar), + ); + + bottomNav.onTap!(1); + appRouter.routes.go(FavoritesPageRoute.path); + + await tester.pumpAndSettle(); + expect(find.byType(FavoritesPage), findsOneWidget); + }); + }); }); }); } From ad930688b111e156389de9a6534fae9a742bc00c Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Wed, 22 Jan 2025 11:09:41 +0100 Subject: [PATCH 07/10] feat: update theme and routes --- lib/app/app_router/routes/routes.dart | 4 +- .../widgets/modal_bottom_sheet_page.dart | 16 + .../app_router/widgets/modal_pop_up_page.dart | 35 -- lib/app/app_router/widgets/widgets.dart | 2 +- lib/theme/app_theme.dart | 320 +++++++++--------- pubspec.lock | 154 +-------- .../{theme_test.dart => app_theme_test.dart} | 8 +- 7 files changed, 182 insertions(+), 357 deletions(-) create mode 100644 lib/app/app_router/widgets/modal_bottom_sheet_page.dart delete mode 100644 lib/app/app_router/widgets/modal_pop_up_page.dart rename test/theme/{theme_test.dart => app_theme_test.dart} (88%) diff --git a/lib/app/app_router/routes/routes.dart b/lib/app/app_router/routes/routes.dart index 4600a23..7770e2a 100644 --- a/lib/app/app_router/routes/routes.dart +++ b/lib/app/app_router/routes/routes.dart @@ -58,8 +58,8 @@ class ThemePageRoute extends GoRouteData { @override Page buildPage(BuildContext context, GoRouterState state) { - return ModalPopupPage( - builder: (context) => const ThemeBottomSheet(), + return ModalBottomSheet( + builder: (_) => const ThemeBottomSheet(), ); } } diff --git a/lib/app/app_router/widgets/modal_bottom_sheet_page.dart b/lib/app/app_router/widgets/modal_bottom_sheet_page.dart new file mode 100644 index 0000000..c462471 --- /dev/null +++ b/lib/app/app_router/widgets/modal_bottom_sheet_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class ModalBottomSheet extends Page { + const ModalBottomSheet({ + required this.builder, + super.key, + }); + final WidgetBuilder builder; + + @override + Route createRoute(BuildContext context) => ModalBottomSheetRoute( + builder: builder, + isScrollControlled: false, + settings: this, + ); +} diff --git a/lib/app/app_router/widgets/modal_pop_up_page.dart b/lib/app/app_router/widgets/modal_pop_up_page.dart deleted file mode 100644 index 247545d..0000000 --- a/lib/app/app_router/widgets/modal_pop_up_page.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/cupertino.dart'; - -class ModalPopupPage extends Page { - const ModalPopupPage({ - required this.builder, - this.anchorPoint, - this.barrierDismissible = true, - this.barrierLabel = 'Dismiss', - this.barrierColor, - this.semanticsDismissible = true, - this.filter, - super.key, - }); - final Offset? anchorPoint; - final Color? barrierColor; - final bool barrierDismissible; - final String barrierLabel; - final bool semanticsDismissible; - final WidgetBuilder builder; - final ImageFilter? filter; - - @override - Route createRoute(BuildContext context) => CupertinoModalPopupRoute( - builder: builder, - barrierDismissible: barrierDismissible, - anchorPoint: anchorPoint, - barrierLabel: barrierLabel, - barrierColor: barrierColor, - filter: filter, - semanticsDismissible: semanticsDismissible, - settings: this, - ); -} diff --git a/lib/app/app_router/widgets/widgets.dart b/lib/app/app_router/widgets/widgets.dart index 276ad0a..c634445 100644 --- a/lib/app/app_router/widgets/widgets.dart +++ b/lib/app/app_router/widgets/widgets.dart @@ -1,2 +1,2 @@ -export 'modal_pop_up_page.dart'; +export 'modal_bottom_sheet_page.dart'; export 'scaffold_with_bottom_navigation.dart'; diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 056f03e..a6c28b9 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -6,51 +6,51 @@ class AppTheme { ColorScheme lightScheme() { return const ColorScheme( brightness: Brightness.light, - primary: Color(0xff1a6b51), - surfaceTint: Color(0xff1a6b51), - onPrimary: Color(0xffffffff), - primaryContainer: Color(0xffa6f2d2), - onPrimaryContainer: Color(0xff002116), - secondary: Color.fromARGB(255, 33, 59, 48), - onSecondary: Color(0xffffffff), - secondaryContainer: Color(0xffcee9db), - onSecondaryContainer: Color(0xff092017), - tertiary: Color(0xff3f6375), - onTertiary: Color(0xffffffff), - tertiaryContainer: Color(0xffc2e8fd), - onTertiaryContainer: Color(0xff001f2a), + primary: Color(0xff5e5f5c), + surfaceTint: Color(0xff5e5f5c), + onPrimary: Color(0xffedece9), + primaryContainer: Color(0xffedece9), + onPrimaryContainer: Color(0xff6a6a68), + secondary: Color(0xff4a3d33), + onSecondary: Color(0xffedece9), + secondaryContainer: Color(0xff625449), + onSecondaryContainer: Color(0xffdcc9bb), + tertiary: Color(0xff5f5e60), + onTertiary: Color(0xffedece9), + tertiaryContainer: Color(0xffeeebed), + onTertiaryContainer: Color(0xff6b6a6c), error: Color(0xffba1a1a), - onError: Color(0xffffffff), + onError: Color(0xffedece9), errorContainer: Color(0xffffdad6), - onErrorContainer: Color(0xff410002), - surface: Color(0xfff5fbf5), - onSurface: Color(0xff171d1a), - onSurfaceVariant: Color(0xff404944), - outline: Color(0xff707974), - outlineVariant: Color(0xffbfc9c2), + onErrorContainer: Color(0xff93000a), + surface: Color(0xfffcf8f7), + onSurface: Color(0xff1c1b1b), + onSurfaceVariant: Color(0xff444844), + outline: Color(0xff757874), + outlineVariant: Color(0xffc5c7c2), shadow: Color(0xff000000), scrim: Color(0xff000000), - inverseSurface: Color(0xff2c322e), - inversePrimary: Color(0xff8ad6b7), - primaryFixed: Color(0xffa6f2d2), - onPrimaryFixed: Color(0xff002116), - primaryFixedDim: Color(0xff8ad6b7), - onPrimaryFixedVariant: Color(0xff00513c), - secondaryFixed: Color(0xffcee9db), - onSecondaryFixed: Color(0xff092017), - secondaryFixedDim: Color(0xffb3ccbf), - onSecondaryFixedVariant: Color(0xff354b42), - tertiaryFixed: Color(0xffc2e8fd), - onTertiaryFixed: Color(0xff001f2a), - tertiaryFixedDim: Color(0xffa6cce0), - onTertiaryFixedVariant: Color(0xff264b5c), - surfaceDim: Color(0xffd6dbd6), - surfaceBright: Color(0xfff5fbf5), - surfaceContainerLowest: Color(0xffffffff), - surfaceContainerLow: Color(0xffeff5f0), - surfaceContainer: Color(0xffe9efea), - surfaceContainerHigh: Color(0xffe4eae4), - surfaceContainerHighest: Color(0xffdee4df), + inverseSurface: Color(0xff313030), + inversePrimary: Color(0xffc7c6c4), + primaryFixed: Color(0xffe3e2df), + onPrimaryFixed: Color(0xff1b1c1a), + primaryFixedDim: Color(0xffc7c6c4), + onPrimaryFixedVariant: Color(0xff464745), + secondaryFixed: Color(0xfff3dfd0), + onSecondaryFixed: Color(0xff241a11), + secondaryFixedDim: Color(0xffd6c3b5), + onSecondaryFixedVariant: Color(0xff51443a), + tertiaryFixed: Color(0xffe4e2e4), + onTertiaryFixed: Color(0xff1b1b1d), + tertiaryFixedDim: Color(0xffc8c6c8), + onTertiaryFixedVariant: Color(0xff474648), + surfaceDim: Color(0xffddd9d8), + surfaceBright: Color(0xfffcf8f7), + surfaceContainerLowest: Color(0xffedece9), + surfaceContainerLow: Color(0xfff7f3f2), + surfaceContainer: Color(0xfff1edec), + surfaceContainerHigh: Color(0xffebe7e6), + surfaceContainerHighest: Color(0xffe5e2e1), ); } @@ -61,51 +61,51 @@ class AppTheme { ColorScheme lightHighContrastScheme() { return const ColorScheme( brightness: Brightness.light, - primary: Color(0xff00281c), - surfaceTint: Color(0xff1a6b51), - onPrimary: Color(0xffffffff), - primaryContainer: Color(0xff004d38), - onPrimaryContainer: Color(0xffffffff), - secondary: Color(0xff10261e), - onSecondary: Color(0xffffffff), - secondaryContainer: Color(0xff31483e), - onSecondaryContainer: Color(0xffffffff), - tertiary: Color(0xff002633), - onTertiary: Color(0xffffffff), - tertiaryContainer: Color(0xff214758), - onTertiaryContainer: Color(0xffffffff), - error: Color(0xff4e0002), - onError: Color(0xffffffff), - errorContainer: Color(0xff8c0009), - onErrorContainer: Color(0xffffffff), - surface: Color(0xfff5fbf5), + primary: Color(0xff2b2c2b), + surfaceTint: Color(0xff5e5f5c), + onPrimary: Color(0xffedece9), + primaryContainer: Color(0xff484947), + onPrimaryContainer: Color(0xffedece9), + secondary: Color(0xff352a20), + onSecondary: Color(0xffedece9), + secondaryContainer: Color(0xff54473c), + onSecondaryContainer: Color(0xffedece9), + tertiary: Color(0xff2c2c2e), + onTertiary: Color(0xffedece9), + tertiaryContainer: Color(0xff49494b), + onTertiaryContainer: Color(0xffedece9), + error: Color(0xff600004), + onError: Color(0xffedece9), + errorContainer: Color(0xff98000a), + onErrorContainer: Color(0xffedece9), + surface: Color(0xfffcf8f7), onSurface: Color(0xff000000), - onSurfaceVariant: Color(0xff1d2622), - outline: Color(0xff3c4540), - outlineVariant: Color(0xff3c4540), + onSurfaceVariant: Color(0xff000000), + outline: Color(0xff2a2d2a), + outlineVariant: Color(0xff474a46), shadow: Color(0xff000000), scrim: Color(0xff000000), - inverseSurface: Color(0xff2c322e), - inversePrimary: Color(0xffaffcdb), - primaryFixed: Color(0xff004d38), - onPrimaryFixed: Color(0xffffffff), - primaryFixedDim: Color(0xff003425), - onPrimaryFixedVariant: Color(0xffffffff), - secondaryFixed: Color(0xff31483e), - onSecondaryFixed: Color(0xffffffff), - secondaryFixedDim: Color(0xff1b3128), - onSecondaryFixedVariant: Color(0xffffffff), - tertiaryFixed: Color(0xff214758), - onTertiaryFixed: Color(0xffffffff), - tertiaryFixedDim: Color(0xff043141), - onTertiaryFixedVariant: Color(0xffffffff), - surfaceDim: Color(0xffd6dbd6), - surfaceBright: Color(0xfff5fbf5), - surfaceContainerLowest: Color(0xffffffff), - surfaceContainerLow: Color(0xffeff5f0), - surfaceContainer: Color(0xffe9efea), - surfaceContainerHigh: Color(0xffe4eae4), - surfaceContainerHighest: Color(0xffdee4df), + inverseSurface: Color(0xff313030), + inversePrimary: Color(0xffc7c6c4), + primaryFixed: Color(0xff484947), + onPrimaryFixed: Color(0xffedece9), + primaryFixedDim: Color(0xff323331), + onPrimaryFixedVariant: Color(0xffedece9), + secondaryFixed: Color(0xff54473c), + onSecondaryFixed: Color(0xffedece9), + secondaryFixedDim: Color(0xff3c3027), + onSecondaryFixedVariant: Color(0xffedece9), + tertiaryFixed: Color(0xff49494b), + onTertiaryFixed: Color(0xffedece9), + tertiaryFixedDim: Color(0xff333234), + onTertiaryFixedVariant: Color(0xffedece9), + surfaceDim: Color(0xffbbb8b7), + surfaceBright: Color(0xfffcf8f7), + surfaceContainerLowest: Color(0xffedece9), + surfaceContainerLow: Color(0xfff4f0ef), + surfaceContainer: Color(0xffe5e2e1), + surfaceContainerHigh: Color(0xffd7d4d3), + surfaceContainerHighest: Color(0xffc9c6c5), ); } @@ -116,51 +116,51 @@ class AppTheme { ColorScheme darkScheme() { return const ColorScheme( brightness: Brightness.dark, - primary: Color(0xff8ad6b7), - surfaceTint: Color(0xff8ad6b7), - onPrimary: Color(0xff003828), - primaryContainer: Color(0xff00513c), - onPrimaryContainer: Color(0xffa6f2d2), - secondary: Color(0xffb3ccbf), - onSecondary: Color(0xff1e352c), - secondaryContainer: Color(0xff354b42), - onSecondaryContainer: Color(0xffcee9db), - tertiary: Color(0xffa6cce0), - onTertiary: Color(0xff0a3545), - tertiaryContainer: Color(0xff264b5c), - onTertiaryContainer: Color(0xffc2e8fd), + primary: Color(0xffedece9), + surfaceTint: Color(0xffc7c6c4), + onPrimary: Color(0xff30312f), + primaryContainer: Color(0xffe3e2df), + onPrimaryContainer: Color(0xff646562), + secondary: Color(0xffd6c3b5), + onSecondary: Color(0xff3a2e25), + secondaryContainer: Color(0xff625449), + onSecondaryContainer: Color(0xffdcc9bb), + tertiary: Color(0xffedece9), + onTertiary: Color(0xff303032), + tertiaryContainer: Color(0xffe4e2e4), + onTertiaryContainer: Color(0xff656466), error: Color(0xffffb4ab), onError: Color(0xff690005), errorContainer: Color(0xff93000a), onErrorContainer: Color(0xffffdad6), - surface: Color(0xff0f1512), - onSurface: Color(0xffdee4df), - onSurfaceVariant: Color(0xffbfc9c2), - outline: Color(0xff89938d), - outlineVariant: Color(0xff404944), + surface: Color(0xff141313), + onSurface: Color(0xffe5e2e1), + onSurfaceVariant: Color(0xffc5c7c2), + outline: Color(0xff8f918d), + outlineVariant: Color(0xff444844), shadow: Color(0xff000000), scrim: Color(0xff000000), - inverseSurface: Color(0xffdee4df), - inversePrimary: Color(0xff1a6b51), - primaryFixed: Color(0xffa6f2d2), - onPrimaryFixed: Color(0xff002116), - primaryFixedDim: Color(0xff8ad6b7), - onPrimaryFixedVariant: Color(0xff00513c), - secondaryFixed: Color(0xffcee9db), - onSecondaryFixed: Color(0xff092017), - secondaryFixedDim: Color(0xffb3ccbf), - onSecondaryFixedVariant: Color(0xff354b42), - tertiaryFixed: Color(0xffc2e8fd), - onTertiaryFixed: Color(0xff001f2a), - tertiaryFixedDim: Color(0xffa6cce0), - onTertiaryFixedVariant: Color(0xff264b5c), - surfaceDim: Color(0xff0f1512), - surfaceBright: Color(0xff343b37), - surfaceContainerLowest: Color(0xff0a0f0d), - surfaceContainerLow: Color(0xff171d1a), - surfaceContainer: Color(0xff1b211e), - surfaceContainerHigh: Color(0xff252b28), - surfaceContainerHighest: Color(0xff303633), + inverseSurface: Color(0xffe5e2e1), + inversePrimary: Color(0xff5e5f5c), + primaryFixed: Color(0xffe3e2df), + onPrimaryFixed: Color(0xff1b1c1a), + primaryFixedDim: Color(0xffc7c6c4), + onPrimaryFixedVariant: Color(0xff464745), + secondaryFixed: Color(0xfff3dfd0), + onSecondaryFixed: Color(0xff241a11), + secondaryFixedDim: Color(0xffd6c3b5), + onSecondaryFixedVariant: Color(0xff51443a), + tertiaryFixed: Color(0xffe4e2e4), + onTertiaryFixed: Color(0xff1b1b1d), + tertiaryFixedDim: Color(0xffc8c6c8), + onTertiaryFixedVariant: Color(0xff474648), + surfaceDim: Color(0xff141313), + surfaceBright: Color(0xff3a3939), + surfaceContainerLowest: Color(0xff0e0e0e), + surfaceContainerLow: Color(0xff1c1b1b), + surfaceContainer: Color(0xff201f1f), + surfaceContainerHigh: Color(0xff2a2a29), + surfaceContainerHighest: Color(0xff353434), ); } @@ -171,51 +171,51 @@ class AppTheme { ColorScheme darkHighContrastScheme() { return const ColorScheme( brightness: Brightness.dark, - primary: Color(0xffedfff4), - surfaceTint: Color(0xff8ad6b7), + primary: Color(0xffedece9), + surfaceTint: Color(0xffc7c6c4), onPrimary: Color(0xff000000), - primaryContainer: Color(0xff8edabb), - onPrimaryContainer: Color(0xff000000), - secondary: Color(0xffedfff4), + primaryContainer: Color(0xffe3e2df), + onPrimaryContainer: Color(0xff292a29), + secondary: Color(0xffffede0), onSecondary: Color(0xff000000), - secondaryContainer: Color(0xffb7d1c3), - onSecondaryContainer: Color(0xff000000), - tertiary: Color(0xfff7fbff), + secondaryContainer: Color(0xffd2bfb1), + onSecondaryContainer: Color(0xff120904), + tertiary: Color(0xffedece9), onTertiary: Color(0xff000000), - tertiaryContainer: Color(0xffabd0e4), - onTertiaryContainer: Color(0xff000000), - error: Color(0xfffff9f9), + tertiaryContainer: Color(0xffe4e2e4), + onTertiaryContainer: Color(0xff2a2a2b), + error: Color(0xffffece9), onError: Color(0xff000000), - errorContainer: Color(0xffffbab1), - onErrorContainer: Color(0xff000000), - surface: Color(0xff0f1512), - onSurface: Color(0xffffffff), - onSurfaceVariant: Color(0xfff3fdf6), - outline: Color(0xffc3cdc6), - outlineVariant: Color(0xffc3cdc6), + errorContainer: Color(0xffffaea4), + onErrorContainer: Color(0xff220001), + surface: Color(0xff141313), + onSurface: Color(0xffedece9), + onSurfaceVariant: Color(0xffedece9), + outline: Color(0xffeff1eb), + outlineVariant: Color(0xffc1c3be), shadow: Color(0xff000000), scrim: Color(0xff000000), - inverseSurface: Color(0xffdee4df), - inversePrimary: Color(0xff003123), - primaryFixed: Color(0xffaaf7d6), + inverseSurface: Color(0xffe5e2e1), + inversePrimary: Color(0xff474846), + primaryFixed: Color(0xffe3e2df), onPrimaryFixed: Color(0xff000000), - primaryFixedDim: Color(0xff8edabb), - onPrimaryFixedVariant: Color(0xff001b11), - secondaryFixed: Color(0xffd3eddf), + primaryFixedDim: Color(0xffc7c6c4), + onPrimaryFixedVariant: Color(0xff101110), + secondaryFixed: Color(0xfff3dfd0), onSecondaryFixed: Color(0xff000000), - secondaryFixedDim: Color(0xffb7d1c3), - onSecondaryFixedVariant: Color(0xff041a12), - tertiaryFixed: Color(0xffcaecff), + secondaryFixedDim: Color(0xffd6c3b5), + onSecondaryFixedVariant: Color(0xff180f07), + tertiaryFixed: Color(0xffe4e2e4), onTertiaryFixed: Color(0xff000000), - tertiaryFixedDim: Color(0xffabd0e4), - onTertiaryFixedVariant: Color(0xff001923), - surfaceDim: Color(0xff0f1512), - surfaceBright: Color(0xff343b37), - surfaceContainerLowest: Color(0xff0a0f0d), - surfaceContainerLow: Color(0xff171d1a), - surfaceContainer: Color(0xff1b211e), - surfaceContainerHigh: Color(0xff252b28), - surfaceContainerHighest: Color(0xff303633), + tertiaryFixedDim: Color(0xffc8c6c8), + onTertiaryFixedVariant: Color(0xff111113), + surfaceDim: Color(0xff141313), + surfaceBright: Color(0xff51504f), + surfaceContainerLowest: Color(0xff000000), + surfaceContainerLow: Color(0xff201f1f), + surfaceContainer: Color(0xff313030), + surfaceContainerHigh: Color(0xff3c3b3b), + surfaceContainerHighest: Color(0xff484646), ); } @@ -242,9 +242,5 @@ class AppTheme { selectedItemColor: colorScheme.primary, unselectedItemColor: colorScheme.onSurfaceVariant, ), - chipTheme: ChipThemeData( - backgroundColor: colorScheme.secondaryContainer, - brightness: colorScheme.brightness, - ), ); } diff --git a/pubspec.lock b/pubspec.lock index 7a64ab1..f8f847a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -71,70 +71,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - build: - dependency: transitive - description: - name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - build_config: - dependency: transitive - description: - name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" - url: "https://pub.dev" - source: hosted - version: "4.0.3" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" - url: "https://pub.dev" - source: hosted - version: "2.4.3" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" - url: "https://pub.dev" - source: hosted - version: "2.4.14" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" - url: "https://pub.dev" - source: hosted - version: "8.0.0" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" - url: "https://pub.dev" - source: hosted - version: "8.9.3" characters: dependency: transitive description: @@ -143,14 +79,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" clock: dependency: transitive description: @@ -159,14 +87,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" - url: "https://pub.dev" - source: hosted - version: "4.10.1" collection: dependency: transitive description: @@ -199,14 +119,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" - url: "https://pub.dev" - source: hosted - version: "3.0.1" diff_match_patch: dependency: transitive description: @@ -247,14 +159,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -307,22 +211,6 @@ packages: url: "https://pub.dev" source: hosted version: "14.6.3" - go_router_builder: - dependency: "direct dev" - description: - name: go_router_builder - sha256: "293e366566c209f70a05508bb5fdd6ce536fa3f75e58ccba1f83341a7943a6be" - url: "https://pub.dev" - source: hosted - version: "2.7.3" - graphs: - dependency: transitive - description: - name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" - source: hosted - version: "2.3.2" hive_ce: dependency: transitive description: @@ -595,14 +483,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.5" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" - source: hosted - version: "1.5.0" shelf: dependency: transitive description: @@ -640,22 +520,6 @@ packages: description: flutter source: sdk version: "0.0.0" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - source_helper: - dependency: transitive - description: - name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" - url: "https://pub.dev" - source: hosted - version: "1.3.5" source_map_stack_trace: dependency: transitive description: @@ -696,14 +560,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" string_scanner: dependency: transitive description: @@ -777,14 +633,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.5" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.dev" - source: hosted - version: "1.0.2" typed_data: dependency: transitive description: @@ -875,4 +723,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.24.0" diff --git a/test/theme/theme_test.dart b/test/theme/app_theme_test.dart similarity index 88% rename from test/theme/theme_test.dart rename to test/theme/app_theme_test.dart index ac9e462..7295366 100644 --- a/test/theme/theme_test.dart +++ b/test/theme/app_theme_test.dart @@ -16,7 +16,7 @@ void main() { test('colorScheme primary is correct', () { expect( appTheme.light.colorScheme.primary, - equals(Color(0xff1a6b51)), + equals(Color(0xff5e5f5c)), ); }); }); @@ -26,7 +26,7 @@ void main() { expect( appTheme.lightHighContrast.colorScheme.primary, equals( - Color(0xff00281c), + Color(0xff2b2c2b), ), ); }); @@ -36,7 +36,7 @@ void main() { test('colorScheme primary is correct', () { expect( appTheme.dark.colorScheme.primary, - equals(Color(0xff8ad6b7)), + equals(Color(0xffedece9)), ); }); }); @@ -45,7 +45,7 @@ void main() { test('colorScheme primary is correct', () { expect( AppTheme().darkHighContrast.colorScheme.primary, - equals(Color(0xffedfff4)), + equals(Color(0xffedece9)), ); }); }); From ff1f12a24abf64ca7cb773f8fe24c237c82e1dab Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Wed, 22 Jan 2025 12:12:22 +0100 Subject: [PATCH 08/10] feat:update pubspec --- pubspec.lock | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index f8f847a..7a64ab1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -71,6 +71,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" + url: "https://pub.dev" + source: hosted + version: "4.0.3" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + url: "https://pub.dev" + source: hosted + version: "2.4.14" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + url: "https://pub.dev" + source: hosted + version: "8.9.3" characters: dependency: transitive description: @@ -79,6 +143,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" clock: dependency: transitive description: @@ -87,6 +159,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" collection: dependency: transitive description: @@ -119,6 +199,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + url: "https://pub.dev" + source: hosted + version: "3.0.1" diff_match_patch: dependency: transitive description: @@ -159,6 +247,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -211,6 +307,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.6.3" + go_router_builder: + dependency: "direct dev" + description: + name: go_router_builder + sha256: "293e366566c209f70a05508bb5fdd6ce536fa3f75e58ccba1f83341a7943a6be" + url: "https://pub.dev" + source: hosted + version: "2.7.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" hive_ce: dependency: transitive description: @@ -483,6 +595,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.5" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" shelf: dependency: transitive description: @@ -520,6 +640,22 @@ packages: description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" source_map_stack_trace: dependency: transitive description: @@ -560,6 +696,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -633,6 +777,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.5" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" typed_data: dependency: transitive description: @@ -723,4 +875,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.27.0" From f58d9e9f08c56db12b1756e5441b45c0193f1c9d Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Sun, 2 Feb 2025 19:45:39 +0100 Subject: [PATCH 09/10] feat: add new route --- lib/album/view/album_view.dart | 2 +- lib/app/app_router/routes/routes.dart | 43 +++++++++-- lib/app/app_router/routes/routes.g.dart | 38 ++++++++- lib/app/view/app.dart | 14 ++-- lib/song/bloc/song_state.dart | 29 ------- lib/song/song.dart | 2 - lib/song/view/view.dart | 2 - .../bloc/songs_bloc.dart} | 16 ++-- .../bloc/songs_event.dart} | 2 +- lib/songs/bloc/songs_state.dart | 29 +++++++ lib/songs/songs.dart | 2 + .../view/songs_page.dart} | 10 +-- .../view/songs_view.dart} | 77 +++++++++++++------ lib/songs/view/view.dart | 2 + .../lib/src/swiftify_repository.dart | 2 +- test/app/app_router/app_router_test.dart | 4 +- test/songs/bloc/song_bloc_test.dart | 20 ++--- test/songs/bloc/song_event_test.dart | 2 +- test/songs/bloc/song_state_test.dart | 48 ++++++------ 19 files changed, 215 insertions(+), 129 deletions(-) delete mode 100644 lib/song/bloc/song_state.dart delete mode 100644 lib/song/song.dart delete mode 100644 lib/song/view/view.dart rename lib/{song/bloc/song_bloc.dart => songs/bloc/songs_bloc.dart} (69%) rename lib/{song/bloc/song_event.dart => songs/bloc/songs_event.dart} (90%) create mode 100644 lib/songs/bloc/songs_state.dart create mode 100644 lib/songs/songs.dart rename lib/{song/view/song_page.dart => songs/view/songs_page.dart} (83%) rename lib/{song/view/song_view.dart => songs/view/songs_view.dart} (68%) create mode 100644 lib/songs/view/view.dart diff --git a/lib/album/view/album_view.dart b/lib/album/view/album_view.dart index ad1ff8a..5a2aed5 100644 --- a/lib/album/view/album_view.dart +++ b/lib/album/view/album_view.dart @@ -81,7 +81,7 @@ class AlbumsContent extends StatelessWidget { itemBuilder: (context, index) { final album = albums[index]; return GestureDetector( - onTap: () => SongPageRoute( + onTap: () => SongsPageRoute( id: album.albumId, albumTitle: album.title, coverAlbum: album.coverAlbum, diff --git a/lib/app/app_router/routes/routes.dart b/lib/app/app_router/routes/routes.dart index 7770e2a..2aaabc2 100644 --- a/lib/app/app_router/routes/routes.dart +++ b/lib/app/app_router/routes/routes.dart @@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:swiftify/album/album.dart'; import 'package:swiftify/app/app_router/router.dart'; import 'package:swiftify/favorites/favorites.dart'; -import 'package:swiftify/song/song.dart'; +import 'package:swiftify/songs/songs.dart'; import 'package:swiftify/theme/theme.dart'; part 'routes.g.dart'; @@ -16,8 +16,13 @@ final rootNavigatorKey = GlobalKey(); TypedGoRoute( path: AlbumPageRoute.path, routes: [ - TypedGoRoute( - path: SongPageRoute.path, + TypedGoRoute( + path: SongsPageRoute.path, + routes: [ + TypedGoRoute( + path: SongDetailPageRoute.path, + ) + ], ), ], ), @@ -29,6 +34,7 @@ final rootNavigatorKey = GlobalKey(); ), ], ) +@immutable class AppShellRoute extends ShellRouteData { const AppShellRoute(); @@ -98,15 +104,38 @@ class AlbumPageRoute extends GoRouteData { } @immutable -class SongPageRoute extends GoRouteData { - const SongPageRoute({ +class SongDetailPageRoute extends GoRouteData { + const SongDetailPageRoute({ + required this.id, + this.coverAlbum, + }); + + final int id; + final String? coverAlbum; + + static const path = 'song/:id'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return CustomTransitionPage( + child: Container(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + ); + } +} + +@immutable +class SongsPageRoute extends GoRouteData { + const SongsPageRoute({ required this.id, this.albumTitle, this.coverAlbum, this.albumReleaseDate, }); - /// The id of the album to show + /// The album id to display the songs for. /// This is passed in the path as a parameter final int id; @@ -135,7 +164,7 @@ class SongPageRoute extends GoRouteData { @override Page buildPage(BuildContext context, GoRouterState state) { return CustomTransitionPage( - child: SongPage( + child: SongsPage( albumId: id, albumTitle: albumTitle, coverAlbum: coverAlbum, diff --git a/lib/app/app_router/routes/routes.g.dart b/lib/app/app_router/routes/routes.g.dart index 438a892..a36838c 100644 --- a/lib/app/app_router/routes/routes.g.dart +++ b/lib/app/app_router/routes/routes.g.dart @@ -20,8 +20,14 @@ RouteBase get $appShellRoute => ShellRouteData.$route( routes: [ GoRouteData.$route( path: 'songs/:id', - parentNavigatorKey: SongPageRoute.$parentNavigatorKey, - factory: $SongPageRouteExtension._fromState, + parentNavigatorKey: SongsPageRoute.$parentNavigatorKey, + factory: $SongsPageRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'song/:id', + factory: $SongDetailPageRouteExtension._fromState, + ), + ], ), ], ), @@ -58,8 +64,8 @@ extension $AlbumPageRouteExtension on AlbumPageRoute { void replace(BuildContext context) => context.replace(location); } -extension $SongPageRouteExtension on SongPageRoute { - static SongPageRoute _fromState(GoRouterState state) => SongPageRoute( +extension $SongsPageRouteExtension on SongsPageRoute { + static SongsPageRoute _fromState(GoRouterState state) => SongsPageRoute( id: int.parse(state.pathParameters['id']!), albumTitle: state.uri.queryParameters['album-title'], coverAlbum: state.uri.queryParameters['cover-album'], @@ -85,6 +91,30 @@ extension $SongPageRouteExtension on SongPageRoute { void replace(BuildContext context) => context.replace(location); } +extension $SongDetailPageRouteExtension on SongDetailPageRoute { + static SongDetailPageRoute _fromState(GoRouterState state) => + SongDetailPageRoute( + id: int.parse(state.pathParameters['id']!), + coverAlbum: state.uri.queryParameters['cover-album'], + ); + + String get location => GoRouteData.$location( + '/songs/${Uri.encodeComponent(id.toString())}/song/${Uri.encodeComponent(id.toString())}', + queryParams: { + if (coverAlbum != null) 'cover-album': coverAlbum, + }, + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + extension $FavoritesPageRouteExtension on FavoritesPageRoute { static FavoritesPageRoute _fromState(GoRouterState state) => const FavoritesPageRoute(); diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index a230ca0..36b8100 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -6,7 +6,7 @@ import 'package:swiftify/theme/app_theme.dart'; import 'package:swiftify/theme/bloc/theme_bloc.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; -class App extends StatefulWidget { +class App extends StatelessWidget { const App({ required SwiftifyRepository swiftifyRepository, super.key, @@ -14,16 +14,14 @@ class App extends StatefulWidget { final SwiftifyRepository _swiftifyRepository; - @override - State createState() => _AppState(); -} - -class _AppState extends State { @override Widget build(BuildContext context) { return RepositoryProvider.value( - value: widget._swiftifyRepository, - child: BlocProvider(create: (_) => ThemeBloc(), child: const AppView()), + value: _swiftifyRepository, + child: BlocProvider( + create: (_) => ThemeBloc(), + child: const AppView(), + ), ); } } diff --git a/lib/song/bloc/song_state.dart b/lib/song/bloc/song_state.dart deleted file mode 100644 index 718c8c2..0000000 --- a/lib/song/bloc/song_state.dart +++ /dev/null @@ -1,29 +0,0 @@ -part of 'song_bloc.dart'; - -enum SongStatus { initial, loading, success, failure } - -class SongState extends Equatable { - const SongState({ - this.status = SongStatus.initial, - this.songs = const [], - }); - - final List songs; - final SongStatus status; - - bool get isLoading => status == SongStatus.loading; - bool get isSuccess => status == SongStatus.success; - - SongState copyWith({ - List? songs, - SongStatus? status, - }) { - return SongState( - songs: songs ?? this.songs, - status: status ?? this.status, - ); - } - - @override - List get props => [songs, status]; -} diff --git a/lib/song/song.dart b/lib/song/song.dart deleted file mode 100644 index 8b2058d..0000000 --- a/lib/song/song.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'bloc/song_bloc.dart'; -export 'view/view.dart'; diff --git a/lib/song/view/view.dart b/lib/song/view/view.dart deleted file mode 100644 index 11a7e1a..0000000 --- a/lib/song/view/view.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'song_page.dart'; -export 'song_view.dart'; diff --git a/lib/song/bloc/song_bloc.dart b/lib/songs/bloc/songs_bloc.dart similarity index 69% rename from lib/song/bloc/song_bloc.dart rename to lib/songs/bloc/songs_bloc.dart index 9cb96fe..cf6afc2 100644 --- a/lib/song/bloc/song_bloc.dart +++ b/lib/songs/bloc/songs_bloc.dart @@ -2,14 +2,14 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; -part 'song_event.dart'; -part 'song_state.dart'; +part 'songs_event.dart'; +part 'songs_state.dart'; -class SongBloc extends Bloc { +class SongBloc extends Bloc { SongBloc({ required SwiftifyRepository swiftifyRepository, }) : _swiftifyRepository = swiftifyRepository, - super(const SongState()) { + super(const SongsState()) { on(_onSongsByAlbumRequested); } @@ -17,9 +17,9 @@ class SongBloc extends Bloc { Future _onSongsByAlbumRequested( SongsRequested event, - Emitter emit, + Emitter emit, ) async { - emit(state.copyWith(status: SongStatus.loading)); + emit(state.copyWith(status: SongsStatus.loading)); try { final songs = await _swiftifyRepository.getSongsByAlbum( albumId: event.albumId, @@ -27,12 +27,12 @@ class SongBloc extends Bloc { emit( state.copyWith( songs: songs, - status: SongStatus.success, + status: SongsStatus.success, ), ); } catch (error, stackTrace) { addError(error, stackTrace); - emit(state.copyWith(status: SongStatus.failure)); + emit(state.copyWith(status: SongsStatus.failure)); } } } diff --git a/lib/song/bloc/song_event.dart b/lib/songs/bloc/songs_event.dart similarity index 90% rename from lib/song/bloc/song_event.dart rename to lib/songs/bloc/songs_event.dart index a7fce76..932b94a 100644 --- a/lib/song/bloc/song_event.dart +++ b/lib/songs/bloc/songs_event.dart @@ -1,4 +1,4 @@ -part of 'song_bloc.dart'; +part of 'songs_bloc.dart'; sealed class SongEvent extends Equatable { const SongEvent(); diff --git a/lib/songs/bloc/songs_state.dart b/lib/songs/bloc/songs_state.dart new file mode 100644 index 0000000..03c6770 --- /dev/null +++ b/lib/songs/bloc/songs_state.dart @@ -0,0 +1,29 @@ +part of 'songs_bloc.dart'; + +enum SongsStatus { initial, loading, success, failure } + +class SongsState extends Equatable { + const SongsState({ + this.status = SongsStatus.initial, + this.songs = const [], + }); + + final List songs; + final SongsStatus status; + + bool get isLoading => status == SongsStatus.loading; + bool get isSuccess => status == SongsStatus.success; + + SongsState copyWith({ + List? songs, + SongsStatus? status, + }) { + return SongsState( + songs: songs ?? this.songs, + status: status ?? this.status, + ); + } + + @override + List get props => [songs, status]; +} diff --git a/lib/songs/songs.dart b/lib/songs/songs.dart new file mode 100644 index 0000000..af120f5 --- /dev/null +++ b/lib/songs/songs.dart @@ -0,0 +1,2 @@ +export 'bloc/songs_bloc.dart'; +export 'view/view.dart'; diff --git a/lib/song/view/song_page.dart b/lib/songs/view/songs_page.dart similarity index 83% rename from lib/song/view/song_page.dart rename to lib/songs/view/songs_page.dart index c5d87d4..c84c0cc 100644 --- a/lib/song/view/song_page.dart +++ b/lib/songs/view/songs_page.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:swiftify/song/song.dart'; +import 'package:swiftify/songs/songs.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; -class SongPage extends StatelessWidget { - const SongPage({ +class SongsPage extends StatelessWidget { + const SongsPage({ required this.albumId, this.albumTitle, this.coverAlbum, @@ -27,9 +27,9 @@ class SongPage extends StatelessWidget { ), child: Scaffold( appBar: AppBar( - title: Text(albumTitle ?? ''), + title: Text('Songs'), ), - body: SongView( + body: SongsView( albumTitle: albumTitle ?? '', coverAlbum: coverAlbum, releaseDate: albumReleaseDate ?? '', diff --git a/lib/song/view/song_view.dart b/lib/songs/view/songs_view.dart similarity index 68% rename from lib/song/view/song_view.dart rename to lib/songs/view/songs_view.dart index d5facec..0d03d5a 100644 --- a/lib/song/view/song_view.dart +++ b/lib/songs/view/songs_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:swiftify/song/song.dart'; +import 'package:swiftify/songs/songs.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; -class SongView extends StatelessWidget { - const SongView({ +class SongsView extends StatelessWidget { + const SongsView({ required this.albumTitle, required this.coverAlbum, required this.releaseDate, @@ -19,20 +19,44 @@ class SongView extends StatelessWidget { Widget build(BuildContext context) { final songs = context.select((SongBloc bloc) => bloc.state.songs); final isLoading = context.select((SongBloc bloc) => bloc.state.isLoading); + final theme = Theme.of(context); + final textTheme = theme.textTheme; return isLoading ? const Center( child: CircularProgressIndicator(), ) - : ListView.builder( - itemCount: songs.length, - itemBuilder: (context, index) { - final song = songs[index]; - return SongCard( - coverAlbum: coverAlbum, - song: song, - ); - }, + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 16, + bottom: 8, + ), + child: Text( + albumTitle, + style: textTheme.headlineLarge, + ), + ), + Expanded( + child: ListView.builder( + itemCount: songs.length, + itemBuilder: (context, index) { + final song = songs[index]; + return SongCard( + coverAlbum: coverAlbum, + song: song, + ); + }, + ), + ), + ], + ), ); } } @@ -49,18 +73,23 @@ class SongCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.all(8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - AlbumCoverImage(coverAlbum: coverAlbum), - SongInformation( - title: song.title, - duration: song.duration, - genres: song.genres, - ), - ], + return GestureDetector( + onTap: () {}, + child: Card( + margin: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AlbumCoverImage(coverAlbum: coverAlbum), + SongInformation( + title: song.title, + duration: song.duration, + genres: song.genres, + ), + ], + ), ), ); } diff --git a/lib/songs/view/view.dart b/lib/songs/view/view.dart new file mode 100644 index 0000000..9c7537f --- /dev/null +++ b/lib/songs/view/view.dart @@ -0,0 +1,2 @@ +export 'songs_page.dart'; +export 'songs_view.dart'; diff --git a/packages/swiftify_repository/lib/src/swiftify_repository.dart b/packages/swiftify_repository/lib/src/swiftify_repository.dart index 93adc73..4ee2721 100644 --- a/packages/swiftify_repository/lib/src/swiftify_repository.dart +++ b/packages/swiftify_repository/lib/src/swiftify_repository.dart @@ -44,7 +44,7 @@ class SwiftifyRepository { } } - /// Get lyrics for a song from the API. + /// Get lyrics by [songId] from the API. Future getSongLyrics({ required int songId, }) async { diff --git a/test/app/app_router/app_router_test.dart b/test/app/app_router/app_router_test.dart index 3441846..481f1b2 100644 --- a/test/app/app_router/app_router_test.dart +++ b/test/app/app_router/app_router_test.dart @@ -8,7 +8,7 @@ import 'package:swiftify/album/album.dart'; import 'package:swiftify/app/app_router/app_router.dart'; import 'package:swiftify/app/app_router/routes/routes.dart'; import 'package:swiftify/favorites/favorites.dart'; -import 'package:swiftify/song/song.dart'; +import 'package:swiftify/songs/songs.dart'; import 'package:swiftify/theme/theme.dart'; import '../../helpers/helpers.dart'; @@ -103,7 +103,7 @@ void main() { appRouter.routes.go('/songs/1'); await tester.pumpAndSettle(); - expect(find.byType(SongPage), findsOneWidget); + expect(find.byType(SongsPage), findsOneWidget); }); group('navigates with BottomNavigationBar', () { diff --git a/test/songs/bloc/song_bloc_test.dart b/test/songs/bloc/song_bloc_test.dart index 76f65bc..4da58aa 100644 --- a/test/songs/bloc/song_bloc_test.dart +++ b/test/songs/bloc/song_bloc_test.dart @@ -3,7 +3,7 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:swiftify/song/song.dart'; +import 'package:swiftify/songs/songs.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; class _MockSwiftifyRepository extends Mock implements SwiftifyRepository {} @@ -21,12 +21,12 @@ void main() { SongBloc( swiftifyRepository: swiftifyRepository, ).state, - const SongState(), + const SongsState(), ); }); group('SongsByAlbumRequested', () { - blocTest( + blocTest( 'emits state with updated songs', setUp: () { when( @@ -44,15 +44,15 @@ void main() { ), ), expect: () => [ - const SongState(status: SongStatus.loading), - SongState( + const SongsState(status: SongsStatus.loading), + SongsState( songs: const [Song(title: 'willow')], - status: SongStatus.success, + status: SongsStatus.success, ), ], ); - blocTest( + blocTest( 'emits failure when an error occurs', setUp: () { when( @@ -69,9 +69,9 @@ void main() { albumId: 1, ), ), - expect: () => [ - const SongState(status: SongStatus.loading), - const SongState(status: SongStatus.failure), + expect: () => [ + const SongsState(status: SongsStatus.loading), + const SongsState(status: SongsStatus.failure), ], ); }); diff --git a/test/songs/bloc/song_event_test.dart b/test/songs/bloc/song_event_test.dart index 5b45d94..1b41dd3 100644 --- a/test/songs/bloc/song_event_test.dart +++ b/test/songs/bloc/song_event_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:swiftify/song/song.dart'; +import 'package:swiftify/songs/songs.dart'; void main() { group('SongsEvent', () { diff --git a/test/songs/bloc/song_state_test.dart b/test/songs/bloc/song_state_test.dart index 90e127b..dcde78d 100644 --- a/test/songs/bloc/song_state_test.dart +++ b/test/songs/bloc/song_state_test.dart @@ -1,35 +1,35 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:swiftify/song/song.dart'; +import 'package:swiftify/songs/songs.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; void main() { group('SongState', () { test('supports value comparisons', () { expect( - SongState( + SongsState( songs: const [Song(title: 'name')], - status: SongStatus.loading, + status: SongsStatus.loading, ), equals( - SongState( + SongsState( songs: const [Song(title: 'name')], - status: SongStatus.loading, + status: SongsStatus.loading, ), ), ); expect( - SongState( + SongsState( songs: const [Song(title: 'name')], - status: SongStatus.loading, + status: SongsStatus.loading, ), isNot( equals( - SongState( + SongsState( songs: const [Song(title: 'different')], - status: SongStatus.loading, + status: SongsStatus.loading, ), ), ), @@ -38,52 +38,52 @@ void main() { test('copyWith comparisons', () { expect( - SongState( + SongsState( songs: const [Song(title: 'name')], - status: SongStatus.loading, + status: SongsStatus.loading, ).copyWith(), equals( - SongState( + SongsState( songs: const [Song(title: 'name')], - status: SongStatus.loading, + status: SongsStatus.loading, ), ), ); expect( - SongState( + SongsState( songs: const [Song(title: 'name')], - status: SongStatus.loading, + status: SongsStatus.loading, ).copyWith(songs: [Song(title: 'different')]), equals( - SongState( + SongsState( songs: const [Song(title: 'different')], - status: SongStatus.loading, + status: SongsStatus.loading, ), ), ); expect( - SongState( + SongsState( songs: const [Song(title: 'name')], - status: SongStatus.loading, - ).copyWith(status: SongStatus.success), + status: SongsStatus.loading, + ).copyWith(status: SongsStatus.success), equals( - SongState( + SongsState( songs: const [Song(title: 'name')], - status: SongStatus.success, + status: SongsStatus.success, ), ), ); }); test('isSuccess', () { - final state = SongState(status: SongStatus.success); + final state = SongsState(status: SongsStatus.success); expect(state.isSuccess, isTrue); }); test('isLoading', () { - final state = SongState(status: SongStatus.loading); + final state = SongsState(status: SongsStatus.loading); expect(state.isLoading, isTrue); }); }); From fdac6debd318f696c5f8077fcddd7e091eea6ee2 Mon Sep 17 00:00:00 2001 From: Ana Polo Date: Sun, 2 Feb 2025 23:51:08 +0100 Subject: [PATCH 10/10] feat: routes --- lib/album/view/album_view.dart | 40 ++++---- lib/app/app_router/routes/routes.dart | 110 +++++++++++---------- lib/app/app_router/routes/routes.g.dart | 109 +++++++++++--------- lib/song_detail/song_detail.dart | 1 + lib/song_detail/view/song_detail_page.dart | 30 ++++++ lib/song_detail/view/song_detail_view.dart | 22 +++++ lib/song_detail/view/view.dart | 2 + lib/songs/view/songs_page.dart | 2 +- lib/songs/view/songs_view.dart | 36 ++++--- 9 files changed, 221 insertions(+), 131 deletions(-) create mode 100644 lib/song_detail/song_detail.dart create mode 100644 lib/song_detail/view/song_detail_page.dart create mode 100644 lib/song_detail/view/song_detail_view.dart create mode 100644 lib/song_detail/view/view.dart diff --git a/lib/album/view/album_view.dart b/lib/album/view/album_view.dart index 5a2aed5..fc84183 100644 --- a/lib/album/view/album_view.dart +++ b/lib/album/view/album_view.dart @@ -11,22 +11,26 @@ class AlbumView extends StatelessWidget { final isLoading = context.select((AlbumBloc bloc) => bloc.state.isLoading); final isSuccess = context.select((AlbumBloc bloc) => bloc.state.isSuccess); - return isLoading - ? const Center( - child: CircularProgressIndicator(), - ) - : isSuccess - ? const Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - AlbumsHeader(), - Expanded( - child: AlbumsContent(), - ), - ], - ) - : const Text('Failed to fetch albums'); + if (isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + if (isSuccess) { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + AlbumsHeader(), + Expanded( + child: AlbumsContent(), + ), + ], + ); + } else { + return const Text('Failed to fetch albums'); + } + } } } @@ -82,11 +86,11 @@ class AlbumsContent extends StatelessWidget { final album = albums[index]; return GestureDetector( onTap: () => SongsPageRoute( - id: album.albumId, + albumId: album.albumId, albumTitle: album.title, coverAlbum: album.coverAlbum, albumReleaseDate: album.releaseDate, - ).go(context), + ).push(context), child: AlbumItem( title: album.title, releaseDate: album.releaseDate, diff --git a/lib/app/app_router/routes/routes.dart b/lib/app/app_router/routes/routes.dart index 2aaabc2..be472fa 100644 --- a/lib/app/app_router/routes/routes.dart +++ b/lib/app/app_router/routes/routes.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:swiftify/album/album.dart'; import 'package:swiftify/app/app_router/router.dart'; import 'package:swiftify/favorites/favorites.dart'; +import 'package:swiftify/song_detail/song_detail.dart'; import 'package:swiftify/songs/songs.dart'; import 'package:swiftify/theme/theme.dart'; @@ -15,26 +16,13 @@ final rootNavigatorKey = GlobalKey(); routes: >[ TypedGoRoute( path: AlbumPageRoute.path, - routes: [ - TypedGoRoute( - path: SongsPageRoute.path, - routes: [ - TypedGoRoute( - path: SongDetailPageRoute.path, - ) - ], - ), - ], ), TypedGoRoute( path: FavoritesPageRoute.path, - ), - TypedGoRoute( - path: ThemePageRoute.path, + name: FavoritesPageRoute.name, ), ], ) -@immutable class AppShellRoute extends ShellRouteData { const AppShellRoute(); @@ -56,11 +44,15 @@ class AppShellRoute extends ShellRouteData { } } -@immutable +@TypedGoRoute( + path: ThemePageRoute.path, + name: ThemePageRoute.name, +) class ThemePageRoute extends GoRouteData { const ThemePageRoute(); static const path = '/theme'; + static const name = 'theme'; @override Page buildPage(BuildContext context, GoRouterState state) { @@ -70,11 +62,11 @@ class ThemePageRoute extends GoRouteData { } } -@immutable class FavoritesPageRoute extends GoRouteData { const FavoritesPageRoute(); static const path = '/favorites'; + static const name = 'favorites'; @override Page buildPage(BuildContext context, GoRouterState state) { @@ -87,7 +79,19 @@ class FavoritesPageRoute extends GoRouteData { } } -@immutable +@TypedGoRoute( + path: AlbumPageRoute.path, + routes: [ + TypedGoRoute( + path: SongsPageRoute.path, + name: SongsPageRoute.name, + ), + TypedGoRoute( + path: SongDetailPageRoute.path, + name: SongDetailPageRoute.name, + ), + ], +) class AlbumPageRoute extends GoRouteData { const AlbumPageRoute(); static const path = '/'; @@ -103,33 +107,9 @@ class AlbumPageRoute extends GoRouteData { } } -@immutable -class SongDetailPageRoute extends GoRouteData { - const SongDetailPageRoute({ - required this.id, - this.coverAlbum, - }); - - final int id; - final String? coverAlbum; - - static const path = 'song/:id'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return CustomTransitionPage( - child: Container(), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeTransition(opacity: animation, child: child); - }, - ); - } -} - -@immutable class SongsPageRoute extends GoRouteData { const SongsPageRoute({ - required this.id, + required this.albumId, this.albumTitle, this.coverAlbum, this.albumReleaseDate, @@ -137,7 +117,7 @@ class SongsPageRoute extends GoRouteData { /// The album id to display the songs for. /// This is passed in the path as a parameter - final int id; + final int albumId; /// The title of the album /// This is passed in as a query parameter. @@ -154,18 +134,15 @@ class SongsPageRoute extends GoRouteData { /// It is optional and can be null. final String? albumReleaseDate; - static const path = 'songs/:id'; - - /// The parent navigator key. - /// This is used to push the page on the parent navigator - /// when the page is a nested route - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + static const path = 'songs/:albumId'; + static const name = 'songs'; @override Page buildPage(BuildContext context, GoRouterState state) { return CustomTransitionPage( + key: state.pageKey, child: SongsPage( - albumId: id, + albumId: albumId, albumTitle: albumTitle, coverAlbum: coverAlbum, ), @@ -175,3 +152,36 @@ class SongsPageRoute extends GoRouteData { ); } } + +class SongDetailPageRoute extends GoRouteData { + const SongDetailPageRoute({ + required this.songId, + this.songTitle, + this.lyrics, + this.coverAlbum, + }); + + final int songId; + final String? lyrics; + final String? songTitle; + final String? coverAlbum; + + static const path = '/song-detail/:songId'; + static const name = 'song-detail'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return CustomTransitionPage( + key: state.pageKey, + child: SongDetailPage( + songId: songId, + songTitle: songTitle ?? '', + lyrics: lyrics ?? '', + coverAlbum: coverAlbum, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + ); + } +} diff --git a/lib/app/app_router/routes/routes.g.dart b/lib/app/app_router/routes/routes.g.dart index a36838c..c7daf54 100644 --- a/lib/app/app_router/routes/routes.g.dart +++ b/lib/app/app_router/routes/routes.g.dart @@ -8,6 +8,8 @@ part of 'routes.dart'; List get $appRoutes => [ $appShellRoute, + $themePageRoute, + $albumPageRoute, ]; RouteBase get $appShellRoute => ShellRouteData.$route( @@ -17,28 +19,12 @@ RouteBase get $appShellRoute => ShellRouteData.$route( GoRouteData.$route( path: '/', factory: $AlbumPageRouteExtension._fromState, - routes: [ - GoRouteData.$route( - path: 'songs/:id', - parentNavigatorKey: SongsPageRoute.$parentNavigatorKey, - factory: $SongsPageRouteExtension._fromState, - routes: [ - GoRouteData.$route( - path: 'song/:id', - factory: $SongDetailPageRouteExtension._fromState, - ), - ], - ), - ], ), GoRouteData.$route( path: '/favorites', + name: 'favorites', factory: $FavoritesPageRouteExtension._fromState, ), - GoRouteData.$route( - path: '/theme', - factory: $ThemePageRouteExtension._fromState, - ), ], ); @@ -64,21 +50,12 @@ extension $AlbumPageRouteExtension on AlbumPageRoute { void replace(BuildContext context) => context.replace(location); } -extension $SongsPageRouteExtension on SongsPageRoute { - static SongsPageRoute _fromState(GoRouterState state) => SongsPageRoute( - id: int.parse(state.pathParameters['id']!), - albumTitle: state.uri.queryParameters['album-title'], - coverAlbum: state.uri.queryParameters['cover-album'], - albumReleaseDate: state.uri.queryParameters['album-release-date'], - ); +extension $FavoritesPageRouteExtension on FavoritesPageRoute { + static FavoritesPageRoute _fromState(GoRouterState state) => + const FavoritesPageRoute(); String get location => GoRouteData.$location( - '/songs/${Uri.encodeComponent(id.toString())}', - queryParams: { - if (albumTitle != null) 'album-title': albumTitle, - if (coverAlbum != null) 'cover-album': coverAlbum, - if (albumReleaseDate != null) 'album-release-date': albumReleaseDate, - }, + '/favorites', ); void go(BuildContext context) => context.go(location); @@ -91,18 +68,18 @@ extension $SongsPageRouteExtension on SongsPageRoute { void replace(BuildContext context) => context.replace(location); } -extension $SongDetailPageRouteExtension on SongDetailPageRoute { - static SongDetailPageRoute _fromState(GoRouterState state) => - SongDetailPageRoute( - id: int.parse(state.pathParameters['id']!), - coverAlbum: state.uri.queryParameters['cover-album'], - ); +RouteBase get $themePageRoute => GoRouteData.$route( + path: '/theme', + name: 'theme', + factory: $ThemePageRouteExtension._fromState, + ); + +extension $ThemePageRouteExtension on ThemePageRoute { + static ThemePageRoute _fromState(GoRouterState state) => + const ThemePageRoute(); String get location => GoRouteData.$location( - '/songs/${Uri.encodeComponent(id.toString())}/song/${Uri.encodeComponent(id.toString())}', - queryParams: { - if (coverAlbum != null) 'cover-album': coverAlbum, - }, + '/theme', ); void go(BuildContext context) => context.go(location); @@ -115,12 +92,38 @@ extension $SongDetailPageRouteExtension on SongDetailPageRoute { void replace(BuildContext context) => context.replace(location); } -extension $FavoritesPageRouteExtension on FavoritesPageRoute { - static FavoritesPageRoute _fromState(GoRouterState state) => - const FavoritesPageRoute(); +RouteBase get $albumPageRoute => GoRouteData.$route( + path: '/', + factory: $AlbumPageRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'songs/:albumId', + name: 'songs', + factory: $SongsPageRouteExtension._fromState, + ), + GoRouteData.$route( + path: '/song-detail/:songId', + name: 'song-detail', + factory: $SongDetailPageRouteExtension._fromState, + ), + ], + ); + +extension $SongsPageRouteExtension on SongsPageRoute { + static SongsPageRoute _fromState(GoRouterState state) => SongsPageRoute( + albumId: int.parse(state.pathParameters['albumId']!), + albumTitle: state.uri.queryParameters['album-title'], + coverAlbum: state.uri.queryParameters['cover-album'], + albumReleaseDate: state.uri.queryParameters['album-release-date'], + ); String get location => GoRouteData.$location( - '/favorites', + '/songs/${Uri.encodeComponent(albumId.toString())}', + queryParams: { + if (albumTitle != null) 'album-title': albumTitle, + if (coverAlbum != null) 'cover-album': coverAlbum, + if (albumReleaseDate != null) 'album-release-date': albumReleaseDate, + }, ); void go(BuildContext context) => context.go(location); @@ -133,12 +136,22 @@ extension $FavoritesPageRouteExtension on FavoritesPageRoute { void replace(BuildContext context) => context.replace(location); } -extension $ThemePageRouteExtension on ThemePageRoute { - static ThemePageRoute _fromState(GoRouterState state) => - const ThemePageRoute(); +extension $SongDetailPageRouteExtension on SongDetailPageRoute { + static SongDetailPageRoute _fromState(GoRouterState state) => + SongDetailPageRoute( + songId: int.parse(state.pathParameters['songId']!), + songTitle: state.uri.queryParameters['song-title'], + lyrics: state.uri.queryParameters['lyrics'], + coverAlbum: state.uri.queryParameters['cover-album'], + ); String get location => GoRouteData.$location( - '/theme', + '/song-detail/${Uri.encodeComponent(songId.toString())}', + queryParams: { + if (songTitle != null) 'song-title': songTitle, + if (lyrics != null) 'lyrics': lyrics, + if (coverAlbum != null) 'cover-album': coverAlbum, + }, ); void go(BuildContext context) => context.go(location); diff --git a/lib/song_detail/song_detail.dart b/lib/song_detail/song_detail.dart new file mode 100644 index 0000000..00ffcf9 --- /dev/null +++ b/lib/song_detail/song_detail.dart @@ -0,0 +1 @@ +export 'view/view.dart'; diff --git a/lib/song_detail/view/song_detail_page.dart b/lib/song_detail/view/song_detail_page.dart new file mode 100644 index 0000000..a898a3d --- /dev/null +++ b/lib/song_detail/view/song_detail_page.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:swiftify/song_detail/song_detail.dart'; + +class SongDetailPage extends StatelessWidget { + const SongDetailPage({ + required this.songId, + required this.lyrics, + required this.songTitle, + this.coverAlbum, + super.key, + }); + + final int songId; + final String lyrics; + final String? coverAlbum; + final String songTitle; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(songTitle), + ), + body: SongDetailView( + lyrics: lyrics, + coverAlbum: coverAlbum, + ), + ); + } +} diff --git a/lib/song_detail/view/song_detail_view.dart b/lib/song_detail/view/song_detail_view.dart new file mode 100644 index 0000000..349a5ed --- /dev/null +++ b/lib/song_detail/view/song_detail_view.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class SongDetailView extends StatelessWidget { + const SongDetailView({ + required this.lyrics, + this.coverAlbum, + super.key, + }); + + final String? coverAlbum; + final String? lyrics; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text(lyrics ?? 'No lyrics found'), + ), + ); + } +} diff --git a/lib/song_detail/view/view.dart b/lib/song_detail/view/view.dart new file mode 100644 index 0000000..3d50d11 --- /dev/null +++ b/lib/song_detail/view/view.dart @@ -0,0 +1,2 @@ +export 'song_detail_page.dart'; +export 'song_detail_view.dart'; diff --git a/lib/songs/view/songs_page.dart b/lib/songs/view/songs_page.dart index c84c0cc..7cbe26e 100644 --- a/lib/songs/view/songs_page.dart +++ b/lib/songs/view/songs_page.dart @@ -27,7 +27,7 @@ class SongsPage extends StatelessWidget { ), child: Scaffold( appBar: AppBar( - title: Text('Songs'), + title: const Text('Songs'), ), body: SongsView( albumTitle: albumTitle ?? '', diff --git a/lib/songs/view/songs_view.dart b/lib/songs/view/songs_view.dart index 0d03d5a..d60c269 100644 --- a/lib/songs/view/songs_view.dart +++ b/lib/songs/view/songs_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:swiftify/app/app_router/router.dart'; import 'package:swiftify/songs/songs.dart'; import 'package:swiftify_repository/swiftify_repository.dart'; @@ -74,7 +75,12 @@ class SongCard extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () {}, + onTap: () => SongDetailPageRoute( + songId: song.songId, + songTitle: song.title, + lyrics: song.lyrics ?? '', + coverAlbum: coverAlbum, + ).push(context), child: Card( margin: const EdgeInsets.symmetric( vertical: 8, @@ -106,19 +112,21 @@ class AlbumCoverImage extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( - child: Container( - constraints: const BoxConstraints( - maxWidth: 80, - ), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - ), - child: Image.network( - coverAlbum!, - fit: BoxFit.cover, - ), - ), + child: coverAlbum != null + ? Container( + constraints: const BoxConstraints( + maxWidth: 80, + ), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: Image.network( + coverAlbum!, + fit: BoxFit.cover, + ), + ) + : const SizedBox(), ); } }