From 4310f140328da033ff3c9050bb4ba1891505a95a Mon Sep 17 00:00:00 2001 From: JothishKamal Date: Wed, 15 Jan 2025 00:38:04 +0530 Subject: [PATCH] feat: updated backend integrations + delete playlist backend integration --- assets/people.svg | 3 + lib/providers/create_screen_provider.dart | 13 +- lib/providers/playlist_provider.dart | 86 +++++++- lib/utils/api_util.dart | 25 ++- .../models/playlist_success_response.dart | 58 ++++- lib/view/screens/admin_screen.dart | 121 ++++++++++- lib/view/screens/create_screen.dart | 7 +- lib/view/screens/home_screen.dart | 198 +++++++++++------- lib/view/widgets/playlist_card.dart | 89 ++++++-- 9 files changed, 475 insertions(+), 125 deletions(-) create mode 100644 assets/people.svg diff --git a/assets/people.svg b/assets/people.svg new file mode 100644 index 0000000..5c5a6c6 --- /dev/null +++ b/assets/people.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/providers/create_screen_provider.dart b/lib/providers/create_screen_provider.dart index 44921f8..ccf6eb4 100644 --- a/lib/providers/create_screen_provider.dart +++ b/lib/providers/create_screen_provider.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:developer'; @@ -6,13 +9,19 @@ import 'package:spotify_collab_app/utils/api_util.dart'; class CreateScreenNotifier extends StateNotifier> { CreateScreenNotifier() : super(const AsyncValue.data(null)); - Future<({bool success, String message})> createPlaylist(String name) async { + Future<({bool success, String message})> createPlaylist(String name, + {File? image}) async { try { state = const AsyncValue.loading(); - final response = await apiUtil.post('/v1/playlists', { + final formData = FormData.fromMap({ 'name': name, + 'image': await MultipartFile.fromFile(image!.path), }); + final response = await apiUtil.post( + '/v1/playlists', + formData, + ); log(response.toString()); diff --git a/lib/providers/playlist_provider.dart b/lib/providers/playlist_provider.dart index 15469dc..ee180e5 100644 --- a/lib/providers/playlist_provider.dart +++ b/lib/providers/playlist_provider.dart @@ -1,30 +1,59 @@ +import 'dart:convert'; import 'dart:developer'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify_collab_app/utils/api_util.dart'; import 'package:spotify_collab_app/view/models/playlist_success_response.dart'; final playlistProvider = - StateNotifierProvider>((ref) { + StateNotifierProvider((ref) { return PlaylistNotifier(); }); class Playlist { final String? name; final String? playlistUuid; + final String? imageUrl; + final int? memberCount; - Playlist({required this.name, this.playlistUuid}); + Playlist( + {required this.name, this.playlistUuid, this.imageUrl, this.memberCount}); factory Playlist.fromJson(Map json) { return Playlist( name: json['name'], playlistUuid: json['playlist_uuid'], + imageUrl: json['image_url'], + memberCount: json['member_count'], ); } } -class PlaylistNotifier extends StateNotifier> { - PlaylistNotifier() : super([]); +class HomeScreenInfo { + final List ownedPlaylists; + final List memberPlaylists; + + const HomeScreenInfo( + {required this.ownedPlaylists, required this.memberPlaylists}); + + HomeScreenInfo copyWith({ + List? ownedPlaylists, + List? memberPlaylists, + }) { + return HomeScreenInfo( + ownedPlaylists: ownedPlaylists ?? this.ownedPlaylists, + memberPlaylists: memberPlaylists ?? this.memberPlaylists, + ); + } +} + +enum DeletePlaylistStatus { success, failure, error } + +class PlaylistNotifier extends StateNotifier { + PlaylistNotifier() + : super(const HomeScreenInfo(ownedPlaylists: [], memberPlaylists: [])); + final isLoadingProvider = StateProvider((ref) => false); String? selectedPlaylistUuid; String? selectedPlaylistName; @@ -32,25 +61,70 @@ class PlaylistNotifier extends StateNotifier> { Future fetchPlaylists() async { try { final response = await apiUtil.get('/v1/playlists'); + log(await SharedPreferences.getInstance() + .then((value) => value.getString('access_token').toString())); + log(jsonEncode(response.data)); if (response.statusCode == 200) { final playlistResponse = PlaylistSuccessResponse.fromJson(response.data); if (playlistResponse.message == "Playlists successfully retrieved") { - final playlists = playlistResponse.data + final ownedPlaylists = playlistResponse.data?.owner ?.map((data) => Playlist( name: data.name, playlistUuid: data.playlistUuid, + imageUrl: data.imageUrl, + memberCount: data.memberCount, )) .toList() ?? []; + final memberPlaylists = playlistResponse.data?.member + ?.map((data) => Playlist( + name: data.name, + playlistUuid: data.playlistUuid, + imageUrl: data.imageUrl, + memberCount: data.memberCount, + )) + .toList() ?? + []; + + state = state.copyWith( + ownedPlaylists: ownedPlaylists, memberPlaylists: memberPlaylists); + } + } + } catch (e) { + log(e.toString()); + } + } + + Future deletePlaylist(String playlistUuid) async { + try { + final response = await apiUtil.delete('/v1/playlists/$playlistUuid'); + log(jsonEncode(response.data)); + + if (response.statusCode == 200) { + final playlistResponse = + PlaylistSuccessResponse.fromJson(response.data); + + if (playlistResponse.message == "Playlist successfully deleted") { + final ownedPlaylists = state.ownedPlaylists + .where((playlist) => playlist.playlistUuid != playlistUuid) + .toList(); + final memberPlaylists = state.memberPlaylists + .where((playlist) => playlist.playlistUuid != playlistUuid) + .toList(); + + state = state.copyWith( + ownedPlaylists: ownedPlaylists, memberPlaylists: memberPlaylists); - state = playlists; + return DeletePlaylistStatus.success; } } + return DeletePlaylistStatus.failure; } catch (e) { log(e.toString()); + return DeletePlaylistStatus.error; } } diff --git a/lib/utils/api_util.dart b/lib/utils/api_util.dart index 39df58c..1e3efa9 100644 --- a/lib/utils/api_util.dart +++ b/lib/utils/api_util.dart @@ -25,7 +25,7 @@ class ApiUtil { factory ApiUtil() => _instance; - Dio dio = Dio(BaseOptions( + final Dio dio = Dio(BaseOptions( baseUrl: devUrl, )); @@ -49,9 +49,24 @@ class ApiUtil { } } - Future post(String endpoint, Map data) async { + Future post(String endpoint, dynamic data) async { try { - Response response = await dio.post(endpoint, data: data); + Response response; + if (data is Map) { + response = await dio.post(endpoint, data: data); + } else if (data is FormData) { + response = await dio.post( + endpoint, + data: data, + options: Options( + contentType: 'multipart/form-data', + ), + ); + } else { + throw ArgumentError( + 'Data must be either Map or FormData'); + } + await _updateTokenFromMetadata(response.data); return response; } catch (e) { @@ -59,9 +74,9 @@ class ApiUtil { } } - Future delete(String endpoint, Map data) async { + Future delete(String endpoint) async { try { - Response response = await dio.post(endpoint, queryParameters: data); + Response response = await dio.delete(endpoint); await _updateTokenFromMetadata(response.data); return response; } catch (e) { diff --git a/lib/view/models/playlist_success_response.dart b/lib/view/models/playlist_success_response.dart index 9f95cce..3339b57 100644 --- a/lib/view/models/playlist_success_response.dart +++ b/lib/view/models/playlist_success_response.dart @@ -1,7 +1,7 @@ class PlaylistSuccessResponse { bool? success; String? message; - List? data; + Data? data; int? statusCode; PlaylistSuccessResponse( @@ -10,12 +10,7 @@ class PlaylistSuccessResponse { PlaylistSuccessResponse.fromJson(Map json) { success = json['success']; message = json['message']; - if (json['data'] != null) { - data = []; - json['data'].forEach((v) { - data!.add(Data.fromJson(v)); - }); - } + data = json['data'] != null ? Data.fromJson(json['data']) : null; statusCode = json['status_code']; } @@ -24,7 +19,7 @@ class PlaylistSuccessResponse { data['success'] = success; data['message'] = message; if (this.data != null) { - data['data'] = this.data!.map((v) => v.toJson()).toList(); + data['data'] = this.data!.toJson(); } data['status_code'] = statusCode; return data; @@ -32,31 +27,70 @@ class PlaylistSuccessResponse { } class Data { + List? member; + List? owner; + + Data({this.member, this.owner}); + + Data.fromJson(Map json) { + if (json['member'] != null) { + member = []; + json['member'].forEach((v) { + member!.add(PlaylistDetails.fromJson(v)); + }); + } + if (json['owner'] != null) { + owner = []; + json['owner'].forEach((v) { + owner!.add(PlaylistDetails.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (member != null) { + data['member'] = member!.map((v) => v.toJson()).toList(); + } + if (owner != null) { + data['owner'] = owner!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class PlaylistDetails { String? userUuid; String? playlistUuid; String? playlistId; String? name; String? playlistCode; + String? imageUrl; String? createdAt; String? updatedAt; + int? memberCount; - Data( + PlaylistDetails( {this.userUuid, this.playlistUuid, this.playlistId, this.name, this.playlistCode, + this.imageUrl, this.createdAt, - this.updatedAt}); + this.updatedAt, + this.memberCount}); - Data.fromJson(Map json) { + PlaylistDetails.fromJson(Map json) { userUuid = json['user_uuid']; playlistUuid = json['playlist_uuid']; playlistId = json['playlist_id']; name = json['name']; playlistCode = json['playlist_code']; + imageUrl = json['image_url']; createdAt = json['created_at']; updatedAt = json['updated_at']; + memberCount = json['member_count']; } Map toJson() { @@ -66,8 +100,10 @@ class Data { data['playlist_id'] = playlistId; data['name'] = name; data['playlist_code'] = playlistCode; + data['image_url'] = imageUrl; data['created_at'] = createdAt; data['updated_at'] = updatedAt; + data['member_count'] = memberCount; return data; } } diff --git a/lib/view/screens/admin_screen.dart b/lib/view/screens/admin_screen.dart index c9359de..01e562e 100644 --- a/lib/view/screens/admin_screen.dart +++ b/lib/view/screens/admin_screen.dart @@ -24,6 +24,7 @@ class _AdminScreenState extends ConsumerState @override void initState() { super.initState(); + _fetchSongs(); final initialIndex = ref.read(tabProvider); _tabController = @@ -88,14 +89,118 @@ class _AdminScreenState extends ConsumerState ), ), actions: [ - IconButton( - onPressed: () {}, - iconSize: 16, - icon: const Icon( - Icons.more_vert, - color: Color(0xffD1D2D9), - ), - ), + (GoRouterState.of(context).extra as Map)['owner'] + ? PopupMenuButton( + icon: const Icon( + Icons.more_vert, + color: Color(0xffD1D2D9), + size: 16, + ), + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon(Icons.delete_outline, color: Colors.red), + SizedBox(width: 8), + Text('Delete', + style: TextStyle(color: Colors.red)), + ], + ), + ), + ], + onSelected: (String value) { + if (value == 'delete') { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Playlist'), + content: const Text( + 'Are you sure you want to delete this playlist?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + final status = await ref + .read(playlistProvider.notifier) + .deletePlaylist(ref + .read(playlistProvider.notifier) + .selectedPlaylistUuid!); + + if (!context.mounted) return; + + switch (status) { + case DeletePlaylistStatus.success: + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Playlist deleted successfully', + style: TextStyle( + fontFamily: 'Gotham', + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + backgroundColor: Colors.green, + ), + ); + context.pop(); + context.pop(); + break; + case DeletePlaylistStatus.failure: + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Failed to delete playlist', + style: TextStyle( + fontFamily: 'Gotham', + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + backgroundColor: Colors.red, + ), + ); + context.pop(); + break; + case DeletePlaylistStatus.error: + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Error occurred while deleting playlist', + style: TextStyle( + fontFamily: 'Gotham', + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + backgroundColor: Colors.red, + ), + ); + context.pop(); + break; + } + }, + child: const Text('Delete', + style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } + }, + ) + : const SizedBox.shrink() ], backgroundColor: Colors.transparent, ), diff --git a/lib/view/screens/create_screen.dart b/lib/view/screens/create_screen.dart index ac1a72b..0c4beee 100644 --- a/lib/view/screens/create_screen.dart +++ b/lib/view/screens/create_screen.dart @@ -157,9 +157,14 @@ class CreateScreen extends ConsumerWidget { return; } + final image = ref.watch(photoUploadProvider); + final result = await ref .read(createScreenProvider.notifier) - .createPlaylist(eventNameController.text); + .createPlaylist( + eventNameController.text, + image: image, + ); if (!context.mounted) return; diff --git a/lib/view/screens/home_screen.dart b/lib/view/screens/home_screen.dart index 38cfc71..2231e48 100644 --- a/lib/view/screens/home_screen.dart +++ b/lib/view/screens/home_screen.dart @@ -23,13 +23,130 @@ class HomeScreen extends ConsumerWidget { ), backgroundColor: Colors.transparent, ), - body: RefreshIndicator( - onRefresh: () async { - await playlistNotifier.fetchPlaylists(); - }, - child: Stack( - children: [ - SizedBox( + body: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Create new Playlist ·", + style: TextStyle( + fontFamily: "Gotham", + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + const SizedBox(height: 10), + InkWell( + borderRadius: const BorderRadius.all(Radius.circular(8)), + onTap: () => context.go('/create'), + child: Ink(child: const NewPlaylistButton()), + ), + const SizedBox(height: 20), + RefreshIndicator( + onRefresh: () => playlistNotifier.fetchPlaylists(), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: FutureBuilder( + future: playlistNotifier.fetchPlaylists(), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return const Center( + child: Text('Failed to load playlists.')); + } else { + final playlists = ref.watch(playlistProvider); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Your Events", + style: TextStyle( + fontFamily: "Gotham", + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + const SizedBox(height: 10), + if (playlists.ownedPlaylists.isEmpty) + const Center( + child: Text('No events. Create one!')) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: playlists.ownedPlaylists.length, + separatorBuilder: (context, index) => + const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + return PlaylistCard( + name: + playlists.ownedPlaylists[index].name, + id: playlists + .ownedPlaylists[index].playlistUuid, + imageUrl: playlists + .ownedPlaylists[index].imageUrl, + isActive: true, + memberCount: playlists + .ownedPlaylists[index].memberCount, + ); + }, + ), + const SizedBox(height: 20), + const Text( + "Joined Events", + style: TextStyle( + fontFamily: "Gotham", + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + const SizedBox(height: 10), + if (playlists.memberPlaylists.isEmpty) + const Center( + child: Text('No events. Join one!')) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: playlists.memberPlaylists.length, + separatorBuilder: (context, index) => + const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + return PlaylistCard( + name: + playlists.memberPlaylists[index].name, + id: playlists + .memberPlaylists[index].playlistUuid, + imageUrl: playlists + .memberPlaylists[index].imageUrl, + memberCount: playlists + .memberPlaylists[index].memberCount, + ); + }, + ), + ], + ); + } + }, + ), + ), + ), + ], + ), + ), + IgnorePointer( + ignoring: true, + child: SizedBox( height: double.maxFinite, width: double.maxFinite, child: SvgPicture.asset( @@ -39,71 +156,8 @@ class HomeScreen extends ConsumerWidget { fit: BoxFit.cover, ), ), - Padding( - padding: const EdgeInsets.all(32.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Create new Playlist ·", - style: TextStyle( - fontFamily: "Gotham", - fontWeight: FontWeight.w700, - fontSize: 16, - ), - ), - const SizedBox(height: 10), - InkWell( - borderRadius: const BorderRadius.all(Radius.circular(8)), - onTap: () => context.go('/create'), - child: Ink(child: const NewPlaylistButton()), - ), - const SizedBox(height: 20), - const Text( - "Active ·", - style: TextStyle( - fontFamily: "Gotham", - fontWeight: FontWeight.w700, - fontSize: 16, - ), - ), - const SizedBox(height: 10), - FutureBuilder( - future: playlistNotifier.fetchPlaylists(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return const Center( - child: Text('Failed to load playlists.')); - } else { - final playlists = ref.watch(playlistProvider); - if (playlists.isEmpty) { - return const Center( - child: Text('No playlists found')); - } - return Expanded( - child: ListView.builder( - itemCount: playlists.length, - itemBuilder: (context, index) { - final playlist = playlists[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: PlaylistCard( - name: playlist.name, - id: playlist.playlistUuid), - ); - }, - ), - ); - } - }, - ), - ], - ), - ), - ], - ), + ), + ], ), ); } diff --git a/lib/view/widgets/playlist_card.dart b/lib/view/widgets/playlist_card.dart index d16b01f..cfd138a 100644 --- a/lib/view/widgets/playlist_card.dart +++ b/lib/view/widgets/playlist_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:spotify_collab_app/providers/playlist_provider.dart'; @@ -9,11 +10,15 @@ class PlaylistCard extends ConsumerWidget { this.isActive = false, required this.name, required this.id, + required this.imageUrl, + required this.memberCount, }); final bool isActive; final String? name; final String? id; + final String? imageUrl; + final int? memberCount; @override Widget build(BuildContext context, WidgetRef ref) { @@ -32,28 +37,72 @@ class PlaylistCard extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Row( children: [ - Container( - height: 57, - width: 57, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey, + if (imageUrl != null && imageUrl!.isNotEmpty) + Container( + height: 57, + width: 57, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: NetworkImage(imageUrl!), + fit: BoxFit.cover, + onError: (_, __) => const Icon(Icons.music_note, + size: 32, color: Colors.white), + ), + ), + ) + else + Container( + height: 57, + width: 57, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey, + ), + child: const Icon(Icons.music_note, + size: 32, color: Colors.white), ), - child: const Icon(Icons.music_note, - size: 32, color: Colors.white), - ), const SizedBox(width: 16), Expanded( - child: Text( - name ?? '', - style: TextStyle( - fontFamily: "Gotham", - fontWeight: FontWeight.w700, - fontSize: 16, - color: isActive ? Colors.white : Colors.black, - ), - overflow: TextOverflow.ellipsis, - maxLines: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + name ?? '', + style: TextStyle( + fontFamily: "Gotham", + fontWeight: FontWeight.w700, + fontSize: 16, + color: isActive ? Colors.white : Colors.black, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const SizedBox(height: 2), + Row( + children: [ + SvgPicture.asset( + 'assets/people.svg', + colorFilter: ColorFilter.mode( + isActive + ? const Color(0xfff5f5f5) + : Colors.black, + BlendMode.srcIn), + ), + const SizedBox(width: 4), + Text( + memberCount?.toString() ?? '', + style: TextStyle( + fontFamily: "Gotham", + fontWeight: FontWeight.w500, + fontSize: 14, + color: isActive ? Colors.white : Colors.black, + ), + ), + ], + ) + ], ), ), const SizedBox(width: 16), @@ -68,7 +117,7 @@ class PlaylistCard extends ConsumerWidget { onPressed: () { playlistNotifier.selectedPlaylistUuid = id ?? ''; playlistNotifier.selectedPlaylistName = name ?? ''; - context.push("/admin"); + context.push("/admin", extra: {"owner": isActive}); }, style: TextButton.styleFrom( padding: EdgeInsets.zero,