From c0ab3837ac2f2d86fce807187b4c458d886c447f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 2 Nov 2025 21:47:37 +0800 Subject: [PATCH] :alien: Make poll load itself to match server updates --- lib/models/poll.dart | 2 +- lib/models/poll.freezed.dart | 66 ++-- lib/models/poll.g.dart | 9 +- lib/pods/activity/activity_rpc.g.dart | 2 +- lib/route.dart | 42 --- lib/screens/explore.g.dart | 2 +- lib/widgets/content/embed/embed_list.dart | 10 +- lib/widgets/poll/poll_submit.dart | 376 ++++++++++++---------- 8 files changed, 254 insertions(+), 255 deletions(-) diff --git a/lib/models/poll.dart b/lib/models/poll.dart index e11552e2..5bf0643f 100644 --- a/lib/models/poll.dart +++ b/lib/models/poll.dart @@ -8,7 +8,7 @@ part 'poll.g.dart'; @freezed sealed class SnPollWithStats with _$SnPollWithStats { const factory SnPollWithStats({ - required Map? userAnswer, + required SnPollAnswer? userAnswer, @Default({}) Map stats, required String id, required List questions, diff --git a/lib/models/poll.freezed.dart b/lib/models/poll.freezed.dart index d54dcdb5..902d91fe 100644 --- a/lib/models/poll.freezed.dart +++ b/lib/models/poll.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$SnPollWithStats { - Map? get userAnswer; Map get stats; String get id; List get questions; String? get title; String? get description; DateTime? get endedAt; String get publisherId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + SnPollAnswer? get userAnswer; Map get stats; String get id; List get questions; String? get title; String? get description; DateTime? get endedAt; String get publisherId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; /// Create a copy of SnPollWithStats /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,12 +28,12 @@ $SnPollWithStatsCopyWith get copyWith => _$SnPollWithStatsCopyW @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPollWithStats&&const DeepCollectionEquality().equals(other.userAnswer, userAnswer)&&const DeepCollectionEquality().equals(other.stats, stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.questions, questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPollWithStats&&(identical(other.userAnswer, userAnswer) || other.userAnswer == userAnswer)&&const DeepCollectionEquality().equals(other.stats, stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.questions, questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(userAnswer),const DeepCollectionEquality().hash(stats),id,const DeepCollectionEquality().hash(questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,userAnswer,const DeepCollectionEquality().hash(stats),id,const DeepCollectionEquality().hash(questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt); @override String toString() { @@ -48,11 +48,11 @@ abstract mixin class $SnPollWithStatsCopyWith<$Res> { factory $SnPollWithStatsCopyWith(SnPollWithStats value, $Res Function(SnPollWithStats) _then) = _$SnPollWithStatsCopyWithImpl; @useResult $Res call({ - Map? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + SnPollAnswer? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); - +$SnPollAnswerCopyWith<$Res>? get userAnswer; } /// @nodoc @@ -68,7 +68,7 @@ class _$SnPollWithStatsCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({Object? userAnswer = freezed,Object? stats = null,Object? id = null,Object? questions = null,Object? title = freezed,Object? description = freezed,Object? endedAt = freezed,Object? publisherId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_self.copyWith( userAnswer: freezed == userAnswer ? _self.userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable -as Map?,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable +as SnPollAnswer?,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable as Map,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,questions: null == questions ? _self.questions : questions // ignore: cast_nullable_to_non_nullable as List,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable @@ -81,7 +81,19 @@ as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ign as DateTime?, )); } +/// Create a copy of SnPollWithStats +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnPollAnswerCopyWith<$Res>? get userAnswer { + if (_self.userAnswer == null) { + return null; + } + return $SnPollAnswerCopyWith<$Res>(_self.userAnswer!, (value) { + return _then(_self.copyWith(userAnswer: value)); + }); +} } @@ -160,7 +172,7 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( Map? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( SnPollAnswer? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _SnPollWithStats() when $default != null: return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: @@ -181,7 +193,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( Map? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( SnPollAnswer? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; switch (_that) { case _SnPollWithStats(): return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);} @@ -198,7 +210,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( Map? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( SnPollAnswer? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; switch (_that) { case _SnPollWithStats() when $default != null: return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: @@ -213,18 +225,10 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl @JsonSerializable() class _SnPollWithStats implements SnPollWithStats { - const _SnPollWithStats({required final Map? userAnswer, final Map stats = const {}, required this.id, required final List questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _userAnswer = userAnswer,_stats = stats,_questions = questions; + const _SnPollWithStats({required this.userAnswer, final Map stats = const {}, required this.id, required final List questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _stats = stats,_questions = questions; factory _SnPollWithStats.fromJson(Map json) => _$SnPollWithStatsFromJson(json); - final Map? _userAnswer; -@override Map? get userAnswer { - final value = _userAnswer; - if (value == null) return null; - if (_userAnswer is EqualUnmodifiableMapView) return _userAnswer; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(value); -} - +@override final SnPollAnswer? userAnswer; final Map _stats; @override@JsonKey() Map get stats { if (_stats is EqualUnmodifiableMapView) return _stats; @@ -261,12 +265,12 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPollWithStats&&const DeepCollectionEquality().equals(other._userAnswer, _userAnswer)&&const DeepCollectionEquality().equals(other._stats, _stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._questions, _questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPollWithStats&&(identical(other.userAnswer, userAnswer) || other.userAnswer == userAnswer)&&const DeepCollectionEquality().equals(other._stats, _stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._questions, _questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_userAnswer),const DeepCollectionEquality().hash(_stats),id,const DeepCollectionEquality().hash(_questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,userAnswer,const DeepCollectionEquality().hash(_stats),id,const DeepCollectionEquality().hash(_questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt); @override String toString() { @@ -281,11 +285,11 @@ abstract mixin class _$SnPollWithStatsCopyWith<$Res> implements $SnPollWithStats factory _$SnPollWithStatsCopyWith(_SnPollWithStats value, $Res Function(_SnPollWithStats) _then) = __$SnPollWithStatsCopyWithImpl; @override @useResult $Res call({ - Map? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + SnPollAnswer? userAnswer, Map stats, String id, List questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); - +@override $SnPollAnswerCopyWith<$Res>? get userAnswer; } /// @nodoc @@ -300,8 +304,8 @@ class __$SnPollWithStatsCopyWithImpl<$Res> /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $Res call({Object? userAnswer = freezed,Object? stats = null,Object? id = null,Object? questions = null,Object? title = freezed,Object? description = freezed,Object? endedAt = freezed,Object? publisherId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_SnPollWithStats( -userAnswer: freezed == userAnswer ? _self._userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable -as Map?,stats: null == stats ? _self._stats : stats // ignore: cast_nullable_to_non_nullable +userAnswer: freezed == userAnswer ? _self.userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable +as SnPollAnswer?,stats: null == stats ? _self._stats : stats // ignore: cast_nullable_to_non_nullable as Map,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,questions: null == questions ? _self._questions : questions // ignore: cast_nullable_to_non_nullable as List,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable @@ -315,7 +319,19 @@ as DateTime?, )); } +/// Create a copy of SnPollWithStats +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnPollAnswerCopyWith<$Res>? get userAnswer { + if (_self.userAnswer == null) { + return null; + } + return $SnPollAnswerCopyWith<$Res>(_self.userAnswer!, (value) { + return _then(_self.copyWith(userAnswer: value)); + }); +} } diff --git a/lib/models/poll.g.dart b/lib/models/poll.g.dart index 31acd33b..da950084 100644 --- a/lib/models/poll.g.dart +++ b/lib/models/poll.g.dart @@ -8,7 +8,12 @@ part of 'poll.dart'; _SnPollWithStats _$SnPollWithStatsFromJson(Map json) => _SnPollWithStats( - userAnswer: json['user_answer'] as Map?, + userAnswer: + json['user_answer'] == null + ? null + : SnPollAnswer.fromJson( + json['user_answer'] as Map, + ), stats: json['stats'] as Map? ?? const {}, id: json['id'] as String, questions: @@ -32,7 +37,7 @@ _SnPollWithStats _$SnPollWithStatsFromJson(Map json) => Map _$SnPollWithStatsToJson(_SnPollWithStats instance) => { - 'user_answer': instance.userAnswer, + 'user_answer': instance.userAnswer?.toJson(), 'stats': instance.stats, 'id': instance.id, 'questions': instance.questions.map((e) => e.toJson()).toList(), diff --git a/lib/pods/activity/activity_rpc.g.dart b/lib/pods/activity/activity_rpc.g.dart index 2f168352..245fd682 100644 --- a/lib/pods/activity/activity_rpc.g.dart +++ b/lib/pods/activity/activity_rpc.g.dart @@ -7,7 +7,7 @@ part of 'activity_rpc.dart'; // ************************************************************************** String _$presenceActivitiesHash() => - r'dcea3cad01b4010c0087f5281413d83a754c2a17'; + r'3bfaa638eeb961ecd62a32d6a7760a6a7e7bf6f2'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/route.dart b/lib/route.dart index 372fedec..e1ad9df1 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -8,11 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/screens/about.dart'; import 'package:island/screens/developers/app_detail.dart'; import 'package:island/screens/developers/bot_detail.dart'; -import 'package:island/screens/developers/edit_app.dart'; -import 'package:island/screens/developers/edit_bot.dart'; import 'package:island/screens/developers/hub.dart'; -import 'package:island/screens/developers/new_app.dart'; -import 'package:island/screens/developers/new_bot.dart'; import 'package:island/screens/developers/edit_project.dart'; import 'package:island/screens/developers/new_project.dart'; import 'package:island/screens/discovery/articles.dart'; @@ -570,25 +566,6 @@ final routerProvider = Provider((ref) { return const SizedBox.shrink(); // Temporary placeholder }, routes: [ - GoRoute( - name: 'developerAppNew', - path: 'apps/new', - builder: - (context, state) => NewCustomAppScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - ), - ), - GoRoute( - name: 'developerAppEdit', - path: 'apps/:id/edit', - builder: - (context, state) => EditAppScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - id: state.pathParameters['id']!, - ), - ), GoRoute( name: 'developerAppDetail', path: 'apps/:appId', @@ -599,15 +576,6 @@ final routerProvider = Provider((ref) { appId: state.pathParameters['appId']!, ), ), - GoRoute( - name: 'developerBotNew', - path: 'bots/new', - builder: - (context, state) => NewBotScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - ), - ), GoRoute( name: 'developerBotDetail', path: 'bots/:botId', @@ -618,16 +586,6 @@ final routerProvider = Provider((ref) { botId: state.pathParameters['botId']!, ), ), - GoRoute( - name: 'developerBotEdit', - path: 'bots/:id/edit', - builder: - (context, state) => EditBotScreen( - publisherName: state.pathParameters['name']!, - projectId: state.pathParameters['projectId']!, - id: state.pathParameters['id']!, - ), - ), ], ), ], diff --git a/lib/screens/explore.g.dart b/lib/screens/explore.g.dart index ade2b890..5dfc59a3 100644 --- a/lib/screens/explore.g.dart +++ b/lib/screens/explore.g.dart @@ -7,7 +7,7 @@ part of 'explore.dart'; // ************************************************************************** String _$activityListNotifierHash() => - r'a3ad3242f08139bef14a2f0fab6591ce8b3cb9f0'; + r'77ffc7852feffa5438b56fa26123d453b7c310cf'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/widgets/content/embed/embed_list.dart b/lib/widgets/content/embed/embed_list.dart index ef49e22d..3e27b642 100644 --- a/lib/widgets/content/embed/embed_list.dart +++ b/lib/widgets/content/embed/embed_list.dart @@ -2,7 +2,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:island/models/embed.dart'; -import 'package:island/models/poll.dart'; import 'package:island/services/responsive.dart'; import 'package:island/utils/mapping.dart'; import 'package:island/widgets/content/embed/link.dart'; @@ -54,13 +53,10 @@ class EmbedListWidget extends StatelessWidget { vertical: 8, ), child: - embedData['poll'] == null - ? const Text('Poll was not loaded...') + embedData['id'] == null + ? const Text('Poll was unavailable...') : PollSubmit( - initialAnswers: - embedData['poll']?['user_answer']?['answer'], - stats: embedData['poll']?['stats'], - poll: SnPollWithStats.fromJson(embedData['poll']), + pollId: embedData['id'], onSubmit: (_) {}, isReadonly: !isInteractive, isInitiallyExpanded: isFullPost, diff --git a/lib/widgets/poll/poll_submit.dart b/lib/widgets/poll/poll_submit.dart index 769be33d..4efafb8f 100644 --- a/lib/widgets/poll/poll_submit.dart +++ b/lib/widgets/poll/poll_submit.dart @@ -4,15 +4,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:island/models/poll.dart'; import 'package:island/pods/network.dart'; +import 'package:island/screens/creators/poll/poll_list.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/poll/poll_stats_widget.dart'; class PollSubmit extends ConsumerStatefulWidget { const PollSubmit({ super.key, - required this.poll, + required this.pollId, required this.onSubmit, - required this.stats, this.initialAnswers, this.onCancel, this.showProgress = true, @@ -20,14 +20,13 @@ class PollSubmit extends ConsumerStatefulWidget { this.isInitiallyExpanded = false, }); - final SnPollWithStats poll; + final String pollId; /// Callback when user submits all answers. Map questionId -> answer. final void Function(Map answers) onSubmit; /// Optional initial answers, keyed by questionId. final Map? initialAnswers; - final Map? stats; /// Optional cancel callback. final VoidCallback? onCancel; @@ -45,7 +44,7 @@ class PollSubmit extends ConsumerStatefulWidget { } class _PollSubmitState extends ConsumerState { - late final List _questions; + List? _questions; int _index = 0; bool _submitting = false; bool _isModifying = false; // New state to track if user is modifying answers @@ -66,14 +65,10 @@ class _PollSubmitState extends ConsumerState { @override void initState() { super.initState(); - // Ensure questions are ordered by `order` - _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 // so the "Modify" button is shown. if (widget.initialAnswers != null && widget.initialAnswers!.isNotEmpty) { @@ -82,23 +77,25 @@ class _PollSubmitState extends ConsumerState { } } + void _initializeFromPollData(SnPollWithStats poll) { + // Initialize answers from poll data if available + if (poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty) { + _answers = Map.from(poll.userAnswer!.answer); + if (!widget.isReadonly && !_isModifying) { + _isModifying = false; // Show modify button if user has answered + } + } + _loadCurrentIntoLocalState(); + } + @override void didUpdateWidget(covariant PollSubmit oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.poll.id != widget.poll.id) { + if (oldWidget.pollId != widget.pollId) { _index = 0; _answers = Map.from(widget.initialAnswers ?? {}); - _questions - ..clear() - ..addAll( - [...widget.poll.questions] - ..sort((a, b) => a.order.compareTo(b.order)), - ); - if (!widget.isReadonly) { - _loadCurrentIntoLocalState(); - // If poll ID changes, reset modification state - _isModifying = false; - } + // Reset modification state when poll changes + _isModifying = false; } } @@ -108,7 +105,7 @@ class _PollSubmitState extends ConsumerState { super.dispose(); } - SnPollQuestion get _current => _questions[_index]; + SnPollQuestion get _current => _questions![_index]; void _loadCurrentIntoLocalState() { final q = _current; @@ -201,7 +198,7 @@ class _PollSubmitState extends ConsumerState { } } - Future _submitToServer() async { + Future _submitToServer(SnPollWithStats poll) async { // Persist current question before final submit _persistCurrentAnswer(); @@ -213,7 +210,7 @@ class _PollSubmitState extends ConsumerState { final dio = ref.read(apiClientProvider); await dio.post( - '/sphere/polls/${widget.poll.id}/answer', + '/sphere/polls/${poll.id}/answer', data: {'answer': _answers}, ); @@ -233,17 +230,17 @@ class _PollSubmitState extends ConsumerState { } } - void _next() { + void _next(SnPollWithStats poll) { if (_submitting) return; _persistCurrentAnswer(); - if (_index < _questions.length - 1) { + if (_index < _questions!.length - 1) { setState(() { _index++; _loadCurrentIntoLocalState(); }); } else { // Final submit to API - _submitToServer(); + _submitToServer(poll); } } @@ -261,41 +258,15 @@ class _PollSubmitState extends ConsumerState { } } - Widget _buildHeader(BuildContext context) { + Widget _buildHeader(BuildContext context, SnPollWithStats poll) { final q = _current; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.poll.title != null || widget.poll.description != null) - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.poll.title != null) - Text( - widget.poll.title!, - style: Theme.of(context).textTheme.titleLarge, - ), - if (widget.poll.description != null) - Padding( - padding: const EdgeInsets.only(top: 4), - child: Text( - widget.poll.description!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of( - context, - ).textTheme.bodyMedium?.color?.withOpacity(0.7), - ), - ), - ), - ], - ), - ), if (widget.showProgress && _isModifying) // Only show progress when modifying Text( - '${_index + 1} / ${_questions.length}', + '${_index + 1} / ${_questions!.length}', style: Theme.of(context).textTheme.labelMedium, ), Row( @@ -334,12 +305,18 @@ class _PollSubmitState extends ConsumerState { ); } - Widget _buildStats(BuildContext context, SnPollQuestion q) { - return PollStatsWidget(question: q, stats: widget.stats); + Widget _buildStats( + BuildContext context, + SnPollQuestion q, + Map? stats, + ) { + return PollStatsWidget(question: q, stats: stats); } - Widget _buildBody(BuildContext context) { - if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) { + Widget _buildBody(BuildContext context, SnPollWithStats poll) { + final hasUserAnswer = + poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty; + if (hasUserAnswer && !widget.isReadonly && !_isModifying) { return const SizedBox.shrink(); // Collapse input fields if already submitted and not modifying } final q = _current; @@ -449,11 +426,13 @@ class _PollSubmitState extends ConsumerState { ); } - Widget _buildNavBar(BuildContext context) { - final isLast = _index == _questions.length - 1; + Widget _buildNavBar(BuildContext context, SnPollWithStats poll) { + final isLast = _index == _questions!.length - 1; final canProceed = _isCurrentAnswered() && !_submitting; + final hasUserAnswer = + poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty; - if (widget.initialAnswers != null && !_isModifying && !widget.isReadonly) { + if (hasUserAnswer && !_isModifying && !widget.isReadonly) { // If poll is submitted and not in modification mode, show "Modify" button return FilledButton.icon( icon: const Icon(Icons.edit), @@ -498,32 +477,32 @@ class _PollSubmitState extends ConsumerState { ) : Icon(isLast ? Icons.check : Icons.arrow_forward), label: Text(isLast ? 'submit'.tr() : 'next'.tr()), - onPressed: canProceed ? _next : null, + onPressed: canProceed ? () => _next(poll) : null, ), ], ); } - Widget _buildSubmittedView(BuildContext context) { + Widget _buildSubmittedView(BuildContext context, SnPollWithStats poll) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.poll.title != null || widget.poll.description != null) + if (poll.title != null || poll.description != null) Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.poll.title?.isNotEmpty ?? false) + if (poll.title?.isNotEmpty ?? false) Text( - widget.poll.title!, + poll.title!, style: Theme.of(context).textTheme.titleLarge, ), - if (widget.poll.description?.isNotEmpty ?? false) + if (poll.description?.isNotEmpty ?? false) Padding( padding: const EdgeInsets.only(top: 4), child: Text( - widget.poll.description!, + poll.description!, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of( context, @@ -534,7 +513,7 @@ class _PollSubmitState extends ConsumerState { ], ), ), - for (final q in _questions) + for (final q in _questions!) Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Column( @@ -574,7 +553,7 @@ class _PollSubmitState extends ConsumerState { ), ), ), - _buildStats(context, q), + _buildStats(context, q, poll.stats), ], ), ), @@ -582,26 +561,26 @@ class _PollSubmitState extends ConsumerState { ); } - Widget _buildReadonlyView(BuildContext context) { + Widget _buildReadonlyView(BuildContext context, SnPollWithStats poll) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.poll.title != null || widget.poll.description != null) + if (poll.title != null || poll.description != null) Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.poll.title != null) + if (poll.title != null) Text( - widget.poll.title!, + poll.title!, style: Theme.of(context).textTheme.titleLarge, ), - if (widget.poll.description != null) + if (poll.description != null) Padding( padding: const EdgeInsets.only(top: 4), child: Text( - widget.poll.description!, + poll.description!, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of( context, @@ -612,7 +591,7 @@ class _PollSubmitState extends ConsumerState { ], ), ), - for (final q in _questions) + for (final q in _questions!) Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Column( @@ -652,7 +631,7 @@ class _PollSubmitState extends ConsumerState { ), ), ), - _buildStats(context, q), + _buildStats(context, q, poll.stats), ], ), ), @@ -660,7 +639,7 @@ class _PollSubmitState extends ConsumerState { ); } - Widget _buildCollapsedView(BuildContext context) { + Widget _buildCollapsedView(BuildContext context, SnPollWithStats poll) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -670,20 +649,20 @@ class _PollSubmitState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.poll.title != null) + if (poll.title != null) Text( - widget.poll.title!, + poll.title!, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), - if (widget.poll.description != null) + if (poll.description != null) Padding( padding: const EdgeInsets.only(top: 2), child: Text( - widget.poll.description!, + poll.description!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( context, @@ -697,7 +676,7 @@ class _PollSubmitState extends ConsumerState { Padding( padding: const EdgeInsets.only(top: 2), child: Text( - '${_questions.length} question${_questions.length == 1 ? '' : 's'}', + '${_questions!.length} question${_questions!.length == 1 ? '' : 's'}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( context, @@ -729,111 +708,156 @@ class _PollSubmitState extends ConsumerState { @override Widget build(BuildContext context) { - if (_questions.isEmpty) { - return const SizedBox.shrink(); - } + final pollAsync = ref.watch(pollWithStatsProvider(widget.pollId)); - // 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: [ - _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)], + return pollAsync.when( + loading: + () => const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), ), ), - ], - ); - } - - // If poll is in readonly mode, show readonly view - if (widget.isReadonly) { - 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), + error: + (error, stack) => Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text('Failed to load poll: $error'), + ), ), - ], - ); - } + data: (poll) { + // Initialize questions when data is available + _questions = [...poll.questions] + ..sort((a, b) => a.order.compareTo(b.order)); - 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: Column( - key: const ValueKey('normal_expanded'), + // Initialize answers from poll data + _initializeFromPollData(poll); + + if (_questions!.isEmpty) { + return const SizedBox.shrink(); + } + + // If collapsed, show collapsed view for all states + if (_isCollapsed) { + return _buildCollapsedView(context, poll); + } + + // If poll is already submitted and not in readonly mode, and not in modification mode, show submitted view + final hasUserAnswer = + poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty; + if (hasUserAnswer && !widget.isReadonly && !_isModifying) { + return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildHeader(context), - const SizedBox(height: 12), - _AnimatedStep( - key: ValueKey(_current.id), + _buildCollapsedView(context, poll), + 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: [ - _buildBody(context), - _buildStats(context, _current), + _buildSubmittedView(context, poll), + _buildNavBar(context, poll), ], ), ), - const SizedBox(height: 16), - _buildNavBar(context), ], - ), - ), - ], + ); + } + + // If poll is in readonly mode, show readonly view + if (widget.isReadonly) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCollapsedView(context, poll), + 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, poll), + ), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCollapsedView(context, poll), + 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: [ + _buildHeader(context, poll), + const SizedBox(height: 12), + _AnimatedStep( + key: ValueKey(_current.id), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildBody(context, poll), + _buildStats(context, _current, poll.stats), + ], + ), + ), + const SizedBox(height: 16), + _buildNavBar(context, poll), + ], + ), + ), + ], + ); + }, ); } }