From b7f46c3a1bb0959e024a9ddbb4357257cd85fa78 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:30:01 +0100 Subject: [PATCH 01/18] project structure created --- lib/features/feedback/presentation/presentation.dart | 2 ++ lib/features/feedback/presentation/screens/screens.dart | 1 + lib/features/feedback/presentation/widgets/widgets.dart | 1 + 3 files changed, 4 insertions(+) create mode 100644 lib/features/feedback/presentation/presentation.dart create mode 100644 lib/features/feedback/presentation/screens/screens.dart create mode 100644 lib/features/feedback/presentation/widgets/widgets.dart diff --git a/lib/features/feedback/presentation/presentation.dart b/lib/features/feedback/presentation/presentation.dart new file mode 100644 index 00000000..998a0979 --- /dev/null +++ b/lib/features/feedback/presentation/presentation.dart @@ -0,0 +1,2 @@ +export 'widgets/widgets.dart'; +export 'screens/screens.dart'; diff --git a/lib/features/feedback/presentation/screens/screens.dart b/lib/features/feedback/presentation/screens/screens.dart new file mode 100644 index 00000000..b9f78bb0 --- /dev/null +++ b/lib/features/feedback/presentation/screens/screens.dart @@ -0,0 +1 @@ +// TODO Implement this library. \ No newline at end of file diff --git a/lib/features/feedback/presentation/widgets/widgets.dart b/lib/features/feedback/presentation/widgets/widgets.dart new file mode 100644 index 00000000..b9f78bb0 --- /dev/null +++ b/lib/features/feedback/presentation/widgets/widgets.dart @@ -0,0 +1 @@ +// TODO Implement this library. \ No newline at end of file From 5aaca945edf89a29e9fa173a6b1a72ffe47ca47b Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Thu, 23 Nov 2023 09:22:08 +0100 Subject: [PATCH 02/18] feedback item added --- .../presentation/widgets/feedback_item.dart | 171 ++++++++++++++ .../presentation/widgets/feedback_page.dart | 213 ++++++++++++++++++ 2 files changed, 384 insertions(+) create mode 100644 lib/features/feedback/presentation/widgets/feedback_item.dart create mode 100644 lib/features/feedback/presentation/widgets/feedback_page.dart diff --git a/lib/features/feedback/presentation/widgets/feedback_item.dart b/lib/features/feedback/presentation/widgets/feedback_item.dart new file mode 100644 index 00000000..9fd0e852 --- /dev/null +++ b/lib/features/feedback/presentation/widgets/feedback_item.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; + +/// Feedback item. +class AdminFeedbackItem extends StatefulWidget { + /// Feedback item. + const AdminFeedbackItem({Key? key, required this.feedbackId}) + : super(key: key); + + /// The id of the feedback to display. + final int feedbackId; + + /// The height of the item. + static const double height = 300; + + /// The size of the user profile img. + static const double imgSize = 55; + + /// The size of the font displaying the username. + static const double usernameFontSize = 20; + + /// The size of the font. + static const double fontSize = 18; + + /// The size of the font, displaying the user tag. + static const double userTagFontSize = 17; + + @override + State createState() { + // TODO: implement createState + throw UnimplementedError(); + } + + @override + Widget build(BuildContext context, t) { + return Consumer(builder: (context, ref, _) { + var feedback = ref.watch(feedbackProvider)[feedbackId]; + + if (feedback == null) return LpShimmer(height: height); + + var user = ref.watch(usersProvider)[feedback.userId]; + + if (user == null) return LpShimmer(height: height); + + var modifyingUser = ref.watch(usersProvider)[feedback.lastModifiedBy]; + + return LpGestureDetector( + onTap: () => AdminFeedbackPageRoute.info + .push(context, params: {"id": feedbackId}), + child: LpCard( + child: Row( + children: [ + Expanded( + child: Row( + children: [ + UserProfileImg(size: imgSize, userId: feedback.userId), + NcSpacing.small(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NcBodyText( + user.fullname, + fontSize: usernameFontSize, + overflow: TextOverflow.fade, + ), + NcBodyText( + t.global_user_vintage(user.vintage), + fontSize: userTagFontSize, + ), + ], + ), + ], + ), + ), + Expanded( + child: LpFeedbackStatusTag( + status: feedback.status, + fontSize: fontSize, + label: true, + ), + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LpIcon( + feedback.type.icon, + color: feedback.type.color, + size: scaleIcon(fontSize), + ), + NcSpacing.xs(), + NcBodyText( + feedback.type.title(context), + fontSize: fontSize, + ), + ], + ), + ), + Expanded( + child: NcBodyText( + feedback.lastModified != null + ? timeago.format(feedback.lastModified!) + : t.admin_feedback_null, + fontSize: fontSize, + textAlign: TextAlign.center, + ), + ), + Expanded( + child: NcBodyText( + modifyingUser != null + ? modifyingUser.fullname + : t.admin_feedback_null, + fontSize: fontSize, + textAlign: TextAlign.center, + ), + ), + Expanded( + child: NcBodyText( + timeago.format(feedback.timestamp), + fontSize: fontSize, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + }); + } +} + +extension _AdminFeedbackItemHelper on FeedbackTypes { + String title(BuildContext context) { + var t = context.t; + + switch (this) { + case FeedbackTypes.bug: + return t.admin_feedback_types_bug; + case FeedbackTypes.suggestion: + return t.admin_feedback_types_suggestion; + case FeedbackTypes.error: + return t.admin_feedback_types_error; + case FeedbackTypes.other: + return t.admin_feedback_types_other; + } + } + + IconData get icon { + switch (this) { + case FeedbackTypes.bug: + return Icons.bug_report; + case FeedbackTypes.suggestion: + return Icons.lightbulb_outline; + case FeedbackTypes.error: + return Icons.error; + case FeedbackTypes.other: + return Icons.help; + } + } + + Color get color { + switch (this) { + case FeedbackTypes.bug: + return errorColor; + case FeedbackTypes.other: + case FeedbackTypes.suggestion: + return accentColor; + case FeedbackTypes.error: + return warningColor; + } + } +} diff --git a/lib/features/feedback/presentation/widgets/feedback_page.dart b/lib/features/feedback/presentation/widgets/feedback_page.dart new file mode 100644 index 00000000..86153e99 --- /dev/null +++ b/lib/features/feedback/presentation/widgets/feedback_page.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lb_planner/features/feedback/domain/domain.dart'; + +/// Admin feedback page displaying details about feedback. +class AdminFeedbackPageRoute extends StatefulWidget { + /// Admin feedback page displaying details about feedback. + const AdminFeedbackPageRoute({Key? key, required this.feedbackId}) + : super(key: key); + + /// The id of the feedback to display. + final int feedbackId; + + @override + State createState() => _AdminFeedbackPageRouteState(); +} + +class _AdminFeedbackPageRouteState extends State { + final commentController = TextEditingController(); + + bool _commenmtInit = false; + + Future? _updateFuture; + Future? _deleteFuture; + + _updateFeedback(WidgetRef ref, Feedback feedback) async { + if (_updateFuture != null) return; + + final controller = ref.watch(feedbackController); + var feedbacks = ref.read(feedbackProvider); + + if (feedback.status.isUnread) { + setState(() { + _updateFuture = + controller.updateFeedbackStatus(feedback.id, FeedbackStatus.read); + }); + + var response = await _updateFuture; + + if (response!.failed) { + if (mounted) { + setState(() { + _updateFuture = null; + }); + } + + return; + } + } + + setState(() { + _updateFuture = + controller.updateFeedbackComment(feedback.id, commentController.text); + }); + + var response = await _updateFuture; + + if (mounted) { + setState(() { + _updateFuture = null; + }); + + if (response!.succeeded) _pushNext(feedbacks, feedback); + } + } + + _pushNext(Map feedbacks, Feedback feedback) { + var sorted = AdminFeedbackRoute.sortFeedbacks(feedbacks.values.toList()); + var currentIndex = sorted.indexOf(feedback); + + if (sorted.length - 1 > currentIndex && currentIndex >= 0) { + var nextFeedback = sorted[currentIndex + 1]; + AdminFeedbackPageRoute.info + .push(context, params: {"id": nextFeedback.id}); + } else { + AdminFeedbackRoute.info.push(context); + } + } + + _deleteFeedback(WidgetRef ref, Feedback feedback) async { + if (_deleteFuture != null) return; + + setState(() { + _deleteFuture = ref.read(feedbackController).deleteFeedback(feedback.id); + }); + + var response = await _deleteFuture; + + if (mounted) { + setState(() { + _deleteFuture = null; + }); + + if (response!.succeeded) _pushNext(ref.read(feedbackProvider), feedback); + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, _) { + var feedback = ref.watch(feedbackProvider)[widget.feedbackId]; + + if (feedback == null) + return LpShimmer(height: AdminFeedbackItem.height); + + if (!_commenmtInit) { + commentController.text = feedback.comment; + _commenmtInit = true; + } + + var user = ref.watch(usersProvider)[feedback.userId]; + + if (user == null) return LpShimmer(height: AdminFeedbackItem.height); + + return LpContainer( + title: feedback.type.title(context), + leading: LpIcon(feedback.type.icon, color: feedback.type.color), + trailing: ConditionalWidget( + condition: _deleteFuture == null, + trueWidget: (_) => HoverBuilder( + builder: (context, hover) => LpIcon( + Icons.delete, + color: hover ? errorColor : neutralColor, + ), + onTap: () => lpShowConfirmDialog( + context, + title: t.admin_feedback_page_deleteTitle, + message: t.admin_feedback_page_deleteText, + confirmIsBad: true, + onConfirm: () => _deleteFeedback(ref, feedback), + ), + ), + falseWidget: (_) => LpLoadingIndicator.circular(color: errorColor), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NcSpacing.small(), + LpFeedbackStatusTag(status: feedback.status), + NcSpacing.small(), + NcCaptionText(t.admin_feedback_page_author(user.fullname), + fontSize: AdminFeedbackItem.fontSize, selectable: true), + if (feedback.type.isBug) + NcCaptionText(t.admin_feedback_page_logFile(feedback.logFile), + fontSize: AdminFeedbackItem.fontSize, selectable: true), + NcCaptionText(t.admin_feedback_page_id(feedback.id), + fontSize: AdminFeedbackItem.fontSize, selectable: true), + NcSpacing.large(), + Expanded( + child: CustomScrollView( + controller: ScrollController(), + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: NcBodyText( + feedback.content, + fontSize: AdminFeedbackItem.fontSize, + selectable: true, + ), + ), + ], + ), + ), + ], + ), + ), + NcSpacing.large(), + Expanded( + child: LpTextField.filled( + multiline: true, + controller: commentController, + placeholder: t.admin_feedback_page_comment, + ), + ), + NcSpacing.large(), + Align( + alignment: Alignment.centerRight, + child: ConditionalWidget( + condition: _updateFuture != null, + trueWidget: (_) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + NcCaptionText( + feedback.status.isRead + ? t.admin_feedback_page_update + : t.admin_feedback_page_markRead, + fontSize: AdminFeedbackItem.fontSize), + NcSpacing.small(), + LpLoadingIndicator.circular(), + ], + ), + falseWidget: (context) => LpTextButton( + text: feedback.status.isRead + ? t.admin_feedback_page_update + : t.admin_feedback_page_markRead, + fontSize: AdminFeedbackItem.fontSize, + trailingIcon: Feather.arrow_right_circle, + onPressed: () => _updateFeedback(ref, feedback), + ), + ), + ), + ], + ), + ); + }, + ); + } +} From 7aabbd0f4241fe83296fd6ca63c12102b5122ec4 Mon Sep 17 00:00:00 2001 From: mcquenji <60017181+mcquenji@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:22:21 +0100 Subject: [PATCH 03/18] added getFeedbackById to FeedbackProviderState --- .../domain/providers/feedback_provider_state.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/features/feedback/domain/providers/feedback_provider_state.dart b/lib/features/feedback/domain/providers/feedback_provider_state.dart index 5167d232..dc2b8408 100644 --- a/lib/features/feedback/domain/providers/feedback_provider_state.dart +++ b/lib/features/feedback/domain/providers/feedback_provider_state.dart @@ -95,4 +95,17 @@ class FeedbackProviderState extends AutoRefreshAsyncNotifier> { return true; }).toList(); } + + /// Returns the [Feedback] with the given [id]. + /// + /// If no feedback with the specified [id] can be found or [state.hasValue] is `false` this method will throw a [StateError]. + getFeedbackById(int id){ + if(!state.hasValue) throw StateError("State is ${state.runtimeType}!"); + + final feedback = state.requireValue.firstWhereOrNull((e) => e.id == id)); + + if(feedback == null) throw StateError("Feedback with id $id not found!"); + + return feedback; + } } From c7474515e3b574efaffea64ff614d7a71c56e2d8 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 28 Nov 2023 15:56:10 +0100 Subject: [PATCH 04/18] implementing finished --- lib/features/auth/domain/models/user.dart | 3 + .../providers/feedback_provider_state.dart | 2 +- .../presentation/widgets/feedback.dart | 158 ++++++++++++ .../presentation/widgets/feedback_item.dart | 234 ++++++++++-------- .../widgets/feedback_status_tag.dart | 73 ++++++ .../presentation/widgets/widgets.dart | 4 +- .../presentation/widgets/color_utils.dart | 29 +++ .../widgets/conditional_widget.dart | 27 ++ lib/shared/presentation/widgets/shimmer.dart | 43 ++++ .../widgets/user_profile_img.dart | 59 +++++ lib/shared/presentation/widgets/widgets.dart | 4 + pubspec.lock | 88 +++++++ pubspec.yaml | 6 + 13 files changed, 626 insertions(+), 104 deletions(-) create mode 100644 lib/features/feedback/presentation/widgets/feedback.dart create mode 100644 lib/features/feedback/presentation/widgets/feedback_status_tag.dart create mode 100644 lib/shared/presentation/widgets/color_utils.dart create mode 100644 lib/shared/presentation/widgets/conditional_widget.dart create mode 100644 lib/shared/presentation/widgets/shimmer.dart create mode 100644 lib/shared/presentation/widgets/user_profile_img.dart diff --git a/lib/features/auth/domain/models/user.dart b/lib/features/auth/domain/models/user.dart index 491cd789..e4a88eec 100644 --- a/lib/features/auth/domain/models/user.dart +++ b/lib/features/auth/domain/models/user.dart @@ -85,6 +85,9 @@ class User with _$User { bool hasCapability(UserCapability capability) => capabilities.contains(capability); + /// Returns the full name of the user. + String get fullname => '$firstname $lastname'; + /// Returns `true` if this user has elevated privileges (i.e. [UserCapability.dev] or [UserCapability.moderator]). Otherwise `false`. bool get isElevated => hasCapability(UserCapability.dev) || diff --git a/lib/features/feedback/domain/providers/feedback_provider_state.dart b/lib/features/feedback/domain/providers/feedback_provider_state.dart index dc2b8408..14b50b50 100644 --- a/lib/features/feedback/domain/providers/feedback_provider_state.dart +++ b/lib/features/feedback/domain/providers/feedback_provider_state.dart @@ -99,7 +99,7 @@ class FeedbackProviderState extends AutoRefreshAsyncNotifier> { /// Returns the [Feedback] with the given [id]. /// /// If no feedback with the specified [id] can be found or [state.hasValue] is `false` this method will throw a [StateError]. - getFeedbackById(int id){ + Feedback getFeedbackById(int id){ if(!state.hasValue) throw StateError("State is ${state.runtimeType}!"); final feedback = state.requireValue.firstWhereOrNull((e) => e.id == id)); diff --git a/lib/features/feedback/presentation/widgets/feedback.dart b/lib/features/feedback/presentation/widgets/feedback.dart new file mode 100644 index 00000000..95294276 --- /dev/null +++ b/lib/features/feedback/presentation/widgets/feedback.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:lb_planner/features/feedback/presentation/presentation.dart'; +import 'package:lb_planner/shared/shared.dart'; + +/// Admin feedback subroute. +class AdminFeedbackRoute extends StatefulWidget { + /// Admin feedback subroute. + const AdminFeedbackRoute({Key? key}) : super(key: key); + + /// The font size of the header. + static const double headerFontSize = 20; + + @override + State createState() => _AdminFeedbackRouteState(); + + /// Sorts the given feedback list + static List sortFeedbacks(List feedbacks) { + feedbacks.sort( + (a, b) { + var status = a.status.index.compareTo(b.status.index); + + if (status != 0) return status; + + var timestamp = b.timestamp.compareTo(a.timestamp); + + return timestamp; + }, + ); + + return feedbacks; + } +} + +class _AdminFeedbackRouteState extends State { + FeedbackProvider? _feedbackController; + + _startAutoRefresh(WidgetRef ref) { + _feedbackController = ref.read(feedbackController); + + _feedbackController?.startAutoRefresh(); + } + + @override + void dispose() { + _feedbackController?.pauseAutoRefresh(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, ref, _) { + var feedbacks = ref.watch(feedbackProvider); + _startAutoRefresh(ref); + + var sortedFeedbacks = + AdminFeedbackRoute.sortFeedbacks(feedbacks.values.toList()); + + return ConditionalWidget( + condition: sortedFeedbacks.isNotEmpty, + trueWidget: (_) => Align( + alignment: Alignment.topLeft, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + t.admin_feedback_headers_user, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600) + ), + ), + Expanded( + child: Text( + t.admin_feedback_headers_status, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600) + ), + ), + Expanded( + child: Text( + t.admin_feedback_headers_type, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600) + ), + ), + Expanded( + child: Text( + t.admin_feedback_headers_lastModified, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600), + ), + ), + ), + Expanded( + child: Text( + t.admin_feedback_headers_lastModifiedBy, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600), + ) + ), + ), + Expanded( + child: Text( + t.admin_feedback_headers_timestamp, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600), + ) + ), + ), + ], + ), + Spacing.large(), + Expanded( + child: ListView( + controller: ScrollController(), + children: [ + for (var feedback in sortedFeedbacks) ...[ + AdminFeedbackItem(feedbackId: feedback.id), + Spacing.medium(), + ] + ], + ), + ), + ], + ), + ), + falseWidget: (_) => Center( + child: Text( + t.admin_feedback_noFeedback, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600), + textAlign: TextAlign.left, + ), + ), + ); + }); + } +} diff --git a/lib/features/feedback/presentation/widgets/feedback_item.dart b/lib/features/feedback/presentation/widgets/feedback_item.dart index 9fd0e852..35def4fe 100644 --- a/lib/features/feedback/presentation/widgets/feedback_item.dart +++ b/lib/features/feedback/presentation/widgets/feedback_item.dart @@ -1,7 +1,15 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lb_planner/features/auth/auth.dart'; +import 'package:lb_planner/features/themes/themes.dart'; +import 'package:lb_planner/shared/shared.dart'; +import 'package:lb_planner/features/feedback/domain/domain.dart'; +import 'package:lb_planner/features/feedback/presentation/presentation.dart'; +import 'package:timeago/timeago.dart' as timeago; /// Feedback item. -class AdminFeedbackItem extends StatefulWidget { +class AdminFeedbackItem extends ConsumerStatefulWidget { /// Feedback item. const AdminFeedbackItem({Key? key, required this.feedbackId}) : super(key: key); @@ -25,147 +33,169 @@ class AdminFeedbackItem extends StatefulWidget { static const double userTagFontSize = 17; @override - State createState() { - // TODO: implement createState - throw UnimplementedError(); - } + ConsumerState createState() => + _AdminFeedbackItemState(); +} +class _AdminFeedbackItemState extends ConsumerState { @override - Widget build(BuildContext context, t) { - return Consumer(builder: (context, ref, _) { - var feedback = ref.watch(feedbackProvider)[feedbackId]; - - if (feedback == null) return LpShimmer(height: height); - - var user = ref.watch(usersProvider)[feedback.userId]; - - if (user == null) return LpShimmer(height: height); - - var modifyingUser = ref.watch(usersProvider)[feedback.lastModifiedBy]; - - return LpGestureDetector( - onTap: () => AdminFeedbackPageRoute.info - .push(context, params: {"id": feedbackId}), - child: LpCard( - child: Row( - children: [ - Expanded( - child: Row( - children: [ - UserProfileImg(size: imgSize, userId: feedback.userId), - NcSpacing.small(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - NcBodyText( - user.fullname, - fontSize: usernameFontSize, + Widget build(BuildContext context) { + int feedbackId = widget.feedbackId; + var controller = ref.watch(feedbackController); + var feedback = controller.getFeedbackById(feedbackId); + + if (ref.read(feedbackProvider).isLoading) { + return ShimmerEffect(height: AdminFeedbackItem.height); + } + + int author = feedback.author; + + int? modifyingUser = feedback.modifiedByUserId; + + return GestureDetector( + onTap: () => context.router.navigate(const LoginRoute()) + AdminFeedbackPageRoute.info.push(context, params: {"id": feedbackId}), + child: Card( + child: Row( + children: [ + Expanded( + child: Row( + children: [ + UserProfileImg( + size: AdminFeedbackItem.imgSize, userId: author), + Spacing.small(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ref.read(usersProvider)[author].fullname, + style: TextStyle( overflow: TextOverflow.fade, + fontSize: AdminFeedbackItem.usernameFontSize, ), - NcBodyText( - t.global_user_vintage(user.vintage), - fontSize: userTagFontSize, + textAlign: TextAlign.left, + ), + Text( + t.global_user_vintage( + ref.read(usersProvider)[author].vintage), + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.userTagFontSize, ), - ], - ), - ], - ), + textAlign: TextAlign.left, + ), + ], + ), + ], ), - Expanded( - child: LpFeedbackStatusTag( - status: feedback.status, - fontSize: fontSize, - label: true, - ), + ), + Expanded( + child: FeedbackStatusTag( + read: feedback.read, + fontSize: AdminFeedbackItem.fontSize, + label: true, ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - LpIcon( - feedback.type.icon, - color: feedback.type.color, - size: scaleIcon(fontSize), + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + feedback.type.icon, + color: feedback.type.color(context), + size: AdminFeedbackItem.fontSize * 1.2, + ), + Spacing.xs(), + Text( + feedback.type.title(context), + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, ), - NcSpacing.xs(), - NcBodyText( - feedback.type.title(context), - fontSize: fontSize, - ), - ], - ), + textAlign: TextAlign.left, + ), + ], ), - Expanded( - child: NcBodyText( - feedback.lastModified != null - ? timeago.format(feedback.lastModified!) - : t.admin_feedback_null, - fontSize: fontSize, - textAlign: TextAlign.center, + ), + Expanded( + child: Text( + feedback.modifiedAt != null + ? timeago.format(feedback.modifiedAt!) + : t.admin_feedback_null, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, ), + textAlign: TextAlign.center, ), - Expanded( - child: NcBodyText( - modifyingUser != null - ? modifyingUser.fullname - : t.admin_feedback_null, - fontSize: fontSize, - textAlign: TextAlign.center, + ), + Expanded( + child: Text( + modifyingUser != null + ? ref.read(usersProvider)[modifyingUser].fullname + : t.admin_feedback_null, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, ), + textAlign: TextAlign.center, ), - Expanded( - child: NcBodyText( - timeago.format(feedback.timestamp), - fontSize: fontSize, - textAlign: TextAlign.center, + ), + Expanded( + child: Text( + timeago.format(feedback.createdAt), + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, ), + textAlign: TextAlign.center, ), - ], - ), + ), + ], ), - ); - }); + ), + ); } } -extension _AdminFeedbackItemHelper on FeedbackTypes { +extension _AdminFeedbackItemHelper on FeedbackType { String title(BuildContext context) { var t = context.t; switch (this) { - case FeedbackTypes.bug: + case FeedbackType.bug: return t.admin_feedback_types_bug; - case FeedbackTypes.suggestion: + case FeedbackType.suggestion: return t.admin_feedback_types_suggestion; - case FeedbackTypes.error: + case FeedbackType.typo: return t.admin_feedback_types_error; - case FeedbackTypes.other: + case FeedbackType.other: return t.admin_feedback_types_other; } } IconData get icon { switch (this) { - case FeedbackTypes.bug: + case FeedbackType.bug: return Icons.bug_report; - case FeedbackTypes.suggestion: + case FeedbackType.suggestion: return Icons.lightbulb_outline; - case FeedbackTypes.error: + case FeedbackType.typo: return Icons.error; - case FeedbackTypes.other: + case FeedbackType.other: return Icons.help; } } - Color get color { + Color color(BuildContext context) { switch (this) { - case FeedbackTypes.bug: - return errorColor; - case FeedbackTypes.other: - case FeedbackTypes.suggestion: - return accentColor; - case FeedbackTypes.error: - return warningColor; + case FeedbackType.bug: + return context.theme.colorScheme.error; + case FeedbackType.other: + case FeedbackType.suggestion: + return context.theme.colorScheme.primary; + case FeedbackType.typo: + return ModuleStatusTheme.of(context).uploadedColor; } } } diff --git a/lib/features/feedback/presentation/widgets/feedback_status_tag.dart b/lib/features/feedback/presentation/widgets/feedback_status_tag.dart new file mode 100644 index 00000000..1c194810 --- /dev/null +++ b/lib/features/feedback/presentation/widgets/feedback_status_tag.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:lb_planner/features/themes/themes.dart'; +import 'package:lb_planner/shared/shared.dart'; + +/// A tag based on [read] +class FeedbackStatusTag extends StatefulWidget { + /// A tag based on [read] + const FeedbackStatusTag( + {Key? key, required this.read, this.fontSize, this.label = false}) + : super(key: key); + + /// The status of the tag. + final bool read; + + /// The font size of the tag. + final double? fontSize; + + /// Whether to display the status as a label. + final bool label; + + /// THe background opacity of the tag. + static const opacity = .5; + + @override + State createState() => _FeedbackStatusTagState(); +} + +class _FeedbackStatusTagState extends State { + @override + Widget build(BuildContext context) { + var color = widget.read + ? ModuleStatusTheme.of(context).doneColor + : widget.label + ? context.theme.textTheme.bodyLarge!.color! + : ModuleStatusTheme.of(context).pendingColor; + return Container( + padding: widget.label + ? null + : const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: widget.label + ? null + : BoxDecoration( + color: color.withOpacity(0.5), + borderRadius: BorderRadius.circular(5), + ), + child: ConditionalWidget( + condition: widget.label, + trueWidget: (context) => Text( + widget.read + ? t.admin_feedback_status_read + : t.admin_feedback_status_unread, + style: TextStyle( + color: color, + fontSize: widget.fontSize, + overflow: TextOverflow.ellipsis, + ), + textAlign: TextAlign.center, + ), + falseWidget: (context) => Text( + widget.read + ? t.admin_feedback_status_read + : t.admin_feedback_status_unread, + style: TextStyle( + color: color, + fontSize: widget.fontSize, + overflow: TextOverflow.ellipsis, + fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/features/feedback/presentation/widgets/widgets.dart b/lib/features/feedback/presentation/widgets/widgets.dart index b9f78bb0..e55f208f 100644 --- a/lib/features/feedback/presentation/widgets/widgets.dart +++ b/lib/features/feedback/presentation/widgets/widgets.dart @@ -1 +1,3 @@ -// TODO Implement this library. \ No newline at end of file +export 'feedback_page.dart'; +export 'feedback_item.dart'; +export 'feedback_status_tag.dart'; diff --git a/lib/shared/presentation/widgets/color_utils.dart b/lib/shared/presentation/widgets/color_utils.dart new file mode 100644 index 00000000..5cb9e4aa --- /dev/null +++ b/lib/shared/presentation/widgets/color_utils.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +/// Utilitiy class to work with colors. +extension ColorUtils on Color { + /// Returns a new [Color] darkened by the given [amount]. + /// + /// Credit to: [NearHuscarl](https://stackoverflow.com/questions/58360989/programmatically-lighten-or-darken-a-hex-color-in-dart#:~:text=For%20people%20who%20want%20to%20darken%20or%20lighten%20Color%20instead%20of%20hex%20string) + Color darken([double amount = .1]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(this); + final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + + return hslDark.toColor(); + } + + /// Returns a new [Color] lightened by the given [amount]. + /// + /// Credit to: [NearHuscarl](https://stackoverflow.com/questions/58360989/programmatically-lighten-or-darken-a-hex-color-in-dart#:~:text=For%20people%20who%20want%20to%20darken%20or%20lighten%20Color%20instead%20of%20hex%20string) + Color lighten([double amount = .1]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(this); + final hslLight = + hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); + + return hslLight.toColor(); + } +} diff --git a/lib/shared/presentation/widgets/conditional_widget.dart b/lib/shared/presentation/widgets/conditional_widget.dart new file mode 100644 index 00000000..5395630d --- /dev/null +++ b/lib/shared/presentation/widgets/conditional_widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +/// Use this to conditionally swap between widgets. +class ConditionalWidget extends StatelessWidget { + /// Constructs a new [ConditionalWidget] with the given [condition], [trueWidget] and [falseWidget]. + /// If [condition] is true, [trueWidget] will be used. + /// If [condition] is false, [falseWidget] will be used. + const ConditionalWidget( + {Key? key, + required this.condition, + required this.trueWidget, + required this.falseWidget}) + : super(key: key); + + /// The condition to check. + final bool condition; + + /// The widget to show if the condition is true. + final WidgetBuilder trueWidget; + + /// The widget to show if the condition is false. + final WidgetBuilder falseWidget; + + @override + Widget build(BuildContext context) => + condition ? trueWidget(context) : falseWidget(context); +} diff --git a/lib/shared/presentation/widgets/shimmer.dart b/lib/shared/presentation/widgets/shimmer.dart new file mode 100644 index 00000000..a1a9268f --- /dev/null +++ b/lib/shared/presentation/widgets/shimmer.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:lb_planner/shared/presentation/widgets/color_utils.dart'; +import 'package:lb_planner/shared/shared.dart'; +import 'package:shimmer/shimmer.dart'; + +class ShimmerEffect extends StatelessWidget { + // ignore: no-magic-number + const ShimmerEffect({Key? key, this.width, this.height = 30, this.child}) + : super(key: key); + + /// The width of the shimmer. + final double? width; + + /// The height of the shimmer. + final double? height; + + /// The child of the shimmer. + /// + /// If not set the shimmer will be a rounded container. + final Widget? child; + + /// Default value for [Shimmer.period] + static const period = Duration(milliseconds: 2000); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + period: period, + child: child ?? + Container( + height: height, + width: width, + decoration: BoxDecoration( + color: context.theme.colorScheme.secondary, + borderRadius: BorderRadius.circular(5), + ), + ), + baseColor: context.theme.colorScheme.secondary, + // ignore: no-magic-number + highlightColor: context.theme.colorScheme.secondary.lighten(0.02), + ); + } +} diff --git a/lib/shared/presentation/widgets/user_profile_img.dart b/lib/shared/presentation/widgets/user_profile_img.dart new file mode 100644 index 00000000..015975a3 --- /dev/null +++ b/lib/shared/presentation/widgets/user_profile_img.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lb_planner/features/auth/auth.dart'; +import 'package:lb_planner/shared/presentation/presentation.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +/// Displays the profile image of the current user with the current size. +class UserProfileImg extends StatelessWidget { + /// Displays the profile image of the current user with the current size. + const UserProfileImg({Key? key, required this.size, this.userId}) + : super(key: key); + + /// The size of the profile image. + final double size; + + /// The id of the user to display. If not specified, the current user is used. + final int? userId; + + /// Cache manager for the profile image. + static CacheManager get cacheManager => CacheManager( + Config( + 'user_profile', + + // ignore: no-magic-number + stalePeriod: Duration(days: 7), + ), + ); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, _) { + var user = userId != null + ? ref.watch(usersProvider)[userId!] + : ref.watch(userProvider); + + return ClipOval( + child: ConditionalWidget( + condition: user != null && user.profileImageUrl.isNotEmpty, + falseWidget: (_) => ShimmerEffect(height: size, width: size), + trueWidget: (_) => CachedNetworkImage( + imageUrl: user!.profileImageUrl, + width: size, + height: size, + fit: BoxFit.contain, + placeholder: (_, __) => CircularProgressIndicator(), + errorWidget: (_, __, ___) => Icon( + Icons.account_circle, + size: size, + ), + cacheManager: cacheManager, + ), + ), + ); + }, + ); + } +} diff --git a/lib/shared/presentation/widgets/widgets.dart b/lib/shared/presentation/widgets/widgets.dart index 6e24c528..0b8203fa 100644 --- a/lib/shared/presentation/widgets/widgets.dart +++ b/lib/shared/presentation/widgets/widgets.dart @@ -10,3 +10,7 @@ export 'scale_on_hover.dart'; export 'offset_on_hover.dart'; export 'hover_builder.dart'; export 'hoverable_widget.dart'; +export 'shimmer.dart'; +export 'color_utils.dart'; +export 'user_profile_img.dart'; +export 'conditional_widget.dart'; diff --git a/pubspec.lock b/pubspec.lock index 682047bb..c63fa87a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.6.3" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f + url: "https://pub.dev" + source: hosted + version: "3.3.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" + url: "https://pub.dev" + source: hosted + version: "1.1.0" catcher: dependency: "direct overridden" description: @@ -279,6 +303,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" flutter_lints: dependency: "direct dev" description: @@ -526,6 +558,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" package_config: dependency: transitive description: @@ -670,6 +710,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" sentry: dependency: transitive description: @@ -694,6 +742,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -723,6 +779,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 + url: "https://pub.dev" + source: hosted + version: "2.5.0+2" stack_trace: dependency: transitive description: @@ -763,6 +835,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" term_glyph: dependency: transitive description: @@ -779,6 +859,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + timeago: + dependency: "direct main" + description: + name: timeago + sha256: c44b80cbc6b44627c00d76960f2af571f6f50e5dbedef4d9215d455e4335165b + url: "https://pub.dev" + source: hosted + version: "3.6.0" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ab8287f6..e8836128 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,11 @@ dependencies: path_provider: ^2.1.1 riverpod: ^2.4.0 url_launcher: ^6.1.2 + shimmer: ^3.0.0 + flutter_cache_manager: ^3.3.1 + cached_network_image: ^3.3.0 + timeago: ^3.6.0 + dev_dependencies: auto_route_generator: ^7.3.2 @@ -50,6 +55,7 @@ dev_dependencies: freezed: ^2.2.0 json_serializable: ^6.7.1 + dependency_overrides: catcher: git: From 596515f473a68de6f424f0be90f7e425c76e243a Mon Sep 17 00:00:00 2001 From: Can42 Date: Tue, 28 Nov 2023 22:46:46 +0100 Subject: [PATCH 05/18] some issues fixxed --- .../providers/feedback_provider_state.dart | 10 +- .../presentation/widgets/feedback_page.dart | 112 ++++++++++------ .../presentation/widgets/container.dart | 121 ++++++++++++++++++ .../presentation/widgets/textbutton.dart | 71 ++++++++++ lib/shared/presentation/widgets/widgets.dart | 1 + 5 files changed, 271 insertions(+), 44 deletions(-) create mode 100644 lib/shared/presentation/widgets/container.dart create mode 100644 lib/shared/presentation/widgets/textbutton.dart diff --git a/lib/features/feedback/domain/providers/feedback_provider_state.dart b/lib/features/feedback/domain/providers/feedback_provider_state.dart index 14b50b50..a5647a9b 100644 --- a/lib/features/feedback/domain/providers/feedback_provider_state.dart +++ b/lib/features/feedback/domain/providers/feedback_provider_state.dart @@ -99,13 +99,13 @@ class FeedbackProviderState extends AutoRefreshAsyncNotifier> { /// Returns the [Feedback] with the given [id]. /// /// If no feedback with the specified [id] can be found or [state.hasValue] is `false` this method will throw a [StateError]. - Feedback getFeedbackById(int id){ - if(!state.hasValue) throw StateError("State is ${state.runtimeType}!"); + Feedback getFeedbackById(int id) { + if (!state.hasValue) throw StateError("State is ${state.runtimeType}!"); - final feedback = state.requireValue.firstWhereOrNull((e) => e.id == id)); + final feedback = state.requireValue.firstWhereOrNull((e) => e.id == id); - if(feedback == null) throw StateError("Feedback with id $id not found!"); + if (feedback == null) throw StateError("Feedback with id $id not found!"); return feedback; - } + } } diff --git a/lib/features/feedback/presentation/widgets/feedback_page.dart b/lib/features/feedback/presentation/widgets/feedback_page.dart index 86153e99..8df3cc69 100644 --- a/lib/features/feedback/presentation/widgets/feedback_page.dart +++ b/lib/features/feedback/presentation/widgets/feedback_page.dart @@ -1,6 +1,9 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lb_planner/features/feedback/domain/domain.dart'; +import 'package:lb_planner/features/themes/domain/models/module_status_theme.dart'; +import 'package:lb_planner/shared/shared.dart'; +import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; /// Admin feedback page displaying details about feedback. class AdminFeedbackPageRoute extends StatefulWidget { @@ -20,16 +23,11 @@ class _AdminFeedbackPageRouteState extends State { bool _commenmtInit = false; - Future? _updateFuture; - Future? _deleteFuture; - _updateFeedback(WidgetRef ref, Feedback feedback) async { - if (_updateFuture != null) return; - final controller = ref.watch(feedbackController); var feedbacks = ref.read(feedbackProvider); - if (feedback.status.isUnread) { + if (feedback.unread) { setState(() { _updateFuture = controller.updateFeedbackStatus(feedback.id, FeedbackStatus.read); @@ -99,29 +97,33 @@ class _AdminFeedbackPageRouteState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, ref, _) { - var feedback = ref.watch(feedbackProvider)[widget.feedbackId]; + int feedbackId = widget.feedbackId; - if (feedback == null) - return LpShimmer(height: AdminFeedbackItem.height); + var controller = ref.watch(feedbackController); + var feedback = controller.getFeedbackById(feedbackId); + + if (ref.read(feedbackProvider).isLoading) { + return ShimmerEffect(height: AdminFeedbackItem.height); + } if (!_commenmtInit) { commentController.text = feedback.comment; _commenmtInit = true; } - var user = ref.watch(usersProvider)[feedback.userId]; - - if (user == null) return LpShimmer(height: AdminFeedbackItem.height); + int user = feedback.author; return LpContainer( title: feedback.type.title(context), - leading: LpIcon(feedback.type.icon, color: feedback.type.color), + leading: Icon(feedback.type.icon, color: feedback.type.color), trailing: ConditionalWidget( condition: _deleteFuture == null, trueWidget: (_) => HoverBuilder( - builder: (context, hover) => LpIcon( + builder: (context, hover) => Icon( Icons.delete, - color: hover ? errorColor : neutralColor, + color: hover + ? context.theme.colorScheme.error + : ModuleStatusTheme.of(context).pendingColor, ), onTap: () => lpShowConfirmDialog( context, @@ -131,22 +133,44 @@ class _AdminFeedbackPageRouteState extends State { onConfirm: () => _deleteFeedback(ref, feedback), ), ), - falseWidget: (_) => LpLoadingIndicator.circular(color: errorColor), + falseWidget: (_) => CircularProgressIndicator( + color: context.theme.colorScheme.error), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - NcSpacing.small(), - LpFeedbackStatusTag(status: feedback.status), - NcSpacing.small(), - NcCaptionText(t.admin_feedback_page_author(user.fullname), - fontSize: AdminFeedbackItem.fontSize, selectable: true), + Spacing.small(), + FeedbackStatusTag(read: feedback.read), + Spacing.small(), + Text( + t.admin_feedback_page_author(user.fullname), + style: TextStyle( + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, + ), + textAlign: TextAlign.left, + ), if (feedback.type.isBug) - NcCaptionText(t.admin_feedback_page_logFile(feedback.logFile), - fontSize: AdminFeedbackItem.fontSize, selectable: true), - NcCaptionText(t.admin_feedback_page_id(feedback.id), - fontSize: AdminFeedbackItem.fontSize, selectable: true), - NcSpacing.large(), + Text( + t.admin_feedback_page_logFile(feedback.logFile), + style: TextStyle( + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, + ), + textAlign: TextAlign.left, + ), + Text( + t.admin_feedback_page_id(feedback.id), + style: TextStyle( + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, + ), + textAlign: TextAlign.left, + ), + Spacing.large(), Expanded( child: CustomScrollView( controller: ScrollController(), @@ -157,10 +181,14 @@ class _AdminFeedbackPageRouteState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: NcBodyText( + child: Text( feedback.content, - fontSize: AdminFeedbackItem.fontSize, - selectable: true, + style: TextStyle( + fontWeight: FontWeight.normal, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, + ), + textAlign: TextAlign.left, ), ), ], @@ -169,7 +197,7 @@ class _AdminFeedbackPageRouteState extends State { ], ), ), - NcSpacing.large(), + Spacing.large(), Expanded( child: LpTextField.filled( multiline: true, @@ -177,7 +205,7 @@ class _AdminFeedbackPageRouteState extends State { placeholder: t.admin_feedback_page_comment, ), ), - NcSpacing.large(), + Spacing.large(), Align( alignment: Alignment.centerRight, child: ConditionalWidget( @@ -185,13 +213,19 @@ class _AdminFeedbackPageRouteState extends State { trueWidget: (_) => Row( mainAxisSize: MainAxisSize.min, children: [ - NcCaptionText( - feedback.status.isRead - ? t.admin_feedback_page_update - : t.admin_feedback_page_markRead, - fontSize: AdminFeedbackItem.fontSize), - NcSpacing.small(), - LpLoadingIndicator.circular(), + Text( + feedback.status.isRead + ? t.admin_feedback_page_update + : t.admin_feedback_page_markRead, + style: TextStyle( + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, + ), + textAlign: TextAlign.left, + ), + Spacing.small(), + CircularProgressIndicator(), ], ), falseWidget: (context) => LpTextButton( diff --git a/lib/shared/presentation/widgets/container.dart b/lib/shared/presentation/widgets/container.dart new file mode 100644 index 00000000..ada37f8c --- /dev/null +++ b/lib/shared/presentation/widgets/container.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:lb_planner/shared/shared.dart'; + +/// Themed [Container] widget. +class LpContainer extends StatelessWidget { + /// Themed [Container] widget. + LpContainer( + {Key? key, + this.title, + this.leading, + this.trailing, + required this.child, + this.width, + this.height, + this.spacing = true}) + : super(key: key) { + window = false; + } + + /// Themed WindowContainer widget. + LpContainer.window( + {Key? key, + this.title, + this.leading, + this.trailing, + required this.child, + this.width, + this.height, + this.spacing = false}) + : super(key: key) { + window = true; + } + + /// The title of the container. + final String? title; + + /// The leading icon of the container. + final Widget? leading; + + /// The trailing icon of the container. + final Widget? trailing; + + /// The body of the container. + final Widget child; + + /// The width of the container. + final double? width; + + /// The height of the container. + final double? height; + + /// If true, the container is a window. + late final bool window; + + /// Spacing between the title and the body. + final bool spacing; + + /// The font size of the title. + static const titleFontSize = 19.0; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: Duration(milliseconds: 100), + padding: window + ? EdgeInsets.only(bottom: Spacing.smallSpacing) + : const EdgeInsets.all(Spacing.smallSpacing), + decoration: BoxDecoration( + color: context.theme.colorScheme.primary, + boxShadow: kElevationToShadow[6], + borderRadius: BorderRadius.circular(5), + ), + width: width, + height: height, + child: ConditionalWrapper( + condition: title != null || leading != null || trailing != null, + wrapper: (context, child) => Column( + children: [ + ConditionalWrapper( + condition: window, + wrapper: (context, child) => AnimatedContainer( + duration: Duration(milliseconds: 100), + padding: EdgeInsets.only( + left: Spacing.smallSpacing, right: Spacing.smallSpacing), + decoration: BoxDecoration( + color: context.theme.colorScheme.secondary, + borderRadius: BorderRadius.vertical(top: Radius.circular(5)), + ), + child: child, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (leading != null) leading!, + if (leading != null) Spacing.small(), + if (title != null) + Text( + title!, + style: TextStyle( + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + fontSize: titleFontSize), + textAlign: TextAlign.left, + ), + ], + ), + if (trailing != null) trailing!, + ], + ), + ), + if (spacing) Spacing.small(), + Expanded(child: child), + ], + ), + child: child, + ), + ); + } +} diff --git a/lib/shared/presentation/widgets/textbutton.dart b/lib/shared/presentation/widgets/textbutton.dart new file mode 100644 index 00000000..91c97258 --- /dev/null +++ b/lib/shared/presentation/widgets/textbutton.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:lb_planner/shared/shared.dart'; + +/// The [LpTextButton] widget is a button that displays clickable text and an icon. +class LpTextButton extends StatelessWidget { + /// The [LpTextButton] widget is a button that displays clickable text and an icon. + const LpTextButton({ + Key? key, + required this.text, + this.onPressed, + this.leadingIcon, + this.trailingIcon, + this.fontSize, + this.color, + this.decoration = TextDecoration.none, + this.leading, + this.trailing, + }) : super(key: key); + + /// The text the button displays. + final String text; + + /// The function to call when the button is pressed. + final VoidCallback? onPressed; + + /// The icon to display on the leading side of the button. + final IconData? leadingIcon; + + /// The icon to display on the trailing side of the button. + final IconData? trailingIcon; + + /// The leading widget to display on the button. This is ignored if [leadingIcon] is not null. + final Widget? leading; + + /// The trailing widget to display on the button. This is ignored if [trailingIcon] is not null. + final Widget? trailing; + + /// The font size of the button text. + final double? fontSize; + + /// The color of the button text. + final Color? color; + + /// The text decoration of the button text. + final TextDecoration decoration; + + @override + Widget build(BuildContext context) { + return TextButton( + style: TextButton.styleFrom( + foregroundColor: context.theme.colorScheme.primary, + ), + onPressed: onPressed, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (leadingIcon != null) + LpIcon(leadingIcon, size: (fontSize * 1.2), color: color), + if (leading != null && leadingIcon == null) leading!, + if (leadingIcon != null || leading != null) NcSpacing.small(), + NcCaptionText(text, + fontSize: fontSize, color: color, decoration: decoration), + if (trailingIcon != null || trailing != null) NcSpacing.small(), + if (trailing != null && trailingIcon == null) trailing!, + if (trailingIcon != null) + LpIcon(trailingIcon, size: scaleIcon(fontSize), color: color), + ], + ), + ); + } +} diff --git a/lib/shared/presentation/widgets/widgets.dart b/lib/shared/presentation/widgets/widgets.dart index 0b8203fa..c86e77d7 100644 --- a/lib/shared/presentation/widgets/widgets.dart +++ b/lib/shared/presentation/widgets/widgets.dart @@ -14,3 +14,4 @@ export 'shimmer.dart'; export 'color_utils.dart'; export 'user_profile_img.dart'; export 'conditional_widget.dart'; +export 'container.dart'; From d31aefcaf9b540e8f529494f70c7a739595f682c Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:56:13 +0100 Subject: [PATCH 06/18] Feedback Page implemented --- .../presentation/widgets/feedback.dart | 101 +++--- .../presentation/widgets/feedback_page.dart | 86 ++--- lib/shared/presentation/widgets/dialog.dart | 338 ++++++++++++++++++ .../presentation/widgets/text_field.dart | 232 ++++++++++++ .../presentation/widgets/textbutton.dart | 71 ---- 5 files changed, 645 insertions(+), 183 deletions(-) create mode 100644 lib/shared/presentation/widgets/dialog.dart create mode 100644 lib/shared/presentation/widgets/text_field.dart delete mode 100644 lib/shared/presentation/widgets/textbutton.dart diff --git a/lib/features/feedback/presentation/widgets/feedback.dart b/lib/features/feedback/presentation/widgets/feedback.dart index 95294276..696bc839 100644 --- a/lib/features/feedback/presentation/widgets/feedback.dart +++ b/lib/features/feedback/presentation/widgets/feedback.dart @@ -1,6 +1,9 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Feedback; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lb_planner/features/feedback/presentation/presentation.dart'; import 'package:lb_planner/shared/shared.dart'; +import 'package:lb_planner/features/feedback/domain/domain.dart'; +import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; /// Admin feedback subroute. class AdminFeedbackRoute extends StatefulWidget { @@ -14,49 +17,44 @@ class AdminFeedbackRoute extends StatefulWidget { State createState() => _AdminFeedbackRouteState(); /// Sorts the given feedback list - static List sortFeedbacks(List feedbacks) { - feedbacks.sort( + static List? sortFeedbacks(AsyncValue> feedbacks) { + feedbacks.value!.sort( (a, b) { - var status = a.status.index.compareTo(b.status.index); + var status = a.readAsInt.compareTo(b.readAsInt); if (status != 0) return status; - var timestamp = b.timestamp.compareTo(a.timestamp); + var timestamp = b.createdAt.compareTo(a.createdAt); return timestamp; }, ); - return feedbacks; + return feedbacks.value; } } class _AdminFeedbackRouteState extends State { - FeedbackProvider? _feedbackController; - - _startAutoRefresh(WidgetRef ref) { - _feedbackController = ref.read(feedbackController); - - _feedbackController?.startAutoRefresh(); - } - - @override - void dispose() { - _feedbackController?.pauseAutoRefresh(); - super.dispose(); - } - @override Widget build(BuildContext context) { return Consumer(builder: (context, ref, _) { var feedbacks = ref.watch(feedbackProvider); - _startAutoRefresh(ref); - var sortedFeedbacks = - AdminFeedbackRoute.sortFeedbacks(feedbacks.values.toList()); + var sortedFeedbacks = AdminFeedbackRoute.sortFeedbacks(feedbacks); return ConditionalWidget( - condition: sortedFeedbacks.isNotEmpty, + falseWidget: (_) => Center( + child: Text( + t.admin_feedback_noFeedback, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.left, + ), + ), + condition: sortedFeedbacks!.isNotEmpty, trueWidget: (_) => Align( alignment: Alignment.topLeft, child: Column( @@ -68,8 +66,9 @@ class _AdminFeedbackRouteState extends State { t.admin_feedback_headers_user, style: TextStyle( overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, - fontWeight: FontWeight.w600) + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600, + ), ), ), Expanded( @@ -77,9 +76,10 @@ class _AdminFeedbackRouteState extends State { t.admin_feedback_headers_status, textAlign: TextAlign.center, style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, - fontWeight: FontWeight.w600) + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600, + ), ), ), Expanded( @@ -87,9 +87,10 @@ class _AdminFeedbackRouteState extends State { t.admin_feedback_headers_type, textAlign: TextAlign.center, style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, - fontWeight: FontWeight.w600) + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600, + ), ), ), Expanded( @@ -97,10 +98,10 @@ class _AdminFeedbackRouteState extends State { t.admin_feedback_headers_lastModified, textAlign: TextAlign.center, style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, - fontWeight: FontWeight.w600), - ), + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600, + ), ), ), Expanded( @@ -108,10 +109,10 @@ class _AdminFeedbackRouteState extends State { t.admin_feedback_headers_lastModifiedBy, textAlign: TextAlign.center, style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, - fontWeight: FontWeight.w600), - ) + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600, + ), ), ), Expanded( @@ -119,10 +120,10 @@ class _AdminFeedbackRouteState extends State { t.admin_feedback_headers_timestamp, textAlign: TextAlign.center, style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, - fontWeight: FontWeight.w600), - ) + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackRoute.headerFontSize, + fontWeight: FontWeight.w600, + ), ), ), ], @@ -135,23 +136,13 @@ class _AdminFeedbackRouteState extends State { for (var feedback in sortedFeedbacks) ...[ AdminFeedbackItem(feedbackId: feedback.id), Spacing.medium(), - ] + ], ], ), ), ], ), ), - falseWidget: (_) => Center( - child: Text( - t.admin_feedback_noFeedback, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, - fontWeight: FontWeight.w600), - textAlign: TextAlign.left, - ), - ), ); }); } diff --git a/lib/features/feedback/presentation/widgets/feedback_page.dart b/lib/features/feedback/presentation/widgets/feedback_page.dart index 8df3cc69..154b7227 100644 --- a/lib/features/feedback/presentation/widgets/feedback_page.dart +++ b/lib/features/feedback/presentation/widgets/feedback_page.dart @@ -1,10 +1,14 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart' hide Feedback; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_vector_icons/flutter_vector_icons.dart'; import 'package:lb_planner/features/feedback/domain/domain.dart'; +import 'package:lb_planner/features/feedback/presentation/widgets/feedback.dart'; import 'package:lb_planner/features/themes/domain/models/module_status_theme.dart'; import 'package:lb_planner/shared/shared.dart'; import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; +@RoutePage() + /// Admin feedback page displaying details about feedback. class AdminFeedbackPageRoute extends StatefulWidget { /// Admin feedback page displaying details about feedback. @@ -21,80 +25,48 @@ class AdminFeedbackPageRoute extends StatefulWidget { class _AdminFeedbackPageRouteState extends State { final commentController = TextEditingController(); - bool _commenmtInit = false; + late int updated; + late int deleted; _updateFeedback(WidgetRef ref, Feedback feedback) async { final controller = ref.watch(feedbackController); - var feedbacks = ref.read(feedbackProvider); if (feedback.unread) { - setState(() { - _updateFuture = - controller.updateFeedbackStatus(feedback.id, FeedbackStatus.read); - }); - - var response = await _updateFuture; - - if (response!.failed) { - if (mounted) { - setState(() { - _updateFuture = null; - }); - } - - return; - } + controller.markFeedbackAsRead(feedback, comment: commentController.text); + updated = 1; } - setState(() { - _updateFuture = - controller.updateFeedbackComment(feedback.id, commentController.text); - }); - - var response = await _updateFuture; - - if (mounted) { - setState(() { - _updateFuture = null; - }); + updated = 0; - if (response!.succeeded) _pushNext(feedbacks, feedback); - } + _pushNext(feedback); } - _pushNext(Map feedbacks, Feedback feedback) { - var sorted = AdminFeedbackRoute.sortFeedbacks(feedbacks.values.toList()); - var currentIndex = sorted.indexOf(feedback); + _pushNext(BuildContext context, WidgetRef ref, feedback) { + var sorted = AdminFeedbackRoute.sortFeedbacks(ref.read(feedbackProvider)); + var currentIndex = sorted?.indexOf(feedback); - if (sorted.length - 1 > currentIndex && currentIndex >= 0) { + if (sorted!.length - 1 > currentIndex! && currentIndex >= 0) { var nextFeedback = sorted[currentIndex + 1]; - AdminFeedbackPageRoute.info - .push(context, params: {"id": nextFeedback.id}); + context.router.push(context, params: {"id": nextFeedback.id}); } else { AdminFeedbackRoute.info.push(context); } } - _deleteFeedback(WidgetRef ref, Feedback feedback) async { - if (_deleteFuture != null) return; - - setState(() { - _deleteFuture = ref.read(feedbackController).deleteFeedback(feedback.id); - }); - - var response = await _deleteFuture; + _deleteFeedback( + BuildContext context, WidgetRef ref, Feedback feedbackToDelete) async { + ref.read(feedbackController).deleteFeedback(feedbackToDelete); + deleted = 1; - if (mounted) { - setState(() { - _deleteFuture = null; - }); + deleted = 0; - if (response!.succeeded) _pushNext(ref.read(feedbackProvider), feedback); - } + _pushNext(context, ref, ref.read(feedbackProvider)); } @override Widget build(BuildContext context) { + bool _commenmtInit = false; + return Consumer( builder: (context, ref, _) { int feedbackId = widget.feedbackId; @@ -117,7 +89,7 @@ class _AdminFeedbackPageRouteState extends State { title: feedback.type.title(context), leading: Icon(feedback.type.icon, color: feedback.type.color), trailing: ConditionalWidget( - condition: _deleteFuture == null, + condition: deleted == 0, trueWidget: (_) => HoverBuilder( builder: (context, hover) => Icon( Icons.delete, @@ -125,7 +97,7 @@ class _AdminFeedbackPageRouteState extends State { ? context.theme.colorScheme.error : ModuleStatusTheme.of(context).pendingColor, ), - onTap: () => lpShowConfirmDialog( + onTap: () => ShowConfirmDialog( context, title: t.admin_feedback_page_deleteTitle, message: t.admin_feedback_page_deleteText, @@ -209,12 +181,12 @@ class _AdminFeedbackPageRouteState extends State { Align( alignment: Alignment.centerRight, child: ConditionalWidget( - condition: _updateFuture != null, + condition: updated != 0, trueWidget: (_) => Row( mainAxisSize: MainAxisSize.min, children: [ Text( - feedback.status.isRead + feedback.read ? t.admin_feedback_page_update : t.admin_feedback_page_markRead, style: TextStyle( @@ -229,7 +201,7 @@ class _AdminFeedbackPageRouteState extends State { ], ), falseWidget: (context) => LpTextButton( - text: feedback.status.isRead + text: feedback.read ? t.admin_feedback_page_update : t.admin_feedback_page_markRead, fontSize: AdminFeedbackItem.fontSize, diff --git a/lib/shared/presentation/widgets/dialog.dart b/lib/shared/presentation/widgets/dialog.dart new file mode 100644 index 00000000..abc4a758 --- /dev/null +++ b/lib/shared/presentation/widgets/dialog.dart @@ -0,0 +1,338 @@ +import 'package:flutter/material.dart'; +import 'package:lb_planner/shared/shared.dart'; + +/// Themed [Dialog] widget. +class LpDialog extends StatefulWidget { + /// Themed ConfirmDialog widget. + LpDialog.confirm({ + Key? key, + required this.header, + required this.body, + this.confirmText, + this.cancelText, + required this.onConfirm, + this.onCancel, + required this.removeFromWidgetTree, + this.confirmIsBad = true, + required this.scrollable, + }) : super(key: key) { + confirmOnly = false; + } + + /// Themed [AlertDialog] widget with just one button. + LpDialog.alert({ + Key? key, + required this.header, + required this.body, + this.onConfirm, + this.confirmText, + required this.removeFromWidgetTree, + required this.scrollable, + }) : super(key: key) { + confirmIsBad = false; + confirmOnly = true; + } + + /// The header of the dialog. + final Widget? header; + + /// The body of the dialog. + final Widget body; + + /// Whether the confirm button should have [errorColor] as it's background color. + late final bool confirmIsBad; + + /// The text of the confirm button. + final String? confirmText; + + /// The text of the cancel button. + late final String? cancelText; + + /// The boolean that determines if the dialog has just one button. + late final bool confirmOnly; + + /// Whether the dialog body should be scrollable. + final bool scrollable; + + /// The callback that is called when the user confirms the dialog. + final Function()? onConfirm; + + /// Called when the dialog has to be removed from the widget tree. + final VoidCallback removeFromWidgetTree; + + /// The callback that is called when the user cancels the dialog. + late final Function()? onCancel; + + /// The padding between the elements in the dialog. + static const double padding = 20; + + /// The width factor of the body in the dialog. + static const double widthFactor = .5; + + /// The height factor of the body in the dialog. + static const double heightFactor = .8; + + /// The font size of the buttons in the dialog. + static const double btnFontSize = 16; + + /// The padding of the buttons in the dialog. + static const double btnPadding = 14; + + /// The font size of the [title]. + static const double titleFontSize = 30; + + @override + State createState() => _LpDialogState(); +} + +class _LpDialogState extends State with TickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + _controller = + AnimationController(vsync: this, duration: kNormalAnimationDuration); + _controller.forward(); + super.initState(); + focusNode.addListener(_ensureFocus); + + focusNode.requestFocus(); + } + + Future close() async { + await _controller.reverse(); + widget.removeFromWidgetTree(); + } + + void _ensureFocus() async { + if (focusNode.hasFocus) return; + + await Future.delayed(kNormalAnimationDuration); + + if (!mounted) return; + + focusNode.requestFocus(); + } + + final focusNode = FocusNode(skipTraversal: true); + + @override + Widget build(BuildContext context) { + return RawKeyboardListener( + autofocus: true, + focusNode: focusNode, + onKey: (event) { + if (event.logicalKey == LogicalKeyboardKey.escape && + focusNode.hasPrimaryFocus) { + close(); + } + }, + child: FadeTransition( + // ignore: no-magic-number + opacity: Tween(begin: 0.4, end: 1).animate( + CurvedAnimation( + parent: _controller, + curve: Interval(0.0, 0.5), + ), + ), + child: ScaleTransition( + child: AlertDialog( + title: widget.header, + titlePadding: EdgeInsets.all(LpDialog.padding), + buttonPadding: EdgeInsets.only( + left: LpDialog.padding, right: LpDialog.padding), + contentPadding: EdgeInsets.only( + bottom: LpDialog.padding, + left: LpDialog.padding, + right: LpDialog.padding), + content: ConstrainedBox( + constraints: BoxConstraints( + minWidth: + MediaQuery.of(context).size.width * LpDialog.widthFactor, + maxHeight: + MediaQuery.of(context).size.height * LpDialog.heightFactor, + maxWidth: + MediaQuery.of(context).size.width * LpDialog.widthFactor, + ), + child: AnimatedSize( + duration: kNormalAnimationDuration, + curve: kAnimationCurve, + child: ConditionalWrapper( + condition: widget.scrollable, + wrapper: (context, child) => SingleChildScrollView( + controller: ScrollController(), + child: GestureDetector( + onTap: () => + FocusManager.instance.primaryFocus?.unfocus(), + child: child, + ), + ), + child: widget.body, + ), + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!widget.confirmOnly) + LpButton( + text: widget.cancelText ?? context.t.dialog_cancel, + color: widget.confirmIsBad ? accentColor : errorColor, + fontSize: LpDialog.btnFontSize, + padding: LpDialog.btnPadding, + onPressed: () async { + await close(); + widget.onCancel?.call(); + }, + ), + NcSpacing.medium(), + LpButton( + text: widget.confirmText ?? + (widget.confirmOnly + ? context.t.alertDialog_confirm + : context.t.dialog_confirm), + color: widget.confirmIsBad ? errorColor : accentColor, + fontSize: LpDialog.btnFontSize, + padding: LpDialog.btnPadding, + onPressed: () async { + await close(); + widget.onConfirm?.call(); + }, + ), + ], + ) + ], + backgroundColor: NcThemes.current.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(kRadius)), + ), + ), + // ignore: no-magic-number + scale: Tween(begin: 1, end: 0.85).animate( + CurvedAnimation( + parent: _controller, + curve: Interval(0.0, 0.5, curve: kDialogAnimationCurve), + ), + ), + ), + ), + ); + } +} + +/// Themed ConfirmDialog widget. +void lpShowConfirmDialog( + BuildContext context, { + String? title, + Widget? header, + Widget? body, + String? confirmText, + String? cancelText, + Function()? onConfirm, + Function()? onCancel, + String? message, + bool confirmIsBad = true, + bool scrollable = true, +}) { + assert(body != null || message != null, + 'Either body or message must be provided.'); + assert(header != null || title != null, + 'Either header or title must be provided.'); + + var key = GlobalKey<_LpDialogState>(); + + OverlayEntry? dialogOverLay; + OverlayEntry background = + _generateBackground(() => key.currentState!.close()); + + var dialog = LpDialog.confirm( + key: key, + header: header ?? NcTitleText(title!, fontSize: LpDialog.titleFontSize), + body: ConditionalWidget( + condition: body != null, + trueWidget: (_) => body!, + falseWidget: (_) => NcCaptionText( + message!, + overflow: TextOverflow.visible, + ), + ), + confirmText: confirmText, + cancelText: cancelText, + onConfirm: onConfirm, + onCancel: onCancel, + confirmIsBad: confirmIsBad, + scrollable: scrollable, + removeFromWidgetTree: () { + dialogOverLay!.remove(); + background.remove(); + }, + ); + + dialogOverLay = OverlayEntry( + builder: (context) => dialog, + ); + + Overlay.of(context)!.insert(background); + Overlay.of(context)!.insert(dialogOverLay); +} + +/// Themed [AlertDialog] widget. +void lpShowAlertDialog( + BuildContext context, { + String? title, + Widget? header, + Widget? body, + String? message, + String? confirmText, + Function()? onConfirm, + bool scrollable = true, +}) { + assert(body != null || message != null, + 'Either body or message must be provided.'); + assert(header != null || title != null, + 'Either header or title must be provided.'); + var key = GlobalKey<_LpDialogState>(); + + OverlayEntry? dialogOverLay; + OverlayEntry background = + _generateBackground(() => key.currentState!.close()); + + var dialog = LpDialog.alert( + key: key, + header: header ?? NcTitleText(title!, fontSize: LpDialog.titleFontSize), + body: ConditionalWidget( + condition: body != null, + trueWidget: (_) => body!, + falseWidget: (_) => NcCaptionText( + message!, + overflow: TextOverflow.visible, + ), + ), + onConfirm: onConfirm, + confirmText: confirmText, + scrollable: scrollable, + removeFromWidgetTree: () { + dialogOverLay!.remove(); + background.remove(); + }, + ); + + dialogOverLay = OverlayEntry( + builder: (context) => dialog, + ); + + Overlay.of(context)!.insert(background); + Overlay.of(context)!.insert(dialogOverLay); +} + +OverlayEntry _generateBackground(Function() dismiss) { + return OverlayEntry( + builder: (context) => GestureDetector( + onTap: dismiss, + child: Container( + color: Colors.black38, + ), + ), + ); +} diff --git a/lib/shared/presentation/widgets/text_field.dart b/lib/shared/presentation/widgets/text_field.dart new file mode 100644 index 00000000..d7889e9e --- /dev/null +++ b/lib/shared/presentation/widgets/text_field.dart @@ -0,0 +1,232 @@ +/// Themed [TextField] widget. +class LpTextField extends StatefulWidget { + /// Themed [TextField] widget. + LpTextField({ + Key? key, + this.controller, + this.focusNode, + this.prefixIcon, + this.suffixIcon, + this.prefix, + this.suffix, + this.placeholder, + this.errorText, + this.helperText, + this.enabled = true, + this.onSubmitted, + this.obscureText = false, + this.autoFocus = false, + this.maxLength, + this.fontSize = defaultFontSize, + this.onCancel, + this.onUnfocus, + this.textAlign = TextAlign.start, + this.readOnly = false, + }) : super(key: key) { + filled = false; + multiline = false; + fillColor = null; + } + + /// Themed [TextField] widget. + LpTextField.filled({ + Key? key, + this.controller, + this.focusNode, + this.prefixIcon, + this.suffixIcon, + this.prefix, + this.suffix, + this.placeholder, + this.errorText, + this.helperText, + this.enabled = true, + this.onSubmitted, + this.obscureText = false, + this.autoFocus = false, + this.fontSize = defaultFontSize, + this.multiline = false, + this.maxLength, + this.onCancel, + Color? fillColor, + this.onUnfocus, + this.textAlign = TextAlign.start, + this.readOnly = false, + }) : super(key: key) { + filled = true; + this.fillColor = fillColor ?? secondaryColor; + } + + /// Whether the [TextField] is read only. + final bool readOnly; + + /// The controller of the [TextField]. + final TextEditingController? controller; + + /// The focus node of the [TextField]. + final FocusNode? focusNode; + + /// Called when the [TextField] is looses focus. + final VoidCallback? onUnfocus; + + /// The maximum number of characters (Unicode scalar values) to allow in the [TextField]. + final int? maxLength; + + /// Called when the user presses the [PhysicalKeyboardKey.escape] key on the keyboard. + final VoidCallback? onCancel; + + /// The prefix icon of the [TextField]. + final IconData? prefixIcon; + + /// The suffix icon of the [TextField]. + final IconData? suffixIcon; + + /// The prefix of the [TextField]. + final Widget? prefix; + + /// The suffix of the [TextField]. + final Widget? suffix; + + /// The placeholder of the [TextField]. + final String? placeholder; + + /// The error text of the [TextField]. + final String? errorText; + + /// The helper text of the [TextField]. + final String? helperText; + + /// Whether the [TextField] is enabled. + final bool enabled; + + /// [TextField.textAlign] + final TextAlign textAlign; + + /// The function to call when the [TextField] is submitted. + final Function(String)? onSubmitted; + + /// Whether the [TextField] is obscured. + final bool obscureText; + + /// Whether the [TextField] should auto focus. + final bool autoFocus; + + /// The font size of the [TextField]. + final double fontSize; + + /// Whether the [TextField] should be mulitline. + late final bool multiline; + + /// Whether the [TextField] should be filled. + late final bool filled; + + /// The fill color of the [TextField]. + late final Color? fillColor; + + /// Font size of any feedback text like errorText + static const double feedbackFontSize = 15; + + /// [InputDecoration.contentPadding] + static const double filledPadding = 12; + + /// The width of the [InputBorder] + static const double borderWidth = 2; + + /// The default [fontSize]. + static const double defaultFontSize = 20; + + /// Generates an [InputBorder] with the given [color]. + static InputBorder? border(Color color, bool filled) { + return filled + ? OutlineInputBorder( + borderRadius: BorderRadius.circular(kRadius), + borderSide: BorderSide(color: Colors.transparent), + ) + : UnderlineInputBorder( + // borderRadius: BorderRadius.all(Radius.circular(kRadius)), + borderSide: BorderSide(color: color, width: borderWidth), + ); + } + + @override + State createState() => _LpTextFieldState(); +} + +class _LpTextFieldState extends State { + late FocusNode focusNode = widget.focusNode ?? FocusNode(); + + @override + void initState() { + focusNode.addListener(() { + if (!focusNode.hasFocus) widget.onUnfocus?.call(); + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return KeyboardListener( + focusNode: FocusNode(skipTraversal: true), + onKeyEvent: (event) { + if (event.physicalKey == PhysicalKeyboardKey.escape) { + widget.onCancel?.call(); + focusNode.unfocus(); + } + }, + child: TextField( + readOnly: widget.readOnly, + autofocus: widget.autoFocus, + obscureText: widget.obscureText, + onSubmitted: widget.onSubmitted, + toolbarOptions: + ToolbarOptions(copy: true, cut: true, paste: true, selectAll: true), + keyboardType: widget.multiline ? TextInputType.multiline : null, + maxLines: widget.multiline ? null : 1, + expands: widget.multiline, + maxLength: widget.maxLength, + maxLengthEnforcement: + widget.maxLength != null ? MaxLengthEnforcement.enforced : null, + textAlignVertical: + widget.multiline ? TextAlignVertical.top : TextAlignVertical.center, + textAlign: widget.textAlign, + decoration: InputDecoration( + contentPadding: widget.filled + ? EdgeInsets.all(LpTextField.filledPadding) + : EdgeInsets.symmetric(vertical: NcSpacing.smallSpacing), + isDense: true, + filled: widget.filled, + fillColor: widget.fillColor, + prefix: widget.prefix, + prefixIcon: widget.prefixIcon != null + ? LpIcon(widget.prefixIcon, size: widget.fontSize) + : null, + suffix: widget.suffix, + suffixIcon: widget.suffixIcon != null + ? LpIcon(widget.suffixIcon, size: widget.fontSize) + : null, + hintText: widget.placeholder, + hintStyle: NcBaseText.style(fontSize: widget.fontSize), + border: LpTextField.border(accentColor, widget.filled), + errorBorder: LpTextField.border(errorColor, widget.filled), + errorStyle: NcBaseText.style( + color: errorColor, fontSize: LpTextField.feedbackFontSize), + helperStyle: NcBaseText.style( + color: textColor, fontSize: LpTextField.feedbackFontSize), + enabledBorder: LpTextField.border(textColor, widget.filled), + disabledBorder: LpTextField.border(tertiaryColor, widget.filled), + focusedErrorBorder: LpTextField.border(accentColor, widget.filled), + errorText: widget.errorText, + helperText: widget.helperText, + enabled: widget.enabled, + focusedBorder: LpTextField.border(accentColor, widget.filled), + focusColor: widget.fillColor, + hoverColor: widget.fillColor, + ), + style: NcBaseText.style(fontSize: widget.fontSize), + controller: widget.controller, + focusNode: focusNode, + ), + ); + } +} diff --git a/lib/shared/presentation/widgets/textbutton.dart b/lib/shared/presentation/widgets/textbutton.dart deleted file mode 100644 index 91c97258..00000000 --- a/lib/shared/presentation/widgets/textbutton.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lb_planner/shared/shared.dart'; - -/// The [LpTextButton] widget is a button that displays clickable text and an icon. -class LpTextButton extends StatelessWidget { - /// The [LpTextButton] widget is a button that displays clickable text and an icon. - const LpTextButton({ - Key? key, - required this.text, - this.onPressed, - this.leadingIcon, - this.trailingIcon, - this.fontSize, - this.color, - this.decoration = TextDecoration.none, - this.leading, - this.trailing, - }) : super(key: key); - - /// The text the button displays. - final String text; - - /// The function to call when the button is pressed. - final VoidCallback? onPressed; - - /// The icon to display on the leading side of the button. - final IconData? leadingIcon; - - /// The icon to display on the trailing side of the button. - final IconData? trailingIcon; - - /// The leading widget to display on the button. This is ignored if [leadingIcon] is not null. - final Widget? leading; - - /// The trailing widget to display on the button. This is ignored if [trailingIcon] is not null. - final Widget? trailing; - - /// The font size of the button text. - final double? fontSize; - - /// The color of the button text. - final Color? color; - - /// The text decoration of the button text. - final TextDecoration decoration; - - @override - Widget build(BuildContext context) { - return TextButton( - style: TextButton.styleFrom( - foregroundColor: context.theme.colorScheme.primary, - ), - onPressed: onPressed, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (leadingIcon != null) - LpIcon(leadingIcon, size: (fontSize * 1.2), color: color), - if (leading != null && leadingIcon == null) leading!, - if (leadingIcon != null || leading != null) NcSpacing.small(), - NcCaptionText(text, - fontSize: fontSize, color: color, decoration: decoration), - if (trailingIcon != null || trailing != null) NcSpacing.small(), - if (trailing != null && trailingIcon == null) trailing!, - if (trailingIcon != null) - LpIcon(trailingIcon, size: scaleIcon(fontSize), color: color), - ], - ), - ); - } -} From db7d3eae89bf6c7674ada505d360f2984f7f3227 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:40:48 +0100 Subject: [PATCH 07/18] push push push push push push push push push push push push push push push push push push --- .../{widgets => screens}/feedback_page.dart | 33 +-- .../presentation/widgets/widgets.dart | 2 +- lib/shared/presentation/widgets/dialog.dart | 1 + lib/shared/presentation/widgets/spacing.dart | 2 +- .../presentation/widgets/text_field.dart | 232 ------------------ lib/shared/presentation/widgets/widgets.dart | 3 + 6 files changed, 25 insertions(+), 248 deletions(-) rename lib/features/feedback/presentation/{widgets => screens}/feedback_page.dart (87%) delete mode 100644 lib/shared/presentation/widgets/text_field.dart diff --git a/lib/features/feedback/presentation/widgets/feedback_page.dart b/lib/features/feedback/presentation/screens/feedback_page.dart similarity index 87% rename from lib/features/feedback/presentation/widgets/feedback_page.dart rename to lib/features/feedback/presentation/screens/feedback_page.dart index 154b7227..135bcba4 100644 --- a/lib/features/feedback/presentation/widgets/feedback_page.dart +++ b/lib/features/feedback/presentation/screens/feedback_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart' hide Feedback; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_vector_icons/flutter_vector_icons.dart'; +import 'package:lb_planner/features/auth/domain/domain.dart'; import 'package:lb_planner/features/feedback/domain/domain.dart'; import 'package:lb_planner/features/feedback/presentation/widgets/feedback.dart'; import 'package:lb_planner/features/themes/domain/models/module_status_theme.dart'; @@ -47,7 +48,8 @@ class _AdminFeedbackPageRouteState extends State { if (sorted!.length - 1 > currentIndex! && currentIndex >= 0) { var nextFeedback = sorted[currentIndex + 1]; - context.router.push(context, params: {"id": nextFeedback.id}); + context.router.push(AdminFeedbackPageRoute( + feedbackId: nextFeedback.id, key: ValueKey(nextFeedback.id))); } else { AdminFeedbackRoute.info.push(context); } @@ -115,7 +117,7 @@ class _AdminFeedbackPageRouteState extends State { FeedbackStatusTag(read: feedback.read), Spacing.small(), Text( - t.admin_feedback_page_author(user.fullname), + t.admin_feedback_page_author(user), style: TextStyle( fontWeight: FontWeight.w600, overflow: TextOverflow.ellipsis, @@ -123,9 +125,9 @@ class _AdminFeedbackPageRouteState extends State { ), textAlign: TextAlign.left, ), - if (feedback.type.isBug) + if (feedback.type == FeedbackType.bug) Text( - t.admin_feedback_page_logFile(feedback.logFile), + t.admin_feedback_page_logFile(feedback.logfile!), style: TextStyle( fontWeight: FontWeight.w600, overflow: TextOverflow.ellipsis, @@ -171,11 +173,13 @@ class _AdminFeedbackPageRouteState extends State { ), Spacing.large(), Expanded( - child: LpTextField.filled( - multiline: true, - controller: commentController, - placeholder: t.admin_feedback_page_comment, - ), + child: TextField( + keyboardType: TextInputType.multiline, + controller: commentController, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: t.admin_feedback_page_comment, + )), ), Spacing.large(), Align( @@ -200,13 +204,14 @@ class _AdminFeedbackPageRouteState extends State { CircularProgressIndicator(), ], ), - falseWidget: (context) => LpTextButton( - text: feedback.read + falseWidget: (context) => TextButton.icon( + label: Text(feedback.read ? t.admin_feedback_page_update - : t.admin_feedback_page_markRead, - fontSize: AdminFeedbackItem.fontSize, - trailingIcon: Feather.arrow_right_circle, + : t.admin_feedback_page_markRead), onPressed: () => _updateFeedback(ref, feedback), + icon: Icon(Feather.arrow_right_circle, + size: AdminFeedbackItem.fontSize * 1.2, + color: context.theme.colorScheme.primary), ), ), ), diff --git a/lib/features/feedback/presentation/widgets/widgets.dart b/lib/features/feedback/presentation/widgets/widgets.dart index e55f208f..9e385542 100644 --- a/lib/features/feedback/presentation/widgets/widgets.dart +++ b/lib/features/feedback/presentation/widgets/widgets.dart @@ -1,3 +1,3 @@ -export 'feedback_page.dart'; +export '../screens/feedback_page.dart'; export 'feedback_item.dart'; export 'feedback_status_tag.dart'; diff --git a/lib/shared/presentation/widgets/dialog.dart b/lib/shared/presentation/widgets/dialog.dart index abc4a758..15415ab0 100644 --- a/lib/shared/presentation/widgets/dialog.dart +++ b/lib/shared/presentation/widgets/dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lb_planner/shared/shared.dart'; /// Themed [Dialog] widget. diff --git a/lib/shared/presentation/widgets/spacing.dart b/lib/shared/presentation/widgets/spacing.dart index b3d86368..2625ef36 100644 --- a/lib/shared/presentation/widgets/spacing.dart +++ b/lib/shared/presentation/widgets/spacing.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; /// Allows you to add spacing or padding in both the vertical and horizontal directions, /// or separately in either the vertical or horizontal direction. diff --git a/lib/shared/presentation/widgets/text_field.dart b/lib/shared/presentation/widgets/text_field.dart deleted file mode 100644 index d7889e9e..00000000 --- a/lib/shared/presentation/widgets/text_field.dart +++ /dev/null @@ -1,232 +0,0 @@ -/// Themed [TextField] widget. -class LpTextField extends StatefulWidget { - /// Themed [TextField] widget. - LpTextField({ - Key? key, - this.controller, - this.focusNode, - this.prefixIcon, - this.suffixIcon, - this.prefix, - this.suffix, - this.placeholder, - this.errorText, - this.helperText, - this.enabled = true, - this.onSubmitted, - this.obscureText = false, - this.autoFocus = false, - this.maxLength, - this.fontSize = defaultFontSize, - this.onCancel, - this.onUnfocus, - this.textAlign = TextAlign.start, - this.readOnly = false, - }) : super(key: key) { - filled = false; - multiline = false; - fillColor = null; - } - - /// Themed [TextField] widget. - LpTextField.filled({ - Key? key, - this.controller, - this.focusNode, - this.prefixIcon, - this.suffixIcon, - this.prefix, - this.suffix, - this.placeholder, - this.errorText, - this.helperText, - this.enabled = true, - this.onSubmitted, - this.obscureText = false, - this.autoFocus = false, - this.fontSize = defaultFontSize, - this.multiline = false, - this.maxLength, - this.onCancel, - Color? fillColor, - this.onUnfocus, - this.textAlign = TextAlign.start, - this.readOnly = false, - }) : super(key: key) { - filled = true; - this.fillColor = fillColor ?? secondaryColor; - } - - /// Whether the [TextField] is read only. - final bool readOnly; - - /// The controller of the [TextField]. - final TextEditingController? controller; - - /// The focus node of the [TextField]. - final FocusNode? focusNode; - - /// Called when the [TextField] is looses focus. - final VoidCallback? onUnfocus; - - /// The maximum number of characters (Unicode scalar values) to allow in the [TextField]. - final int? maxLength; - - /// Called when the user presses the [PhysicalKeyboardKey.escape] key on the keyboard. - final VoidCallback? onCancel; - - /// The prefix icon of the [TextField]. - final IconData? prefixIcon; - - /// The suffix icon of the [TextField]. - final IconData? suffixIcon; - - /// The prefix of the [TextField]. - final Widget? prefix; - - /// The suffix of the [TextField]. - final Widget? suffix; - - /// The placeholder of the [TextField]. - final String? placeholder; - - /// The error text of the [TextField]. - final String? errorText; - - /// The helper text of the [TextField]. - final String? helperText; - - /// Whether the [TextField] is enabled. - final bool enabled; - - /// [TextField.textAlign] - final TextAlign textAlign; - - /// The function to call when the [TextField] is submitted. - final Function(String)? onSubmitted; - - /// Whether the [TextField] is obscured. - final bool obscureText; - - /// Whether the [TextField] should auto focus. - final bool autoFocus; - - /// The font size of the [TextField]. - final double fontSize; - - /// Whether the [TextField] should be mulitline. - late final bool multiline; - - /// Whether the [TextField] should be filled. - late final bool filled; - - /// The fill color of the [TextField]. - late final Color? fillColor; - - /// Font size of any feedback text like errorText - static const double feedbackFontSize = 15; - - /// [InputDecoration.contentPadding] - static const double filledPadding = 12; - - /// The width of the [InputBorder] - static const double borderWidth = 2; - - /// The default [fontSize]. - static const double defaultFontSize = 20; - - /// Generates an [InputBorder] with the given [color]. - static InputBorder? border(Color color, bool filled) { - return filled - ? OutlineInputBorder( - borderRadius: BorderRadius.circular(kRadius), - borderSide: BorderSide(color: Colors.transparent), - ) - : UnderlineInputBorder( - // borderRadius: BorderRadius.all(Radius.circular(kRadius)), - borderSide: BorderSide(color: color, width: borderWidth), - ); - } - - @override - State createState() => _LpTextFieldState(); -} - -class _LpTextFieldState extends State { - late FocusNode focusNode = widget.focusNode ?? FocusNode(); - - @override - void initState() { - focusNode.addListener(() { - if (!focusNode.hasFocus) widget.onUnfocus?.call(); - }); - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return KeyboardListener( - focusNode: FocusNode(skipTraversal: true), - onKeyEvent: (event) { - if (event.physicalKey == PhysicalKeyboardKey.escape) { - widget.onCancel?.call(); - focusNode.unfocus(); - } - }, - child: TextField( - readOnly: widget.readOnly, - autofocus: widget.autoFocus, - obscureText: widget.obscureText, - onSubmitted: widget.onSubmitted, - toolbarOptions: - ToolbarOptions(copy: true, cut: true, paste: true, selectAll: true), - keyboardType: widget.multiline ? TextInputType.multiline : null, - maxLines: widget.multiline ? null : 1, - expands: widget.multiline, - maxLength: widget.maxLength, - maxLengthEnforcement: - widget.maxLength != null ? MaxLengthEnforcement.enforced : null, - textAlignVertical: - widget.multiline ? TextAlignVertical.top : TextAlignVertical.center, - textAlign: widget.textAlign, - decoration: InputDecoration( - contentPadding: widget.filled - ? EdgeInsets.all(LpTextField.filledPadding) - : EdgeInsets.symmetric(vertical: NcSpacing.smallSpacing), - isDense: true, - filled: widget.filled, - fillColor: widget.fillColor, - prefix: widget.prefix, - prefixIcon: widget.prefixIcon != null - ? LpIcon(widget.prefixIcon, size: widget.fontSize) - : null, - suffix: widget.suffix, - suffixIcon: widget.suffixIcon != null - ? LpIcon(widget.suffixIcon, size: widget.fontSize) - : null, - hintText: widget.placeholder, - hintStyle: NcBaseText.style(fontSize: widget.fontSize), - border: LpTextField.border(accentColor, widget.filled), - errorBorder: LpTextField.border(errorColor, widget.filled), - errorStyle: NcBaseText.style( - color: errorColor, fontSize: LpTextField.feedbackFontSize), - helperStyle: NcBaseText.style( - color: textColor, fontSize: LpTextField.feedbackFontSize), - enabledBorder: LpTextField.border(textColor, widget.filled), - disabledBorder: LpTextField.border(tertiaryColor, widget.filled), - focusedErrorBorder: LpTextField.border(accentColor, widget.filled), - errorText: widget.errorText, - helperText: widget.helperText, - enabled: widget.enabled, - focusedBorder: LpTextField.border(accentColor, widget.filled), - focusColor: widget.fillColor, - hoverColor: widget.fillColor, - ), - style: NcBaseText.style(fontSize: widget.fontSize), - controller: widget.controller, - focusNode: focusNode, - ), - ); - } -} diff --git a/lib/shared/presentation/widgets/widgets.dart b/lib/shared/presentation/widgets/widgets.dart index c86e77d7..1f4f68b4 100644 --- a/lib/shared/presentation/widgets/widgets.dart +++ b/lib/shared/presentation/widgets/widgets.dart @@ -15,3 +15,6 @@ export 'color_utils.dart'; export 'user_profile_img.dart'; export 'conditional_widget.dart'; export 'container.dart'; +export 'textbutton.dart'; +export 'text_field.dart'; +export 'dialog.dart'; From 1c48bb5694301e5da4697b79136c5c37abcdfe67 Mon Sep 17 00:00:00 2001 From: Can42 Date: Mon, 5 Feb 2024 17:19:31 +0100 Subject: [PATCH 08/18] Update feedback_page.dart --- .../feedback/presentation/screens/feedback_page.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/features/feedback/presentation/screens/feedback_page.dart b/lib/features/feedback/presentation/screens/feedback_page.dart index 135bcba4..0c7a322a 100644 --- a/lib/features/feedback/presentation/screens/feedback_page.dart +++ b/lib/features/feedback/presentation/screens/feedback_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart' hide Feedback; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_vector_icons/flutter_vector_icons.dart'; -import 'package:lb_planner/features/auth/domain/domain.dart'; import 'package:lb_planner/features/feedback/domain/domain.dart'; import 'package:lb_planner/features/feedback/presentation/widgets/feedback.dart'; import 'package:lb_planner/features/themes/domain/models/module_status_theme.dart'; @@ -39,7 +38,7 @@ class _AdminFeedbackPageRouteState extends State { updated = 0; - _pushNext(feedback); + _pushNext(context, ref, feedback); } _pushNext(BuildContext context, WidgetRef ref, feedback) { @@ -104,7 +103,7 @@ class _AdminFeedbackPageRouteState extends State { title: t.admin_feedback_page_deleteTitle, message: t.admin_feedback_page_deleteText, confirmIsBad: true, - onConfirm: () => _deleteFeedback(ref, feedback), + onConfirm: () => _deleteFeedback(context, ref, feedback), ), ), falseWidget: (_) => CircularProgressIndicator( From ac0751a6b7620cdb8d4c0e3e23afc136b5b3d0b3 Mon Sep 17 00:00:00 2001 From: Can42 Date: Thu, 22 Feb 2024 20:27:50 +0100 Subject: [PATCH 09/18] Update dialog.dart --- lib/shared/presentation/widgets/dialog.dart | 148 +++++++++++--------- 1 file changed, 84 insertions(+), 64 deletions(-) diff --git a/lib/shared/presentation/widgets/dialog.dart b/lib/shared/presentation/widgets/dialog.dart index 15415ab0..980c075b 100644 --- a/lib/shared/presentation/widgets/dialog.dart +++ b/lib/shared/presentation/widgets/dialog.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lb_planner/shared/shared.dart'; -/// Themed [Dialog] widget. -class LpDialog extends StatefulWidget { - /// Themed ConfirmDialog widget. - LpDialog.confirm({ +/// Shows custom dialogs +class Dialog extends StatefulWidget { + /// Creates the structure of the confirm dialog + Dialog.confirm({ Key? key, required this.header, required this.body, @@ -20,8 +20,8 @@ class LpDialog extends StatefulWidget { confirmOnly = false; } - /// Themed [AlertDialog] widget with just one button. - LpDialog.alert({ + /// Creates the structure of the alert dialog + Dialog.alert({ Key? key, required this.header, required this.body, @@ -40,7 +40,7 @@ class LpDialog extends StatefulWidget { /// The body of the dialog. final Widget body; - /// Whether the confirm button should have [errorColor] as it's background color. + /// Whether the confirm button should have errorColor as it's background color. late final bool confirmIsBad; /// The text of the confirm button. @@ -79,20 +79,20 @@ class LpDialog extends StatefulWidget { /// The padding of the buttons in the dialog. static const double btnPadding = 14; - /// The font size of the [title]. + /// The font size of the title. static const double titleFontSize = 30; @override - State createState() => _LpDialogState(); + State createState() => _DialogState(); } -class _LpDialogState extends State with TickerProviderStateMixin { +class _DialogState extends State with TickerProviderStateMixin { late AnimationController _controller; @override void initState() { _controller = - AnimationController(vsync: this, duration: kNormalAnimationDuration); + AnimationController(vsync: this, duration: Duration(milliseconds: 300)); _controller.forward(); super.initState(); focusNode.addListener(_ensureFocus); @@ -100,15 +100,17 @@ class _LpDialogState extends State with TickerProviderStateMixin { focusNode.requestFocus(); } + /// Removes the widget from the widget tree. Future close() async { await _controller.reverse(); widget.removeFromWidgetTree(); } + ///Ensures focus if the focusNode doesn't have focus. void _ensureFocus() async { if (focusNode.hasFocus) return; - await Future.delayed(kNormalAnimationDuration); + await Future.delayed(Duration(milliseconds: 300)); if (!mounted) return; @@ -129,7 +131,6 @@ class _LpDialogState extends State with TickerProviderStateMixin { } }, child: FadeTransition( - // ignore: no-magic-number opacity: Tween(begin: 0.4, end: 1).animate( CurvedAnimation( parent: _controller, @@ -139,25 +140,25 @@ class _LpDialogState extends State with TickerProviderStateMixin { child: ScaleTransition( child: AlertDialog( title: widget.header, - titlePadding: EdgeInsets.all(LpDialog.padding), - buttonPadding: EdgeInsets.only( - left: LpDialog.padding, right: LpDialog.padding), + titlePadding: EdgeInsets.all(Dialog.padding), + buttonPadding: + EdgeInsets.only(left: Dialog.padding, right: Dialog.padding), contentPadding: EdgeInsets.only( - bottom: LpDialog.padding, - left: LpDialog.padding, - right: LpDialog.padding), + bottom: Dialog.padding, + left: Dialog.padding, + right: Dialog.padding), content: ConstrainedBox( constraints: BoxConstraints( minWidth: - MediaQuery.of(context).size.width * LpDialog.widthFactor, + MediaQuery.of(context).size.width * Dialog.widthFactor, maxHeight: - MediaQuery.of(context).size.height * LpDialog.heightFactor, + MediaQuery.of(context).size.height * Dialog.heightFactor, maxWidth: - MediaQuery.of(context).size.width * LpDialog.widthFactor, + MediaQuery.of(context).size.width * Dialog.widthFactor, ), child: AnimatedSize( - duration: kNormalAnimationDuration, - curve: kAnimationCurve, + duration: Duration(milliseconds: 300), + curve: Curves.easeOutCubic, child: ConditionalWrapper( condition: widget.scrollable, wrapper: (context, child) => SingleChildScrollView( @@ -177,25 +178,33 @@ class _LpDialogState extends State with TickerProviderStateMixin { mainAxisAlignment: MainAxisAlignment.end, children: [ if (!widget.confirmOnly) - LpButton( - text: widget.cancelText ?? context.t.dialog_cancel, - color: widget.confirmIsBad ? accentColor : errorColor, - fontSize: LpDialog.btnFontSize, - padding: LpDialog.btnPadding, + ElevatedButton( + child: Text(widget.cancelText ?? t.dialog_cancel), + style: ElevatedButton.styleFrom( + backgroundColor: widget.confirmIsBad + ? context.theme.colorScheme.primary + : context.theme.colorScheme.error, + textStyle: TextStyle(fontSize: Dialog.btnFontSize), + padding: EdgeInsets.all(Dialog.btnPadding), + ), onPressed: () async { await close(); widget.onCancel?.call(); }, ), - NcSpacing.medium(), - LpButton( - text: widget.confirmText ?? + Spacing.medium(), + ElevatedButton( + child: Text(widget.confirmText ?? (widget.confirmOnly - ? context.t.alertDialog_confirm - : context.t.dialog_confirm), - color: widget.confirmIsBad ? errorColor : accentColor, - fontSize: LpDialog.btnFontSize, - padding: LpDialog.btnPadding, + ? t.alertDialog_confirm + : t.dialog_confirm)), + style: ElevatedButton.styleFrom( + backgroundColor: widget.confirmIsBad + ? context.theme.colorScheme.error + : context.theme.colorScheme.primary, + textStyle: TextStyle(fontSize: Dialog.btnFontSize), + padding: EdgeInsets.all(Dialog.btnPadding), + ), onPressed: () async { await close(); widget.onConfirm?.call(); @@ -204,16 +213,16 @@ class _LpDialogState extends State with TickerProviderStateMixin { ], ) ], - backgroundColor: NcThemes.current.primaryColor, + backgroundColor: context.theme.colorScheme.surface, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(kRadius)), + borderRadius: BorderRadius.all(Radius.circular(5)), ), ), // ignore: no-magic-number scale: Tween(begin: 1, end: 0.85).animate( CurvedAnimation( parent: _controller, - curve: Interval(0.0, 0.5, curve: kDialogAnimationCurve), + curve: Interval(0.0, 0.5, curve: Curves.easeOut), ), ), ), @@ -222,8 +231,8 @@ class _LpDialogState extends State with TickerProviderStateMixin { } } -/// Themed ConfirmDialog widget. -void lpShowConfirmDialog( +/// Shows an confirm dialog +void showConfirmDialog( BuildContext context, { String? title, Widget? header, @@ -241,22 +250,27 @@ void lpShowConfirmDialog( assert(header != null || title != null, 'Either header or title must be provided.'); - var key = GlobalKey<_LpDialogState>(); + var key = GlobalKey<_DialogState>(); OverlayEntry? dialogOverLay; OverlayEntry background = _generateBackground(() => key.currentState!.close()); - var dialog = LpDialog.confirm( + var dialog = Dialog.confirm( key: key, - header: header ?? NcTitleText(title!, fontSize: LpDialog.titleFontSize), + header: header ?? + Text( + title!, + style: TextStyle(fontSize: Dialog.titleFontSize), + ), body: ConditionalWidget( condition: body != null, - trueWidget: (_) => body!, - falseWidget: (_) => NcCaptionText( - message!, - overflow: TextOverflow.visible, - ), + ifTrue: body!, + ifFalse: Text(message!, + style: TextStyle( + overflow: TextOverflow.visible, + fontSize: 12, + letterSpacing: 0.4)), ), confirmText: confirmText, cancelText: cancelText, @@ -274,12 +288,12 @@ void lpShowConfirmDialog( builder: (context) => dialog, ); - Overlay.of(context)!.insert(background); - Overlay.of(context)!.insert(dialogOverLay); + Overlay.of(context).insert(background); + Overlay.of(context).insert(dialogOverLay); } -/// Themed [AlertDialog] widget. -void lpShowAlertDialog( +/// Shows an alert dialog +void showAlertDialog( BuildContext context, { String? title, Widget? header, @@ -293,22 +307,27 @@ void lpShowAlertDialog( 'Either body or message must be provided.'); assert(header != null || title != null, 'Either header or title must be provided.'); - var key = GlobalKey<_LpDialogState>(); + var key = GlobalKey<_DialogState>(); OverlayEntry? dialogOverLay; OverlayEntry background = _generateBackground(() => key.currentState!.close()); - var dialog = LpDialog.alert( + var dialog = Dialog.alert( key: key, - header: header ?? NcTitleText(title!, fontSize: LpDialog.titleFontSize), + header: header ?? + Text( + title!, + style: TextStyle(fontSize: Dialog.titleFontSize), + ), body: ConditionalWidget( condition: body != null, - trueWidget: (_) => body!, - falseWidget: (_) => NcCaptionText( - message!, - overflow: TextOverflow.visible, - ), + ifTrue: body!, + ifFalse: Text(message!, + style: TextStyle( + overflow: TextOverflow.visible, + fontSize: 12, + letterSpacing: 0.4)), ), onConfirm: onConfirm, confirmText: confirmText, @@ -323,10 +342,11 @@ void lpShowAlertDialog( builder: (context) => dialog, ); - Overlay.of(context)!.insert(background); - Overlay.of(context)!.insert(dialogOverLay); + Overlay.of(context).insert(background); + Overlay.of(context).insert(dialogOverLay); } +/// generates a background overlay for dismissing the dialog OverlayEntry _generateBackground(Function() dismiss) { return OverlayEntry( builder: (context) => GestureDetector( From cc4a65d2d5d5d73ca75569e855f40f89e2edfb77 Mon Sep 17 00:00:00 2001 From: Can42 Date: Thu, 22 Feb 2024 20:44:32 +0100 Subject: [PATCH 10/18] issues fixxed --- .../presentation/screens/feedback_page.dart | 16 +++++++++++----- .../feedback/presentation/widgets/feedback.dart | 4 ++-- .../widgets/feedback_status_tag.dart | 4 ++-- .../widgets/conditional_widget.dart | 17 ++++++++--------- .../presentation/widgets/user_profile_img.dart | 4 ++-- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/lib/features/feedback/presentation/screens/feedback_page.dart b/lib/features/feedback/presentation/screens/feedback_page.dart index 0c7a322a..b44b1fd6 100644 --- a/lib/features/feedback/presentation/screens/feedback_page.dart +++ b/lib/features/feedback/presentation/screens/feedback_page.dart @@ -46,11 +46,17 @@ class _AdminFeedbackPageRouteState extends State { var currentIndex = sorted?.indexOf(feedback); if (sorted!.length - 1 > currentIndex! && currentIndex >= 0) { + // If there's a next item, navigate to the next feedback detail page var nextFeedback = sorted[currentIndex + 1]; context.router.push(AdminFeedbackPageRoute( - feedbackId: nextFeedback.id, key: ValueKey(nextFeedback.id))); + feedbackId: nextFeedback.id, + key: ValueKey(nextFeedback.id), + )); } else { + // If there's no next item, navigate to the admin feedback list page AdminFeedbackRoute.info.push(context); + // Assuming AdminFeedbackRoute.info is a valid way to navigate back + // to the admin feedback list. If not, adjust this part according to your setup. } } @@ -91,7 +97,7 @@ class _AdminFeedbackPageRouteState extends State { leading: Icon(feedback.type.icon, color: feedback.type.color), trailing: ConditionalWidget( condition: deleted == 0, - trueWidget: (_) => HoverBuilder( + ifTrue: HoverBuilder( builder: (context, hover) => Icon( Icons.delete, color: hover @@ -106,7 +112,7 @@ class _AdminFeedbackPageRouteState extends State { onConfirm: () => _deleteFeedback(context, ref, feedback), ), ), - falseWidget: (_) => CircularProgressIndicator( + ifFalse: CircularProgressIndicator( color: context.theme.colorScheme.error), ), child: Column( @@ -185,7 +191,7 @@ class _AdminFeedbackPageRouteState extends State { alignment: Alignment.centerRight, child: ConditionalWidget( condition: updated != 0, - trueWidget: (_) => Row( + ifTrue: Row( mainAxisSize: MainAxisSize.min, children: [ Text( @@ -203,7 +209,7 @@ class _AdminFeedbackPageRouteState extends State { CircularProgressIndicator(), ], ), - falseWidget: (context) => TextButton.icon( + ifFalse: TextButton.icon( label: Text(feedback.read ? t.admin_feedback_page_update : t.admin_feedback_page_markRead), diff --git a/lib/features/feedback/presentation/widgets/feedback.dart b/lib/features/feedback/presentation/widgets/feedback.dart index 696bc839..be423b97 100644 --- a/lib/features/feedback/presentation/widgets/feedback.dart +++ b/lib/features/feedback/presentation/widgets/feedback.dart @@ -43,7 +43,7 @@ class _AdminFeedbackRouteState extends State { var sortedFeedbacks = AdminFeedbackRoute.sortFeedbacks(feedbacks); return ConditionalWidget( - falseWidget: (_) => Center( + ifFalse: Center( child: Text( t.admin_feedback_noFeedback, style: TextStyle( @@ -55,7 +55,7 @@ class _AdminFeedbackRouteState extends State { ), ), condition: sortedFeedbacks!.isNotEmpty, - trueWidget: (_) => Align( + ifTrue: Align( alignment: Alignment.topLeft, child: Column( children: [ diff --git a/lib/features/feedback/presentation/widgets/feedback_status_tag.dart b/lib/features/feedback/presentation/widgets/feedback_status_tag.dart index 1c194810..4232be29 100644 --- a/lib/features/feedback/presentation/widgets/feedback_status_tag.dart +++ b/lib/features/feedback/presentation/widgets/feedback_status_tag.dart @@ -45,7 +45,7 @@ class _FeedbackStatusTagState extends State { ), child: ConditionalWidget( condition: widget.label, - trueWidget: (context) => Text( + ifTrue: Text( widget.read ? t.admin_feedback_status_read : t.admin_feedback_status_unread, @@ -56,7 +56,7 @@ class _FeedbackStatusTagState extends State { ), textAlign: TextAlign.center, ), - falseWidget: (context) => Text( + ifFalse: Text( widget.read ? t.admin_feedback_status_read : t.admin_feedback_status_unread, diff --git a/lib/shared/presentation/widgets/conditional_widget.dart b/lib/shared/presentation/widgets/conditional_widget.dart index 5395630d..453872a8 100644 --- a/lib/shared/presentation/widgets/conditional_widget.dart +++ b/lib/shared/presentation/widgets/conditional_widget.dart @@ -2,26 +2,25 @@ import 'package:flutter/material.dart'; /// Use this to conditionally swap between widgets. class ConditionalWidget extends StatelessWidget { - /// Constructs a new [ConditionalWidget] with the given [condition], [trueWidget] and [falseWidget]. - /// If [condition] is true, [trueWidget] will be used. - /// If [condition] is false, [falseWidget] will be used. + /// Constructs a new [ConditionalWidget] with the given [condition], [ifTrue] and [ifFalse]. + /// If [condition] is true, [ifTrue] will be used. + /// If [condition] is false, [ifFalse] will be used. const ConditionalWidget( {Key? key, required this.condition, - required this.trueWidget, - required this.falseWidget}) + required this.ifTrue, + required this.ifFalse}) : super(key: key); /// The condition to check. final bool condition; /// The widget to show if the condition is true. - final WidgetBuilder trueWidget; + final Widget ifTrue; /// The widget to show if the condition is false. - final WidgetBuilder falseWidget; + final Widget ifFalse; @override - Widget build(BuildContext context) => - condition ? trueWidget(context) : falseWidget(context); + Widget build(BuildContext context) => condition ? ifTrue : ifFalse; } diff --git a/lib/shared/presentation/widgets/user_profile_img.dart b/lib/shared/presentation/widgets/user_profile_img.dart index 015975a3..687fd840 100644 --- a/lib/shared/presentation/widgets/user_profile_img.dart +++ b/lib/shared/presentation/widgets/user_profile_img.dart @@ -38,8 +38,7 @@ class UserProfileImg extends StatelessWidget { return ClipOval( child: ConditionalWidget( condition: user != null && user.profileImageUrl.isNotEmpty, - falseWidget: (_) => ShimmerEffect(height: size, width: size), - trueWidget: (_) => CachedNetworkImage( + ifTrue: CachedNetworkImage( imageUrl: user!.profileImageUrl, width: size, height: size, @@ -51,6 +50,7 @@ class UserProfileImg extends StatelessWidget { ), cacheManager: cacheManager, ), + ifFalse: ShimmerEffect(height: size, width: size), ), ); }, From cb25ec6a968064060beb10dc78cba9f6fcbe61ef Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:30:39 +0100 Subject: [PATCH 11/18] some issues fixxed --- lib/app_router.dart | 4 +- lib/app_router.gr.dart | 48 +++++++++++++++++++ .../{widgets => screens}/feedback.dart | 24 +++++----- .../presentation/screens/feedback_page.dart | 21 ++++---- .../presentation/widgets/feedback_item.dart | 7 ++- lib/shared/presentation/widgets/widgets.dart | 2 - 6 files changed, 79 insertions(+), 27 deletions(-) rename lib/features/feedback/presentation/{widgets => screens}/feedback.dart (84%) diff --git a/lib/app_router.dart b/lib/app_router.dart index b3dab4c1..8152a426 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; +import 'package:lb_planner/features/feedback/presentation/screens/feedback_page.dart'; import 'package:lb_planner/features/themes/themes.dart'; import 'package:lb_planner/shared/shared.dart'; import 'package:auto_route/auto_route.dart'; @@ -109,7 +111,7 @@ class AppRouter extends _$AppRouter { path: '/theme-development', ), DefaultRoute( - page: LoginRoute.page, + page: AdminFeedbackRoute.page, path: '/login', ) ]; diff --git a/lib/app_router.gr.dart b/lib/app_router.gr.dart index 39def6dc..faa34929 100644 --- a/lib/app_router.gr.dart +++ b/lib/app_router.gr.dart @@ -15,6 +15,16 @@ abstract class _$AppRouter extends RootStackRouter { @override final Map pagesMap = { + AdminFeedbackRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: AdminFeedbackScreen( + key: args.key, + feedbackId: args.feedbackId, + ), + ); + }, LoginRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, @@ -30,6 +40,44 @@ abstract class _$AppRouter extends RootStackRouter { }; } +/// generated route for +/// [AdminFeedbackScreen] +class AdminFeedbackRoute extends PageRouteInfo { + AdminFeedbackRoute({ + Key? key, + required int feedbackId, + List? children, + }) : super( + AdminFeedbackRoute.name, + args: AdminFeedbackRouteArgs( + key: key, + feedbackId: feedbackId, + ), + initialChildren: children, + ); + + static const String name = 'AdminFeedbackRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class AdminFeedbackRouteArgs { + const AdminFeedbackRouteArgs({ + this.key, + required this.feedbackId, + }); + + final Key? key; + + final int feedbackId; + + @override + String toString() { + return 'AdminFeedbackRouteArgs{key: $key, feedbackId: $feedbackId}'; + } +} + /// generated route for /// [LoginScreen] class LoginRoute extends PageRouteInfo { diff --git a/lib/features/feedback/presentation/widgets/feedback.dart b/lib/features/feedback/presentation/screens/feedback.dart similarity index 84% rename from lib/features/feedback/presentation/widgets/feedback.dart rename to lib/features/feedback/presentation/screens/feedback.dart index be423b97..1c906a83 100644 --- a/lib/features/feedback/presentation/widgets/feedback.dart +++ b/lib/features/feedback/presentation/screens/feedback.dart @@ -6,15 +6,15 @@ import 'package:lb_planner/features/feedback/domain/domain.dart'; import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; /// Admin feedback subroute. -class AdminFeedbackRoute extends StatefulWidget { +class AdminFeedbacksScreen extends StatefulWidget { /// Admin feedback subroute. - const AdminFeedbackRoute({Key? key}) : super(key: key); + const AdminFeedbacksScreen({Key? key}) : super(key: key); /// The font size of the header. static const double headerFontSize = 20; @override - State createState() => _AdminFeedbackRouteState(); + State createState() => _AdminFeedbacksScreenState(); /// Sorts the given feedback list static List? sortFeedbacks(AsyncValue> feedbacks) { @@ -34,13 +34,13 @@ class AdminFeedbackRoute extends StatefulWidget { } } -class _AdminFeedbackRouteState extends State { +class _AdminFeedbacksScreenState extends State { @override Widget build(BuildContext context) { return Consumer(builder: (context, ref, _) { var feedbacks = ref.watch(feedbackProvider); - var sortedFeedbacks = AdminFeedbackRoute.sortFeedbacks(feedbacks); + var sortedFeedbacks = AdminFeedbacksScreen.sortFeedbacks(feedbacks); return ConditionalWidget( ifFalse: Center( @@ -48,7 +48,7 @@ class _AdminFeedbackRouteState extends State { t.admin_feedback_noFeedback, style: TextStyle( overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, + fontSize: AdminFeedbacksScreen.headerFontSize, fontWeight: FontWeight.w600, ), textAlign: TextAlign.left, @@ -66,7 +66,7 @@ class _AdminFeedbackRouteState extends State { t.admin_feedback_headers_user, style: TextStyle( overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, + fontSize: AdminFeedbacksScreen.headerFontSize, fontWeight: FontWeight.w600, ), ), @@ -77,7 +77,7 @@ class _AdminFeedbackRouteState extends State { textAlign: TextAlign.center, style: TextStyle( overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, + fontSize: AdminFeedbacksScreen.headerFontSize, fontWeight: FontWeight.w600, ), ), @@ -88,7 +88,7 @@ class _AdminFeedbackRouteState extends State { textAlign: TextAlign.center, style: TextStyle( overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, + fontSize: AdminFeedbacksScreen.headerFontSize, fontWeight: FontWeight.w600, ), ), @@ -99,7 +99,7 @@ class _AdminFeedbackRouteState extends State { textAlign: TextAlign.center, style: TextStyle( overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, + fontSize: AdminFeedbacksScreen.headerFontSize, fontWeight: FontWeight.w600, ), ), @@ -110,7 +110,7 @@ class _AdminFeedbackRouteState extends State { textAlign: TextAlign.center, style: TextStyle( overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, + fontSize: AdminFeedbacksScreen.headerFontSize, fontWeight: FontWeight.w600, ), ), @@ -121,7 +121,7 @@ class _AdminFeedbackRouteState extends State { textAlign: TextAlign.center, style: TextStyle( overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackRoute.headerFontSize, + fontSize: AdminFeedbacksScreen.headerFontSize, fontWeight: FontWeight.w600, ), ), diff --git a/lib/features/feedback/presentation/screens/feedback_page.dart b/lib/features/feedback/presentation/screens/feedback_page.dart index b44b1fd6..3c56a477 100644 --- a/lib/features/feedback/presentation/screens/feedback_page.dart +++ b/lib/features/feedback/presentation/screens/feedback_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart' hide Feedback; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_vector_icons/flutter_vector_icons.dart'; import 'package:lb_planner/features/feedback/domain/domain.dart'; -import 'package:lb_planner/features/feedback/presentation/widgets/feedback.dart'; +import 'package:lb_planner/features/feedback/presentation/screens/feedback.dart'; import 'package:lb_planner/features/themes/domain/models/module_status_theme.dart'; import 'package:lb_planner/shared/shared.dart'; import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; @@ -10,19 +10,19 @@ import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; @RoutePage() /// Admin feedback page displaying details about feedback. -class AdminFeedbackPageRoute extends StatefulWidget { +class AdminFeedbackScreen extends StatefulWidget { /// Admin feedback page displaying details about feedback. - const AdminFeedbackPageRoute({Key? key, required this.feedbackId}) + const AdminFeedbackScreen({Key? key, required this.feedbackId}) : super(key: key); /// The id of the feedback to display. final int feedbackId; @override - State createState() => _AdminFeedbackPageRouteState(); + State createState() => _AdminFeedbackScreenState(); } -class _AdminFeedbackPageRouteState extends State { +class _AdminFeedbackScreenState extends State { final commentController = TextEditingController(); late int updated; @@ -42,13 +42,13 @@ class _AdminFeedbackPageRouteState extends State { } _pushNext(BuildContext context, WidgetRef ref, feedback) { - var sorted = AdminFeedbackRoute.sortFeedbacks(ref.read(feedbackProvider)); + var sorted = AdminFeedbacksScreen.sortFeedbacks(ref.read(feedbackProvider)); var currentIndex = sorted?.indexOf(feedback); if (sorted!.length - 1 > currentIndex! && currentIndex >= 0) { // If there's a next item, navigate to the next feedback detail page var nextFeedback = sorted[currentIndex + 1]; - context.router.push(AdminFeedbackPageRoute( + context.router.push(AdminFeedbackRoute( feedbackId: nextFeedback.id, key: ValueKey(nextFeedback.id), )); @@ -93,8 +93,9 @@ class _AdminFeedbackPageRouteState extends State { int user = feedback.author; return LpContainer( - title: feedback.type.title(context), - leading: Icon(feedback.type.icon, color: feedback.type.color), + title: feedback.type.title(context), //swtich jsonvalues + leading: + Icon(feedback.type.icon, color: feedback.type.color(context)), trailing: ConditionalWidget( condition: deleted == 0, ifTrue: HoverBuilder( @@ -122,7 +123,7 @@ class _AdminFeedbackPageRouteState extends State { FeedbackStatusTag(read: feedback.read), Spacing.small(), Text( - t.admin_feedback_page_author(user), + t.admin_feedback_page_author(user as String), style: TextStyle( fontWeight: FontWeight.w600, overflow: TextOverflow.ellipsis, diff --git a/lib/features/feedback/presentation/widgets/feedback_item.dart b/lib/features/feedback/presentation/widgets/feedback_item.dart index 35def4fe..369faac9 100644 --- a/lib/features/feedback/presentation/widgets/feedback_item.dart +++ b/lib/features/feedback/presentation/widgets/feedback_item.dart @@ -1,3 +1,5 @@ +// ignore_for_file: public_member_api_docs + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -54,7 +56,7 @@ class _AdminFeedbackItemState extends ConsumerState { return GestureDetector( onTap: () => context.router.navigate(const LoginRoute()) - AdminFeedbackPageRoute.info.push(context, params: {"id": feedbackId}), + AdminFeedbackScreen.info.push(context, params: {"id": feedbackId}), child: Card( child: Row( children: [ @@ -158,7 +160,7 @@ class _AdminFeedbackItemState extends ConsumerState { } } -extension _AdminFeedbackItemHelper on FeedbackType { +extension AdminFeedbackItemHelper on FeedbackType { String title(BuildContext context) { var t = context.t; @@ -199,3 +201,4 @@ extension _AdminFeedbackItemHelper on FeedbackType { } } } + diff --git a/lib/shared/presentation/widgets/widgets.dart b/lib/shared/presentation/widgets/widgets.dart index 1f4f68b4..8955e79f 100644 --- a/lib/shared/presentation/widgets/widgets.dart +++ b/lib/shared/presentation/widgets/widgets.dart @@ -15,6 +15,4 @@ export 'color_utils.dart'; export 'user_profile_img.dart'; export 'conditional_widget.dart'; export 'container.dart'; -export 'textbutton.dart'; -export 'text_field.dart'; export 'dialog.dart'; From fe8fcc562a23a0de4b5c30e3f65c7ecfe94dcd8d Mon Sep 17 00:00:00 2001 From: Can42 Date: Sat, 24 Feb 2024 01:08:41 +0100 Subject: [PATCH 12/18] feedbackroute implemented --- lib/app_router.dart | 2 +- lib/features/feedback/presentation/screens/feedback.dart | 1 + .../feedback/presentation/screens/feedback_page.dart | 7 +++---- .../feedback/presentation/widgets/feedback_item.dart | 5 ++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/app_router.dart b/lib/app_router.dart index 8152a426..5d52143a 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -112,7 +112,7 @@ class AppRouter extends _$AppRouter { ), DefaultRoute( page: AdminFeedbackRoute.page, - path: '/login', + path: '/feedback', ) ]; } diff --git a/lib/features/feedback/presentation/screens/feedback.dart b/lib/features/feedback/presentation/screens/feedback.dart index 1c906a83..ef2b59c5 100644 --- a/lib/features/feedback/presentation/screens/feedback.dart +++ b/lib/features/feedback/presentation/screens/feedback.dart @@ -8,6 +8,7 @@ import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; /// Admin feedback subroute. class AdminFeedbacksScreen extends StatefulWidget { /// Admin feedback subroute. + const AdminFeedbacksScreen({Key? key}) : super(key: key); /// The font size of the header. diff --git a/lib/features/feedback/presentation/screens/feedback_page.dart b/lib/features/feedback/presentation/screens/feedback_page.dart index 3c56a477..b1468ab6 100644 --- a/lib/features/feedback/presentation/screens/feedback_page.dart +++ b/lib/features/feedback/presentation/screens/feedback_page.dart @@ -54,9 +54,8 @@ class _AdminFeedbackScreenState extends State { )); } else { // If there's no next item, navigate to the admin feedback list page - AdminFeedbackRoute.info.push(context); - // Assuming AdminFeedbackRoute.info is a valid way to navigate back - // to the admin feedback list. If not, adjust this part according to your setup. + + context.router.push(AdminFeedbackRoute(feedbackId: )); } } @@ -105,7 +104,7 @@ class _AdminFeedbackScreenState extends State { ? context.theme.colorScheme.error : ModuleStatusTheme.of(context).pendingColor, ), - onTap: () => ShowConfirmDialog( + onTap: () => showConfirmDialog( context, title: t.admin_feedback_page_deleteTitle, message: t.admin_feedback_page_deleteText, diff --git a/lib/features/feedback/presentation/widgets/feedback_item.dart b/lib/features/feedback/presentation/widgets/feedback_item.dart index 369faac9..8dd49a8a 100644 --- a/lib/features/feedback/presentation/widgets/feedback_item.dart +++ b/lib/features/feedback/presentation/widgets/feedback_item.dart @@ -55,8 +55,8 @@ class _AdminFeedbackItemState extends ConsumerState { int? modifyingUser = feedback.modifiedByUserId; return GestureDetector( - onTap: () => context.router.navigate(const LoginRoute()) - AdminFeedbackScreen.info.push(context, params: {"id": feedbackId}), + onTap: () => + context.router.push(AdminFeedbackRoute(feedbackId: feedbackId)), child: Card( child: Row( children: [ @@ -201,4 +201,3 @@ extension AdminFeedbackItemHelper on FeedbackType { } } } - From 14e48a2dbc83ef7c4b51ee52f9547f153a7b156b Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Mon, 26 Feb 2024 11:03:56 +0100 Subject: [PATCH 13/18] issues fixxed --- lib/app_router.dart | 4 +++- lib/app_router.gr.dart | 20 +++++++++++++++++++ .../presentation/screens/feedback.dart | 15 ++++++++------ .../presentation/screens/feedback_page.dart | 2 +- .../presentation/screens/screens.dart | 3 ++- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/app_router.dart b/lib/app_router.dart index 5d52143a..6761edc7 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -4,6 +4,7 @@ import 'package:lb_planner/features/themes/themes.dart'; import 'package:lb_planner/shared/shared.dart'; import 'package:auto_route/auto_route.dart'; import 'package:lb_planner/features/auth/auth.dart'; +import 'package:lb_planner/features/feedback/presentation/screens/feedback.dart'; part 'app_router.gr.dart'; @@ -111,8 +112,9 @@ class AppRouter extends _$AppRouter { path: '/theme-development', ), DefaultRoute( - page: AdminFeedbackRoute.page, + page: AdminFeedbacksRoute.page, path: '/feedback', + initial: true, ) ]; } diff --git a/lib/app_router.gr.dart b/lib/app_router.gr.dart index faa34929..4cf5a02e 100644 --- a/lib/app_router.gr.dart +++ b/lib/app_router.gr.dart @@ -25,6 +25,12 @@ abstract class _$AppRouter extends RootStackRouter { ), ); }, + AdminFeedbacksRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const AdminFeedbacksScreen(), + ); + }, LoginRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, @@ -78,6 +84,20 @@ class AdminFeedbackRouteArgs { } } +/// generated route for +/// [AdminFeedbacksScreen] +class AdminFeedbacksRoute extends PageRouteInfo { + const AdminFeedbacksRoute({List? children}) + : super( + AdminFeedbacksRoute.name, + initialChildren: children, + ); + + static const String name = 'AdminFeedbacksRoute'; + + static const PageInfo page = PageInfo(name); +} + /// generated route for /// [LoginScreen] class LoginRoute extends PageRouteInfo { diff --git a/lib/features/feedback/presentation/screens/feedback.dart b/lib/features/feedback/presentation/screens/feedback.dart index ef2b59c5..74cddfbd 100644 --- a/lib/features/feedback/presentation/screens/feedback.dart +++ b/lib/features/feedback/presentation/screens/feedback.dart @@ -5,6 +5,8 @@ import 'package:lb_planner/shared/shared.dart'; import 'package:lb_planner/features/feedback/domain/domain.dart'; import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; +@RoutePage() + /// Admin feedback subroute. class AdminFeedbacksScreen extends StatefulWidget { /// Admin feedback subroute. @@ -19,7 +21,7 @@ class AdminFeedbacksScreen extends StatefulWidget { /// Sorts the given feedback list static List? sortFeedbacks(AsyncValue> feedbacks) { - feedbacks.value!.sort( + feedbacks.value?.sort( (a, b) { var status = a.readAsInt.compareTo(b.readAsInt); @@ -55,7 +57,7 @@ class _AdminFeedbacksScreenState extends State { textAlign: TextAlign.left, ), ), - condition: sortedFeedbacks!.isNotEmpty, + condition: sortedFeedbacks?.isNotEmpty ?? false, ifTrue: Align( alignment: Alignment.topLeft, child: Column( @@ -134,10 +136,11 @@ class _AdminFeedbacksScreenState extends State { child: ListView( controller: ScrollController(), children: [ - for (var feedback in sortedFeedbacks) ...[ - AdminFeedbackItem(feedbackId: feedback.id), - Spacing.medium(), - ], + if (sortedFeedbacks != null) + for (var feedback in sortedFeedbacks) ...[ + AdminFeedbackItem(feedbackId: feedback.id), + Spacing.medium(), + ], ], ), ), diff --git a/lib/features/feedback/presentation/screens/feedback_page.dart b/lib/features/feedback/presentation/screens/feedback_page.dart index b1468ab6..5a0471ec 100644 --- a/lib/features/feedback/presentation/screens/feedback_page.dart +++ b/lib/features/feedback/presentation/screens/feedback_page.dart @@ -55,7 +55,7 @@ class _AdminFeedbackScreenState extends State { } else { // If there's no next item, navigate to the admin feedback list page - context.router.push(AdminFeedbackRoute(feedbackId: )); + context.router.push(AdminFeedbacksRoute()); } } diff --git a/lib/features/feedback/presentation/screens/screens.dart b/lib/features/feedback/presentation/screens/screens.dart index b9f78bb0..37bf1997 100644 --- a/lib/features/feedback/presentation/screens/screens.dart +++ b/lib/features/feedback/presentation/screens/screens.dart @@ -1 +1,2 @@ -// TODO Implement this library. \ No newline at end of file +export 'feedback.dart'; +export 'feedback_page.dart'; From 979798c42377f0271c4a9346703fa42beda6ad9f Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Mon, 26 Feb 2024 11:24:22 +0100 Subject: [PATCH 14/18] finished --- lib/app_router.dart | 3 +- .../presentation/screens/feedback.dart | 192 ++++++------ .../presentation/screens/feedback_page.dart | 282 +++++++++--------- 3 files changed, 242 insertions(+), 235 deletions(-) diff --git a/lib/app_router.dart b/lib/app_router.dart index 6761edc7..57bb16f0 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -114,8 +114,7 @@ class AppRouter extends _$AppRouter { DefaultRoute( page: AdminFeedbacksRoute.page, path: '/feedback', - initial: true, - ) + ), ]; } diff --git a/lib/features/feedback/presentation/screens/feedback.dart b/lib/features/feedback/presentation/screens/feedback.dart index 74cddfbd..fecf5767 100644 --- a/lib/features/feedback/presentation/screens/feedback.dart +++ b/lib/features/feedback/presentation/screens/feedback.dart @@ -40,114 +40,118 @@ class AdminFeedbacksScreen extends StatefulWidget { class _AdminFeedbacksScreenState extends State { @override Widget build(BuildContext context) { - return Consumer(builder: (context, ref, _) { - var feedbacks = ref.watch(feedbackProvider); + return Material( + child: Sidebar( + body: Consumer(builder: (context, ref, _) { + var feedbacks = ref.watch(feedbackProvider); - var sortedFeedbacks = AdminFeedbacksScreen.sortFeedbacks(feedbacks); + var sortedFeedbacks = AdminFeedbacksScreen.sortFeedbacks(feedbacks); - return ConditionalWidget( - ifFalse: Center( - child: Text( - t.admin_feedback_noFeedback, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbacksScreen.headerFontSize, - fontWeight: FontWeight.w600, + return ConditionalWidget( + ifFalse: Center( + child: Text( + t.admin_feedback_noFeedback, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbacksScreen.headerFontSize, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.left, + ), ), - textAlign: TextAlign.left, - ), - ), - condition: sortedFeedbacks?.isNotEmpty ?? false, - ifTrue: Align( - alignment: Alignment.topLeft, - child: Column( - children: [ - Row( + condition: sortedFeedbacks?.isNotEmpty ?? false, + ifTrue: Align( + alignment: Alignment.topLeft, + child: Column( children: [ - Expanded( - child: Text( - t.admin_feedback_headers_user, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbacksScreen.headerFontSize, - fontWeight: FontWeight.w600, + Row( + children: [ + Expanded( + child: Text( + t.admin_feedback_headers_user, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbacksScreen.headerFontSize, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), - Expanded( - child: Text( - t.admin_feedback_headers_status, - textAlign: TextAlign.center, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbacksScreen.headerFontSize, - fontWeight: FontWeight.w600, + Expanded( + child: Text( + t.admin_feedback_headers_status, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbacksScreen.headerFontSize, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), - Expanded( - child: Text( - t.admin_feedback_headers_type, - textAlign: TextAlign.center, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbacksScreen.headerFontSize, - fontWeight: FontWeight.w600, + Expanded( + child: Text( + t.admin_feedback_headers_type, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbacksScreen.headerFontSize, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), - Expanded( - child: Text( - t.admin_feedback_headers_lastModified, - textAlign: TextAlign.center, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbacksScreen.headerFontSize, - fontWeight: FontWeight.w600, + Expanded( + child: Text( + t.admin_feedback_headers_lastModified, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbacksScreen.headerFontSize, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), - Expanded( - child: Text( - t.admin_feedback_headers_lastModifiedBy, - textAlign: TextAlign.center, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbacksScreen.headerFontSize, - fontWeight: FontWeight.w600, + Expanded( + child: Text( + t.admin_feedback_headers_lastModifiedBy, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbacksScreen.headerFontSize, + fontWeight: FontWeight.w600, + ), + ), ), - ), + Expanded( + child: Text( + t.admin_feedback_headers_timestamp, + textAlign: TextAlign.center, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbacksScreen.headerFontSize, + fontWeight: FontWeight.w600, + ), + ), + ), + ], ), + Spacing.large(), Expanded( - child: Text( - t.admin_feedback_headers_timestamp, - textAlign: TextAlign.center, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbacksScreen.headerFontSize, - fontWeight: FontWeight.w600, - ), + child: ListView( + controller: ScrollController(), + children: [ + if (sortedFeedbacks != null) + for (var feedback in sortedFeedbacks) ...[ + AdminFeedbackItem(feedbackId: feedback.id), + Spacing.medium(), + ], + ], ), ), ], ), - Spacing.large(), - Expanded( - child: ListView( - controller: ScrollController(), - children: [ - if (sortedFeedbacks != null) - for (var feedback in sortedFeedbacks) ...[ - AdminFeedbackItem(feedbackId: feedback.id), - Spacing.medium(), - ], - ], - ), - ), - ], - ), - ), - ); - }); + ), + ); + }), + ), + ); } } diff --git a/lib/features/feedback/presentation/screens/feedback_page.dart b/lib/features/feedback/presentation/screens/feedback_page.dart index 5a0471ec..314b71a7 100644 --- a/lib/features/feedback/presentation/screens/feedback_page.dart +++ b/lib/features/feedback/presentation/screens/feedback_page.dart @@ -73,157 +73,161 @@ class _AdminFeedbackScreenState extends State { Widget build(BuildContext context) { bool _commenmtInit = false; - return Consumer( - builder: (context, ref, _) { - int feedbackId = widget.feedbackId; - - var controller = ref.watch(feedbackController); - var feedback = controller.getFeedbackById(feedbackId); - - if (ref.read(feedbackProvider).isLoading) { - return ShimmerEffect(height: AdminFeedbackItem.height); - } - - if (!_commenmtInit) { - commentController.text = feedback.comment; - _commenmtInit = true; - } - - int user = feedback.author; - - return LpContainer( - title: feedback.type.title(context), //swtich jsonvalues - leading: - Icon(feedback.type.icon, color: feedback.type.color(context)), - trailing: ConditionalWidget( - condition: deleted == 0, - ifTrue: HoverBuilder( - builder: (context, hover) => Icon( - Icons.delete, - color: hover - ? context.theme.colorScheme.error - : ModuleStatusTheme.of(context).pendingColor, - ), - onTap: () => showConfirmDialog( - context, - title: t.admin_feedback_page_deleteTitle, - message: t.admin_feedback_page_deleteText, - confirmIsBad: true, - onConfirm: () => _deleteFeedback(context, ref, feedback), - ), - ), - ifFalse: CircularProgressIndicator( - color: context.theme.colorScheme.error), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Spacing.small(), - FeedbackStatusTag(read: feedback.read), - Spacing.small(), - Text( - t.admin_feedback_page_author(user as String), - style: TextStyle( - fontWeight: FontWeight.w600, - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackItem.fontSize, - ), - textAlign: TextAlign.left, - ), - if (feedback.type == FeedbackType.bug) - Text( - t.admin_feedback_page_logFile(feedback.logfile!), - style: TextStyle( - fontWeight: FontWeight.w600, - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackItem.fontSize, + return Material( + child: Sidebar( + body: Consumer( + builder: (context, ref, _) { + int feedbackId = widget.feedbackId; + + var controller = ref.watch(feedbackController); + var feedback = controller.getFeedbackById(feedbackId); + + if (ref.read(feedbackProvider).isLoading) { + return ShimmerEffect(height: AdminFeedbackItem.height); + } + + if (!_commenmtInit) { + commentController.text = feedback.comment; + _commenmtInit = true; + } + + int user = feedback.author; + + return LpContainer( + title: feedback.type.title(context), //swtich jsonvalues + leading: + Icon(feedback.type.icon, color: feedback.type.color(context)), + trailing: ConditionalWidget( + condition: deleted == 0, + ifTrue: HoverBuilder( + builder: (context, hover) => Icon( + Icons.delete, + color: hover + ? context.theme.colorScheme.error + : ModuleStatusTheme.of(context).pendingColor, + ), + onTap: () => showConfirmDialog( + context, + title: t.admin_feedback_page_deleteTitle, + message: t.admin_feedback_page_deleteText, + confirmIsBad: true, + onConfirm: () => _deleteFeedback(context, ref, feedback), ), - textAlign: TextAlign.left, - ), - Text( - t.admin_feedback_page_id(feedback.id), - style: TextStyle( - fontWeight: FontWeight.w600, - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackItem.fontSize, ), - textAlign: TextAlign.left, + ifFalse: CircularProgressIndicator( + color: context.theme.colorScheme.error), ), - Spacing.large(), - Expanded( - child: CustomScrollView( - controller: ScrollController(), - slivers: [ - SliverFillRemaining( - hasScrollBody: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - feedback.content, - style: TextStyle( - fontWeight: FontWeight.normal, - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackItem.fontSize, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Spacing.small(), + FeedbackStatusTag(read: feedback.read), + Spacing.small(), + Text( + t.admin_feedback_page_author(user as String), + style: TextStyle( + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, + ), + textAlign: TextAlign.left, + ), + if (feedback.type == FeedbackType.bug) + Text( + t.admin_feedback_page_logFile(feedback.logfile!), + style: TextStyle( + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, + ), + textAlign: TextAlign.left, + ), + Text( + t.admin_feedback_page_id(feedback.id), + style: TextStyle( + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, + ), + textAlign: TextAlign.left, + ), + Spacing.large(), + Expanded( + child: CustomScrollView( + controller: ScrollController(), + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + feedback.content, + style: TextStyle( + fontWeight: FontWeight.normal, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, + ), + textAlign: TextAlign.left, + ), ), - textAlign: TextAlign.left, + ], + ), + ), + ], + ), + ), + Spacing.large(), + Expanded( + child: TextField( + keyboardType: TextInputType.multiline, + controller: commentController, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: t.admin_feedback_page_comment, + )), + ), + Spacing.large(), + Align( + alignment: Alignment.centerRight, + child: ConditionalWidget( + condition: updated != 0, + ifTrue: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + feedback.read + ? t.admin_feedback_page_update + : t.admin_feedback_page_markRead, + style: TextStyle( + fontWeight: FontWeight.w600, + overflow: TextOverflow.ellipsis, + fontSize: AdminFeedbackItem.fontSize, ), + textAlign: TextAlign.left, ), + Spacing.small(), + CircularProgressIndicator(), ], ), - ), - ], - ), - ), - Spacing.large(), - Expanded( - child: TextField( - keyboardType: TextInputType.multiline, - controller: commentController, - decoration: InputDecoration( - border: OutlineInputBorder(), - hintText: t.admin_feedback_page_comment, - )), - ), - Spacing.large(), - Align( - alignment: Alignment.centerRight, - child: ConditionalWidget( - condition: updated != 0, - ifTrue: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - feedback.read + ifFalse: TextButton.icon( + label: Text(feedback.read ? t.admin_feedback_page_update - : t.admin_feedback_page_markRead, - style: TextStyle( - fontWeight: FontWeight.w600, - overflow: TextOverflow.ellipsis, - fontSize: AdminFeedbackItem.fontSize, - ), - textAlign: TextAlign.left, + : t.admin_feedback_page_markRead), + onPressed: () => _updateFeedback(ref, feedback), + icon: Icon(Feather.arrow_right_circle, + size: AdminFeedbackItem.fontSize * 1.2, + color: context.theme.colorScheme.primary), ), - Spacing.small(), - CircularProgressIndicator(), - ], - ), - ifFalse: TextButton.icon( - label: Text(feedback.read - ? t.admin_feedback_page_update - : t.admin_feedback_page_markRead), - onPressed: () => _updateFeedback(ref, feedback), - icon: Icon(Feather.arrow_right_circle, - size: AdminFeedbackItem.fontSize * 1.2, - color: context.theme.colorScheme.primary), + ), ), - ), + ], ), - ], - ), - ); - }, + ); + }, + ), + ), ); } } From 2655e91d03ac6170563cc7688f380336ff9240e2 Mon Sep 17 00:00:00 2001 From: Can42 Date: Mon, 26 Feb 2024 18:42:45 +0100 Subject: [PATCH 15/18] Update app_router.dart --- lib/app_router.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/app_router.dart b/lib/app_router.dart index 57bb16f0..9f8a31f4 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -114,6 +114,7 @@ class AppRouter extends _$AppRouter { DefaultRoute( page: AdminFeedbacksRoute.page, path: '/feedback', + initial: true, ), ]; } From a1f6d1f9f9da4d4509777d75541be25ac9ad4396 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:34:11 +0100 Subject: [PATCH 16/18] route name fixxed --- lib/app_router.dart | 4 +++- lib/shared/presentation/widgets/screen_title_bar.dart | 2 +- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/app_router.dart b/lib/app_router.dart index 9f8a31f4..9056960d 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -115,6 +115,7 @@ class AppRouter extends _$AppRouter { page: AdminFeedbacksRoute.page, path: '/feedback', initial: true, + title: (context, data) => context.t.admin_feedback_routeName, ), ]; } @@ -122,6 +123,7 @@ class AppRouter extends _$AppRouter { /// Implements [CustomRoute] with some default settings. class DefaultRoute extends CustomRoute { /// Implements [CustomRoute] with some default settings. - DefaultRoute({required super.page, required super.path, super.initial}) + DefaultRoute( + {required super.page, required super.path, super.initial, super.title}) : super(transitionsBuilder: TransitionsBuilders.noTransition); } diff --git a/lib/shared/presentation/widgets/screen_title_bar.dart b/lib/shared/presentation/widgets/screen_title_bar.dart index a853510d..041c302b 100644 --- a/lib/shared/presentation/widgets/screen_title_bar.dart +++ b/lib/shared/presentation/widgets/screen_title_bar.dart @@ -71,7 +71,7 @@ class ScreenTitleBar extends ConsumerWidget { width: profileImageSize, height: profileImageSize, child: CircleAvatar( - // TODO: backgroundImage: fallback image + child: Icon(Icons.account_circle_rounded), foregroundImage: NetworkImage(user.profileImageUrl), ), ), diff --git a/pubspec.lock b/pubspec.lock index c63fa87a..09e1136d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -266,6 +266,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + enum_to_string: + dependency: "direct main" + description: + name: enum_to_string + sha256: bd9e83a33b754cb43a75b36a9af2a0b92a757bfd9847d2621ca0b1bed45f8e7a + url: "https://pub.dev" + source: hosted + version: "2.0.1" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e8836128..4eb9c68f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: flutter_cache_manager: ^3.3.1 cached_network_image: ^3.3.0 timeago: ^3.6.0 + enum_to_string: ^2.0.1 dev_dependencies: From da4f43d13e14761f67e0719f1e7699440c37be1f Mon Sep 17 00:00:00 2001 From: Can42 Date: Tue, 27 Feb 2024 19:53:18 +0100 Subject: [PATCH 17/18] Update shimmer.dart --- lib/shared/presentation/widgets/shimmer.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/shared/presentation/widgets/shimmer.dart b/lib/shared/presentation/widgets/shimmer.dart index a1a9268f..6e5a050a 100644 --- a/lib/shared/presentation/widgets/shimmer.dart +++ b/lib/shared/presentation/widgets/shimmer.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:lb_planner/shared/presentation/widgets/color_utils.dart'; import 'package:lb_planner/shared/shared.dart'; import 'package:shimmer/shimmer.dart'; From 2045f70b47f1b7c68b3439f47066ab0a2c8dbb9c Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:03:43 +0100 Subject: [PATCH 18/18] classes commented --- .../feedback/presentation/screens/feedback.dart | 5 ++--- .../feedback/presentation/screens/feedback_page.dart | 4 ++-- .../feedback/presentation/widgets/feedback_item.dart | 11 ++++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/features/feedback/presentation/screens/feedback.dart b/lib/features/feedback/presentation/screens/feedback.dart index fecf5767..5cceb3e8 100644 --- a/lib/features/feedback/presentation/screens/feedback.dart +++ b/lib/features/feedback/presentation/screens/feedback.dart @@ -7,10 +7,9 @@ import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; @RoutePage() -/// Admin feedback subroute. +/// Shows all feedbacks for the admin. class AdminFeedbacksScreen extends StatefulWidget { - /// Admin feedback subroute. - + /// Shows all feedbacks for the admin. const AdminFeedbacksScreen({Key? key}) : super(key: key); /// The font size of the header. diff --git a/lib/features/feedback/presentation/screens/feedback_page.dart b/lib/features/feedback/presentation/screens/feedback_page.dart index 314b71a7..617451f5 100644 --- a/lib/features/feedback/presentation/screens/feedback_page.dart +++ b/lib/features/feedback/presentation/screens/feedback_page.dart @@ -9,9 +9,9 @@ import 'package:lb_planner/features/feedback/presentation/widgets/widgets.dart'; @RoutePage() -/// Admin feedback page displaying details about feedback. +/// Displays details about feedback. class AdminFeedbackScreen extends StatefulWidget { - /// Admin feedback page displaying details about feedback. + /// Displays details about feedback. const AdminFeedbackScreen({Key? key, required this.feedbackId}) : super(key: key); diff --git a/lib/features/feedback/presentation/widgets/feedback_item.dart b/lib/features/feedback/presentation/widgets/feedback_item.dart index 8dd49a8a..bed53b57 100644 --- a/lib/features/feedback/presentation/widgets/feedback_item.dart +++ b/lib/features/feedback/presentation/widgets/feedback_item.dart @@ -1,6 +1,3 @@ -// ignore_for_file: public_member_api_docs - -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lb_planner/features/auth/auth.dart'; @@ -10,9 +7,9 @@ import 'package:lb_planner/features/feedback/domain/domain.dart'; import 'package:lb_planner/features/feedback/presentation/presentation.dart'; import 'package:timeago/timeago.dart' as timeago; -/// Feedback item. +/// Shows the feedback item for the admin. class AdminFeedbackItem extends ConsumerStatefulWidget { - /// Feedback item. + /// Shows the feedback item for the admin. const AdminFeedbackItem({Key? key, required this.feedbackId}) : super(key: key); @@ -160,7 +157,9 @@ class _AdminFeedbackItemState extends ConsumerState { } } +/// Helps to return the title of the feedback type. extension AdminFeedbackItemHelper on FeedbackType { + /// Returns the title of the feedback type. String title(BuildContext context) { var t = context.t; @@ -176,6 +175,7 @@ extension AdminFeedbackItemHelper on FeedbackType { } } + /// Returns the icon of the feedback type. IconData get icon { switch (this) { case FeedbackType.bug: @@ -189,6 +189,7 @@ extension AdminFeedbackItemHelper on FeedbackType { } } + /// Returns the color of the feedback type. Color color(BuildContext context) { switch (this) { case FeedbackType.bug: