Merge branch 'v3' of https://github.com/Solsynth/Solian into v3
This commit is contained in:
@@ -761,6 +761,7 @@
|
|||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
"pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.",
|
"pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.",
|
||||||
|
"pollQuestions": "Questions",
|
||||||
"publisher": "Publisher",
|
"publisher": "Publisher",
|
||||||
"publisherHint": "Enter the publisher name",
|
"publisherHint": "Enter the publisher name",
|
||||||
"publisherCannotBeEmpty": "Publisher cannot be empty",
|
"publisherCannotBeEmpty": "Publisher cannot be empty",
|
||||||
@@ -798,5 +799,42 @@
|
|||||||
"filesListAdditional": {
|
"filesListAdditional": {
|
||||||
"one": "+{} file remaining",
|
"one": "+{} file remaining",
|
||||||
"other": "+{} files remaining"
|
"other": "+{} files remaining"
|
||||||
}
|
},
|
||||||
}
|
"pollAnswerSubmitted": "Poll answer has been submitted.",
|
||||||
|
"modifyAnswers": "Modify Answers",
|
||||||
|
"back": "Back",
|
||||||
|
"submit": "Submit",
|
||||||
|
"pollOptionDefaultLabel": "Option 1",
|
||||||
|
"pollUpdated": "Poll updated.",
|
||||||
|
"pollCreated": "Poll created.",
|
||||||
|
"pollCreate": "Create Poll",
|
||||||
|
"pollEdit": "Edit Poll",
|
||||||
|
"pollPreviewJsonDebug": "Debug Preview",
|
||||||
|
"pollTitleRequired": "Title is required",
|
||||||
|
"pollEndDateOptional": "End date & time (optional)",
|
||||||
|
"notSet": "Not set",
|
||||||
|
"pick": "Pick",
|
||||||
|
"clear": "Clear",
|
||||||
|
"questions": "Questions",
|
||||||
|
"pollAddQuestion": "Add question",
|
||||||
|
"pollQuestionTypeSingleChoice": "Single choice",
|
||||||
|
"pollQuestionTypeMultipleChoice": "Multiple choice",
|
||||||
|
"pollQuestionTypeFreeText": "Free text",
|
||||||
|
"pollQuestionTypeYesNo": "Yes / No",
|
||||||
|
"pollQuestionTypeRating": "Rating",
|
||||||
|
"pollNoQuestionsYet": "No questions yet",
|
||||||
|
"pollNoQuestionsHint": "Use \"Add question\" to start building your poll.",
|
||||||
|
"pollDebugPreview": "Debug Preview",
|
||||||
|
"pollUntitledQuestion": "Untitled question",
|
||||||
|
"moveUp": "Move up",
|
||||||
|
"moveDown": "Move down",
|
||||||
|
"required": "Required",
|
||||||
|
"pollQuestionTitle": "Question title",
|
||||||
|
"pollQuestionTitleRequired": "Question title is required",
|
||||||
|
"pollQuestionDescriptionOptional": "Question description (optional)",
|
||||||
|
"options": "Options",
|
||||||
|
"pollAddOption": "Add option",
|
||||||
|
"pollOptionLabel": "Option label",
|
||||||
|
"pollLongTextAnswerPreview": "Long text answer (preview)",
|
||||||
|
"pollShortTextAnswerPreview": "Short text answer (preview)"
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ part 'poll.g.dart';
|
|||||||
sealed class SnPollWithStats with _$SnPollWithStats {
|
sealed class SnPollWithStats with _$SnPollWithStats {
|
||||||
const factory SnPollWithStats({
|
const factory SnPollWithStats({
|
||||||
required Map<String, dynamic>? userAnswer,
|
required Map<String, dynamic>? userAnswer,
|
||||||
required Map<String, dynamic> stats,
|
@Default({}) Map<String, dynamic> stats,
|
||||||
required String id,
|
required String id,
|
||||||
required List<SnPollQuestion> questions,
|
required List<SnPollQuestion> questions,
|
||||||
String? title,
|
String? title,
|
||||||
|
@@ -213,7 +213,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _SnPollWithStats implements SnPollWithStats {
|
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);
|
factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json);
|
||||||
|
|
||||||
final Map<String, dynamic>? _userAnswer;
|
final Map<String, dynamic>? _userAnswer;
|
||||||
@@ -226,7 +226,7 @@ class _SnPollWithStats implements SnPollWithStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, dynamic> _stats;
|
final Map<String, dynamic> _stats;
|
||||||
@override Map<String, dynamic> get stats {
|
@override@JsonKey() Map<String, dynamic> get stats {
|
||||||
if (_stats is EqualUnmodifiableMapView) return _stats;
|
if (_stats is EqualUnmodifiableMapView) return _stats;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
return EqualUnmodifiableMapView(_stats);
|
return EqualUnmodifiableMapView(_stats);
|
||||||
|
@@ -9,7 +9,7 @@ part of 'poll.dart';
|
|||||||
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
||||||
_SnPollWithStats(
|
_SnPollWithStats(
|
||||||
userAnswer: json['user_answer'] as Map<String, dynamic>?,
|
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,
|
id: json['id'] as String,
|
||||||
questions:
|
questions:
|
||||||
(json['questions'] as List<dynamic>)
|
(json['questions'] as List<dynamic>)
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:firebase_analytics/observer.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
@@ -166,7 +166,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
|||||||
webAuthenticationOptions: WebAuthenticationOptions(
|
webAuthenticationOptions: WebAuthenticationOptions(
|
||||||
clientId: 'dev.solsynth.solarpass',
|
clientId: 'dev.solsynth.solarpass',
|
||||||
redirectUri: Uri.parse(
|
redirectUri: Uri.parse(
|
||||||
'https://nt.solian.app/auth/callback/apple',
|
'https://id.solian.app/auth/callback/apple',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -14,17 +14,19 @@ part 'poll_list.g.dart';
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class PollListNotifier extends _$PollListNotifier
|
class PollListNotifier extends _$PollListNotifier
|
||||||
with CursorPagingNotifierMixin<SnPoll> {
|
with CursorPagingNotifierMixin<SnPollWithStats> {
|
||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<SnPoll>> build(String? pubName) {
|
Future<CursorPagingData<SnPollWithStats>> build(String? pubName) {
|
||||||
// immediately load first page
|
// immediately load first page
|
||||||
return fetch(cursor: null);
|
return fetch(cursor: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<SnPoll>> fetch({required String? cursor}) async {
|
Future<CursorPagingData<SnPollWithStats>> fetch({
|
||||||
|
required String? cursor,
|
||||||
|
}) async {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
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 total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||||
final List<dynamic> data = response.data;
|
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 hasMore = offset + items.length < total;
|
||||||
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
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 {
|
class CreatorPollListScreen extends HookConsumerWidget {
|
||||||
const CreatorPollListScreen({super.key, required this.pubName});
|
const CreatorPollListScreen({super.key, required this.pubName});
|
||||||
|
|
||||||
@@ -64,7 +73,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
|||||||
final result = await GoRouter.of(
|
final result = await GoRouter.of(
|
||||||
context,
|
context,
|
||||||
).pushNamed('creatorPollNew', pathParameters: {'name': pubName});
|
).pushNamed('creatorPollNew', pathParameters: {'name': pubName});
|
||||||
if (result is SnPoll && context.mounted) {
|
if (result is SnPollWithStats && context.mounted) {
|
||||||
Navigator.of(context).maybePop(result);
|
Navigator.of(context).maybePop(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,8 +101,11 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
|||||||
if (index == widgetCount - 1) {
|
if (index == widgetCount - 1) {
|
||||||
return endItemView;
|
return endItemView;
|
||||||
}
|
}
|
||||||
final poll = data.items[index];
|
final pollWithStats = data.items[index];
|
||||||
return _CreatorPollItem(poll: poll, pubName: pubName);
|
return _CreatorPollItem(
|
||||||
|
pollWithStats: pollWithStats,
|
||||||
|
pubName: pubName,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -106,14 +118,14 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
class _CreatorPollItem extends StatelessWidget {
|
class _CreatorPollItem extends StatelessWidget {
|
||||||
final String pubName;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final ended = poll.endedAt;
|
final ended = pollWithStats.endedAt;
|
||||||
final endedText =
|
final endedText =
|
||||||
ended == null
|
ended == null
|
||||||
? 'No end'
|
? 'No end'
|
||||||
@@ -123,15 +135,16 @@ class _CreatorPollItem extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(poll.title ?? 'Untitled poll'),
|
title: Text(pollWithStats.title ?? 'Untitled poll'),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (poll.description != null && poll.description!.isNotEmpty)
|
if (pollWithStats.description != null &&
|
||||||
|
pollWithStats.description!.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
poll.description!,
|
pollWithStats.description!,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -139,7 +152,7 @@ class _CreatorPollItem extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Questions: ${poll.questions.length} · Ends: $endedText',
|
'Questions: ${pollWithStats.questions.length} · Ends: $endedText',
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -159,7 +172,7 @@ class _CreatorPollItem extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'creatorPollEdit',
|
'creatorPollEdit',
|
||||||
pathParameters: {'name': pubName, 'id': poll.id},
|
pathParameters: {'name': pubName, 'id': pollWithStats.id},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -170,8 +183,7 @@ class _CreatorPollItem extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder:
|
builder: (context) => PollFeedbackSheet(pollId: pollWithStats.id),
|
||||||
(context) => PollFeedbackSheet(pollId: poll.id, poll: poll),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@@ -6,7 +6,7 @@ part of 'poll_list.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$pollListNotifierHash() => r'd3da24ff6bbb8f35b06d57fc41625dc0312508e4';
|
String _$pollWithStatsHash() => r'6bb910046ce1e09368f9922dbec52fdc2cc86740';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
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
|
abstract class _$PollListNotifier
|
||||||
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPoll>> {
|
extends
|
||||||
|
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPollWithStats>> {
|
||||||
late final String? pubName;
|
late final String? pubName;
|
||||||
|
|
||||||
FutureOr<CursorPagingData<SnPoll>> build(String? pubName);
|
FutureOr<CursorPagingData<SnPollWithStats>> build(String? pubName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See also [PollListNotifier].
|
/// See also [PollListNotifier].
|
||||||
@@ -42,7 +164,7 @@ const pollListNotifierProvider = PollListNotifierFamily();
|
|||||||
|
|
||||||
/// See also [PollListNotifier].
|
/// See also [PollListNotifier].
|
||||||
class PollListNotifierFamily
|
class PollListNotifierFamily
|
||||||
extends Family<AsyncValue<CursorPagingData<SnPoll>>> {
|
extends Family<AsyncValue<CursorPagingData<SnPollWithStats>>> {
|
||||||
/// See also [PollListNotifier].
|
/// See also [PollListNotifier].
|
||||||
const PollListNotifierFamily();
|
const PollListNotifierFamily();
|
||||||
|
|
||||||
@@ -78,7 +200,7 @@ class PollListNotifierProvider
|
|||||||
extends
|
extends
|
||||||
AutoDisposeAsyncNotifierProviderImpl<
|
AutoDisposeAsyncNotifierProviderImpl<
|
||||||
PollListNotifier,
|
PollListNotifier,
|
||||||
CursorPagingData<SnPoll>
|
CursorPagingData<SnPollWithStats>
|
||||||
> {
|
> {
|
||||||
/// See also [PollListNotifier].
|
/// See also [PollListNotifier].
|
||||||
PollListNotifierProvider(String? pubName)
|
PollListNotifierProvider(String? pubName)
|
||||||
@@ -109,7 +231,7 @@ class PollListNotifierProvider
|
|||||||
final String? pubName;
|
final String? pubName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<CursorPagingData<SnPoll>> runNotifierBuild(
|
FutureOr<CursorPagingData<SnPollWithStats>> runNotifierBuild(
|
||||||
covariant PollListNotifier notifier,
|
covariant PollListNotifier notifier,
|
||||||
) {
|
) {
|
||||||
return notifier.build(pubName);
|
return notifier.build(pubName);
|
||||||
@@ -134,7 +256,7 @@ class PollListNotifierProvider
|
|||||||
@override
|
@override
|
||||||
AutoDisposeAsyncNotifierProviderElement<
|
AutoDisposeAsyncNotifierProviderElement<
|
||||||
PollListNotifier,
|
PollListNotifier,
|
||||||
CursorPagingData<SnPoll>
|
CursorPagingData<SnPollWithStats>
|
||||||
>
|
>
|
||||||
createElement() {
|
createElement() {
|
||||||
return _PollListNotifierProviderElement(this);
|
return _PollListNotifierProviderElement(this);
|
||||||
@@ -157,7 +279,7 @@ class PollListNotifierProvider
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
mixin PollListNotifierRef
|
mixin PollListNotifierRef
|
||||||
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPoll>> {
|
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPollWithStats>> {
|
||||||
/// The parameter `pubName` of this provider.
|
/// The parameter `pubName` of this provider.
|
||||||
String? get pubName;
|
String? get pubName;
|
||||||
}
|
}
|
||||||
@@ -166,7 +288,7 @@ class _PollListNotifierProviderElement
|
|||||||
extends
|
extends
|
||||||
AutoDisposeAsyncNotifierProviderElement<
|
AutoDisposeAsyncNotifierProviderElement<
|
||||||
PollListNotifier,
|
PollListNotifier,
|
||||||
CursorPagingData<SnPoll>
|
CursorPagingData<SnPollWithStats>
|
||||||
>
|
>
|
||||||
with PollListNotifierRef {
|
with PollListNotifierRef {
|
||||||
_PollListNotifierProviderElement(super.provider);
|
_PollListNotifierProviderElement(super.provider);
|
||||||
|
@@ -11,6 +11,7 @@ import 'package:island/widgets/alert.dart';
|
|||||||
import 'package:island/models/poll.dart';
|
import 'package:island/models/poll.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
class PollEditorState {
|
class PollEditorState {
|
||||||
String? id; // for editing
|
String? id; // for editing
|
||||||
@@ -110,7 +111,7 @@ class PollEditor extends Notifier<PollEditorState> {
|
|||||||
? [
|
? [
|
||||||
SnPollOption(
|
SnPollOption(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
label: 'Option 1',
|
label: 'pollOptionDefaultLabel'.tr(),
|
||||||
order: 0,
|
order: 0,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -191,7 +192,7 @@ class PollEditor extends Notifier<PollEditorState> {
|
|||||||
: [
|
: [
|
||||||
SnPollOption(
|
SnPollOption(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
label: 'Option 1',
|
label: 'pollOptionDefaultLabel'.tr(),
|
||||||
order: 0,
|
order: 0,
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
@@ -389,7 +390,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
data: body,
|
data: body,
|
||||||
));
|
));
|
||||||
|
|
||||||
showSnackBar(isUpdate ? 'Poll updated.' : 'Poll created.');
|
showSnackBar(isUpdate ? 'pollUpdated'.tr() : 'pollCreated'.tr());
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
Navigator.of(context).maybePop(res.data);
|
Navigator.of(context).maybePop(res.data);
|
||||||
@@ -416,11 +417,11 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'),
|
title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()),
|
||||||
actions: [
|
actions: [
|
||||||
if (kDebugMode)
|
if (kDebugMode)
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Preview JSON (debug)',
|
tooltip: 'pollPreviewJsonDebug'.tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showDebugPreview(context, model);
|
_showDebugPreview(context, model);
|
||||||
},
|
},
|
||||||
@@ -439,8 +440,8 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: model.title ?? '',
|
initialValue: model.title ?? '',
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Title',
|
labelText: 'title'.tr(),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -452,7 +453,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v == null || v.trim().isEmpty) {
|
if (v == null || v.trim().isEmpty) {
|
||||||
return 'Title is required';
|
return 'pollTitleRequired'.tr();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -460,8 +461,8 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: model.description ?? '',
|
initialValue: model.description ?? '',
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Description',
|
labelText: 'description'.tr(),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
@@ -482,7 +483,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Questions',
|
'questions'.tr(),
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@@ -495,7 +496,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
: controller.open();
|
: controller.open();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text('Add question'),
|
label: Text('pollAddQuestion'.tr()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
menuChildren:
|
menuChildren:
|
||||||
@@ -514,9 +515,9 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (model.questions.isEmpty)
|
if (model.questions.isEmpty)
|
||||||
_EmptyState(
|
_EmptyState(
|
||||||
title: 'No questions yet',
|
title: 'pollNoQuestionsYet'.tr(),
|
||||||
subtitle:
|
subtitle:
|
||||||
'Use "Add question" to start building your poll.',
|
'pollNoQuestionsHint'.tr(),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ReorderableListView.builder(
|
ReorderableListView.builder(
|
||||||
@@ -585,7 +586,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
Navigator.of(context).maybePop();
|
Navigator.of(context).maybePop();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
label: const Text('Cancel'),
|
label: Text('cancel'.tr()),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
@@ -593,7 +594,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
_submitPoll(context, ref);
|
_submitPoll(context, ref);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.cloud_upload_outlined),
|
icon: const Icon(Icons.cloud_upload_outlined),
|
||||||
label: Text(model.id == null ? 'Create' : 'Update'),
|
label: Text(model.id == null ? 'create'.tr() : 'update'.tr()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -637,14 +638,14 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
(_) => AlertDialog(
|
(_) => AlertDialog(
|
||||||
title: const Text('Debug Preview'),
|
title: Text('pollDebugPreview'.tr()),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: SelectableText(buf.toString()),
|
child: SelectableText(buf.toString()),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
child: const Text('Close'),
|
child: Text('close'.tr()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -673,15 +674,15 @@ IconData _iconForType(SnPollQuestionType t) {
|
|||||||
String _labelForType(SnPollQuestionType t) {
|
String _labelForType(SnPollQuestionType t) {
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case SnPollQuestionType.singleChoice:
|
case SnPollQuestionType.singleChoice:
|
||||||
return 'Single choice';
|
return 'pollQuestionTypeSingleChoice'.tr();
|
||||||
case SnPollQuestionType.multipleChoice:
|
case SnPollQuestionType.multipleChoice:
|
||||||
return 'Multiple choice';
|
return 'pollQuestionTypeMultipleChoice'.tr();
|
||||||
case SnPollQuestionType.freeText:
|
case SnPollQuestionType.freeText:
|
||||||
return 'Free text';
|
return 'pollQuestionTypeFreeText'.tr();
|
||||||
case SnPollQuestionType.yesNo:
|
case SnPollQuestionType.yesNo:
|
||||||
return 'Yes / No';
|
return 'pollQuestionTypeYesNo'.tr();
|
||||||
case SnPollQuestionType.rating:
|
case SnPollQuestionType.rating:
|
||||||
return 'Rating';
|
return 'pollQuestionTypeRating'.tr();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,8 +699,8 @@ class _EndDatePicker extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: InputDecorator(
|
child: InputDecorator(
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'End date & time (optional)',
|
labelText: 'pollEndDateOptional'.tr(),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -711,7 +712,7 @@ class _EndDatePicker extends StatelessWidget {
|
|||||||
Icon(Icons.event, color: Theme.of(context).colorScheme.primary),
|
Icon(Icons.event, color: Theme.of(context).colorScheme.primary),
|
||||||
Text(
|
Text(
|
||||||
value == null
|
value == null
|
||||||
? 'Not set'
|
? 'notSet'.tr()
|
||||||
: MaterialLocalizations.of(
|
: MaterialLocalizations.of(
|
||||||
context,
|
context,
|
||||||
).formatFullDate(value!),
|
).formatFullDate(value!),
|
||||||
@@ -759,12 +760,12 @@ class _EndDatePicker extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
onChanged(dt);
|
onChanged(dt);
|
||||||
},
|
},
|
||||||
child: const Text('Pick'),
|
child: Text('pick'.tr()),
|
||||||
),
|
),
|
||||||
if (value != null)
|
if (value != null)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => onChanged(null),
|
onPressed: () => onChanged(null),
|
||||||
child: const Text('Clear'),
|
child: Text('clear'.tr()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -799,7 +800,7 @@ class _QuestionHeader extends StatelessWidget {
|
|||||||
child: const Icon(Icons.drag_handle),
|
child: const Icon(Icons.drag_handle),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
question.title.isEmpty ? 'Untitled question' : question.title,
|
question.title.isEmpty ? 'pollUntitledQuestion'.tr() : question.title,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -808,17 +809,17 @@ class _QuestionHeader extends StatelessWidget {
|
|||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Move up',
|
tooltip: 'moveUp'.tr(),
|
||||||
onPressed: onMoveUp,
|
onPressed: onMoveUp,
|
||||||
icon: const Icon(Icons.arrow_upward),
|
icon: const Icon(Icons.arrow_upward),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Move down',
|
tooltip: 'moveDown'.tr(),
|
||||||
onPressed: onMoveDown,
|
onPressed: onMoveDown,
|
||||||
icon: const Icon(Icons.arrow_downward),
|
icon: const Icon(Icons.arrow_downward),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Delete',
|
tooltip: 'delete'.tr(),
|
||||||
onPressed: onDelete,
|
onPressed: onDelete,
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
@@ -853,7 +854,7 @@ class _QuestionEditor extends ConsumerWidget {
|
|||||||
onChanged: (t) => notifier.setQuestionType(index, t),
|
onChanged: (t) => notifier.setQuestionType(index, t),
|
||||||
),
|
),
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: const Text('Required'),
|
label: Text('required'.tr()),
|
||||||
selected: question.isRequired,
|
selected: question.isRequired,
|
||||||
onSelected: (v) => notifier.setQuestionRequired(index, v),
|
onSelected: (v) => notifier.setQuestionRequired(index, v),
|
||||||
avatar: Icon(
|
avatar: Icon(
|
||||||
@@ -867,8 +868,8 @@ class _QuestionEditor extends ConsumerWidget {
|
|||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: question.title,
|
initialValue: question.title,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Question title',
|
labelText: 'pollQuestionTitle'.tr(),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -879,7 +880,7 @@ class _QuestionEditor extends ConsumerWidget {
|
|||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v == null || v.trim().isEmpty) {
|
if (v == null || v.trim().isEmpty) {
|
||||||
return 'Question title is required';
|
return 'pollQuestionTitleRequired'.tr();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -887,8 +888,8 @@ class _QuestionEditor extends ConsumerWidget {
|
|||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: question.description ?? '',
|
initialValue: question.description ?? '',
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Question description (optional)',
|
labelText: 'pollQuestionDescriptionOptional'.tr(),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -902,7 +903,7 @@ class _QuestionEditor extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (question.options != null) ...[
|
if (question.options != null) ...[
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('Options', style: Theme.of(context).textTheme.titleMedium),
|
Text('options'.tr(), style: Theme.of(context).textTheme.titleMedium),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
_OptionsEditor(index: index, options: question.options!),
|
_OptionsEditor(index: index, options: question.options!),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
@@ -911,7 +912,7 @@ class _QuestionEditor extends ConsumerWidget {
|
|||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () => notifier.addOption(index),
|
onPressed: () => notifier.addOption(index),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text('Add option'),
|
label: Text('pollAddOption'.tr()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -937,8 +938,8 @@ class _QuestionTypePicker extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DropdownButtonFormField<SnPollQuestionType>(
|
return DropdownButtonFormField<SnPollQuestionType>(
|
||||||
value: value,
|
value: value,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Type',
|
labelText: 'Type'.tr(),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -987,8 +988,8 @@ class _OptionsEditor extends ConsumerWidget {
|
|||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
key: ValueKey(options[i].id),
|
key: ValueKey(options[i].id),
|
||||||
initialValue: options[i].label,
|
initialValue: options[i].label,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Option label',
|
labelText: 'pollOptionLabel'.tr(),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -1003,7 +1004,7 @@ class _OptionsEditor extends ConsumerWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
tooltip: 'Move up',
|
tooltip: 'moveUp'.tr(),
|
||||||
onPressed:
|
onPressed:
|
||||||
i > 0 ? () => notifier.moveOptionUp(index, i) : null,
|
i > 0 ? () => notifier.moveOptionUp(index, i) : null,
|
||||||
icon: const Icon(Icons.arrow_upward),
|
icon: const Icon(Icons.arrow_upward),
|
||||||
@@ -1012,7 +1013,7 @@ class _OptionsEditor extends ConsumerWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
tooltip: 'Move down',
|
tooltip: 'moveDown'.tr(),
|
||||||
onPressed:
|
onPressed:
|
||||||
i < options.length - 1
|
i < options.length - 1
|
||||||
? () => notifier.moveOptionDown(index, i)
|
? () => notifier.moveOptionDown(index, i)
|
||||||
@@ -1023,7 +1024,7 @@ class _OptionsEditor extends ConsumerWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
tooltip: 'Delete',
|
tooltip: 'delete'.tr(),
|
||||||
onPressed: () => notifier.removeOption(index, i),
|
onPressed: () => notifier.removeOption(index, i),
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
),
|
),
|
||||||
@@ -1048,7 +1049,7 @@ class _TextAnswerPreview extends StatelessWidget {
|
|||||||
maxLines: long ? 4 : 1,
|
maxLines: long ? 4 : 1,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText:
|
labelText:
|
||||||
long ? 'Long text answer (preview)' : 'Short text answer (preview)',
|
long ? 'pollLongTextAnswerPreview'.tr() : 'pollShortTextAnswerPreview'.tr(),
|
||||||
border: const OutlineInputBorder(
|
border: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -1082,9 +1083,9 @@ class _EmptyState extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title, style: Theme.of(context).textTheme.titleMedium),
|
Text('pollNoQuestionsYet'.tr(), style: Theme.of(context).textTheme.titleMedium),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
|
Text('pollNoQuestionsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -69,7 +69,7 @@ void showLoadingModal(BuildContext context) {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(year2023: true),
|
CircularProgressIndicator(year2023: false),
|
||||||
const Gap(24),
|
const Gap(24),
|
||||||
Text('loading'.tr()),
|
Text('loading'.tr()),
|
||||||
],
|
],
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/poll.dart';
|
import 'package:island/models/poll.dart';
|
||||||
import 'package:island/pods/network.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/services/time.dart';
|
||||||
import 'package:island/widgets/content/sheet.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_annotation/riverpod_annotation.dart';
|
||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@@ -52,59 +57,60 @@ class PollFeedbackNotifier extends _$PollFeedbackNotifier
|
|||||||
class PollFeedbackSheet extends HookConsumerWidget {
|
class PollFeedbackSheet extends HookConsumerWidget {
|
||||||
final String pollId;
|
final String pollId;
|
||||||
final String? title;
|
final String? title;
|
||||||
final SnPoll poll;
|
const PollFeedbackSheet({super.key, required this.pollId, this.title});
|
||||||
final Map<String, dynamic>? stats; // stats object similar to PollSubmit
|
|
||||||
const PollFeedbackSheet({
|
|
||||||
super.key,
|
|
||||||
required this.pollId,
|
|
||||||
required this.poll,
|
|
||||||
this.title,
|
|
||||||
this.stats,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final poll = ref.watch(pollWithStatsProvider(pollId));
|
||||||
|
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText: title ?? 'Poll feedback',
|
titleText: title ?? 'Poll feedback',
|
||||||
child: Column(
|
child: poll.when(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
data:
|
||||||
children: [
|
(data) => CustomScrollView(
|
||||||
_PollHeader(poll: poll, stats: stats),
|
slivers: [
|
||||||
const Divider(height: 1),
|
SliverToBoxAdapter(child: _PollHeader(poll: data)),
|
||||||
Expanded(
|
SliverToBoxAdapter(child: const Divider(height: 1)),
|
||||||
child: PagingHelperView(
|
SliverGap(4),
|
||||||
provider: pollFeedbackNotifierProvider(pollId),
|
PagingHelperSliverView(
|
||||||
futureRefreshable: pollFeedbackNotifierProvider(pollId).future,
|
provider: pollFeedbackNotifierProvider(pollId),
|
||||||
notifierRefreshable:
|
futureRefreshable:
|
||||||
pollFeedbackNotifierProvider(pollId).notifier,
|
pollFeedbackNotifierProvider(pollId).future,
|
||||||
contentBuilder:
|
notifierRefreshable:
|
||||||
(data, widgetCount, endItemView) => ListView.separated(
|
pollFeedbackNotifierProvider(pollId).notifier,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
contentBuilder:
|
||||||
itemCount: widgetCount,
|
(val, widgetCount, endItemView) => SliverList.separated(
|
||||||
itemBuilder: (context, index) {
|
itemCount: widgetCount,
|
||||||
if (index == widgetCount - 1) {
|
itemBuilder: (context, index) {
|
||||||
// Provided by PagingHelperView to indicate end/loading
|
if (index == widgetCount - 1) {
|
||||||
return endItemView;
|
// Provided by PagingHelperView to indicate end/loading
|
||||||
}
|
return endItemView;
|
||||||
final answer = data.items[index];
|
}
|
||||||
return _PollAnswerTile(answer: answer, poll: poll);
|
final answer = val.items[index];
|
||||||
},
|
return _PollAnswerTile(answer: answer, poll: data);
|
||||||
separatorBuilder:
|
},
|
||||||
(context, index) =>
|
separatorBuilder:
|
||||||
const Divider(height: 1).padding(vertical: 4),
|
(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 {
|
class _PollHeader extends StatelessWidget {
|
||||||
const _PollHeader({required this.poll, this.stats});
|
const _PollHeader({required this.poll});
|
||||||
final SnPoll poll;
|
final SnPollWithStats poll;
|
||||||
final Map<String, dynamic>? stats;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -112,18 +118,32 @@ class _PollHeader extends StatelessWidget {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
if (poll.title != null)
|
if (poll.title != null || (poll.description?.isNotEmpty ?? false))
|
||||||
Text(poll.title!, style: theme.textTheme.titleLarge),
|
Column(
|
||||||
if (poll.description != null)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.only(top: 2),
|
if (poll.title != null)
|
||||||
child: Text(
|
Text(poll.title!, style: theme.textTheme.titleLarge),
|
||||||
poll.description!,
|
if (poll.description?.isNotEmpty ?? false)
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
Text(
|
||||||
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7),
|
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);
|
).padding(horizontal: 20, vertical: 16);
|
||||||
@@ -132,7 +152,7 @@ class _PollHeader extends StatelessWidget {
|
|||||||
|
|
||||||
class _PollAnswerTile extends StatelessWidget {
|
class _PollAnswerTile extends StatelessWidget {
|
||||||
final SnPollAnswer answer;
|
final SnPollAnswer answer;
|
||||||
final SnPoll poll;
|
final SnPollWithStats poll;
|
||||||
const _PollAnswerTile({required this.answer, required this.poll});
|
const _PollAnswerTile({required this.answer, required this.poll});
|
||||||
|
|
||||||
String _formatPerQuestionAnswer(
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:island/models/poll.dart';
|
import 'package:island/models/poll.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/poll/poll_stats_widget.dart';
|
||||||
|
|
||||||
class PollSubmit extends ConsumerStatefulWidget {
|
class PollSubmit extends ConsumerStatefulWidget {
|
||||||
const PollSubmit({
|
const PollSubmit({
|
||||||
@@ -42,6 +44,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
late final List<SnPollQuestion> _questions;
|
late final List<SnPollQuestion> _questions;
|
||||||
int _index = 0;
|
int _index = 0;
|
||||||
bool _submitting = false;
|
bool _submitting = false;
|
||||||
|
bool _isModifying = false; // New state to track if user is modifying answers
|
||||||
|
|
||||||
/// Collected answers, keyed by questionId
|
/// Collected answers, keyed by questionId
|
||||||
late Map<String, dynamic> _answers;
|
late Map<String, dynamic> _answers;
|
||||||
@@ -64,6 +67,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
||||||
if (!widget.isReadonly) {
|
if (!widget.isReadonly) {
|
||||||
_loadCurrentIntoLocalState();
|
_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 +89,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
);
|
);
|
||||||
if (!widget.isReadonly) {
|
if (!widget.isReadonly) {
|
||||||
_loadCurrentIntoLocalState();
|
_loadCurrentIntoLocalState();
|
||||||
|
// If poll ID changes, reset modification state
|
||||||
|
_isModifying = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +213,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
// Only call onSubmit after server accepts
|
// Only call onSubmit after server accepts
|
||||||
widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers));
|
widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers));
|
||||||
|
|
||||||
showSnackBar('Poll answer has been submitted.');
|
showSnackBar('pollAnswerSubmitted'.tr());
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
@@ -266,16 +276,17 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
widget.poll.description!,
|
widget.poll.description!,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.showProgress)
|
if (widget.showProgress &&
|
||||||
|
_isModifying) // Only show progress when modifying
|
||||||
Text(
|
Text(
|
||||||
'${_index + 1} / ${_questions.length}',
|
'${_index + 1} / ${_questions.length}',
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
@@ -294,8 +305,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'*',
|
'*',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -306,10 +317,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
q.description!,
|
q.description!,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodySmall?.color?.withOpacity(0.7),
|
).textTheme.bodySmall?.color?.withOpacity(0.7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -317,152 +328,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStats(BuildContext context, SnPollQuestion q) {
|
Widget _buildStats(BuildContext context, SnPollQuestion q) {
|
||||||
if (widget.stats == null) return const SizedBox.shrink();
|
return PollStatsWidget(question: q, stats: widget.stats);
|
||||||
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,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(BuildContext context) {
|
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;
|
final q = _current;
|
||||||
switch (q.type) {
|
switch (q.type) {
|
||||||
case SnPollQuestionType.singleChoice:
|
case SnPollQuestionType.singleChoice:
|
||||||
@@ -522,9 +394,9 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SegmentedButton<bool>(
|
child: SegmentedButton<bool>(
|
||||||
segments: const [
|
segments: [
|
||||||
ButtonSegment(value: true, label: Text('Yes')),
|
ButtonSegment(value: true, label: Text('yes'.tr())),
|
||||||
ButtonSegment(value: false, label: Text('No')),
|
ButtonSegment(value: false, label: Text('no'.tr())),
|
||||||
],
|
],
|
||||||
selected: _yesNoSelected == null ? {} : {_yesNoSelected!},
|
selected: _yesNoSelected == null ? {} : {_yesNoSelected!},
|
||||||
onSelectionChanged: (sel) {
|
onSelectionChanged: (sel) {
|
||||||
@@ -573,29 +445,135 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
final isLast = _index == _questions.length - 1;
|
final isLast = _index == _questions.length - 1;
|
||||||
final canProceed = _isCurrentAnswered() && !_submitting;
|
final canProceed = _isCurrentAnswered() && !_submitting;
|
||||||
|
|
||||||
|
if (widget.initialAnswers != null && !_isModifying && !widget.isReadonly) {
|
||||||
|
// If poll is submitted and not in modification mode, show "Modify" button
|
||||||
|
return FilledButton.icon(
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
label: Text('modifyAnswers'.tr()),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isModifying = true;
|
||||||
|
_index = 0; // Reset to first question for modification
|
||||||
|
_loadCurrentIntoLocalState();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
label: Text(_index == 0 ? 'Cancel' : 'Back'),
|
label: Text(_index == 0 ? 'cancel'.tr() : 'back'.tr()),
|
||||||
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(),
|
const Spacer(),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
icon: _submitting
|
icon:
|
||||||
? const SizedBox(
|
_submitting
|
||||||
width: 16,
|
? const SizedBox(
|
||||||
height: 16,
|
width: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
height: 16,
|
||||||
)
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
: Icon(isLast ? Icons.check : Icons.arrow_forward),
|
)
|
||||||
label: Text(isLast ? 'Submit' : 'Next'),
|
: Icon(isLast ? Icons.check : Icons.arrow_forward),
|
||||||
|
label: Text(isLast ? 'submit'.tr() : 'next'.tr()),
|
||||||
onPressed: canProceed ? _next : null,
|
onPressed: canProceed ? _next : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
Widget _buildReadonlyView(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -617,10 +595,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
widget.poll.description!,
|
widget.poll.description!,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -645,9 +623,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
padding: const EdgeInsets.only(left: 8),
|
padding: const EdgeInsets.only(left: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'*',
|
'*',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(
|
||||||
color: Theme.of(context).colorScheme.error,
|
context,
|
||||||
),
|
).textTheme.titleMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -658,10 +638,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
q.description!,
|
q.description!,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodySmall?.color?.withOpacity(0.7),
|
).textTheme.bodySmall?.color?.withOpacity(0.7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildStats(context, q),
|
_buildStats(context, q),
|
||||||
@@ -678,6 +658,15 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
return const SizedBox.shrink();
|
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), _buildNavBar(context)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If poll is in readonly mode, show readonly view
|
||||||
if (widget.isReadonly) {
|
if (widget.isReadonly) {
|
||||||
return _buildReadonlyView(context);
|
return _buildReadonlyView(context);
|
||||||
}
|
}
|
||||||
@@ -701,76 +690,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.
|
/// Simple fade/slide transition between questions.
|
||||||
class _AnimatedStep extends StatelessWidget {
|
class _AnimatedStep extends StatelessWidget {
|
||||||
const _AnimatedStep({super.key, required this.child});
|
const _AnimatedStep({super.key, required this.child});
|
||||||
@@ -794,4 +713,4 @@ class _AnimatedStep extends StatelessWidget {
|
|||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -186,10 +186,9 @@ class ComposePollSheet extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? _buildPollSubtitle(SnPoll poll) {
|
Widget? _buildPollSubtitle(SnPollWithStats poll) {
|
||||||
try {
|
try {
|
||||||
final SnPoll dyn = poll;
|
final List<SnPollQuestion> options = poll.questions;
|
||||||
final List<SnPollQuestion> options = dyn.questions;
|
|
||||||
if (options.isEmpty) return null;
|
if (options.isEmpty) return null;
|
||||||
final preview = options.take(3).map((e) => e.title).join(' · ');
|
final preview = options.take(3).map((e) => e.title).join(' · ');
|
||||||
if (preview.trim().isEmpty) return null;
|
if (preview.trim().isEmpty) return null;
|
||||||
|
@@ -354,6 +354,7 @@ class PostItem extends HookConsumerWidget {
|
|||||||
final translatedWidget =
|
final translatedWidget =
|
||||||
(translatedText.value?.isNotEmpty ?? false)
|
(translatedText.value?.isNotEmpty ?? false)
|
||||||
? Column(
|
? Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -398,6 +399,7 @@ class PostItem extends HookConsumerWidget {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
final translationSection = Column(
|
final translationSection = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (translatedWidget != null) translatedWidget,
|
if (translatedWidget != null) translatedWidget,
|
||||||
if (translatableWidget != null) translatableWidget,
|
if (translatableWidget != null) translatableWidget,
|
||||||
|
@@ -639,13 +639,18 @@ class PostBody extends ConsumerWidget {
|
|||||||
if (!isFullPost && item.type == 1)
|
if (!isFullPost && item.type == 1)
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
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),
|
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(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
Reference in New Issue
Block a user