From e6699735a68763787a74941b3402007738683c03 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Sun, 23 Apr 2023 17:24:03 -0600 Subject: Initial WIP for practice 6 --- lib/models/album.dart | 37 ++++++++++ lib/models/artist.dart | 28 ++++++++ lib/network/music_api.dart | 32 +++++++++ lib/routes.dart | 4 ++ lib/screens/album_detail_screen.dart | 130 +++++++++++++++++++++++++++++++++++ lib/screens/albums_screen.dart | 70 +++++++++++++++++++ lib/screens/dashboard_screen.dart | 7 ++ lib/widgets/album_item.dart | 72 +++++++++++++++++++ lib/widgets/album_placeholder.dart | 21 ++++++ pubspec.lock | 8 +++ pubspec.yaml | 1 + 11 files changed, 410 insertions(+) create mode 100644 lib/models/album.dart create mode 100644 lib/models/artist.dart create mode 100644 lib/network/music_api.dart create mode 100644 lib/screens/album_detail_screen.dart create mode 100644 lib/screens/albums_screen.dart create mode 100644 lib/widgets/album_item.dart create mode 100644 lib/widgets/album_placeholder.dart diff --git a/lib/models/album.dart b/lib/models/album.dart new file mode 100644 index 0000000..a7a0f91 --- /dev/null +++ b/lib/models/album.dart @@ -0,0 +1,37 @@ +class Album { + final String title; + final String firstReleaseDate; + final List? secondaryTypes; + final String primaryTypeId; + final String id; + final String primaryType; + final List? secondaryTypeIds; + final String disambiguation; + + Uri get coverUri => + Uri.parse('https://coverartarchive.org/release-group/$id/front'); + + const Album({ + required this.title, + required this.firstReleaseDate, + required this.secondaryTypes, + required this.primaryTypeId, + required this.id, + required this.primaryType, + required this.secondaryTypeIds, + required this.disambiguation, + }); + + factory Album.fromMap(Map map) { + return Album( + title: map["title"], + firstReleaseDate: map["first-release-date"], + secondaryTypes: map["secondary-types"], + primaryTypeId: map["primary-type-id"], + id: map["id"], + primaryType: map["primary-type"], + secondaryTypeIds: map["secondary-types-id"], + disambiguation: map["disambiguation"], + ); + } +} diff --git a/lib/models/artist.dart b/lib/models/artist.dart new file mode 100644 index 0000000..70c17db --- /dev/null +++ b/lib/models/artist.dart @@ -0,0 +1,28 @@ +class Artist { + final String type; + final String name; + final String typeId; + final String sortName; + final String disambiguation; + final String id; + + const Artist({ + required this.type, + required this.name, + required this.typeId, + required this.sortName, + required this.disambiguation, + required this.id, + }); + + factory Artist.fromMap(Map map) { + return Artist( + type: map["type"], + name: map["name"], + typeId: map["type-id"], + sortName: map["sort-name"], + disambiguation: map["disambiguation"], + id: map["id"], + ); + } +} diff --git a/lib/network/music_api.dart b/lib/network/music_api.dart new file mode 100644 index 0000000..490c51d --- /dev/null +++ b/lib/network/music_api.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:pmsna1/models/album.dart'; +import 'package:pmsna1/models/artist.dart'; + +class MusicApi { + Uri artistUri(String artistId) => Uri.parse( + 'https://musicbrainz.org/ws/2/artist/$artistId?inc=release-groups&fmt=json'); + + Uri releaseGroupUri(String releaseGroupId) => Uri.parse( + 'https://musicbrainz.org/ws/2/release-group/$releaseGroupId?inc=artists&fmt=json'); + + Future?> getAlvvaysAlbums() async { + String id = '99450990-b24e-4132-bb68-235f8c3e2564'; + http.Response result = await http.get(artistUri(id)); + var list = jsonDecode(result.body)['release-groups'] as List; + if (result.statusCode != 200) { + return null; + } + return list.map((map) => Album.fromMap(map)).toList(); + } + + Future?> getAlbumArtists(String id) async { + http.Response result = await http.get(releaseGroupUri(id)); + var list = jsonDecode(result.body)['artist-credit'] as List; + if (result.statusCode != 200) { + return null; + } + return list.map((map) => Artist.fromMap(map["artist"])).toList(); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 766273b..4259727 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:pmsna1/screens/album_detail_screen.dart'; +import 'package:pmsna1/screens/albums_screen.dart'; import 'package:pmsna1/screens/dashboard_screen.dart'; import 'package:pmsna1/screens/events_screen.dart'; import 'package:pmsna1/screens/new_event_screen.dart'; @@ -19,5 +21,7 @@ Map getApplicationRoutes() { '/events': (BuildContext context) => const EventsScreen(), '/popular': (BuildContext context) => const PopularScreen(), '/newevent': (BuildContext context) => const NewEventScreen(), + '/albums': (BuildContext context) => const AlbumsScreen(), + '/album': (BuildContext context) => const AlbumDetailScreen(), }; } diff --git a/lib/screens/album_detail_screen.dart b/lib/screens/album_detail_screen.dart new file mode 100644 index 0000000..478c0b6 --- /dev/null +++ b/lib/screens/album_detail_screen.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:pmsna1/models/album.dart'; +import 'package:pmsna1/models/artist.dart'; +import 'package:pmsna1/network/music_api.dart'; +import 'package:pmsna1/widgets/album_placeholder.dart'; +import 'package:pmsna1/widgets/responsive.dart'; + +class AlbumDetailScreen extends StatefulWidget { + const AlbumDetailScreen({super.key}); + + @override + State createState() => _AlbumDetailScreenState(); +} + +class _AlbumDetailScreenState extends State { + MusicApi? api; + Album? album; + + @override + void initState() { + super.initState(); + api = MusicApi(); + } + + Widget getArtistCard(Artist artist, BuildContext context) => Card( + margin: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 2.0), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + artist.name, + style: Theme.of(context) + .typography + .englishLike + .bodyLarge + ?.copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox(height: 4.0), + Text(artist.disambiguation), + ], + ), + ), + ); + + // Widget getLeft() => Image.network(album!.coverUri.toString()); + Widget getLeft() => const SizedBox.shrink(); + + Widget getRight() => Column( + children: [ + ListTile( + title: const Text('Fecha de lanzamiento'), + subtitle: Text(album!.firstReleaseDate), + ), + ListTile( + title: const Text('Tipo'), + subtitle: Text(album!.primaryType), + ), + ListTile( + title: const Text('Artistas'), + subtitle: FutureBuilder( + future: api!.getAlbumArtists(album!.id), + initialData: const [], + builder: (context, snapshot) => snapshot.hasData + ? ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: snapshot.data?.length, + itemBuilder: (context, index) { + Artist artist = snapshot.data![index]; + return getArtistCard(artist, context); + }) + : snapshot.hasError + ? const Text('Ocurrió un error') + : const CircularProgressIndicator(), + ), + ) + ], + ); + + @override + Widget build(BuildContext context) { + album = ModalRoute.of(context)?.settings.arguments as Album?; + if (album == null) return const Placeholder(); + + Image cover = Image.network( + album!.coverUri.toString(), + fit: BoxFit.cover, + alignment: Alignment.center, + errorBuilder: (context, error, stackTrace) => const AlbumPlaceholder(400), + loadingBuilder: (context, child, loadingProgress) => + loadingProgress == null ? child : const AlbumPlaceholder(400), + ); + + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) => + [ + SliverAppBar( + expandedHeight: 400.0, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + title: Text(album!.title, + style: TextStyle( + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + color: Theme.of(context).colorScheme.onPrimaryContainer, + )), + background: cover, + ), + ) + ], + body: SingleChildScrollView( + child: Responsive( + mobile: Column(children: [getLeft(), getRight()]), + tablet: Row(children: [ + Expanded(child: getLeft()), + Expanded(child: getRight()), + ]), + desktop: Row(children: [ + Expanded(child: getLeft()), + Expanded(child: getRight()), + ]), + ), + ), + ), + ); + } +} diff --git a/lib/screens/albums_screen.dart b/lib/screens/albums_screen.dart new file mode 100644 index 0000000..d376e35 --- /dev/null +++ b/lib/screens/albums_screen.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:pmsna1/models/album.dart'; +import 'package:pmsna1/network/music_api.dart'; +import 'package:pmsna1/widgets/album_item.dart'; +import 'package:pmsna1/widgets/responsive.dart'; + +import '../widgets/loading_modal_widget.dart'; + +class AlbumsScreen extends StatefulWidget { + const AlbumsScreen({super.key}); + + @override + State createState() => _AlbumsScreenState(); +} + +class _AlbumsScreenState extends State { + MusicApi? musicApi; + + @override + void initState() { + super.initState(); + musicApi = MusicApi(); + } + + Widget getGridForCount( + int count, + int itemCount, + Widget Function(BuildContext, int) itemBuilder, + ) { + return MasonryGridView.builder( + gridDelegate: SliverSimpleGridDelegateWithFixedCrossAxisCount( + crossAxisCount: count, + ), + itemCount: itemCount, + itemBuilder: itemBuilder, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Música (Alvvays)')), + body: FutureBuilder( + future: musicApi!.getAlvvaysAlbums(), + builder: (context, AsyncSnapshot?> snapshot) { + int itemCount = snapshot.data != null ? snapshot.data!.length : 0; + itemBuilder(BuildContext context, int index) { + return AlbumItem(snapshot.data![index]); + } + + if (snapshot.hasData) { + return Responsive( + mobile: getGridForCount(2, itemCount, itemBuilder), + tablet: getGridForCount(3, itemCount, itemBuilder), + desktop: getGridForCount(4, itemCount, itemBuilder), + ); + } else if (snapshot.hasError) { + print(snapshot.error); + return const Center( + child: Text('Ocurrió un error'), + ); + } else { + return const LoadingModal(); + } + }, + ), + ); + } +} diff --git a/lib/screens/dashboard_screen.dart b/lib/screens/dashboard_screen.dart index 03b84e7..914749c 100644 --- a/lib/screens/dashboard_screen.dart +++ b/lib/screens/dashboard_screen.dart @@ -51,6 +51,13 @@ class DashboardScreen extends StatelessWidget { Navigator.of(context).pushNamed('/events'); }, ), + ListTile( + title: const Text('Música'), + leading: const Icon(Icons.music_note), + onTap: () { + Navigator.of(context).pushNamed('/albums'); + }, + ), ListTile( title: const Text('Tema'), trailing: SegmentedButton( diff --git a/lib/widgets/album_item.dart b/lib/widgets/album_item.dart new file mode 100644 index 0000000..8794c24 --- /dev/null +++ b/lib/widgets/album_item.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:pmsna1/models/album.dart'; +import 'package:pmsna1/widgets/album_placeholder.dart'; + +class AlbumItem extends StatelessWidget { + final Album album; + + const AlbumItem(this.album, {super.key}); + + @override + Widget build(BuildContext context) { + Color textColor = Theme.of(context).colorScheme.onSurface; + return Card( + margin: const EdgeInsets.all(5.0), + semanticContainer: true, + clipBehavior: Clip.antiAliasWithSaveLayer, + child: InkWell( + onTap: () { + Navigator.of(context).pushNamed('/album', arguments: album); + }, + child: Column( + children: [ + FadeInImage( + placeholder: const AssetImage('assets/loading.gif'), + imageErrorBuilder: (context, error, stackTrace) => + const AspectRatio( + aspectRatio: 1.0, + child: AlbumPlaceholder(400), + ), + image: NetworkImage(album.coverUri.toString()), + fit: BoxFit.fill, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + album.title, + style: Theme.of(context) + .typography + .englishLike + .labelLarge + ?.copyWith(color: textColor), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4.0), + Text( + album.firstReleaseDate, + style: Theme.of(context) + .typography + .englishLike + .labelMedium + ?.copyWith(color: textColor), + ), + const SizedBox(height: 4.0), + Text( + "(${album.primaryType})", + style: Theme.of(context) + .typography + .englishLike + .labelSmall + ?.copyWith(color: textColor), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/album_placeholder.dart b/lib/widgets/album_placeholder.dart new file mode 100644 index 0000000..420c2e9 --- /dev/null +++ b/lib/widgets/album_placeholder.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class AlbumPlaceholder extends StatelessWidget { + final double size; + + const AlbumPlaceholder(this.size, {super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: size, + width: size, + color: Theme.of(context).colorScheme.secondaryContainer, + child: Icon( + Icons.album, + size: 60.0, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index f6a708d..2f6c37c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -206,6 +206,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.8" + flutter_staggered_grid_view: + dependency: "direct main" + description: + name: flutter_staggered_grid_view + sha256: "1312314293acceb65b92754298754801b0e1f26a1845833b740b30415bbbcf07" + url: "https://pub.dev" + source: hosted + version: "0.6.2" flutter_svg: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5cfe56b..a189e04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: firebase_core: ^2.8.0 firebase_auth: ^4.3.0 table_calendar: <=3.0.9 + flutter_staggered_grid_view: ^0.6.2 dev_dependencies: flutter_test: -- cgit v1.2.3