💄 Optimize poll

This commit is contained in:
2025-08-12 22:52:05 +08:00
parent 3a36915140
commit 16987c758e
12 changed files with 653 additions and 332 deletions

View File

@@ -1,9 +1,14 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_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/services/time.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/poll/poll_stats_widget.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -52,59 +57,60 @@ class PollFeedbackNotifier extends _$PollFeedbackNotifier
class PollFeedbackSheet extends HookConsumerWidget {
final String pollId;
final String? title;
final SnPoll poll;
final Map<String, dynamic>? stats; // stats object similar to PollSubmit
const PollFeedbackSheet({
super.key,
required this.pollId,
required this.poll,
this.title,
this.stats,
});
const PollFeedbackSheet({super.key, required this.pollId, this.title});
@override
Widget build(BuildContext context, WidgetRef ref) {
final poll = ref.watch(pollWithStatsProvider(pollId));
return SheetScaffold(
titleText: title ?? 'Poll feedback',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_PollHeader(poll: poll, stats: stats),
const Divider(height: 1),
Expanded(
child: PagingHelperView(
provider: pollFeedbackNotifierProvider(pollId),
futureRefreshable: pollFeedbackNotifierProvider(pollId).future,
notifierRefreshable:
pollFeedbackNotifierProvider(pollId).notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
// Provided by PagingHelperView to indicate end/loading
return endItemView;
}
final answer = data.items[index];
return _PollAnswerTile(answer: answer, poll: poll);
},
separatorBuilder:
(context, index) =>
const Divider(height: 1).padding(vertical: 4),
),
child: poll.when(
data:
(data) => CustomScrollView(
slivers: [
SliverToBoxAdapter(child: _PollHeader(poll: data)),
SliverToBoxAdapter(child: const Divider(height: 1)),
SliverGap(4),
PagingHelperSliverView(
provider: pollFeedbackNotifierProvider(pollId),
futureRefreshable:
pollFeedbackNotifierProvider(pollId).future,
notifierRefreshable:
pollFeedbackNotifierProvider(pollId).notifier,
contentBuilder:
(val, widgetCount, endItemView) => SliverList.separated(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
// Provided by PagingHelperView to indicate end/loading
return endItemView;
}
final answer = val.items[index];
return _PollAnswerTile(answer: answer, poll: data);
},
separatorBuilder:
(context, index) =>
const Divider(height: 1).padding(vertical: 4),
),
),
SliverGap(4 + MediaQuery.of(context).padding.bottom),
],
),
),
],
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(pollWithStatsProvider(pollId)),
),
loading: () => ResponseLoadingWidget(),
),
);
}
}
class _PollHeader extends StatelessWidget {
const _PollHeader({required this.poll, this.stats});
final SnPoll poll;
final Map<String, dynamic>? stats;
const _PollHeader({required this.poll});
final SnPollWithStats poll;
@override
Widget build(BuildContext context) {
@@ -112,18 +118,32 @@ class _PollHeader extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
if (poll.title != null)
Text(poll.title!, style: theme.textTheme.titleLarge),
if (poll.description != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
poll.description!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7),
),
),
if (poll.title != null || (poll.description?.isNotEmpty ?? false))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (poll.title != null)
Text(poll.title!, style: theme.textTheme.titleLarge),
if (poll.description?.isNotEmpty ?? false)
Text(
poll.description!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7),
),
),
],
),
Text('pollQuestions').tr().fontSize(17).bold(),
for (final q in poll.questions)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (q.title.isNotEmpty) Text(q.title).bold(),
if (q.description?.isNotEmpty ?? false) Text(q.description!),
PollStatsWidget(question: q, stats: poll.stats),
],
),
],
).padding(horizontal: 20, vertical: 16);
@@ -132,7 +152,7 @@ class _PollHeader extends StatelessWidget {
class _PollAnswerTile extends StatelessWidget {
final SnPollAnswer answer;
final SnPoll poll;
final SnPollWithStats poll;
const _PollAnswerTile({required this.answer, required this.poll});
String _formatPerQuestionAnswer(

View File

@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:island/models/poll.dart';
class PollStatsWidget extends StatelessWidget {
const PollStatsWidget({
super.key,
required this.question,
required this.stats,
});
final SnPollQuestion question;
final Map<String, dynamic>? stats;
@override
Widget build(BuildContext context) {
if (stats == null) return const SizedBox.shrink();
final raw = stats![question.id];
if (raw == null) return const SizedBox.shrink();
Widget? body;
switch (question.type) {
case SnPollQuestionType.rating:
// rating: avg score (double or int)
final avg = (raw['rating'] as num?)?.toDouble();
if (avg == null) break;
final theme = Theme.of(context);
body = Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(Icons.star, color: Colors.amber.shade600, size: 18),
const SizedBox(width: 6),
Text(
avg.toStringAsFixed(1),
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
);
break;
case SnPollQuestionType.yesNo:
// yes/no: map {true: count, false: count}
if (raw is Map) {
final int yes =
(raw[true] is int)
? raw[true] as int
: int.tryParse('${raw[true]}') ?? 0;
final int no =
(raw[false] is int)
? raw[false] as int
: int.tryParse('${raw[false]}') ?? 0;
final total = (yes + no).clamp(0, 1 << 31);
final yesPct = total == 0 ? 0.0 : yes / total;
final noPct = total == 0 ? 0.0 : no / total;
final theme = Theme.of(context);
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_BarStatRow(
label: 'Yes',
count: yes,
fraction: yesPct,
color: Colors.green.shade600,
),
const SizedBox(height: 6),
_BarStatRow(
label: 'No',
count: no,
fraction: noPct,
color: Colors.red.shade600,
),
const SizedBox(height: 4),
Text(
'Total: $total',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
);
}
break;
case SnPollQuestionType.singleChoice:
case SnPollQuestionType.multipleChoice:
// map optionId -> count
if (raw is Map) {
final options = [...?question.options]
..sort((a, b) => a.order.compareTo(b.order));
final List<_OptionCount> items = [];
int total = 0;
for (final opt in options) {
final dynamic v = raw[opt.id];
final int count = v is int ? v : int.tryParse('$v') ?? 0;
total += count;
items.add(_OptionCount(id: opt.id, label: opt.label, count: count));
}
if (items.isNotEmpty) {
items.sort(
(a, b) => b.count.compareTo(a.count),
); // show highest first
}
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final it in items)
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: _BarStatRow(
label: it.label,
count: it.count,
fraction: total == 0 ? 0 : it.count / total,
),
),
if (items.isNotEmpty)
Text(
'Total: $total',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
break;
case SnPollQuestionType.freeText:
// No stats
break;
}
if (body == null) return Text('No stats available');
return Padding(
padding: const EdgeInsets.only(top: 8),
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.35),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Stats',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
body,
],
),
),
),
);
}
}
class _OptionCount {
final String id;
final String label;
final int count;
const _OptionCount({
required this.id,
required this.label,
required this.count,
});
}
class _BarStatRow extends StatelessWidget {
const _BarStatRow({
required this.label,
required this.count,
required this.fraction,
this.color,
});
final String label;
final int count;
final double fraction;
final Color? color;
@override
Widget build(BuildContext context) {
final barColor = color ?? Theme.of(context).colorScheme.primary;
final bgColor = Theme.of(
context,
).colorScheme.surfaceVariant.withOpacity(0.6);
final fg =
(fraction.isNaN || fraction.isInfinite)
? 0.0
: fraction.clamp(0.0, 1.0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$label · $count', style: Theme.of(context).textTheme.labelMedium),
const SizedBox(height: 4),
LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final filled = width * fg;
return Stack(
children: [
Container(
height: 8,
width: width,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(999),
),
),
Container(
height: 8,
width: filled,
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(999),
),
),
],
);
},
),
],
);
}
}

View File

@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/poll/poll_stats_widget.dart';
class PollSubmit extends ConsumerStatefulWidget {
const PollSubmit({
@@ -42,6 +43,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
late final List<SnPollQuestion> _questions;
int _index = 0;
bool _submitting = false;
bool _isModifying = false; // New state to track if user is modifying answers
/// Collected answers, keyed by questionId
late Map<String, dynamic> _answers;
@@ -64,6 +66,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
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) {
_isModifying = false;
}
}
}
@@ -81,6 +88,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
);
if (!widget.isReadonly) {
_loadCurrentIntoLocalState();
// If poll ID changes, reset modification state
_isModifying = false;
}
}
}
@@ -266,16 +275,17 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
child: Text(
widget.poll.description!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(
context,
).textTheme.bodyMedium?.color?.withOpacity(0.7),
),
color: Theme.of(
context,
).textTheme.bodyMedium?.color?.withOpacity(0.7),
),
),
),
],
),
),
if (widget.showProgress)
if (widget.showProgress &&
_isModifying) // Only show progress when modifying
Text(
'${_index + 1} / ${_questions.length}',
style: Theme.of(context).textTheme.labelMedium,
@@ -294,8 +304,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
child: Text(
'*',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
color: Theme.of(context).colorScheme.error,
),
),
),
],
@@ -306,10 +316,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
child: Text(
q.description!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).textTheme.bodySmall?.color?.withOpacity(0.7),
),
color: Theme.of(
context,
).textTheme.bodySmall?.color?.withOpacity(0.7),
),
),
),
],
@@ -317,152 +327,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
}
Widget _buildStats(BuildContext context, SnPollQuestion q) {
if (widget.stats == null) return const SizedBox.shrink();
final raw = widget.stats![q.id];
if (raw == null) return const SizedBox.shrink();
Widget? body;
switch (q.type) {
case SnPollQuestionType.rating:
// rating: avg score (double or int)
final avg = (raw['rating'] as num?)?.toDouble();
if (avg == null) break;
final theme = Theme.of(context);
body = Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(Icons.star, color: Colors.amber.shade600, size: 18),
const SizedBox(width: 6),
Text(
avg.toStringAsFixed(1),
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
);
break;
case SnPollQuestionType.yesNo:
// yes/no: map {true: count, false: count}
if (raw is Map) {
final int yes = (raw[true] is int)
? raw[true] as int
: int.tryParse('${raw[true]}') ?? 0;
final int no = (raw[false] is int)
? raw[false] as int
: int.tryParse('${raw[false]}') ?? 0;
final total = (yes + no).clamp(0, 1 << 31);
final yesPct = total == 0 ? 0.0 : yes / total;
final noPct = total == 0 ? 0.0 : no / total;
final theme = Theme.of(context);
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_BarStatRow(
label: 'Yes',
count: yes,
fraction: yesPct,
color: Colors.green.shade600,
),
const SizedBox(height: 6),
_BarStatRow(
label: 'No',
count: no,
fraction: noPct,
color: Colors.red.shade600,
),
const SizedBox(height: 4),
Text(
'Total: $total',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
);
}
break;
case SnPollQuestionType.singleChoice:
case SnPollQuestionType.multipleChoice:
// map optionId -> count
if (raw is Map) {
final options = [...?q.options]
..sort((a, b) => a.order.compareTo(b.order));
final List<_OptionCount> items = [];
int total = 0;
for (final opt in options) {
final dynamic v = raw[opt.id];
final int count = v is int ? v : int.tryParse('$v') ?? 0;
total += count;
items.add(_OptionCount(id: opt.id, label: opt.label, count: count));
}
if (items.isNotEmpty) {
items.sort(
(a, b) => b.count.compareTo(a.count),
); // show highest first
}
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final it in items)
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: _BarStatRow(
label: it.label,
count: it.count,
fraction: total == 0 ? 0 : it.count / total,
),
),
if (items.isNotEmpty)
Text(
'Total: $total',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
break;
case SnPollQuestionType.freeText:
// No stats
break;
}
if (body == null) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 8),
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.35),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Stats',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
body,
],
),
),
),
);
return PollStatsWidget(question: q, stats: widget.stats);
}
Widget _buildBody(BuildContext context) {
if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
return const SizedBox.shrink(); // Collapse input fields if already submitted and not modifying
}
final q = _current;
switch (q.type) {
case SnPollQuestionType.singleChoice:
@@ -573,22 +444,55 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
final isLast = _index == _questions.length - 1;
final canProceed = _isCurrentAnswered() && !_submitting;
if (widget.initialAnswers != null && !_isModifying && !widget.isReadonly) {
// If poll is submitted and not in modification mode, show "Modify" button
return Row(
children: [
const Spacer(),
FilledButton.icon(
icon: const Icon(Icons.edit),
label: const Text('Modify Answers'),
onPressed: () {
setState(() {
_isModifying = true;
_index = 0; // Reset to first question for modification
_loadCurrentIntoLocalState();
});
},
),
],
);
}
return Row(
children: [
OutlinedButton.icon(
icon: const Icon(Icons.arrow_back),
label: Text(_index == 0 ? 'Cancel' : 'Back'),
onPressed: _submitting ? null : _back,
onPressed:
_submitting
? null
: () {
if (_index == 0 && _isModifying) {
// If at first question and in modification mode, go back to submitted view
setState(() {
_isModifying = false;
});
} else {
_back();
}
},
),
const Spacer(),
FilledButton.icon(
icon: _submitting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(isLast ? Icons.check : Icons.arrow_forward),
icon:
_submitting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(isLast ? Icons.check : Icons.arrow_forward),
label: Text(isLast ? 'Submit' : 'Next'),
onPressed: canProceed ? _next : null,
),
@@ -596,6 +500,84 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
);
}
Widget _buildSubmittedView(BuildContext context) {
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?.isNotEmpty ?? false)
Text(
widget.poll.title!,
style: Theme.of(context).textTheme.titleLarge,
),
if (widget.poll.description?.isNotEmpty ?? false)
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),
),
),
),
],
),
),
for (final q in _questions)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
q.title,
style: Theme.of(context).textTheme.titleMedium,
),
),
if (q.isRequired)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'*',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
if (q.description != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
q.description!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).textTheme.bodySmall?.color?.withOpacity(0.7),
),
),
),
_buildStats(context, q),
],
),
),
],
);
}
Widget _buildReadonlyView(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -617,10 +599,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
child: Text(
widget.poll.description!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(
context,
).textTheme.bodyMedium?.color?.withOpacity(0.7),
),
color: Theme.of(
context,
).textTheme.bodyMedium?.color?.withOpacity(0.7),
),
),
),
],
@@ -645,9 +627,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
padding: const EdgeInsets.only(left: 8),
child: Text(
'*',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
],
@@ -658,10 +642,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
child: Text(
q.description!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).textTheme.bodySmall?.color?.withOpacity(0.7),
),
color: Theme.of(
context,
).textTheme.bodySmall?.color?.withOpacity(0.7),
),
),
),
_buildStats(context, q),
@@ -678,6 +662,19 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
return const SizedBox.shrink();
}
// 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),
const SizedBox(height: 16),
_buildNavBar(context),
],
);
}
// If poll is in readonly mode, show readonly view
if (widget.isReadonly) {
return _buildReadonlyView(context);
}
@@ -701,76 +698,6 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
}
}
class _OptionCount {
final String id;
final String label;
final int count;
const _OptionCount({
required this.id,
required this.label,
required this.count,
});
}
class _BarStatRow extends StatelessWidget {
const _BarStatRow({
required this.label,
required this.count,
required this.fraction,
this.color,
});
final String label;
final int count;
final double fraction;
final Color? color;
@override
Widget build(BuildContext context) {
final barColor = color ?? Theme.of(context).colorScheme.primary;
final bgColor = Theme.of(
context,
).colorScheme.surfaceVariant.withOpacity(0.6);
final fg = (fraction.isNaN || fraction.isInfinite)
? 0.0
: fraction.clamp(0.0, 1.0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$label · $count', style: Theme.of(context).textTheme.labelMedium),
const SizedBox(height: 4),
LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final filled = width * fg;
return Stack(
children: [
Container(
height: 8,
width: width,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(999),
),
),
Container(
height: 8,
width: filled,
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(999),
),
),
],
);
},
),
],
);
}
}
/// Simple fade/slide transition between questions.
class _AnimatedStep extends StatelessWidget {
const _AnimatedStep({super.key, required this.child});
@@ -794,4 +721,4 @@ class _AnimatedStep extends StatelessWidget {
child: child,
);
}
}
}