From 4937dee182c4e82a2893fc457be54a437e651bdf Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 12 Feb 2025 23:56:45 +0800 Subject: [PATCH] :sparkles: Poll editor --- assets/translations/en-US.json | 10 +- assets/translations/zh-CN.json | 10 +- lib/controllers/post_write_controller.dart | 11 + lib/providers/post.dart | 19 + .../account/publishers/publisher_edit.dart | 1 - lib/screens/post/post_detail.dart | 2 - lib/screens/post/post_editor.dart | 43 +- lib/screens/realm/realm_discovery.dart | 3 +- lib/types/poll.dart | 45 ++ lib/types/poll.freezed.dart | 714 ++++++++++++++++++ lib/types/poll.g.dart | 60 ++ lib/types/post.dart | 3 + lib/types/post.freezed.dart | 74 +- lib/types/post.g.dart | 6 + lib/widgets/chat/chat_message_input.dart | 14 +- lib/widgets/post/post_poll.dart | 201 +++++ 16 files changed, 1187 insertions(+), 29 deletions(-) create mode 100644 lib/types/poll.dart create mode 100644 lib/types/poll.freezed.dart create mode 100644 lib/types/poll.g.dart create mode 100644 lib/widgets/post/post_poll.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 5aa6d0c..c1c71de 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -625,5 +625,13 @@ "realmCommunityHint": "This realm is a community realm, you can freely join.", "realmCommunityPublicChannelsHint": "The public channels in this realm", "realmJoined": "Joined realm {}.", - "join": "Join" + "join": "Join", + "pollEditorNew": "New Poll", + "pollEditorEdit": "Edit Poll", + "pollEditorDelete": "Delete Poll", + "pollEditorDeleteDescription": "Are you sure you want to delete this poll? This operation is irreversible.", + "pollEditorUnlink": "Unlink Poll", + "pollOptionAdd": "Add Option", + "pollOptionName": "Option Name", + "pollLinkExisting": "Link existing poll" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index d1ecb7b..6c53f7c 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -624,5 +624,13 @@ "realmCommunityHint": "该领域是一个社区领域,你可以自由加入。", "realmCommunityPublicChannelsHint": "该领域包含的公共频道", "realmJoined": "已加入领域 {}。", - "join": "加入" + "join": "加入", + "pollEditorNew": "新投票", + "pollEditorEdit": "编辑投票", + "pollEditorDelete": "删除投票", + "pollEditorDeleteDescription": "你确定要删除这个投票吗?该操作不可撤销。", + "pollEditorUnlink": "解除链接", + "pollOptionAdd": "添加选项", + "pollOptionName": "选项名称", + "pollLinkExisting": "链接现有投票" } diff --git a/lib/controllers/post_write_controller.dart b/lib/controllers/post_write_controller.dart index 450cf67..7c5e50a 100644 --- a/lib/controllers/post_write_controller.dart +++ b/lib/controllers/post_write_controller.dart @@ -16,6 +16,7 @@ import 'package:surface/providers/post.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; +import 'package:surface/types/poll.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/universal_image.dart'; @@ -199,6 +200,7 @@ class PostWriteController extends ChangeNotifier { List attachments = List.empty(growable: true); DateTime? publishedAt, publishedUntil; SnAttachment? videoAttachment; + SnPoll? poll; Future fetchRelatedPost( BuildContext context, { @@ -229,6 +231,7 @@ class PostWriteController extends ChangeNotifier { tags = List.from(post.tags.map((ele) => ele.alias), growable: true); categories = List.from(post.categories.map((ele) => ele.alias), growable: true); attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); + poll = post.preload?.poll; if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { thumbnail = PostWriteMedia(post.preload!.thumbnail); @@ -367,6 +370,7 @@ class PostWriteController extends ChangeNotifier { if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(), if (replyingPost != null) 'reply_to': replyingPost!.toJson(), if (repostingPost != null) 'repost_to': repostingPost!.toJson(), + if (poll != null) 'poll': poll!.toJson(), }), ); }); @@ -396,6 +400,7 @@ class PostWriteController extends ChangeNotifier { if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal(); replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null; repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null; + poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null; temporaryRestored = true; notifyListeners(); }); @@ -511,6 +516,7 @@ class PostWriteController extends ChangeNotifier { if (repostingPost != null) 'repost_to': repostingPost!.id, if (reward != null) 'reward': reward, if (videoAttachment != null) 'video': videoAttachment!.rid, + if (poll != null) 'poll': poll!.id, }, onSendProgress: (count, total) { progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2); @@ -642,6 +648,11 @@ class PostWriteController extends ChangeNotifier { notifyListeners(); } + void setPoll(SnPoll? value) { + poll = value; + notifyListeners(); + } + void reset() { publishedAt = null; publishedUntil = null; diff --git a/lib/providers/post.dart b/lib/providers/post.dart index 6e612e6..8e264fc 100644 --- a/lib/providers/post.dart +++ b/lib/providers/post.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/user_directory.dart'; +import 'package:surface/types/poll.dart'; import 'package:surface/types/post.dart'; class SnPostContentProvider { @@ -16,6 +17,11 @@ class SnPostContentProvider { _attach = context.read(); } + Future _fetchPoll(int id) async { + final resp = await _sn.client.get('/cgi/co/polls/$id'); + return SnPoll.fromJson(resp.data); + } + Future> _preloadRelatedDataInBatch(List out) async { Set rids = {}; for (var i = 0; i < out.length; i++) { @@ -35,11 +41,17 @@ class SnPostContentProvider { final attachments = await _attach.getMultiple(rids.toList()); for (var i = 0; i < out.length; i++) { + SnPoll? poll; + if (out[i].pollId != null) { + poll = await _fetchPoll(out[i].pollId!); + } + out[i] = out[i].copyWith( preload: SnPostPreload( thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull, attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(), video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull, + poll: poll, ), ); } @@ -67,11 +79,18 @@ class SnPostContentProvider { } final attachments = await _attach.getMultiple(rids.toList()); + + SnPoll? poll; + if (out.pollId != null) { + poll = await _fetchPoll(out.pollId!); + } + out = out.copyWith( preload: SnPostPreload( thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull, attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(), video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull, + poll: poll, ), ); diff --git a/lib/screens/account/publishers/publisher_edit.dart b/lib/screens/account/publishers/publisher_edit.dart index 5a1546c..0b6dde6 100644 --- a/lib/screens/account/publishers/publisher_edit.dart +++ b/lib/screens/account/publishers/publisher_edit.dart @@ -16,7 +16,6 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/account/account_image.dart'; -import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart index 0aec245..04686f1 100644 --- a/lib/screens/post/post_detail.dart +++ b/lib/screens/post/post_detail.dart @@ -6,7 +6,6 @@ import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; -import 'package:responsive_framework/responsive_framework.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/post.dart'; import 'package:surface/providers/userinfo.dart'; @@ -17,7 +16,6 @@ import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_comment_list.dart'; import 'package:surface/widgets/post/post_item.dart'; -import 'package:surface/widgets/post/post_mini_editor.dart'; class PostDetailScreen extends StatefulWidget { final String slug; diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 9a3e1cf..f9cdae7 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -18,8 +18,10 @@ import 'package:surface/providers/config.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; +import 'package:surface/types/poll.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/account/account_image.dart'; +import 'package:surface/widgets/attachment/attachment_input.dart'; import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/pending_attachment_alt.dart'; import 'package:surface/widgets/attachment/pending_attachment_boost.dart'; @@ -30,10 +32,9 @@ import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_meta_editor.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:provider/provider.dart'; +import 'package:surface/widgets/post/post_poll.dart'; import 'package:uuid/uuid.dart'; -import '../../widgets/attachment/attachment_input.dart'; - class PostEditorExtra { final String? text; final String? title; @@ -140,6 +141,23 @@ class _PostEditorScreenState extends State { ); } + void _showPollEditorDialog() async { + final poll = await showDialog( + context: context, + builder: (context) => PollEditorDialog( + poll: _writeController.poll, + ), + ); + if (poll == null) return; + if (!mounted) return; + + if (poll == false) { + _writeController.setPoll(null); + } else { + _writeController.setPoll(poll); + } + } + @override void dispose() { _writeController.dispose(); @@ -238,7 +256,7 @@ class _PostEditorScreenState extends State { children: [ SingleChildScrollView( padding: EdgeInsets.only(bottom: 160), - child: switch (_writeController.mode) { + child: StyledWidget(switch (_writeController.mode) { 'stories' => _PostStoryEditor( controller: _writeController, onTapPublisher: _showPublisherPopup, @@ -256,7 +274,8 @@ class _PostEditorScreenState extends State { onTapPublisher: _showPublisherPopup, ), _ => const Placeholder(), - }, + }) + .padding(top: 8), ), if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null) Positioned( @@ -304,7 +323,6 @@ class _PostEditorScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LoadingIndicator(isActive: _isLoading), if (_writeController.isBusy && _writeController.progress != null) TweenAnimationBuilder( tween: Tween(begin: 0, end: _writeController.progress), @@ -313,6 +331,8 @@ class _PostEditorScreenState extends State { ) else if (_writeController.isBusy) const LinearProgressIndicator(value: null, minHeight: 2), + LoadingIndicator(isActive: _isLoading), + const Gap(4), Container( child: _writeController.temporaryRestored ? Container( @@ -360,6 +380,18 @@ class _PostEditorScreenState extends State { }); }, ), + if (_writeController.mode == 'stories') + IconButton( + icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary), + style: ButtonStyle( + backgroundColor: _writeController.poll == null + ? null + : WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer), + ), + onPressed: () { + _showPollEditorDialog(); + }, + ), ], ), ), @@ -382,7 +414,6 @@ class _PostEditorScreenState extends State { ], ).padding( bottom: MediaQuery.of(context).padding.bottom + 8, - top: 4, ), ), ], diff --git a/lib/screens/realm/realm_discovery.dart b/lib/screens/realm/realm_discovery.dart index 49eb486..817edf6 100644 --- a/lib/screens/realm/realm_discovery.dart +++ b/lib/screens/realm/realm_discovery.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -261,7 +260,7 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> { itemBuilder: (context, index) { final channel = _channels![index]; return CheckboxListTile( - value: _planJoinChannels.contains(channel.alias) ?? false, + value: _planJoinChannels.contains(channel.alias), title: Text(channel.name), subtitle: Text( channel.description, diff --git a/lib/types/poll.dart b/lib/types/poll.dart new file mode 100644 index 0000000..7d0a355 --- /dev/null +++ b/lib/types/poll.dart @@ -0,0 +1,45 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'poll.freezed.dart'; +part 'poll.g.dart'; + +@freezed +class SnPoll with _$SnPoll { + const factory SnPoll({ + required int id, + required DateTime createdAt, + required DateTime updatedAt, + required dynamic deletedAt, + required dynamic expiredAt, + required List options, + required int accountId, + required SnPollMetric metric, + }) = _SnPoll; + + factory SnPoll.fromJson(Map json) => + _$SnPollFromJson(json); +} + +@freezed +class SnPollMetric with _$SnPollMetric { + const factory SnPollMetric({ + required int totalAnswer, + required dynamic byOptions, + }) = _SnPollMetric; + + factory SnPollMetric.fromJson(Map json) + => _$SnPollMetricFromJson(json); +} + +@freezed +class SnPollOption with _$SnPollOption { + const factory SnPollOption({ + required String id, + required String icon, + required String name, + required String description, + }) = _SnPollOption; + + factory SnPollOption.fromJson(Map json) + => _$SnPollOptionFromJson(json); +} diff --git a/lib/types/poll.freezed.dart b/lib/types/poll.freezed.dart new file mode 100644 index 0000000..2d2f4ad --- /dev/null +++ b/lib/types/poll.freezed.dart @@ -0,0 +1,714 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'poll.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +SnPoll _$SnPollFromJson(Map json) { + return _SnPoll.fromJson(json); +} + +/// @nodoc +mixin _$SnPoll { + int get id => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + DateTime get updatedAt => throw _privateConstructorUsedError; + dynamic get deletedAt => throw _privateConstructorUsedError; + dynamic get expiredAt => throw _privateConstructorUsedError; + List get options => throw _privateConstructorUsedError; + int get accountId => throw _privateConstructorUsedError; + SnPollMetric get metric => throw _privateConstructorUsedError; + + /// Serializes this SnPoll to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SnPoll + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SnPollCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SnPollCopyWith<$Res> { + factory $SnPollCopyWith(SnPoll value, $Res Function(SnPoll) then) = + _$SnPollCopyWithImpl<$Res, SnPoll>; + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + dynamic deletedAt, + dynamic expiredAt, + List options, + int accountId, + SnPollMetric metric}); + + $SnPollMetricCopyWith<$Res> get metric; +} + +/// @nodoc +class _$SnPollCopyWithImpl<$Res, $Val extends SnPoll> + implements $SnPollCopyWith<$Res> { + _$SnPollCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SnPoll + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? expiredAt = freezed, + Object? options = null, + Object? accountId = null, + Object? metric = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as dynamic, + expiredAt: freezed == expiredAt + ? _value.expiredAt + : expiredAt // ignore: cast_nullable_to_non_nullable + as dynamic, + options: null == options + ? _value.options + : options // ignore: cast_nullable_to_non_nullable + as List, + accountId: null == accountId + ? _value.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int, + metric: null == metric + ? _value.metric + : metric // ignore: cast_nullable_to_non_nullable + as SnPollMetric, + ) as $Val); + } + + /// Create a copy of SnPoll + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnPollMetricCopyWith<$Res> get metric { + return $SnPollMetricCopyWith<$Res>(_value.metric, (value) { + return _then(_value.copyWith(metric: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$SnPollImplCopyWith<$Res> implements $SnPollCopyWith<$Res> { + factory _$$SnPollImplCopyWith( + _$SnPollImpl value, $Res Function(_$SnPollImpl) then) = + __$$SnPollImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + dynamic deletedAt, + dynamic expiredAt, + List options, + int accountId, + SnPollMetric metric}); + + @override + $SnPollMetricCopyWith<$Res> get metric; +} + +/// @nodoc +class __$$SnPollImplCopyWithImpl<$Res> + extends _$SnPollCopyWithImpl<$Res, _$SnPollImpl> + implements _$$SnPollImplCopyWith<$Res> { + __$$SnPollImplCopyWithImpl( + _$SnPollImpl _value, $Res Function(_$SnPollImpl) _then) + : super(_value, _then); + + /// Create a copy of SnPoll + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? expiredAt = freezed, + Object? options = null, + Object? accountId = null, + Object? metric = null, + }) { + return _then(_$SnPollImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as dynamic, + expiredAt: freezed == expiredAt + ? _value.expiredAt + : expiredAt // ignore: cast_nullable_to_non_nullable + as dynamic, + options: null == options + ? _value._options + : options // ignore: cast_nullable_to_non_nullable + as List, + accountId: null == accountId + ? _value.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int, + metric: null == metric + ? _value.metric + : metric // ignore: cast_nullable_to_non_nullable + as SnPollMetric, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnPollImpl implements _SnPoll { + const _$SnPollImpl( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.expiredAt, + required final List options, + required this.accountId, + required this.metric}) + : _options = options; + + factory _$SnPollImpl.fromJson(Map json) => + _$$SnPollImplFromJson(json); + + @override + final int id; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + @override + final dynamic deletedAt; + @override + final dynamic expiredAt; + final List _options; + @override + List get options { + if (_options is EqualUnmodifiableListView) return _options; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_options); + } + + @override + final int accountId; + @override + final SnPollMetric metric; + + @override + String toString() { + return 'SnPoll(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, expiredAt: $expiredAt, options: $options, accountId: $accountId, metric: $metric)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SnPollImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + const DeepCollectionEquality().equals(other.deletedAt, deletedAt) && + const DeepCollectionEquality().equals(other.expiredAt, expiredAt) && + const DeepCollectionEquality().equals(other._options, _options) && + (identical(other.accountId, accountId) || + other.accountId == accountId) && + (identical(other.metric, metric) || other.metric == metric)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + createdAt, + updatedAt, + const DeepCollectionEquality().hash(deletedAt), + const DeepCollectionEquality().hash(expiredAt), + const DeepCollectionEquality().hash(_options), + accountId, + metric); + + /// Create a copy of SnPoll + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SnPollImplCopyWith<_$SnPollImpl> get copyWith => + __$$SnPollImplCopyWithImpl<_$SnPollImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SnPollImplToJson( + this, + ); + } +} + +abstract class _SnPoll implements SnPoll { + const factory _SnPoll( + {required final int id, + required final DateTime createdAt, + required final DateTime updatedAt, + required final dynamic deletedAt, + required final dynamic expiredAt, + required final List options, + required final int accountId, + required final SnPollMetric metric}) = _$SnPollImpl; + + factory _SnPoll.fromJson(Map json) = _$SnPollImpl.fromJson; + + @override + int get id; + @override + DateTime get createdAt; + @override + DateTime get updatedAt; + @override + dynamic get deletedAt; + @override + dynamic get expiredAt; + @override + List get options; + @override + int get accountId; + @override + SnPollMetric get metric; + + /// Create a copy of SnPoll + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SnPollImplCopyWith<_$SnPollImpl> get copyWith => + throw _privateConstructorUsedError; +} + +SnPollMetric _$SnPollMetricFromJson(Map json) { + return _SnPollMetric.fromJson(json); +} + +/// @nodoc +mixin _$SnPollMetric { + int get totalAnswer => throw _privateConstructorUsedError; + dynamic get byOptions => throw _privateConstructorUsedError; + + /// Serializes this SnPollMetric to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SnPollMetric + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SnPollMetricCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SnPollMetricCopyWith<$Res> { + factory $SnPollMetricCopyWith( + SnPollMetric value, $Res Function(SnPollMetric) then) = + _$SnPollMetricCopyWithImpl<$Res, SnPollMetric>; + @useResult + $Res call({int totalAnswer, dynamic byOptions}); +} + +/// @nodoc +class _$SnPollMetricCopyWithImpl<$Res, $Val extends SnPollMetric> + implements $SnPollMetricCopyWith<$Res> { + _$SnPollMetricCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SnPollMetric + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? totalAnswer = null, + Object? byOptions = freezed, + }) { + return _then(_value.copyWith( + totalAnswer: null == totalAnswer + ? _value.totalAnswer + : totalAnswer // ignore: cast_nullable_to_non_nullable + as int, + byOptions: freezed == byOptions + ? _value.byOptions + : byOptions // ignore: cast_nullable_to_non_nullable + as dynamic, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SnPollMetricImplCopyWith<$Res> + implements $SnPollMetricCopyWith<$Res> { + factory _$$SnPollMetricImplCopyWith( + _$SnPollMetricImpl value, $Res Function(_$SnPollMetricImpl) then) = + __$$SnPollMetricImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int totalAnswer, dynamic byOptions}); +} + +/// @nodoc +class __$$SnPollMetricImplCopyWithImpl<$Res> + extends _$SnPollMetricCopyWithImpl<$Res, _$SnPollMetricImpl> + implements _$$SnPollMetricImplCopyWith<$Res> { + __$$SnPollMetricImplCopyWithImpl( + _$SnPollMetricImpl _value, $Res Function(_$SnPollMetricImpl) _then) + : super(_value, _then); + + /// Create a copy of SnPollMetric + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? totalAnswer = null, + Object? byOptions = freezed, + }) { + return _then(_$SnPollMetricImpl( + totalAnswer: null == totalAnswer + ? _value.totalAnswer + : totalAnswer // ignore: cast_nullable_to_non_nullable + as int, + byOptions: freezed == byOptions + ? _value.byOptions + : byOptions // ignore: cast_nullable_to_non_nullable + as dynamic, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnPollMetricImpl implements _SnPollMetric { + const _$SnPollMetricImpl( + {required this.totalAnswer, required this.byOptions}); + + factory _$SnPollMetricImpl.fromJson(Map json) => + _$$SnPollMetricImplFromJson(json); + + @override + final int totalAnswer; + @override + final dynamic byOptions; + + @override + String toString() { + return 'SnPollMetric(totalAnswer: $totalAnswer, byOptions: $byOptions)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SnPollMetricImpl && + (identical(other.totalAnswer, totalAnswer) || + other.totalAnswer == totalAnswer) && + const DeepCollectionEquality().equals(other.byOptions, byOptions)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, totalAnswer, const DeepCollectionEquality().hash(byOptions)); + + /// Create a copy of SnPollMetric + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith => + __$$SnPollMetricImplCopyWithImpl<_$SnPollMetricImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SnPollMetricImplToJson( + this, + ); + } +} + +abstract class _SnPollMetric implements SnPollMetric { + const factory _SnPollMetric( + {required final int totalAnswer, + required final dynamic byOptions}) = _$SnPollMetricImpl; + + factory _SnPollMetric.fromJson(Map json) = + _$SnPollMetricImpl.fromJson; + + @override + int get totalAnswer; + @override + dynamic get byOptions; + + /// Create a copy of SnPollMetric + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith => + throw _privateConstructorUsedError; +} + +SnPollOption _$SnPollOptionFromJson(Map json) { + return _SnPollOption.fromJson(json); +} + +/// @nodoc +mixin _$SnPollOption { + String get id => throw _privateConstructorUsedError; + String get icon => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + + /// Serializes this SnPollOption to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SnPollOption + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SnPollOptionCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SnPollOptionCopyWith<$Res> { + factory $SnPollOptionCopyWith( + SnPollOption value, $Res Function(SnPollOption) then) = + _$SnPollOptionCopyWithImpl<$Res, SnPollOption>; + @useResult + $Res call({String id, String icon, String name, String description}); +} + +/// @nodoc +class _$SnPollOptionCopyWithImpl<$Res, $Val extends SnPollOption> + implements $SnPollOptionCopyWith<$Res> { + _$SnPollOptionCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SnPollOption + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? icon = null, + Object? name = null, + Object? description = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + icon: null == icon + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SnPollOptionImplCopyWith<$Res> + implements $SnPollOptionCopyWith<$Res> { + factory _$$SnPollOptionImplCopyWith( + _$SnPollOptionImpl value, $Res Function(_$SnPollOptionImpl) then) = + __$$SnPollOptionImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String id, String icon, String name, String description}); +} + +/// @nodoc +class __$$SnPollOptionImplCopyWithImpl<$Res> + extends _$SnPollOptionCopyWithImpl<$Res, _$SnPollOptionImpl> + implements _$$SnPollOptionImplCopyWith<$Res> { + __$$SnPollOptionImplCopyWithImpl( + _$SnPollOptionImpl _value, $Res Function(_$SnPollOptionImpl) _then) + : super(_value, _then); + + /// Create a copy of SnPollOption + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? icon = null, + Object? name = null, + Object? description = null, + }) { + return _then(_$SnPollOptionImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + icon: null == icon + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnPollOptionImpl implements _SnPollOption { + const _$SnPollOptionImpl( + {required this.id, + required this.icon, + required this.name, + required this.description}); + + factory _$SnPollOptionImpl.fromJson(Map json) => + _$$SnPollOptionImplFromJson(json); + + @override + final String id; + @override + final String icon; + @override + final String name; + @override + final String description; + + @override + String toString() { + return 'SnPollOption(id: $id, icon: $icon, name: $name, description: $description)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SnPollOptionImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.icon, icon) || other.icon == icon) && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, icon, name, description); + + /// Create a copy of SnPollOption + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith => + __$$SnPollOptionImplCopyWithImpl<_$SnPollOptionImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SnPollOptionImplToJson( + this, + ); + } +} + +abstract class _SnPollOption implements SnPollOption { + const factory _SnPollOption( + {required final String id, + required final String icon, + required final String name, + required final String description}) = _$SnPollOptionImpl; + + factory _SnPollOption.fromJson(Map json) = + _$SnPollOptionImpl.fromJson; + + @override + String get id; + @override + String get icon; + @override + String get name; + @override + String get description; + + /// Create a copy of SnPollOption + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/types/poll.g.dart b/lib/types/poll.g.dart new file mode 100644 index 0000000..9c6b093 --- /dev/null +++ b/lib/types/poll.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'poll.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$SnPollImpl _$$SnPollImplFromJson(Map json) => _$SnPollImpl( + id: (json['id'] as num).toInt(), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'], + expiredAt: json['expired_at'], + options: (json['options'] as List) + .map((e) => SnPollOption.fromJson(e as Map)) + .toList(), + accountId: (json['account_id'] as num).toInt(), + metric: SnPollMetric.fromJson(json['metric'] as Map), + ); + +Map _$$SnPollImplToJson(_$SnPollImpl instance) => + { + 'id': instance.id, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt, + 'expired_at': instance.expiredAt, + 'options': instance.options.map((e) => e.toJson()).toList(), + 'account_id': instance.accountId, + 'metric': instance.metric.toJson(), + }; + +_$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map json) => + _$SnPollMetricImpl( + totalAnswer: (json['total_answer'] as num).toInt(), + byOptions: json['by_options'], + ); + +Map _$$SnPollMetricImplToJson(_$SnPollMetricImpl instance) => + { + 'total_answer': instance.totalAnswer, + 'by_options': instance.byOptions, + }; + +_$SnPollOptionImpl _$$SnPollOptionImplFromJson(Map json) => + _$SnPollOptionImpl( + id: json['id'] as String, + icon: json['icon'] as String, + name: json['name'] as String, + description: json['description'] as String, + ); + +Map _$$SnPollOptionImplToJson(_$SnPollOptionImpl instance) => + { + 'id': instance.id, + 'icon': instance.icon, + 'name': instance.name, + 'description': instance.description, + }; diff --git a/lib/types/post.dart b/lib/types/post.dart index 3912eb1..aeab920 100644 --- a/lib/types/post.dart +++ b/lib/types/post.dart @@ -1,5 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:surface/types/attachment.dart'; +import 'package:surface/types/poll.dart'; part 'post.freezed.dart'; part 'post.g.dart'; @@ -37,6 +38,7 @@ class SnPost with _$SnPost { required int totalUpvote, required int totalDownvote, required int publisherId, + required int? pollId, required SnPublisher publisher, required SnMetric metric, SnPostPreload? preload, @@ -90,6 +92,7 @@ class SnPostPreload with _$SnPostPreload { required SnAttachment? thumbnail, required List? attachments, required SnAttachment? video, + required SnPoll? poll, }) = _SnPostPreload; factory SnPostPreload.fromJson(Map json) => diff --git a/lib/types/post.freezed.dart b/lib/types/post.freezed.dart index 89712ef..b16bc44 100644 --- a/lib/types/post.freezed.dart +++ b/lib/types/post.freezed.dart @@ -48,6 +48,7 @@ mixin _$SnPost { int get totalUpvote => throw _privateConstructorUsedError; int get totalDownvote => throw _privateConstructorUsedError; int get publisherId => throw _privateConstructorUsedError; + int? get pollId => throw _privateConstructorUsedError; SnPublisher get publisher => throw _privateConstructorUsedError; SnMetric get metric => throw _privateConstructorUsedError; SnPostPreload? get preload => throw _privateConstructorUsedError; @@ -95,6 +96,7 @@ abstract class $SnPostCopyWith<$Res> { int totalUpvote, int totalDownvote, int publisherId, + int? pollId, SnPublisher publisher, SnMetric metric, SnPostPreload? preload}); @@ -149,6 +151,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> Object? totalUpvote = null, Object? totalDownvote = null, Object? publisherId = null, + Object? pollId = freezed, Object? publisher = null, Object? metric = null, Object? preload = freezed, @@ -266,6 +269,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> ? _value.publisherId : publisherId // ignore: cast_nullable_to_non_nullable as int, + pollId: freezed == pollId + ? _value.pollId + : pollId // ignore: cast_nullable_to_non_nullable + as int?, publisher: null == publisher ? _value.publisher : publisher // ignore: cast_nullable_to_non_nullable @@ -380,6 +387,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { int totalUpvote, int totalDownvote, int publisherId, + int? pollId, SnPublisher publisher, SnMetric metric, SnPostPreload? preload}); @@ -437,6 +445,7 @@ class __$$SnPostImplCopyWithImpl<$Res> Object? totalUpvote = null, Object? totalDownvote = null, Object? publisherId = null, + Object? pollId = freezed, Object? publisher = null, Object? metric = null, Object? preload = freezed, @@ -554,6 +563,10 @@ class __$$SnPostImplCopyWithImpl<$Res> ? _value.publisherId : publisherId // ignore: cast_nullable_to_non_nullable as int, + pollId: freezed == pollId + ? _value.pollId + : pollId // ignore: cast_nullable_to_non_nullable + as int?, publisher: null == publisher ? _value.publisher : publisher // ignore: cast_nullable_to_non_nullable @@ -602,6 +615,7 @@ class _$SnPostImpl extends _SnPost { required this.totalUpvote, required this.totalDownvote, required this.publisherId, + required this.pollId, required this.publisher, required this.metric, this.preload}) @@ -719,6 +733,8 @@ class _$SnPostImpl extends _SnPost { @override final int publisherId; @override + final int? pollId; + @override final SnPublisher publisher; @override final SnMetric metric; @@ -727,7 +743,7 @@ class _$SnPostImpl extends _SnPost { @override String toString() { - return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)'; + return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)'; } @override @@ -782,6 +798,7 @@ class _$SnPostImpl extends _SnPost { other.totalDownvote == totalDownvote) && (identical(other.publisherId, publisherId) || other.publisherId == publisherId) && + (identical(other.pollId, pollId) || other.pollId == pollId) && (identical(other.publisher, publisher) || other.publisher == publisher) && (identical(other.metric, metric) || other.metric == metric) && @@ -820,6 +837,7 @@ class _$SnPostImpl extends _SnPost { totalUpvote, totalDownvote, publisherId, + pollId, publisher, metric, preload @@ -871,6 +889,7 @@ abstract class _SnPost extends SnPost { required final int totalUpvote, required final int totalDownvote, required final int publisherId, + required final int? pollId, required final SnPublisher publisher, required final SnMetric metric, final SnPostPreload? preload}) = _$SnPostImpl; @@ -935,6 +954,8 @@ abstract class _SnPost extends SnPost { @override int get publisherId; @override + int? get pollId; + @override SnPublisher get publisher; @override SnMetric get metric; @@ -1568,6 +1589,7 @@ mixin _$SnPostPreload { SnAttachment? get thumbnail => throw _privateConstructorUsedError; List? get attachments => throw _privateConstructorUsedError; SnAttachment? get video => throw _privateConstructorUsedError; + SnPoll? get poll => throw _privateConstructorUsedError; /// Serializes this SnPostPreload to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -1588,10 +1610,12 @@ abstract class $SnPostPreloadCopyWith<$Res> { $Res call( {SnAttachment? thumbnail, List? attachments, - SnAttachment? video}); + SnAttachment? video, + SnPoll? poll}); $SnAttachmentCopyWith<$Res>? get thumbnail; $SnAttachmentCopyWith<$Res>? get video; + $SnPollCopyWith<$Res>? get poll; } /// @nodoc @@ -1612,6 +1636,7 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload> Object? thumbnail = freezed, Object? attachments = freezed, Object? video = freezed, + Object? poll = freezed, }) { return _then(_value.copyWith( thumbnail: freezed == thumbnail @@ -1626,6 +1651,10 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload> ? _value.video : video // ignore: cast_nullable_to_non_nullable as SnAttachment?, + poll: freezed == poll + ? _value.poll + : poll // ignore: cast_nullable_to_non_nullable + as SnPoll?, ) as $Val); } @@ -1656,6 +1685,20 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload> return _then(_value.copyWith(video: value) as $Val); }); } + + /// Create a copy of SnPostPreload + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnPollCopyWith<$Res>? get poll { + if (_value.poll == null) { + return null; + } + + return $SnPollCopyWith<$Res>(_value.poll!, (value) { + return _then(_value.copyWith(poll: value) as $Val); + }); + } } /// @nodoc @@ -1669,12 +1712,15 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res> $Res call( {SnAttachment? thumbnail, List? attachments, - SnAttachment? video}); + SnAttachment? video, + SnPoll? poll}); @override $SnAttachmentCopyWith<$Res>? get thumbnail; @override $SnAttachmentCopyWith<$Res>? get video; + @override + $SnPollCopyWith<$Res>? get poll; } /// @nodoc @@ -1693,6 +1739,7 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res> Object? thumbnail = freezed, Object? attachments = freezed, Object? video = freezed, + Object? poll = freezed, }) { return _then(_$SnPostPreloadImpl( thumbnail: freezed == thumbnail @@ -1707,6 +1754,10 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res> ? _value.video : video // ignore: cast_nullable_to_non_nullable as SnAttachment?, + poll: freezed == poll + ? _value.poll + : poll // ignore: cast_nullable_to_non_nullable + as SnPoll?, )); } } @@ -1717,7 +1768,8 @@ class _$SnPostPreloadImpl implements _SnPostPreload { const _$SnPostPreloadImpl( {required this.thumbnail, required final List? attachments, - required this.video}) + required this.video, + required this.poll}) : _attachments = attachments; factory _$SnPostPreloadImpl.fromJson(Map json) => @@ -1737,10 +1789,12 @@ class _$SnPostPreloadImpl implements _SnPostPreload { @override final SnAttachment? video; + @override + final SnPoll? poll; @override String toString() { - return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video)'; + return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video, poll: $poll)'; } @override @@ -1752,13 +1806,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload { other.thumbnail == thumbnail) && const DeepCollectionEquality() .equals(other._attachments, _attachments) && - (identical(other.video, video) || other.video == video)); + (identical(other.video, video) || other.video == video) && + (identical(other.poll, poll) || other.poll == poll)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, thumbnail, - const DeepCollectionEquality().hash(_attachments), video); + const DeepCollectionEquality().hash(_attachments), video, poll); /// Create a copy of SnPostPreload /// with the given fields replaced by the non-null parameter values. @@ -1780,7 +1835,8 @@ abstract class _SnPostPreload implements SnPostPreload { const factory _SnPostPreload( {required final SnAttachment? thumbnail, required final List? attachments, - required final SnAttachment? video}) = _$SnPostPreloadImpl; + required final SnAttachment? video, + required final SnPoll? poll}) = _$SnPostPreloadImpl; factory _SnPostPreload.fromJson(Map json) = _$SnPostPreloadImpl.fromJson; @@ -1791,6 +1847,8 @@ abstract class _SnPostPreload implements SnPostPreload { List? get attachments; @override SnAttachment? get video; + @override + SnPoll? get poll; /// Create a copy of SnPostPreload /// with the given fields replaced by the non-null parameter values. diff --git a/lib/types/post.g.dart b/lib/types/post.g.dart index d2cb706..ea98319 100644 --- a/lib/types/post.g.dart +++ b/lib/types/post.g.dart @@ -63,6 +63,7 @@ _$SnPostImpl _$$SnPostImplFromJson(Map json) => _$SnPostImpl( totalUpvote: (json['total_upvote'] as num).toInt(), totalDownvote: (json['total_downvote'] as num).toInt(), publisherId: (json['publisher_id'] as num).toInt(), + pollId: (json['poll_id'] as num?)?.toInt(), publisher: SnPublisher.fromJson(json['publisher'] as Map), metric: SnMetric.fromJson(json['metric'] as Map), @@ -101,6 +102,7 @@ Map _$$SnPostImplToJson(_$SnPostImpl instance) => 'total_upvote': instance.totalUpvote, 'total_downvote': instance.totalDownvote, 'publisher_id': instance.publisherId, + 'poll_id': instance.pollId, 'publisher': instance.publisher.toJson(), 'metric': instance.metric.toJson(), 'preload': instance.preload?.toJson(), @@ -168,6 +170,9 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map json) => video: json['video'] == null ? null : SnAttachment.fromJson(json['video'] as Map), + poll: json['poll'] == null + ? null + : SnPoll.fromJson(json['poll'] as Map), ); Map _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) => @@ -175,6 +180,7 @@ Map _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) => 'thumbnail': instance.thumbnail?.toJson(), 'attachments': instance.attachments?.map((e) => e?.toJson()).toList(), 'video': instance.video?.toJson(), + 'poll': instance.poll?.toJson(), }; _$SnBodyImpl _$$SnBodyImplFromJson(Map json) => _$SnBodyImpl( diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index 5355c20..df4c1cc 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -352,10 +352,9 @@ class ChatMessageInputState extends State { Symbols.mood, color: Theme.of(context).colorScheme.primary, ), - visualDensity: const VisualDensity( - horizontal: -4, - vertical: -4, - ), + visualDensity: const VisualDensity(horizontal: -4, vertical: -4), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), onPressed: () { _showEmojiPicker(context); }, @@ -373,10 +372,9 @@ class ChatMessageInputState extends State { Symbols.send, color: Theme.of(context).colorScheme.primary, ), - visualDensity: const VisualDensity( - horizontal: -4, - vertical: -4, - ), + visualDensity: const VisualDensity(horizontal: -4, vertical: -4), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), ), ], ), diff --git a/lib/widgets/post/post_poll.dart b/lib/widgets/post/post_poll.dart new file mode 100644 index 0000000..7c17bfd --- /dev/null +++ b/lib/widgets/post/post_poll.dart @@ -0,0 +1,201 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:provider/provider.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/poll.dart'; +import 'package:surface/widgets/dialog.dart'; +import 'package:uuid/uuid.dart'; + +class PollEditorDialog extends StatefulWidget { + final SnPoll? poll; + + const PollEditorDialog({super.key, this.poll}); + + @override + State createState() => _PollEditorDialogState(); +} + +class _PollEditorDialogState extends State { + final TextEditingController _linkController = TextEditingController(); + final List _pollOptions = List.empty(growable: true); + + bool _isBusy = false; + + Future _fetchPoll() async { + if (_linkController.text.isEmpty) return; + try { + setState(() => _isBusy = true); + final sn = context.read(); + final resp = await sn.client.get('/cgi/co/polls/${_linkController.text}'); + final out = SnPoll.fromJson(resp.data); + if (!mounted) return; + Navigator.pop(context, out); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + Future _applyPost() async { + try { + setState(() => _isBusy = true); + final sn = context.read(); + final resp = widget.poll == null + ? await sn.client.post('/cgi/co/polls', data: { + 'options': _pollOptions.where((ele) => ele.name.isNotEmpty).toList(), + }) + : await sn.client.put('/cgi/co/polls/${widget.poll!.id}', data: { + 'options': _pollOptions.where((ele) => ele.name.isNotEmpty).toList(), + }); + final out = SnPoll.fromJson(resp.data); + if (!mounted) return; + Navigator.pop(context, out); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + Future _deletePoll() async { + final confirm = await context.showConfirmDialog( + 'pollEditorDelete'.tr(), + 'pollEditorDeleteDescription'.tr(), + ); + if (!confirm) return; + if (!mounted) return; + + try { + setState(() => _isBusy = true); + final sn = context.read(); + await sn.client.delete('/cgi/co/polls/${widget.poll!.id}'); + if (!mounted) return; + Navigator.pop(context, false); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + _pollOptions.addAll(widget.poll?.options ?? []); + } + + @override + void dispose() { + _linkController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(widget.poll == null ? 'pollEditorNew' : 'pollEditorEdit').tr(), + content: Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + if (widget.poll == null) + TextField( + controller: _linkController, + decoration: InputDecoration( + isDense: true, + labelText: 'pollLinkExisting'.tr(), + prefixText: '#', + suffixIcon: IconButton( + visualDensity: const VisualDensity(horizontal: -4, vertical: -4), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + onPressed: _isBusy ? null : () => _fetchPoll(), + icon: const Icon(Icons.keyboard_arrow_right), + ), + border: const OutlineInputBorder(), + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + Card( + margin: EdgeInsets.zero, + child: Column( + children: [ + for (int i = 0; i < _pollOptions.length; i++) + ListTile( + leading: const Icon(Symbols.circle), + title: TextFormField( + decoration: InputDecoration.collapsed( + hintText: 'pollOptionName'.tr(), + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + initialValue: _pollOptions[i].name, + onChanged: (value) { + // Looks like we don't need set state here cuz it got internal updated. + _pollOptions[i] = _pollOptions[i].copyWith(name: value); + }, + ), + trailing: IconButton( + visualDensity: const VisualDensity(horizontal: -4, vertical: -4), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + setState(() => _pollOptions.removeAt(i)); + }, + icon: const Icon(Icons.close), + ), + ), + ListTile( + leading: const Icon(Symbols.add), + title: Text('pollOptionAdd').tr(), + onTap: () { + setState( + () => _pollOptions.add( + SnPollOption(id: const Uuid().v4(), icon: '', name: '', description: ''), + ), + ); + }, + ), + ], + ), + ), + if (widget.poll != null) + Card( + margin: EdgeInsets.zero, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Symbols.delete), + trailing: const Icon(Symbols.chevron_right), + title: Text('pollEditorDelete').tr(), + onTap: _isBusy ? null : () => _deletePoll(), + ), + ListTile( + leading: const Icon(Symbols.link_off), + trailing: const Icon(Symbols.chevron_right), + title: Text('pollEditorUnlink').tr(), + onTap: _isBusy ? null : () => Navigator.pop(context, false), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: _isBusy ? null : () => Navigator.pop(context), + child: Text('cancel'.tr()), + ), + TextButton( + onPressed: _isBusy ? null : () => _applyPost(), + child: Text('dialogConfirm'.tr()), + ), + ], + ); + } +}