From 8a299fa153ed1ab2aee45451541efcb21ad05b0c Mon Sep 17 00:00:00 2001 From: adelp13 Date: Sun, 2 Feb 2025 06:34:23 +0200 Subject: [PATCH] finished sources, edited frontend and profile --- backend/controllers/auth.dart | 69 +++++++++- lib/Auth/guest_account.dart | 3 + lib/Auth/signup_bloc.dart | 2 + lib/Library.dart | 12 +- lib/Menu.dart | 21 ++- lib/ProfilePage.dart | 235 +++++++++++++++++++++++++-------- lib/Services/auth_service.dart | 40 ++++++ lib/UserListPage.dart | 137 +++++++++++++++++++ lib/Widgets/media_widgets.dart | 103 ++++++++++++++- 9 files changed, 554 insertions(+), 68 deletions(-) create mode 100644 lib/UserListPage.dart diff --git a/backend/controllers/auth.dart b/backend/controllers/auth.dart index a169e8b..ccd8b33 100644 --- a/backend/controllers/auth.dart +++ b/backend/controllers/auth.dart @@ -16,18 +16,82 @@ RouterPlus authRouter() { final user = await getUser(req); if (user != null) { //print("User Data: ${user.toJson()}"); - print(user.createdAt); + //print(user.createdAt); return { 'id': user.id, 'name': user.userMetadata?['name'], 'email': user.email, 'lastSignIn': user.lastSignInAt, 'createdAt': user.createdAt, + 'photoUrl': user.userMetadata?['photoUrl'], }; } return {'error': 'User not found'}; }); + router.get('/users/', (Request req, String userId) async { + final user = await getUser(req); + final response = await supabase.auth.admin.getUserById(userId); + final userData = response.user; + + if (userData != null) { + //print(userData); + return { + 'id': userData.id, + 'name': userData.userMetadata?['name'], + 'photoUrl': userData.userMetadata?['photoUrl'], + 'email': userData.email, + 'lastSignIn': userData.lastSignInAt, + 'createdAt': userData.createdAt, + }; + } + return {'error': 'User not found'}; + }); + + router.get('/users', (Request req) async { + final response = await supabase.auth.admin.listUsers(); + + final users = response.map((user) => { + 'id': user.id, + 'name': user.userMetadata?['name'] ?? 'Unknown', + 'email': user.email, + }).toList(); + + return users; + }); + + router.post('/updateUser', (Request req) async { + final body = await req.body.asJson; + validateFromBody(body, fields: ['name', 'photoUrl']); + + final user = await getUser(req); + if (user == null) { + return {'error': 'User not found'}; + } + + try { + final response = await supabase.auth.admin.updateUserById( + user.id, + attributes: AdminUserAttributes( + userMetadata: { + 'name': body['name'], + 'photoUrl': body['photoUrl'], + }, + ), + ); + + final updatedUser = response.user; + return { + 'id': updatedUser?.id, + 'name': updatedUser?.userMetadata?['name'], + 'photoUrl': updatedUser?.userMetadata?['photoUrl'], + }; + } catch (e) { + return {'error': 'Failed to update metadata: $e'}; + } + }); + + router.post('/login', (Request req) async { final body = await req.body.asJson; validateFromBody(body, fields: @@ -54,6 +118,7 @@ RouterPlus authRouter() { 'password', 'name', 'isGuest', + 'photoUrl', ] ); @@ -62,7 +127,7 @@ RouterPlus authRouter() { final response = await supabase.auth.admin.createUser(AdminUserAttributes( email: body['email'], password: body['password'], - userMetadata: {'name': body['name'], 'isGuest': body['isGuest']}, + userMetadata: {'name': body['name'], 'isGuest': body['isGuest'], 'photoUrl': body['photoUrl']}, emailConfirm: true, )); final User? user = response.user; diff --git a/lib/Auth/guest_account.dart b/lib/Auth/guest_account.dart index 51e6e84..7e1685d 100644 --- a/lib/Auth/guest_account.dart +++ b/lib/Auth/guest_account.dart @@ -16,12 +16,15 @@ Future createGuestUser() async { final email = '$name${_generateRandomString()}@gmail.com'; final password = _generateRandomString(); + const String defaultPhotoUrl = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y'; + try { await AuthService.instance.signup( name : name, email : email, password: password, isGuest : true, + photoUrl : defaultPhotoUrl, ); await AuthService.instance.login( email : email, diff --git a/lib/Auth/signup_bloc.dart b/lib/Auth/signup_bloc.dart index 152f65a..b388553 100644 --- a/lib/Auth/signup_bloc.dart +++ b/lib/Auth/signup_bloc.dart @@ -19,11 +19,13 @@ class SignUpBloc extends Bloc { throw Exception('The passwords don\'t match.'); } + const String defaultPhotoUrl = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y'; try { await AuthService.instance.signup( name : body['name']!, email : body['email']!, password: body['password']!, + photoUrl: defaultPhotoUrl, ); } catch (e) { diff --git a/lib/Library.dart b/lib/Library.dart index e2952a9..0461c87 100644 --- a/lib/Library.dart +++ b/lib/Library.dart @@ -36,6 +36,7 @@ import 'Main.dart'; import 'UserSystem.dart'; import 'ProfilePage.dart'; import 'Menu.dart'; +import 'UserListPage.dart'; class Library extends StatefulWidget { late final bool isWishlist; @@ -445,10 +446,19 @@ class LibraryState extends State { icon: const Icon(Icons.dark_mode), tooltip: 'Toggle dark mode', ), + TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => UserListPage()), + ); + }, + style: navigationButton(context).filledButtonTheme.style, + child: Text('See Users'), + ), TextButton( onPressed: () { Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ProfilePage()), + MaterialPageRoute(builder: (context) => ProfilePage(userId: UserSystem.instance.getCurrentUserId())), ); }, style: navigationButton(context) diff --git a/lib/Menu.dart b/lib/Menu.dart index 2803ed1..5197da3 100644 --- a/lib/Menu.dart +++ b/lib/Menu.dart @@ -11,6 +11,7 @@ import 'Models/tv_series.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_theme/adaptive_theme.dart'; import 'ProfilePage.dart'; +import 'UserListPage.dart'; class MenuPage extends StatelessWidget { const MenuPage({super.key}); @@ -95,6 +96,7 @@ class MenuState extends State { @override Widget build(BuildContext context) { + String userIdToNavigate = UserSystem.instance.getCurrentUserId(); MenuMediaType currentRendering = MenuMediaType.Game; Map hoverState = { for (var type in MenuMediaType.values) type: false @@ -113,14 +115,25 @@ class MenuState extends State { icon: const Icon(Icons.dark_mode), tooltip: 'Toggle dark mode', ), - IconButton( + TextButton( onPressed: () { Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ProfilePage()), + MaterialPageRoute(builder: (context) => UserListPage()), + ); + }, + style: navigationButton(context).filledButtonTheme.style, + child: Text('See Users'), + ), + TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ProfilePage(userId: UserSystem.instance.getCurrentUserId())), ); }, - icon: const Icon(Icons.account_circle), - tooltip: 'Go to Profile', + style: navigationButton(context) + .filledButtonTheme + .style, + child: Text(UserSystem.instance.currentUserData!['name']), ), IconButton( onPressed: () async { diff --git a/lib/ProfilePage.dart b/lib/ProfilePage.dart index 8190745..2e8bec9 100644 --- a/lib/ProfilePage.dart +++ b/lib/ProfilePage.dart @@ -2,64 +2,153 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:mediamaster/Widgets/themes.dart'; import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:collection/collection.dart'; import 'Services/wishlist_service.dart'; import 'Services/media_user_service.dart'; +import 'Services/media_service.dart'; +import 'Services/auth_service.dart'; import 'UserSystem.dart'; import 'Main.dart'; import 'Menu.dart'; +import 'UserListPage.dart'; class ProfilePage extends StatefulWidget { - const ProfilePage({super.key}); + final String userId; + const ProfilePage({Key? key, required this.userId}) : super(key: key); @override _ProfilePageState createState() => _ProfilePageState(); } class _ProfilePageState extends State { - late String name; + late String name; + late String visitedUserId; + late Map visitedUser; + late String email; + late String lastSignInRaw; + late String memberSinceRaw; + late String photoUrl = ""; + bool _isLoading = true; + String _profileImageUrl = 'https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png'; + var currentUserId = UserSystem.instance.getCurrentUserId(); @override void initState() { super.initState(); + visitedUserId = widget.userId; + fetchUserData(); + } - name = (UserSystem.instance.currentUserData!['name'] ?? 'Unknown User'); - name = name[0].toUpperCase() + name.substring(1); + Future fetchUserData() async { + try { + visitedUser = await AuthService.instance.getUserById(visitedUserId); + + setState(() { + name = visitedUser['name'] ?? 'Unknown User'; + name = name[0].toUpperCase() + name.substring(1); + email = visitedUser['email'] ?? 'Unknown Email'; + lastSignInRaw = visitedUser['lastSignIn'] ?? ''; + memberSinceRaw = visitedUser['createdAt'] ?? ''; + photoUrl = visitedUser['photoUrl'] ?? _profileImageUrl; + _isLoading = false; + }); + } catch (e) { + print('Error fetching user data: $e'); + setState(() { + _isLoading = false; + }); + } } + + String _pluralize(String word, int count) { + word = word.replaceAll('_', ' '); + + if (count == 1) return word; + + if (word.endsWith('s')) return word; - var email = UserSystem.instance.currentUserData!['email'] ?? 'Unknown Email'; - var lastSignInRaw = UserSystem.instance.currentUserData!['lastSignIn'] ?? ''; - var memberSinceRaw = UserSystem.instance.currentUserData!['createdAt'] ?? ''; - String _profileImageUrl = 'https://picsum.photos/200/200?random=1'; + if (word.endsWith('y')) { + return word.substring(0, word.length - 1) + 'ies'; + } + if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z') || + word.endsWith('sh') || word.endsWith('ch')) { + return word + 'es'; + } + + return word + 's'; + } String formatLastLogin(String dateString) { - if (dateString.isEmpty) return "Unknown"; + if (dateString.isEmpty) return 'Unknown'; DateTime date = DateTime.parse(dateString); return DateFormat('dd MMM yyyy HH:mm').format(date); } String formatMemberSince(String dateString) { - if (dateString.isEmpty) return "Unknown"; + if (dateString.isEmpty) return 'Unknown'; DateTime date = DateTime.parse(dateString); return DateFormat('dd MMM yyyy').format(date); } + Map getUserMediaCounts(String currentUserId) { + var userMedia = MediaUserService + .instance + .items + .where((mu) => mu.userId == currentUserId); + + var mediaMap = {for (var media in MediaService.instance.items) media.id: media.mediaType}; + + var groupedMedia = groupBy(userMedia, (mu) => mediaMap[mu.mediaId] ?? 'Unknown'); + + return groupedMedia.map((key, value) => MapEntry(key, value.length)); + } + @override Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + appBar: AppBar( + title: const Text('Profile Page'), + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ), + body: const Center( + child: CircularProgressIndicator(), + ), + ); + } + + Map userMediaCounts = getUserMediaCounts(currentUserId); + + String mediaCountsText; + + if (visitedUserId == currentUserId) { + mediaCountsText = userMediaCounts.isEmpty + ? "You don't have items in the library" + : userMediaCounts.entries + .map((entry) => "• ${entry.value} ${_pluralize(entry.key, entry.value)}") + .join("\n"); + } else { + mediaCountsText = userMediaCounts.isEmpty + ? "This user doesn't have items in the library" + : userMediaCounts.entries + .map((entry) => "• ${entry.value} ${_pluralize(entry.key, entry.value)}") + .join("\n"); + } + return Scaffold( appBar: AppBar( title: const Text('Profile Page'), backgroundColor: Colors.black, foregroundColor: Colors.white, - actions: [ + actions: [ TextButton( - onPressed: () { + onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => MenuPage()), ); }, - style: navigationButton(context) - .filledButtonTheme - .style, + style: navigationButton(context).filledButtonTheme.style, child: Text('Menu'), ), IconButton( @@ -71,6 +160,27 @@ class _ProfilePageState extends State { icon: const Icon(Icons.dark_mode), tooltip: 'Toggle dark mode', ), + if (visitedUserId != currentUserId) + IconButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ProfilePage(userId: currentUserId), + ), + ); + }, + icon: const Icon(Icons.account_circle), + tooltip: 'My Profile', + ), + TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => UserListPage()), + ); + }, + style: navigationButton(context).filledButtonTheme.style, + child: Text('See Users'), + ), IconButton( onPressed: () { UserSystem.instance.logout(); @@ -89,10 +199,14 @@ class _ProfilePageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ GestureDetector( - onTap: _showImageSelectionDialog, + onTap: () { + if (visitedUserId == currentUserId) { + _showImageSelectionDialog(); + } + }, child: CircleAvatar( radius: 50, - backgroundImage: NetworkImage(_profileImageUrl), + backgroundImage: NetworkImage(photoUrl), backgroundColor: Colors.grey, ), ), @@ -116,9 +230,11 @@ class _ProfilePageState extends State { ], const SizedBox(height: 16), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _infoCard(context, "Last Login", formatLastLogin(lastSignInRaw), "Member Since", formatMemberSince(memberSinceRaw)), + _infoCard(context, 'Last Login', formatLastLogin(lastSignInRaw), + 'Member Since', formatMemberSince(memberSinceRaw)), + _infoCard(context, 'Library', mediaCountsText, "", ""), ], ), ], @@ -127,49 +243,59 @@ class _ProfilePageState extends State { ); } - void _showImageSelectionDialog() { - List imageUrls = List.generate( - 10, (index) => 'https://picsum.photos/200/200?random=${index + 1}' - ); + void _showImageSelectionDialog() { + List imageUrls = List.generate( + 10, (index) => 'https://picsum.photos/200/200?random=${index + 1}' + ); - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Choose Profile Picture', style: TextStyle(color: Colors.white)), - backgroundColor: Colors.black, - content: SingleChildScrollView( - child: Wrap( - spacing: 8.0, - children: imageUrls.map((imageUrl) { - return GestureDetector( - onTap: () { - setState(() { - _profileImageUrl = imageUrl; - }); - Navigator.of(context).pop(); - }, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: CircleAvatar( - radius: 30, - backgroundImage: NetworkImage(imageUrl), - backgroundColor: Colors.grey, + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Choose Profile Picture', style: TextStyle(color: Colors.white)), + backgroundColor: Colors.black, + content: SingleChildScrollView( + child: Wrap( + spacing: 8.0, + children: imageUrls.map((imageUrl) { + return GestureDetector( + onTap: () async { + setState(() { + photoUrl = imageUrl; + }); + + try { + await AuthService.instance.updateUserProfile(name, imageUrl); + Navigator.of(context).pop(); + } catch (error) { + print("Error updating profile: $error"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to update profile image")) + ); + } + }, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: CircleAvatar( + radius: 30, + backgroundImage: NetworkImage(imageUrl), + backgroundColor: Colors.grey, + ), ), - ), - ); - }).toList(), + ); + }).toList(), + ), ), - ), - ); - }, - ); - } + ); + }, + ); + } Widget _infoCard(BuildContext context, String title1, String value1, String title2, String value2) { return Container( padding: const EdgeInsets.all(16), width: MediaQuery.of(context).size.width / 2.5, + height: 190, decoration: BoxDecoration( color: Colors.grey[800], borderRadius: BorderRadius.circular(8), @@ -189,4 +315,5 @@ class _ProfilePageState extends State { ), ); } + } diff --git a/lib/Services/auth_service.dart b/lib/Services/auth_service.dart index 9a59a28..10c49e3 100644 --- a/lib/Services/auth_service.dart +++ b/lib/Services/auth_service.dart @@ -1,5 +1,7 @@ import 'general/config.dart'; import 'general/request.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; class AuthService { final String endpoint = '/auth'; @@ -17,6 +19,42 @@ class AuthService { ); } + Future>> getAllUsers() async { + final response = await getRequest>( + endpoint: '$endpoint/users', + fromJson: (json) => json as List, + ); + + return response.map((user) => Map.from(user)).toList(); + } + + Future> getUserById(String userId) async { + final response = await getRequest>( + endpoint: '$endpoint/users/$userId', + fromJson: (json) => Map.from(json), + ); + return response; + } + + Future> updateUserProfile(String name, String photoUrl) async { + final response = await postRequest>( + endpoint: '$endpoint/updateUser', + body: { + 'name': name, + 'photoUrl': photoUrl, + }, + fromJson: (json) => Map.from(json), + ); + + if (response != null) { + print('User profile updated successfully'); + return response; + } else { + print('Failed to update user profile'); + throw Exception('Failed to update profile'); + } + } + Future login({ required String email, required String password, @@ -36,6 +74,7 @@ class AuthService { required String email, required String password, bool isGuest = false, + String? photoUrl, }) { return postRequest>( endpoint: '$endpoint/signup', @@ -44,6 +83,7 @@ class AuthService { 'password': password, 'name': name, 'isGuest': isGuest, + 'photoUrl': photoUrl, }, fromJson: (json) => json, ); diff --git a/lib/UserListPage.dart b/lib/UserListPage.dart new file mode 100644 index 0000000..6dff6f8 --- /dev/null +++ b/lib/UserListPage.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:mediamaster/Widgets/themes.dart'; +import 'package:adaptive_theme/adaptive_theme.dart'; +import 'Services/auth_service.dart'; +import 'Menu.dart'; +import 'Main.dart'; +import 'UserSystem.dart'; +import 'ProfilePage.dart'; + +class UserListPage extends StatefulWidget { + const UserListPage({super.key}); + + @override + _UserListPageState createState() => _UserListPageState(); +} + +class _UserListPageState extends State { + late Future>> _usersFuture; + var currentUserId = UserSystem.instance.getCurrentUserId(); + + @override + void initState() { + super.initState(); + _usersFuture = AuthService.instance.getAllUsers(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('User List'), + backgroundColor: Colors.black, + foregroundColor: Colors.white, + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => MenuPage()), + ); + }, + style: navigationButton(context).filledButtonTheme.style, + child: Text('Menu'), + ), + IconButton( + onPressed: () { + AdaptiveTheme.of(context).mode == AdaptiveThemeMode.light + ? AdaptiveTheme.of(context).setDark() + : AdaptiveTheme.of(context).setLight(); + }, + icon: const Icon(Icons.dark_mode), + tooltip: 'Toggle dark mode', + ), + IconButton( + onPressed: () { + UserSystem.instance.logout(); + Navigator.pop(context); + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const Home())); + }, + icon: const Icon(Icons.logout), + tooltip: 'Log out', + ), + ], + ), + body: FutureBuilder>>( + future: _usersFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('No users found')); + } + + List> users = snapshot.data! + .where((user) => user['name']?.toLowerCase() != 'guest') + .toList(); + + if (users.isEmpty) { + return const Center(child: Text('No valid users found')); + } + + return ListView.builder( + itemCount: users.length, + itemBuilder: (context, index) { + final user = users[index]; + + String displayName = user['name'] ?? 'Unknown'; + bool isCurrentUser = user['id'] == currentUserId; + + return Card( + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + color: Colors.grey[800], + child: ListTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + displayName, + style: const TextStyle(color: Colors.white), + ), + if (isCurrentUser) + const Padding( + padding: EdgeInsets.only(left: 8.0), + child: Text( + "You", + style: TextStyle(color: Colors.green), + ), + ), + ], + ), + subtitle: Text( + user['email'] ?? 'No email', + style: const TextStyle(color: Colors.white70), + ), + leading: const Icon(Icons.person, color: Colors.white), + onTap: () { + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProfilePage(userId: user['id']), + ), + ); + } + ), + ); + + }, + ); + }, + ), + + ); + } +} diff --git a/lib/Widgets/media_widgets.dart b/lib/Widgets/media_widgets.dart index f8717fa..285fa6f 100644 --- a/lib/Widgets/media_widgets.dart +++ b/lib/Widgets/media_widgets.dart @@ -17,11 +17,13 @@ import '../Models/user_tag.dart'; import '../Models/tv_series.dart'; import '../Models/media_user.dart'; import '../Models/media_user_tag.dart'; +import '../Models/media_user_source.dart'; import '../Models/general/model.dart'; import '../Models/general/media_type.dart'; import '../Services/user_tag_service.dart'; import '../Services/wishlist_service.dart'; import '../Services/media_user_service.dart'; +import '../Services/media_user_source_service.dart'; import '../Services/source_service.dart'; import '../Services/media_user_tag_service.dart'; import '../UserSystem.dart'; @@ -145,21 +147,50 @@ Widget getRatingsWidget(Media media) { return getListWidget('Ratings', List.of([criticScoreString, communityScoreString])); } + Widget getSourcesWidget(Media media) { + var mus = MediaUserSourceService.instance.items; + var sources = mus.where((mus) => mus.mediaId == media.id && mus.userId == UserSystem.instance.getCurrentUserId()).toList(); + var sourceIds = sources.map((source) => source.sourceId).toList(); + var allSources = SourceService.instance.items; + List sourceNames = []; + for (var id in sourceIds) { + var source = allSources.firstWhere((s) => s.id == id); + sourceNames.add(source?.name ?? 'N/A'); + } + + if (sourceNames.isEmpty) { + sourceNames.add('N/A'); + } + + return getListWidget('Source${sourceNames.length <= 1 ? '' : 's'}', sourceNames); +} + +List getAvailableSources(Media media) { var sources = SourceService .instance .items .where((source) => source.mediaType == 'all' || source.mediaType == media.mediaType) .toList(); + var sourceNames = sources.map((source) => source.name).toList(); + return sourceNames.isEmpty ? ['N/A'] : sourceNames; +} - var sourceNames = sources.map((source) { - if (source.name == 'physical' || source.name == 'digital') { - return '${source.name} format'; +List getSelectedSources(Media media) { + var mus = MediaUserSourceService.instance.items; + var sources = mus.where((mus) => mus.mediaId == media.id && mus.userId == UserSystem.instance.getCurrentUserId()).toList(); + var sourceIds = sources.map((source) => source.sourceId).toList(); + var allSources = SourceService.instance.items; + List sourceNames = []; + + for (var id in sourceIds) { + var source = allSources.firstWhere((s) => s.id == id); + if (source != null) { + sourceNames.add(source.name); } - return source.name; - }).toList(); + } - return getListWidget('Available on', sourceNames.isEmpty ? ['N/A'] : sourceNames); + return sourceNames; } @@ -454,12 +485,16 @@ Future showSettingsDialog(MT mt, BuildContext contex throw UnimplementedError('Tracking not implemented for this media type'); } } - + return showDialog( context: context, builder: (context) { + List sources = getAvailableSources(mt.media); + List selectedSources = getSelectedSources(mt.media); + return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { + void setFullState() { resetState(); setState(() {}); @@ -670,10 +705,64 @@ Future showSettingsDialog(MT mt, BuildContext contex ), ), ), + + Text('Edit Sources', style: titleStyle), + ...sources.map((source) { + return Row( + children: [ + Checkbox( + value: selectedSources.contains(source), + onChanged: (value) { + setState(() { + if (value == true) { + if (!selectedSources.contains(source)) { + selectedSources.add(source); + print(selectedSources); + } + } else { + selectedSources.remove(source); + } + }); + }, + ), + Text(source, style: subtitleStyle), + ], + ); + }).toList(), ], ), ), ), + actions: [ + TextButton( + onPressed: () async { + List existingSources = getSelectedSources(mt.media); + var sourcesToAdd = selectedSources.where((s) => !existingSources.contains(s)).toList(); + var sourcesToRemove = existingSources.where((s) => !selectedSources.contains(s)).toList(); + + for (var sourceName in sourcesToAdd) { + var source = SourceService.instance.items.firstWhere((s) => s.name == sourceName); + if (source != null) { + var mus = MediaUserSource( + mediaId: mt.getMediaId(), + userId: UserSystem.instance.getCurrentUserId(), + sourceId: source.id, + ); + await MediaUserSourceService.instance.create(mus); + } + } + + for (var sourceName in sourcesToRemove) { + var source = SourceService.instance.items.firstWhere((s) => s.name == sourceName); + if (source != null) { + await MediaUserSourceService.instance.delete([mt.getMediaId(), source.id]); + } + } + + }, + child: Text("Save Sources"), + ), + ], ); }, );