From 93df294142ac6b08807c75bbbeda8d2837c1d8e2 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 7 Sep 2025 23:00:29 +0800 Subject: [PATCH] :sparkles: Poll collapse --- assets/i18n/en-US.json | 23 ++++- lib/widgets/poll/poll_submit.dart | 165 ++++++++++++++++++++++++++++-- 2 files changed, 178 insertions(+), 10 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 96d2d042..24f0b9f3 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -133,6 +133,25 @@ "other": "{} replies" }, "forward": "Forward", + "award": "Award", + "awardPost": "Award Post", + "awardMessage": "Message", + "awardMessageHint": "Enter your award message...", + "awardAttitude": "Attitude", + "awardAttitudePositive": "Positive", + "awardAttitudeNegative": "Negative", + "awardAmount": "Amount", + "awardAmountHint": "Enter amount...", + "awardAmountRequired": "Amount is required", + "awardAmountInvalid": "Please enter a valid amount", + "awardMessageTooLong": "Message is too long (max 4096 characters)", + "awardSuccess": "Award sent successfully!", + "awardSubmit": "Award", + "awardPostPreview": "Post Preview", + "awardNoContent": "No content available", + "awardByPublisher": "By {}", + "awardBenefits": "Award Benefits", + "awardBenefitsDescription": "Awarding this post increases its value and visibility. Higher valued posts have a better chance of being featured and highlighted in the community.", "repliedTo": "Replied to", "forwarded": "Forwarded", "hasAttachments": { @@ -977,5 +996,7 @@ "pinned": "Pinned", "noResultsFound": "No results found", "toggleFilters": "Toggle filters", - "notableDayNext": "{} is in" + "notableDayNext": "{} is in", + "expandPoll": "Expand Poll", + "collapsePoll": "Collapse Poll" } diff --git a/lib/widgets/poll/poll_submit.dart b/lib/widgets/poll/poll_submit.dart index b03ceaa9..769be33d 100644 --- a/lib/widgets/poll/poll_submit.dart +++ b/lib/widgets/poll/poll_submit.dart @@ -17,6 +17,7 @@ class PollSubmit extends ConsumerStatefulWidget { this.onCancel, this.showProgress = true, this.isReadonly = false, + this.isInitiallyExpanded = false, }); final SnPollWithStats poll; @@ -36,6 +37,9 @@ class PollSubmit extends ConsumerStatefulWidget { final bool isReadonly; + /// Whether the poll should start expanded instead of collapsed. + final bool isInitiallyExpanded; + @override ConsumerState createState() => _PollSubmitState(); } @@ -45,6 +49,7 @@ class _PollSubmitState extends ConsumerState { int _index = 0; bool _submitting = false; bool _isModifying = false; // New state to track if user is modifying answers + bool _isCollapsed = true; // New state to track collapse/expand /// Collected answers, keyed by questionId late Map _answers; @@ -65,6 +70,8 @@ class _PollSubmitState extends ConsumerState { _questions = [...widget.poll.questions] ..sort((a, b) => a.order.compareTo(b.order)); _answers = Map.from(widget.initialAnswers ?? {}); + // Set initial collapse state based on the parameter + _isCollapsed = !widget.isInitiallyExpanded; if (!widget.isReadonly) { _loadCurrentIntoLocalState(); // If initial answers are provided, set _isModifying to false initially @@ -653,39 +660,179 @@ class _PollSubmitState extends ConsumerState { ); } + Widget _buildCollapsedView(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.poll.title != null) + Text( + widget.poll.title!, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (widget.poll.description != null) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + widget.poll.description!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).textTheme.bodySmall?.color?.withOpacity(0.7), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ) + else + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + '${_questions.length} question${_questions.length == 1 ? '' : 's'}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).textTheme.bodySmall?.color?.withOpacity(0.7), + ), + ), + ), + ], + ), + ), + IconButton( + icon: Icon( + _isCollapsed ? Icons.expand_more : Icons.expand_less, + size: 20, + ), + onPressed: () { + setState(() { + _isCollapsed = !_isCollapsed; + }); + }, + visualDensity: VisualDensity.compact, + tooltip: _isCollapsed ? 'expandPoll'.tr() : 'collapsePoll'.tr(), + ), + ], + ), + ], + ); + } + @override Widget build(BuildContext context) { if (_questions.isEmpty) { return const SizedBox.shrink(); } + // If collapsed, show collapsed view for all states + if (_isCollapsed) { + return _buildCollapsedView(context); + } + // If poll is already submitted and not in readonly mode, and not in modification mode, show submitted view if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [_buildSubmittedView(context), _buildNavBar(context)], + children: [ + _buildCollapsedView(context), + const SizedBox(height: 8), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, anim) { + final offset = Tween( + begin: const Offset(0, -0.1), + end: Offset.zero, + ).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut)); + final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut); + return FadeTransition( + opacity: fade, + child: SlideTransition(position: offset, child: child), + ); + }, + child: Column( + key: const ValueKey('submitted_expanded'), + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [_buildSubmittedView(context), _buildNavBar(context)], + ), + ), + ], ); } // If poll is in readonly mode, show readonly view if (widget.isReadonly) { - return _buildReadonlyView(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCollapsedView(context), + const SizedBox(height: 8), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, anim) { + final offset = Tween( + begin: const Offset(0, -0.1), + end: Offset.zero, + ).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut)); + final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut); + return FadeTransition( + opacity: fade, + child: SlideTransition(position: offset, child: child), + ); + }, + child: _buildReadonlyView(context), + ), + ], + ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildHeader(context), - const SizedBox(height: 12), - _AnimatedStep( - key: ValueKey(_current.id), + _buildCollapsedView(context), + const SizedBox(height: 8), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, anim) { + final offset = Tween( + begin: const Offset(0, -0.1), + end: Offset.zero, + ).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut)); + final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut); + return FadeTransition( + opacity: fade, + child: SlideTransition(position: offset, child: child), + ); + }, child: Column( + key: const ValueKey('normal_expanded'), crossAxisAlignment: CrossAxisAlignment.stretch, - children: [_buildBody(context), _buildStats(context, _current)], + children: [ + _buildHeader(context), + const SizedBox(height: 12), + _AnimatedStep( + key: ValueKey(_current.id), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildBody(context), + _buildStats(context, _current), + ], + ), + ), + const SizedBox(height: 16), + _buildNavBar(context), + ], ), ), - const SizedBox(height: 16), - _buildNavBar(context), ], ); }