👽 Make poll load itself to match server updates

This commit is contained in:
2025-11-02 21:47:37 +08:00
parent 59d38c0d8d
commit c0ab3837ac
8 changed files with 254 additions and 255 deletions

View File

@@ -8,7 +8,7 @@ part 'poll.g.dart';
@freezed
sealed class SnPollWithStats with _$SnPollWithStats {
const factory SnPollWithStats({
required Map<String, dynamic>? userAnswer,
required SnPollAnswer? userAnswer,
@Default({}) Map<String, dynamic> stats,
required String id,
required List<SnPollQuestion> questions,

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPollWithStats {
Map<String, dynamic>? get userAnswer; Map<String, dynamic> get stats; String get id; List<SnPollQuestion> 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<String, dynamic> get stats; String get id; List<SnPollQuestion> 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<SnPollWithStats> 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<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> 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<String, dynamic>?,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<String, dynamic>,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<SnPollQuestion>,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 extends Object?>(TResult Function( Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> 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 extends Object?>(TResult Function( SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> 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 extends Object?>(TResult Function( Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> 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 extends Object?>(TResult? Function( Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> 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<String, dynamic>? userAnswer, final Map<String, dynamic> stats = const {}, required this.id, required final List<SnPollQuestion> 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<String, dynamic> stats = const {}, required this.id, required final List<SnPollQuestion> 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<String, dynamic> json) => _$SnPollWithStatsFromJson(json);
final Map<String, dynamic>? _userAnswer;
@override Map<String, dynamic>? 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<String, dynamic> _stats;
@override@JsonKey() Map<String, dynamic> get stats {
if (_stats is EqualUnmodifiableMapView) return _stats;
@@ -261,12 +265,12 @@ Map<String, dynamic> 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<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> 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<String, dynamic>?,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<String, dynamic>,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<SnPollQuestion>,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));
});
}
}

View File

@@ -8,7 +8,12 @@ part of 'poll.dart';
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
_SnPollWithStats(
userAnswer: json['user_answer'] as Map<String, dynamic>?,
userAnswer:
json['user_answer'] == null
? null
: SnPollAnswer.fromJson(
json['user_answer'] as Map<String, dynamic>,
),
stats: json['stats'] as Map<String, dynamic>? ?? const {},
id: json['id'] as String,
questions:
@@ -32,7 +37,7 @@ _SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$SnPollWithStatsToJson(_SnPollWithStats instance) =>
<String, dynamic>{
'user_answer': instance.userAnswer,
'user_answer': instance.userAnswer?.toJson(),
'stats': instance.stats,
'id': instance.id,
'questions': instance.questions.map((e) => e.toJson()).toList(),

View File

@@ -7,7 +7,7 @@ part of 'activity_rpc.dart';
// **************************************************************************
String _$presenceActivitiesHash() =>
r'dcea3cad01b4010c0087f5281413d83a754c2a17';
r'3bfaa638eeb961ecd62a32d6a7760a6a7e7bf6f2';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -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<GoRouter>((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<GoRouter>((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<GoRouter>((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']!,
),
),
],
),
],

View File

@@ -7,7 +7,7 @@ part of 'explore.dart';
// **************************************************************************
String _$activityListNotifierHash() =>
r'a3ad3242f08139bef14a2f0fab6591ce8b3cb9f0';
r'77ffc7852feffa5438b56fa26123d453b7c310cf';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -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,

View File

@@ -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<String, dynamic> answers) onSubmit;
/// Optional initial answers, keyed by questionId.
final Map<String, dynamic>? initialAnswers;
final Map<String, dynamic>? stats;
/// Optional cancel callback.
final VoidCallback? onCancel;
@@ -45,7 +44,7 @@ class PollSubmit extends ConsumerStatefulWidget {
}
class _PollSubmitState extends ConsumerState<PollSubmit> {
late final List<SnPollQuestion> _questions;
List<SnPollQuestion>? _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<PollSubmit> {
@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<String, dynamic>.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<PollSubmit> {
}
}
void _initializeFromPollData(SnPollWithStats poll) {
// Initialize answers from poll data if available
if (poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty) {
_answers = Map<String, dynamic>.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<String, dynamic>.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<PollSubmit> {
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<PollSubmit> {
}
}
Future<void> _submitToServer() async {
Future<void> _submitToServer(SnPollWithStats poll) async {
// Persist current question before final submit
_persistCurrentAnswer();
@@ -213,7 +210,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
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<PollSubmit> {
}
}
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<PollSubmit> {
}
}
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<PollSubmit> {
);
}
Widget _buildStats(BuildContext context, SnPollQuestion q) {
return PollStatsWidget(question: q, stats: widget.stats);
Widget _buildStats(
BuildContext context,
SnPollQuestion q,
Map<String, dynamic>? 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<PollSubmit> {
);
}
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<PollSubmit> {
)
: 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<PollSubmit> {
],
),
),
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<PollSubmit> {
),
),
),
_buildStats(context, q),
_buildStats(context, q, poll.stats),
],
),
),
@@ -582,26 +561,26 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
);
}
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<PollSubmit> {
],
),
),
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<PollSubmit> {
),
),
),
_buildStats(context, q),
_buildStats(context, q, poll.stats),
],
),
),
@@ -660,7 +639,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
);
}
Widget _buildCollapsedView(BuildContext context) {
Widget _buildCollapsedView(BuildContext context, SnPollWithStats poll) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -670,20 +649,20 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
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<PollSubmit> {
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<PollSubmit> {
@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<Offset>(
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<Offset>(
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<Offset>(
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<Offset>(
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<Offset>(
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<Offset>(
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),
],
),
),
],
);
},
);
}
}