💄 Optimize poll
This commit is contained in:
@@ -761,6 +761,7 @@
|
||||
"pollsRecent": "Recent Polls",
|
||||
"pollCreateNew": "Create New",
|
||||
"pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.",
|
||||
"pollQuestions": "Questions",
|
||||
"publisher": "Publisher",
|
||||
"publisherHint": "Enter the publisher name",
|
||||
"publisherCannotBeEmpty": "Publisher cannot be empty",
|
||||
|
@@ -8,7 +8,7 @@ part 'poll.g.dart';
|
||||
sealed class SnPollWithStats with _$SnPollWithStats {
|
||||
const factory SnPollWithStats({
|
||||
required Map<String, dynamic>? userAnswer,
|
||||
required Map<String, dynamic> stats,
|
||||
@Default({}) Map<String, dynamic> stats,
|
||||
required String id,
|
||||
required List<SnPollQuestion> questions,
|
||||
String? title,
|
||||
|
@@ -213,7 +213,7 @@ 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, required final Map<String, dynamic> stats, 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 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;
|
||||
factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json);
|
||||
|
||||
final Map<String, dynamic>? _userAnswer;
|
||||
@@ -226,7 +226,7 @@ class _SnPollWithStats implements SnPollWithStats {
|
||||
}
|
||||
|
||||
final Map<String, dynamic> _stats;
|
||||
@override Map<String, dynamic> get stats {
|
||||
@override@JsonKey() Map<String, dynamic> get stats {
|
||||
if (_stats is EqualUnmodifiableMapView) return _stats;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_stats);
|
||||
|
@@ -9,7 +9,7 @@ part of 'poll.dart';
|
||||
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
||||
_SnPollWithStats(
|
||||
userAnswer: json['user_answer'] as Map<String, dynamic>?,
|
||||
stats: json['stats'] as Map<String, dynamic>,
|
||||
stats: json['stats'] as Map<String, dynamic>? ?? const {},
|
||||
id: json['id'] as String,
|
||||
questions:
|
||||
(json['questions'] as List<dynamic>)
|
||||
|
@@ -14,17 +14,19 @@ part 'poll_list.g.dart';
|
||||
|
||||
@riverpod
|
||||
class PollListNotifier extends _$PollListNotifier
|
||||
with CursorPagingNotifierMixin<SnPoll> {
|
||||
with CursorPagingNotifierMixin<SnPollWithStats> {
|
||||
static const int _pageSize = 20;
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnPoll>> build(String? pubName) {
|
||||
Future<CursorPagingData<SnPollWithStats>> build(String? pubName) {
|
||||
// immediately load first page
|
||||
return fetch(cursor: null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnPoll>> fetch({required String? cursor}) async {
|
||||
Future<CursorPagingData<SnPollWithStats>> fetch({
|
||||
required String? cursor,
|
||||
}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||
|
||||
@@ -42,7 +44,7 @@ class PollListNotifier extends _$PollListNotifier
|
||||
);
|
||||
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final List<dynamic> data = response.data;
|
||||
final items = data.map((json) => SnPoll.fromJson(json)).toList();
|
||||
final items = data.map((json) => SnPollWithStats.fromJson(json)).toList();
|
||||
|
||||
final hasMore = offset + items.length < total;
|
||||
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
||||
@@ -55,6 +57,13 @@ class PollListNotifier extends _$PollListNotifier
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnPollWithStats> pollWithStats(Ref ref, String id) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/sphere/polls/$id');
|
||||
return SnPollWithStats.fromJson(resp.data);
|
||||
}
|
||||
|
||||
class CreatorPollListScreen extends HookConsumerWidget {
|
||||
const CreatorPollListScreen({super.key, required this.pubName});
|
||||
|
||||
@@ -64,7 +73,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
||||
final result = await GoRouter.of(
|
||||
context,
|
||||
).pushNamed('creatorPollNew', pathParameters: {'name': pubName});
|
||||
if (result is SnPoll && context.mounted) {
|
||||
if (result is SnPollWithStats && context.mounted) {
|
||||
Navigator.of(context).maybePop(result);
|
||||
}
|
||||
}
|
||||
@@ -92,8 +101,11 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
final poll = data.items[index];
|
||||
return _CreatorPollItem(poll: poll, pubName: pubName);
|
||||
final pollWithStats = data.items[index];
|
||||
return _CreatorPollItem(
|
||||
pollWithStats: pollWithStats,
|
||||
pubName: pubName,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -106,14 +118,14 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
||||
|
||||
class _CreatorPollItem extends StatelessWidget {
|
||||
final String pubName;
|
||||
const _CreatorPollItem({required this.poll, required this.pubName});
|
||||
const _CreatorPollItem({required this.pollWithStats, required this.pubName});
|
||||
|
||||
final SnPoll poll;
|
||||
final SnPollWithStats pollWithStats;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final ended = poll.endedAt;
|
||||
final ended = pollWithStats.endedAt;
|
||||
final endedText =
|
||||
ended == null
|
||||
? 'No end'
|
||||
@@ -123,15 +135,16 @@ class _CreatorPollItem extends StatelessWidget {
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ListTile(
|
||||
title: Text(poll.title ?? 'Untitled poll'),
|
||||
title: Text(pollWithStats.title ?? 'Untitled poll'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (poll.description != null && poll.description!.isNotEmpty)
|
||||
if (pollWithStats.description != null &&
|
||||
pollWithStats.description!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
poll.description!,
|
||||
pollWithStats.description!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -139,7 +152,7 @@ class _CreatorPollItem extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'Questions: ${poll.questions.length} · Ends: $endedText',
|
||||
'Questions: ${pollWithStats.questions.length} · Ends: $endedText',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
@@ -159,7 +172,7 @@ class _CreatorPollItem extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'creatorPollEdit',
|
||||
pathParameters: {'name': pubName, 'id': poll.id},
|
||||
pathParameters: {'name': pubName, 'id': pollWithStats.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -170,8 +183,7 @@ class _CreatorPollItem extends StatelessWidget {
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => PollFeedbackSheet(pollId: poll.id, poll: poll),
|
||||
builder: (context) => PollFeedbackSheet(pollId: pollWithStats.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@@ -6,7 +6,7 @@ part of 'poll_list.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$pollListNotifierHash() => r'd3da24ff6bbb8f35b06d57fc41625dc0312508e4';
|
||||
String _$pollWithStatsHash() => r'6bb910046ce1e09368f9922dbec52fdc2cc86740';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -29,11 +29,133 @@ class _SystemHash {
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [pollWithStats].
|
||||
@ProviderFor(pollWithStats)
|
||||
const pollWithStatsProvider = PollWithStatsFamily();
|
||||
|
||||
/// See also [pollWithStats].
|
||||
class PollWithStatsFamily extends Family<AsyncValue<SnPollWithStats>> {
|
||||
/// See also [pollWithStats].
|
||||
const PollWithStatsFamily();
|
||||
|
||||
/// See also [pollWithStats].
|
||||
PollWithStatsProvider call(String id) {
|
||||
return PollWithStatsProvider(id);
|
||||
}
|
||||
|
||||
@override
|
||||
PollWithStatsProvider getProviderOverride(
|
||||
covariant PollWithStatsProvider provider,
|
||||
) {
|
||||
return call(provider.id);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'pollWithStatsProvider';
|
||||
}
|
||||
|
||||
/// See also [pollWithStats].
|
||||
class PollWithStatsProvider extends AutoDisposeFutureProvider<SnPollWithStats> {
|
||||
/// See also [pollWithStats].
|
||||
PollWithStatsProvider(String id)
|
||||
: this._internal(
|
||||
(ref) => pollWithStats(ref as PollWithStatsRef, id),
|
||||
from: pollWithStatsProvider,
|
||||
name: r'pollWithStatsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$pollWithStatsHash,
|
||||
dependencies: PollWithStatsFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
PollWithStatsFamily._allTransitiveDependencies,
|
||||
id: id,
|
||||
);
|
||||
|
||||
PollWithStatsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.id,
|
||||
}) : super.internal();
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<SnPollWithStats> Function(PollWithStatsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: PollWithStatsProvider._internal(
|
||||
(ref) => create(ref as PollWithStatsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
id: id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<SnPollWithStats> createElement() {
|
||||
return _PollWithStatsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PollWithStatsProvider && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, id.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin PollWithStatsRef on AutoDisposeFutureProviderRef<SnPollWithStats> {
|
||||
/// The parameter `id` of this provider.
|
||||
String get id;
|
||||
}
|
||||
|
||||
class _PollWithStatsProviderElement
|
||||
extends AutoDisposeFutureProviderElement<SnPollWithStats>
|
||||
with PollWithStatsRef {
|
||||
_PollWithStatsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get id => (origin as PollWithStatsProvider).id;
|
||||
}
|
||||
|
||||
String _$pollListNotifierHash() => r'd5b822e737788be8982f5cb3b501d460441930c1';
|
||||
|
||||
abstract class _$PollListNotifier
|
||||
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPoll>> {
|
||||
extends
|
||||
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPollWithStats>> {
|
||||
late final String? pubName;
|
||||
|
||||
FutureOr<CursorPagingData<SnPoll>> build(String? pubName);
|
||||
FutureOr<CursorPagingData<SnPollWithStats>> build(String? pubName);
|
||||
}
|
||||
|
||||
/// See also [PollListNotifier].
|
||||
@@ -42,7 +164,7 @@ const pollListNotifierProvider = PollListNotifierFamily();
|
||||
|
||||
/// See also [PollListNotifier].
|
||||
class PollListNotifierFamily
|
||||
extends Family<AsyncValue<CursorPagingData<SnPoll>>> {
|
||||
extends Family<AsyncValue<CursorPagingData<SnPollWithStats>>> {
|
||||
/// See also [PollListNotifier].
|
||||
const PollListNotifierFamily();
|
||||
|
||||
@@ -78,7 +200,7 @@ class PollListNotifierProvider
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderImpl<
|
||||
PollListNotifier,
|
||||
CursorPagingData<SnPoll>
|
||||
CursorPagingData<SnPollWithStats>
|
||||
> {
|
||||
/// See also [PollListNotifier].
|
||||
PollListNotifierProvider(String? pubName)
|
||||
@@ -109,7 +231,7 @@ class PollListNotifierProvider
|
||||
final String? pubName;
|
||||
|
||||
@override
|
||||
FutureOr<CursorPagingData<SnPoll>> runNotifierBuild(
|
||||
FutureOr<CursorPagingData<SnPollWithStats>> runNotifierBuild(
|
||||
covariant PollListNotifier notifier,
|
||||
) {
|
||||
return notifier.build(pubName);
|
||||
@@ -134,7 +256,7 @@ class PollListNotifierProvider
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<
|
||||
PollListNotifier,
|
||||
CursorPagingData<SnPoll>
|
||||
CursorPagingData<SnPollWithStats>
|
||||
>
|
||||
createElement() {
|
||||
return _PollListNotifierProviderElement(this);
|
||||
@@ -157,7 +279,7 @@ class PollListNotifierProvider
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin PollListNotifierRef
|
||||
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPoll>> {
|
||||
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPollWithStats>> {
|
||||
/// The parameter `pubName` of this provider.
|
||||
String? get pubName;
|
||||
}
|
||||
@@ -166,7 +288,7 @@ class _PollListNotifierProviderElement
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderElement<
|
||||
PollListNotifier,
|
||||
CursorPagingData<SnPoll>
|
||||
CursorPagingData<SnPollWithStats>
|
||||
>
|
||||
with PollListNotifierRef {
|
||||
_PollListNotifierProviderElement(super.provider);
|
||||
|
@@ -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(
|
||||
|
233
lib/widgets/poll/poll_stats_widget.dart
Normal file
233
lib/widgets/poll/poll_stats_widget.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -186,10 +186,9 @@ class ComposePollSheet extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildPollSubtitle(SnPoll poll) {
|
||||
Widget? _buildPollSubtitle(SnPollWithStats poll) {
|
||||
try {
|
||||
final SnPoll dyn = poll;
|
||||
final List<SnPollQuestion> options = dyn.questions;
|
||||
final List<SnPollQuestion> options = poll.questions;
|
||||
if (options.isEmpty) return null;
|
||||
final preview = options.take(3).map((e) => e.title).join(' · ');
|
||||
if (preview.trim().isEmpty) return null;
|
||||
|
@@ -354,6 +354,7 @@ class PostItem extends HookConsumerWidget {
|
||||
final translatedWidget =
|
||||
(translatedText.value?.isNotEmpty ?? false)
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@@ -398,6 +399,7 @@ class PostItem extends HookConsumerWidget {
|
||||
: null;
|
||||
|
||||
final translationSection = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (translatedWidget != null) translatedWidget,
|
||||
if (translatableWidget != null) translatableWidget,
|
||||
|
@@ -639,13 +639,18 @@ class PostBody extends ConsumerWidget {
|
||||
if (!isFullPost && item.type == 1)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
margin: EdgeInsets.only(
|
||||
top: 4,
|
||||
left: renderingPadding.horizontal,
|
||||
right: renderingPadding.vertical,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
Reference in New Issue
Block a user