diff --git a/Sources/justMUSIC/assets/images/saved.png b/Sources/justMUSIC/assets/images/saved.png new file mode 100644 index 0000000..1fe5ae9 Binary files /dev/null and b/Sources/justMUSIC/assets/images/saved.png differ diff --git a/Sources/justMUSIC/lib/main.dart b/Sources/justMUSIC/lib/main.dart index 4ac0733..383e0f6 100644 --- a/Sources/justMUSIC/lib/main.dart +++ b/Sources/justMUSIC/lib/main.dart @@ -29,6 +29,7 @@ import 'package:timezone/data/latest.dart' as tz; Future main() async { tz.initializeTimeZones(); + Paint.enableDithering = true; WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, @@ -45,7 +46,6 @@ class MyApp extends StatefulWidget { static PostViewModel postViewModel = PostViewModel(); static AudioPlayer audioPlayer = AudioPlayer(); static CommentViewModel commentViewModel = CommentViewModel(); - static const keyManager = 'customCacheKey'; const MyApp({super.key}); diff --git a/Sources/justMUSIC/lib/model/Music.dart b/Sources/justMUSIC/lib/model/Music.dart index ed9d9f4..48862ab 100644 --- a/Sources/justMUSIC/lib/model/Music.dart +++ b/Sources/justMUSIC/lib/model/Music.dart @@ -14,11 +14,11 @@ class Music { List _artists; // Constructor - Music(this._id, this._title, this._cover, this._previewUrl, this._date, - this._duration, this._explicit, this._artists); + Music( + this._id, this._title, this._cover, this._previewUrl, this._date, this._duration, this._explicit, this._artists); //Getters and setters - String? get id => _id; + String get id => _id; String? get title => _title; diff --git a/Sources/justMUSIC/lib/model/User.dart b/Sources/justMUSIC/lib/model/User.dart index d88a05e..0cf474a 100644 --- a/Sources/justMUSIC/lib/model/User.dart +++ b/Sources/justMUSIC/lib/model/User.dart @@ -6,12 +6,14 @@ class User { String _pp; String _token; List _followers; + List _musics_likes; + int _capsules; List _followed; // Constructor - User(this._id, this._pseudo, this._uniquePseudo, this._mail, this._pp, - this._token, this._followers, this._capsules, this._followed); + User(this._id, this._pseudo, this._uniquePseudo, this._mail, this._pp, this._token, this._followers, + this._musics_likes, this._capsules, this._followed); //Getters and setters String get id => _id; @@ -22,6 +24,12 @@ class User { _pseudo = value; } + List get musics_likes => _musics_likes; + + set musics_likes(List value) { + _musics_likes = value; + } + String get uniquePseudo => _uniquePseudo; set uniquePseudo(String value) { diff --git a/Sources/justMUSIC/lib/model/mapper/UserMapper.dart b/Sources/justMUSIC/lib/model/mapper/UserMapper.dart index 7d0c5ab..e21ff5c 100644 --- a/Sources/justMUSIC/lib/model/mapper/UserMapper.dart +++ b/Sources/justMUSIC/lib/model/mapper/UserMapper.dart @@ -12,6 +12,7 @@ class UserMapper { data?["picture"], data?["token_notify"], List.from(data?["followers"] as List), + List.from(data?["musics_likes"] as List), data?["nbCapsules"] ?? 0, List.from(data?["followed"] as List)); } diff --git a/Sources/justMUSIC/lib/screens/feed_screen.dart b/Sources/justMUSIC/lib/screens/feed_screen.dart index 4382192..4ad8120 100644 --- a/Sources/justMUSIC/lib/screens/feed_screen.dart +++ b/Sources/justMUSIC/lib/screens/feed_screen.dart @@ -91,7 +91,14 @@ class _FeedScreenState extends State with SingleTickerProviderStateM builder: ((BuildContext context) { return ClipRRect( borderRadius: BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), - child: DetailPostScreen(post: post)); + child: SizedBox( + height: 760.h, + child: Scaffold( + primary: false, + extendBody: false, + backgroundColor: Colors.transparent, + body: DetailPostScreen(post: post)), + )); }), ); } diff --git a/Sources/justMUSIC/lib/screens/post_screen.dart b/Sources/justMUSIC/lib/screens/post_screen.dart index c7ffce9..fb6dbed 100644 --- a/Sources/justMUSIC/lib/screens/post_screen.dart +++ b/Sources/justMUSIC/lib/screens/post_screen.dart @@ -11,7 +11,6 @@ import '../components/editable_post_component.dart'; import '../components/post_button_component.dart'; import '../main.dart'; import '../model/Music.dart'; -import '../model/Post.dart'; import '../values/constants.dart'; class PostScreen extends StatefulWidget { @@ -93,7 +92,7 @@ class _PostScreenState extends State with SingleTickerProviderStateM } handleSubmit() async { - MyApp.postViewModel.addPost(description, (selectedMusic?.id)!, selectedImage, selectedCity); + MyApp.postViewModel.addPost(description, selectedMusic!.id, selectedImage, selectedCity); quit(); } diff --git a/Sources/justMUSIC/lib/screens/search_song_screen.dart b/Sources/justMUSIC/lib/screens/search_song_screen.dart index 8b702de..05353df 100644 --- a/Sources/justMUSIC/lib/screens/search_song_screen.dart +++ b/Sources/justMUSIC/lib/screens/search_song_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/Material.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:justmusic/model/Music.dart'; @@ -21,6 +22,7 @@ class SearchSongScreen extends StatefulWidget { class _SearchSongScreenState extends State { final ScrollController _scrollController = ScrollController(); final TextEditingController _textEditingController = TextEditingController(); + PageController controller = PageController(); int? playingIndex; @@ -81,6 +83,10 @@ class _SearchSongScreenState extends State { } } + Future> _fetchSavedSong() async { + return await MyApp.musicViewModel.getFavoriteMusicsByUserId(MyApp.userViewModel.userCurrent.id); + } + @override void dispose() { MyApp.audioPlayer.pause(); @@ -124,7 +130,6 @@ class _SearchSongScreenState extends State { child: SizedBox( height: 40, child: TextField( - autofocus: true, controller: _textEditingController, keyboardAppearance: Brightness.dark, onEditingComplete: resetFullScreen, @@ -132,11 +137,15 @@ class _SearchSongScreenState extends State { if (_textEditingController.text.isEmpty) { fetchTrendingMusic(); } else { + setState(() { + filteredData = []; + }); filteredData = await MyApp.musicViewModel.getMusicsWithNameOrArtistName(value); setState(() { filteredData = filteredData; }); } + controller.animateTo(0, duration: Duration(milliseconds: 200), curve: Curves.easeIn); }, cursorColor: Colors.white, keyboardType: TextInputType.text, @@ -163,42 +172,94 @@ class _SearchSongScreenState extends State { ), ), Flexible( - child: ScrollConfiguration( - behavior: ScrollBehavior().copyWith(scrollbars: true), - child: ListView.builder( - physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), - controller: _scrollController, - itemCount: filteredData.length, - itemBuilder: (context, index) { - if (playingIndex == index) { - return InkWell( - onTap: () { - widget.callback(filteredData[index]); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: MusicListComponent( - music: filteredData[index], - playing: true, - callback: playMusic, - index: index, - ), - )); - } - return InkWell( - onTap: () { - widget.callback(filteredData[index]); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: MusicListComponent( - music: filteredData[index], - playing: false, - callback: playMusic, - index: index, - ), - )); - }), + child: PageView( + controller: controller, + physics: BouncingScrollPhysics(), + children: [ + ScrollConfiguration( + behavior: ScrollBehavior().copyWith(scrollbars: true), + child: ListView.builder( + physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), + controller: _scrollController, + itemCount: filteredData.length, + itemBuilder: (context, index) { + if (playingIndex == index) { + return InkWell( + onTap: () { + widget.callback(filteredData[index]); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: MusicListComponent( + music: filteredData[index], + playing: true, + callback: playMusic, + index: index, + ), + )); + } + return InkWell( + onTap: () { + widget.callback(filteredData[index]); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: MusicListComponent( + music: filteredData[index], + playing: false, + callback: playMusic, + index: index, + ), + )); + }), + ), + ScrollConfiguration( + behavior: ScrollBehavior().copyWith(scrollbars: true), + child: FutureBuilder( + future: _fetchSavedSong(), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return ListView.builder( + physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast), + controller: _scrollController, + itemCount: snapshot.data?.length, + itemBuilder: (context, index) { + if (playingIndex == index) { + return InkWell( + onTap: () { + widget.callback(filteredData[index]); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: MusicListComponent( + music: (snapshot.data?[index])!, + playing: true, + callback: playMusic, + index: index, + ), + )); + } + return InkWell( + onTap: () { + widget.callback((snapshot.data?[index])!); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: MusicListComponent( + music: (snapshot.data?[index])!, + playing: false, + callback: playMusic, + index: index, + ), + )); + }); + } else { + return CupertinoActivityIndicator(); + } + }, + ), + ) + ], )) ], ), diff --git a/Sources/justMUSIC/lib/services/AuthService.dart b/Sources/justMUSIC/lib/services/AuthService.dart index b668fba..c11be8e 100644 --- a/Sources/justMUSIC/lib/services/AuthService.dart +++ b/Sources/justMUSIC/lib/services/AuthService.dart @@ -29,7 +29,7 @@ class AuthService { "nbCapsules": 0, "followers": [], "token_notify": token, - "musics_likes": [], + "saved_musics": [], "picture": "https://firebasestorage.googleapis.com/v0/b/justmusic-435d5.appspot.com/o/justMusicDefaultImage.png?alt=media&token=020d0fcb-b7df-4d4d-b380-e99597293fcc" }; @@ -55,10 +55,8 @@ class AuthService { Future generateUniqueId(String pseudo) async { String uniqueId = '$pseudo#0001'; int suffix = 1; - final CollectionReference usersCollection = - FirebaseFirestore.instance.collection("users"); - final QuerySnapshot querySnapshot = - await usersCollection.where('pseudo', isEqualTo: pseudo).get(); + final CollectionReference usersCollection = FirebaseFirestore.instance.collection("users"); + final QuerySnapshot querySnapshot = await usersCollection.where('pseudo', isEqualTo: pseudo).get(); querySnapshot.docs.forEach((snapshot) { suffix++; @@ -70,8 +68,7 @@ class AuthService { login(String email, String password) async { try { - await FirebaseAuth.instance - .signInWithEmailAndPassword(email: email, password: password); + await FirebaseAuth.instance.signInWithEmailAndPassword(email: email, password: password); } on FirebaseAuthException catch (e) { if (e.code == 'user-not-found') { throw ('Mail incorrect'); @@ -98,8 +95,7 @@ class AuthService { .doc(currentUser?.uid) .delete() .then((value) => print("Firestore deleted user")) - .catchError( - (error) => print("Error deleting user from Firestore: $error")); + .catchError((error) => print("Error deleting user from Firestore: $error")); await currentUser?.delete(); await FirebaseAuth.instance.signOut(); diff --git a/Sources/justMUSIC/lib/services/MusicService.dart b/Sources/justMUSIC/lib/services/MusicService.dart index 8d4f4f6..307177e 100644 --- a/Sources/justMUSIC/lib/services/MusicService.dart +++ b/Sources/justMUSIC/lib/services/MusicService.dart @@ -5,45 +5,30 @@ import '../main.dart'; class MusicService { Future getFavoriteMusicsByUserId(String id) async { - var response = - await FirebaseFirestore.instance.collection("users").doc(id).get(); + var response = await FirebaseFirestore.instance.collection("users").doc(id).get(); if (response.exists) { - var musicFavorite = response.get("musics_likes"); + var musicFavorite = response.get("saved_musics"); return List.from(musicFavorite); } else { return []; } } - deleteFavoriteMusic(String id) async { - var userRef = await FirebaseFirestore.instance - .collection("users") - .doc(MyApp.userViewModel.userCurrent.id); - var response = await userRef.get(); - - List musicFavorite = List.from(response.get("musics_likes")); - if (!musicFavorite.contains(id)) { - musicFavorite.remove(id); - await userRef.update({"musics_likes": musicFavorite}); - } else { - print("Delete error: The music is not in the user's favorite music list"); - } - } - Future addOrDeleteFavoriteMusic(String id) async { - var userRef = await FirebaseFirestore.instance - .collection("users") - .doc(MyApp.userViewModel.userCurrent.id); + var userRef = await FirebaseFirestore.instance.collection("users").doc(MyApp.userViewModel.userCurrent.id); var response = await userRef.get(); - List musicFavorite = List.from(response.get("musics_likes")); + List musicFavorite = List.from(response.get("musics_likes")); + if (!musicFavorite.contains(id)) { musicFavorite.add(id); await userRef.update({"musics_likes": musicFavorite}); + MyApp.userViewModel.userCurrent.musics_likes.add(id); return false; } else { musicFavorite.remove(id); await userRef.update({"musics_likes": musicFavorite}); + MyApp.userViewModel.userCurrent.musics_likes.remove(id); return true; } } diff --git a/Sources/justMUSIC/lib/view_model/MusicViewModel.dart b/Sources/justMUSIC/lib/view_model/MusicViewModel.dart index 1e2c531..78c0f8b 100644 --- a/Sources/justMUSIC/lib/view_model/MusicViewModel.dart +++ b/Sources/justMUSIC/lib/view_model/MusicViewModel.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:justmusic/view_model/TokenSpotify.dart'; import 'package:http/http.dart' as http; -import 'package:tuple/tuple.dart'; +import '../main.dart'; import '../model/Artist.dart'; import '../model/Music.dart'; import '../services/MusicService.dart'; @@ -28,8 +28,7 @@ class MusicViewModel { return _getMusicFromResponse(responseData); } else { - throw Exception( - 'Error retrieving music information : ${response.statusCode} ${response.reasonPhrase}'); + throw Exception('Error retrieving music information : ${response.statusCode} ${response.reasonPhrase}'); } } @@ -49,15 +48,12 @@ class MusicViewModel { artists); } - Future> getMusicsWithName(String name, - {int limit = 20, int offset = 0, String market = "FR"}) async { + Future> getMusicsWithName(String name, {int limit = 20, int offset = 0, String market = "FR"}) async { var accessToken = await _token.getAccessToken(); - var response = await http.get( - Uri.parse( - '$API_URL/search?q=track%3A$name&type=track&market=fr&limit=$limit&offset=$offset'), - headers: { - 'Authorization': 'Bearer $accessToken', - }); + var response = await http + .get(Uri.parse('$API_URL/search?q=track%3A$name&type=track&market=fr&limit=$limit&offset=$offset'), headers: { + 'Authorization': 'Bearer $accessToken', + }); if (response.statusCode == 200) { Map responseData = jsonDecode(response.body); @@ -65,20 +61,17 @@ class MusicViewModel { return _getMusicFromResponse(track); })); } else { - throw Exception( - 'Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); + throw Exception('Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); } } Future> getMusicsWithArtistName(String name, {int limit = 20, int offset = 0, String market = "FR"}) async { var accessToken = await _token.getAccessToken(); - var response = await http.get( - Uri.parse( - '$API_URL/search?q=artist%3A$name&type=track&market=fr&limit=$limit&offset=$offset'), - headers: { - 'Authorization': 'Bearer $accessToken', - }); + var response = await http + .get(Uri.parse('$API_URL/search?q=artist%3A$name&type=track&market=fr&limit=$limit&offset=$offset'), headers: { + 'Authorization': 'Bearer $accessToken', + }); if (response.statusCode == 200) { Map responseData = jsonDecode(response.body); @@ -86,24 +79,19 @@ class MusicViewModel { return _getMusicFromResponse(track); })); } else { - throw Exception( - 'Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); + throw Exception('Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); } } Future getArtistWithName(String name, {String market = "FR"}) async { var accessToken = await _token.getAccessToken(); - var response = await http.get( - Uri.parse( - '$API_URL/search?q=artist%3A$name&type=artist&market=$market'), - headers: { - 'Authorization': 'Bearer $accessToken', - }); + var response = await http.get(Uri.parse('$API_URL/search?q=artist%3A$name&type=artist&market=$market'), headers: { + 'Authorization': 'Bearer $accessToken', + }); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); - List artists = - List.from(responseData['artists']['items'].map((artist) { + List artists = List.from(responseData['artists']['items'].map((artist) { String image = ''; if (!artist['images'].isEmpty) { image = artist['images'][0]['url']; @@ -119,25 +107,21 @@ class MusicViewModel { throw Exception('Artist not found : ${name}'); } else { - throw Exception( - 'Error retrieving artist information : ${response.statusCode} ${response.reasonPhrase}'); + throw Exception('Error retrieving artist information : ${response.statusCode} ${response.reasonPhrase}'); } } - Future> getArtistsWithName(String name, - {int limit = 20, int offset = 0, String market = "FR"}) async { + Future> getArtistsWithName(String name, {int limit = 20, int offset = 0, String market = "FR"}) async { var accessToken = await _token.getAccessToken(); var response = await http.get( - Uri.parse( - '$API_URL/search?q=artist%3A$name&type=artist&market=$market&limit=$limit&offset=$offset'), + Uri.parse('$API_URL/search?q=artist%3A$name&type=artist&market=$market&limit=$limit&offset=$offset'), headers: { 'Authorization': 'Bearer $accessToken', }); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); - List artists = - List.from(responseData['artists']['items'].map((artist) { + List artists = List.from(responseData['artists']['items'].map((artist) { String image = ''; if (!artist['images'].isEmpty) { image = artist['images'][0]['url']; @@ -147,19 +131,15 @@ class MusicViewModel { return artists; } else { - throw Exception( - 'Error while retrieving artist : ${response.statusCode} ${response.reasonPhrase}'); + throw Exception('Error while retrieving artist : ${response.statusCode} ${response.reasonPhrase}'); } } - Future> getTopMusicsWithArtistId(String id, - {String market = "FR"}) async { + Future> getTopMusicsWithArtistId(String id, {String market = "FR"}) async { var accessToken = await _token.getAccessToken(); - var response = await http.get( - Uri.parse('$API_URL/artists/$id/top-tracks?market=$market'), - headers: { - 'Authorization': 'Bearer $accessToken', - }); + var response = await http.get(Uri.parse('$API_URL/artists/$id/top-tracks?market=$market'), headers: { + 'Authorization': 'Bearer $accessToken', + }); if (response.statusCode == 200) { Map responseData = jsonDecode(response.body); @@ -167,16 +147,13 @@ class MusicViewModel { return _getMusicFromResponse(track); })); } else { - throw Exception( - 'Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); + throw Exception('Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); } } - Future> getMusicsWithPlaylistId(String id, - {String market = "FR"}) async { + Future> getMusicsWithPlaylistId(String id, {String market = "FR"}) async { var accessToken = await _token.getAccessToken(); - var response = await http - .get(Uri.parse('$API_URL/playlists/$id?market=$market'), headers: { + var response = await http.get(Uri.parse('$API_URL/playlists/$id?market=$market'), headers: { 'Authorization': 'Bearer $accessToken', }); @@ -192,13 +169,11 @@ class MusicViewModel { return musics; } else { - throw Exception( - 'Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); + throw Exception('Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); } } - Future> getMusicsWithIds(List ids, - {String market = "FR"}) async { + Future> getMusicsWithIds(List ids, {String market = "FR"}) async { var accessToken = await _token.getAccessToken(); String url = API_URL + '/tracks?market=$market&ids='; @@ -216,8 +191,7 @@ class MusicViewModel { return _getMusicFromResponse(track); })); } else { - throw Exception( - 'Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); + throw Exception('Error while retrieving music : ${response.statusCode} ${response.reasonPhrase}'); } } @@ -227,18 +201,21 @@ class MusicViewModel { List musics = []; Artist artist = await getArtistWithName(name, market: market); musics.addAll(await getTopMusicsWithArtistId(artist.id)); - musics.addAll(await getMusicsWithName(name, - limit: limit, offset: offset, market: market)); + musics.addAll(await getMusicsWithName(name, limit: limit, offset: offset, market: market)); return musics; } catch (e) { - return await getMusicsWithName(name, - limit: limit, offset: offset, market: market); + return await getMusicsWithName(name, limit: limit, offset: offset, market: market); } } Future> getFavoriteMusicsByUserId(String id) async { try { - var idMusics = await _musicService.getFavoriteMusicsByUserId(id); + List idMusics = []; + if (id == MyApp.userViewModel.userCurrent.id) { + idMusics = MyApp.userViewModel.userCurrent.musics_likes; + } else { + idMusics = await _musicService.getFavoriteMusicsByUserId(id); + } return await getMusicsWithIds(idMusics); } catch (e) { print(e); @@ -254,26 +231,4 @@ class MusicViewModel { rethrow; } } - - Future>> getHistoryCapsulesMonthWhitIdUser( - String idUser, int month, int year) async { - try { - List> capsules = []; - var capsulesData = await _musicService.getHistoryCapsulesMonthWhitIdUser( - idUser, month, year); - - var musics = await getMusicsWithIds( - capsulesData.map((capsule) => capsule.item2).toList()); - - for (var capsule in capsulesData) { - var music = musics.firstWhere((music) => music.id == capsule.item2); - capsules.add(Tuple2(capsule.item1, music)); - } - - return capsules; - } catch (e) { - print(e); - rethrow; - } - } }