Loading lib/search/search_page.dart +83 −64 Original line number Diff line number Diff line Loading @@ -15,8 +15,26 @@ class SearchPage extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; return PageTemplate( body: Column( body: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding(padding: const EdgeInsets.only(left: 8.0), child: Text( 'Search', style: Theme.of(context) .textTheme .headlineMedium ?.copyWith( color: colorScheme.onSurface, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 16.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0).copyWith(top: 10.0), child: TextField( Loading @@ -40,12 +58,12 @@ class SearchPage extends StatelessWidget { ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0).copyWith(bottom: 104.0), padding: const EdgeInsets.symmetric(horizontal: 0.0).copyWith(bottom: 104.0), child: StreamBuilder<List<SongPostDTO>>( stream: songPostService.getSearchResults(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { Loading Loading @@ -89,6 +107,7 @@ class SearchPage extends StatelessWidget { ), ], ), ), ); } } lib/songPost/detail/song_comment.dart +86 −33 Original line number Diff line number Diff line import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:harmonis/songPost/post_type.dart'; import 'package:harmonis/songPost/service/song_post_service.dart'; import 'package:harmonis/userInteraction/model/comment_DTO.dart'; import 'package:harmonis/common/comment_input_field.dart'; import '../../user/services/userService.dart'; import '../post_type.dart'; class SongComment extends StatefulWidget { final CommentDTO comment; Loading @@ -15,12 +14,12 @@ class SongComment extends StatefulWidget { final VoidCallback onExpand; SongComment({ super.key, Key? key, required this.comment, required this.songPostUid, required this.expanded, required this.onExpand, }); }) : super(key: key); @override State<SongComment> createState() => _SongCommentState(); Loading @@ -28,7 +27,8 @@ class SongComment extends StatefulWidget { class _SongCommentState extends State<SongComment> { final TextEditingController replyController = TextEditingController(); final userService = GetIt.instance<UserService>(); final SongPostService songPostService = GetIt.instance<SongPostService>(); final UserService userService = GetIt.instance<UserService>(); String? authorName; Loading @@ -51,23 +51,29 @@ class _SongCommentState extends State<SongComment> { }); } void _addReply(String commentId, String replyText) { Future<void> _addReply(String commentId, String replyText) async { if (replyText.trim().isEmpty) return; final currentUser = FirebaseAuth.instance.currentUser; if (currentUser == null) { return; } final reply = CommentDTO( uid: DateTime.now().toString(), uid: null, text: replyText.trim(), authorUId: FirebaseAuth.instance.currentUser!.uid, postType: PostType.songPost, authorUId: currentUser.uid, postType: PostType.comment, replies: [], timeStamp: DateTime.now(), ); GetIt.instance<SongPostService>().addReply(widget.songPostUid, commentId, reply); setState(() { widget.comment.replies.add(reply); }); try { await songPostService.addReply(widget.songPostUid, commentId, reply); replyController.clear(); } catch (e) { print('Failed to add reply: $e'); } } @override Loading Loading @@ -128,32 +134,79 @@ class _SongCommentState extends State<SongComment> { ), if (widget.expanded) ...[ const SizedBox(height: 8), for (CommentDTO reply in widget.comment.replies) Padding( padding: const EdgeInsets.only(bottom: 8.0), StreamBuilder<List<CommentDTO>>( stream: songPostService.getReplies(widget.songPostUid, widget.comment.uid!), builder: (context, snapshot) { if (snapshot.hasError) { return Text( 'Error loading replies.', style: TextStyle(color: Colors.red), ); } if (!snapshot.hasData) { return Center(child: CircularProgressIndicator()); } final replies = snapshot.data!; if (replies.isEmpty) { return Text( 'No replies yet', style: TextStyle(color: Colors.grey[600]), ); } return Column( children: replies.map((reply) { return Padding( padding: const EdgeInsets.only(bottom: 8.0, left: 4.0, right: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.reply, size: 16, color: Colors.grey), const SizedBox(width: 8), Expanded( child: Text( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( reply.authorUId, style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: Colors.grey[600], fontWeight: FontWeight.normal), ), const SizedBox(height: 2), Text( reply.text, style: Theme.of(context) .textTheme .bodyLarge ?.copyWith(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.normal), ), const SizedBox(height: 2), Text( '${reply.timeStamp.hour.toString().padLeft(2, '0')}:${reply.timeStamp.minute.toString().padLeft(2, '0')} on ${reply.timeStamp.day}/${reply.timeStamp.month}/${reply.timeStamp.year}', style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: Colors.grey[600]), ), const SizedBox(height: 8), ], ), ), ], ), ); }).toList(), ); }, ), const SizedBox(height: 8), CommentInputField( controller: replyController, onSend: () { final replyText = replyController.text.trim(); if (replyText.isNotEmpty) { _addReply(widget.comment.uid!, replyText); replyController.clear(); } }, ), Loading lib/songPost/form/add_song_post_page.dart +1 −1 Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ class AddSongPostPage extends StatelessWidget { return PageTemplate( body: Padding( padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Loading lib/songPost/service/song_post_repository.dart +21 −0 Original line number Diff line number Diff line Loading @@ -166,6 +166,7 @@ class SongPostRepository { .collection('songPosts') .doc(songPostId) .collection('comments') .orderBy('timeStamp', descending: true) .snapshots() .map((snapshot) { return snapshot.docs Loading Loading @@ -203,4 +204,24 @@ class SongPostRepository { .collection('replies'); await repliesRef.add(reply.toJson()); } Stream<List<CommentDTO>> getRepliesForComment(String songPostId, String commentId) { return fireStore .collection('songPosts') .doc(songPostId) .collection('comments') .doc(commentId) .collection('replies') .orderBy('timeStamp', descending: false) .snapshots() .map((snapshot) { return snapshot.docs.map((doc) { final data = doc.data(); return CommentDTO.fromJson(data).copyWith(uid: doc.id); }).toList(); }).handleError((error) { print('Error fetching replies for comment $commentId: $error'); return []; }); } } lib/songPost/service/song_post_service.dart +6 −2 Original line number Diff line number Diff line Loading @@ -88,6 +88,10 @@ class SongPostService { } } Stream<List<CommentDTO>> getReplies(String songPostId, String commentId) { return songPostRepository.getRepliesForComment(songPostId, commentId); } Stream<Comments> getComments(String postUid) { return songPostRepository.getCommentsForSongPost(postUid); } Loading Loading @@ -123,8 +127,8 @@ class SongPostService { return false; } void addReply(String postId, String commentId, CommentDTO reply) { songPostRepository.addReplyToComment(postId, commentId, reply); Future<void> addReply(String postId, String commentId, CommentDTO reply) async { await songPostRepository.addReplyToComment(postId, commentId, reply); } Future<void> deleteSongPost(String postId) async { Loading Loading
lib/search/search_page.dart +83 −64 Original line number Diff line number Diff line Loading @@ -15,8 +15,26 @@ class SearchPage extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; return PageTemplate( body: Column( body: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding(padding: const EdgeInsets.only(left: 8.0), child: Text( 'Search', style: Theme.of(context) .textTheme .headlineMedium ?.copyWith( color: colorScheme.onSurface, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 16.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0).copyWith(top: 10.0), child: TextField( Loading @@ -40,12 +58,12 @@ class SearchPage extends StatelessWidget { ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0).copyWith(bottom: 104.0), padding: const EdgeInsets.symmetric(horizontal: 0.0).copyWith(bottom: 104.0), child: StreamBuilder<List<SongPostDTO>>( stream: songPostService.getSearchResults(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { Loading Loading @@ -89,6 +107,7 @@ class SearchPage extends StatelessWidget { ), ], ), ), ); } }
lib/songPost/detail/song_comment.dart +86 −33 Original line number Diff line number Diff line import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:harmonis/songPost/post_type.dart'; import 'package:harmonis/songPost/service/song_post_service.dart'; import 'package:harmonis/userInteraction/model/comment_DTO.dart'; import 'package:harmonis/common/comment_input_field.dart'; import '../../user/services/userService.dart'; import '../post_type.dart'; class SongComment extends StatefulWidget { final CommentDTO comment; Loading @@ -15,12 +14,12 @@ class SongComment extends StatefulWidget { final VoidCallback onExpand; SongComment({ super.key, Key? key, required this.comment, required this.songPostUid, required this.expanded, required this.onExpand, }); }) : super(key: key); @override State<SongComment> createState() => _SongCommentState(); Loading @@ -28,7 +27,8 @@ class SongComment extends StatefulWidget { class _SongCommentState extends State<SongComment> { final TextEditingController replyController = TextEditingController(); final userService = GetIt.instance<UserService>(); final SongPostService songPostService = GetIt.instance<SongPostService>(); final UserService userService = GetIt.instance<UserService>(); String? authorName; Loading @@ -51,23 +51,29 @@ class _SongCommentState extends State<SongComment> { }); } void _addReply(String commentId, String replyText) { Future<void> _addReply(String commentId, String replyText) async { if (replyText.trim().isEmpty) return; final currentUser = FirebaseAuth.instance.currentUser; if (currentUser == null) { return; } final reply = CommentDTO( uid: DateTime.now().toString(), uid: null, text: replyText.trim(), authorUId: FirebaseAuth.instance.currentUser!.uid, postType: PostType.songPost, authorUId: currentUser.uid, postType: PostType.comment, replies: [], timeStamp: DateTime.now(), ); GetIt.instance<SongPostService>().addReply(widget.songPostUid, commentId, reply); setState(() { widget.comment.replies.add(reply); }); try { await songPostService.addReply(widget.songPostUid, commentId, reply); replyController.clear(); } catch (e) { print('Failed to add reply: $e'); } } @override Loading Loading @@ -128,32 +134,79 @@ class _SongCommentState extends State<SongComment> { ), if (widget.expanded) ...[ const SizedBox(height: 8), for (CommentDTO reply in widget.comment.replies) Padding( padding: const EdgeInsets.only(bottom: 8.0), StreamBuilder<List<CommentDTO>>( stream: songPostService.getReplies(widget.songPostUid, widget.comment.uid!), builder: (context, snapshot) { if (snapshot.hasError) { return Text( 'Error loading replies.', style: TextStyle(color: Colors.red), ); } if (!snapshot.hasData) { return Center(child: CircularProgressIndicator()); } final replies = snapshot.data!; if (replies.isEmpty) { return Text( 'No replies yet', style: TextStyle(color: Colors.grey[600]), ); } return Column( children: replies.map((reply) { return Padding( padding: const EdgeInsets.only(bottom: 8.0, left: 4.0, right: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.reply, size: 16, color: Colors.grey), const SizedBox(width: 8), Expanded( child: Text( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( reply.authorUId, style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: Colors.grey[600], fontWeight: FontWeight.normal), ), const SizedBox(height: 2), Text( reply.text, style: Theme.of(context) .textTheme .bodyLarge ?.copyWith(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.normal), ), const SizedBox(height: 2), Text( '${reply.timeStamp.hour.toString().padLeft(2, '0')}:${reply.timeStamp.minute.toString().padLeft(2, '0')} on ${reply.timeStamp.day}/${reply.timeStamp.month}/${reply.timeStamp.year}', style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: Colors.grey[600]), ), const SizedBox(height: 8), ], ), ), ], ), ); }).toList(), ); }, ), const SizedBox(height: 8), CommentInputField( controller: replyController, onSend: () { final replyText = replyController.text.trim(); if (replyText.isNotEmpty) { _addReply(widget.comment.uid!, replyText); replyController.clear(); } }, ), Loading
lib/songPost/form/add_song_post_page.dart +1 −1 Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ class AddSongPostPage extends StatelessWidget { return PageTemplate( body: Padding( padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Loading
lib/songPost/service/song_post_repository.dart +21 −0 Original line number Diff line number Diff line Loading @@ -166,6 +166,7 @@ class SongPostRepository { .collection('songPosts') .doc(songPostId) .collection('comments') .orderBy('timeStamp', descending: true) .snapshots() .map((snapshot) { return snapshot.docs Loading Loading @@ -203,4 +204,24 @@ class SongPostRepository { .collection('replies'); await repliesRef.add(reply.toJson()); } Stream<List<CommentDTO>> getRepliesForComment(String songPostId, String commentId) { return fireStore .collection('songPosts') .doc(songPostId) .collection('comments') .doc(commentId) .collection('replies') .orderBy('timeStamp', descending: false) .snapshots() .map((snapshot) { return snapshot.docs.map((doc) { final data = doc.data(); return CommentDTO.fromJson(data).copyWith(uid: doc.id); }).toList(); }).handleError((error) { print('Error fetching replies for comment $commentId: $error'); return []; }); } }
lib/songPost/service/song_post_service.dart +6 −2 Original line number Diff line number Diff line Loading @@ -88,6 +88,10 @@ class SongPostService { } } Stream<List<CommentDTO>> getReplies(String songPostId, String commentId) { return songPostRepository.getRepliesForComment(songPostId, commentId); } Stream<Comments> getComments(String postUid) { return songPostRepository.getCommentsForSongPost(postUid); } Loading Loading @@ -123,8 +127,8 @@ class SongPostService { return false; } void addReply(String postId, String commentId, CommentDTO reply) { songPostRepository.addReplyToComment(postId, commentId, reply); Future<void> addReply(String postId, String commentId, CommentDTO reply) async { await songPostRepository.addReplyToComment(postId, commentId, reply); } Future<void> deleteSongPost(String postId) async { Loading