✨ Poll participate
This commit is contained in:
parent
4937dee182
commit
cb24bd953d
@ -633,5 +633,10 @@
|
|||||||
"pollEditorUnlink": "Unlink Poll",
|
"pollEditorUnlink": "Unlink Poll",
|
||||||
"pollOptionAdd": "Add Option",
|
"pollOptionAdd": "Add Option",
|
||||||
"pollOptionName": "Option Name",
|
"pollOptionName": "Option Name",
|
||||||
"pollLinkExisting": "Link existing poll"
|
"pollLinkExisting": "Link existing poll",
|
||||||
|
"pollAnswered": "Answered the poll.",
|
||||||
|
"pollVotes": {
|
||||||
|
"one": "{} vote",
|
||||||
|
"other": "{} votes"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -632,5 +632,10 @@
|
|||||||
"pollEditorUnlink": "解除链接",
|
"pollEditorUnlink": "解除链接",
|
||||||
"pollOptionAdd": "添加选项",
|
"pollOptionAdd": "添加选项",
|
||||||
"pollOptionName": "选项名称",
|
"pollOptionName": "选项名称",
|
||||||
"pollLinkExisting": "链接现有投票"
|
"pollLinkExisting": "链接现有投票",
|
||||||
|
"pollAnswered": "答案已经反馈。",
|
||||||
|
"pollVotes": {
|
||||||
|
"one": "{} 票",
|
||||||
|
"other": "{} 票"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import 'package:surface/providers/config.dart';
|
|||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/poll.dart';
|
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_input.dart';
|
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||||
@ -32,7 +31,7 @@ import 'package:surface/widgets/post/post_media_pending_list.dart';
|
|||||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/widgets/post/post_poll.dart';
|
import 'package:surface/widgets/post/post_poll_editor.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class PostEditorExtra {
|
class PostEditorExtra {
|
||||||
|
@ -24,7 +24,8 @@ class SnPoll with _$SnPoll {
|
|||||||
class SnPollMetric with _$SnPollMetric {
|
class SnPollMetric with _$SnPollMetric {
|
||||||
const factory SnPollMetric({
|
const factory SnPollMetric({
|
||||||
required int totalAnswer,
|
required int totalAnswer,
|
||||||
required dynamic byOptions,
|
@Default({}) Map<String, int> byOptions,
|
||||||
|
@Default({}) Map<String, int> byOptionsPercentage,
|
||||||
}) = _SnPollMetric;
|
}) = _SnPollMetric;
|
||||||
|
|
||||||
factory SnPollMetric.fromJson(Map<String, Object?> json)
|
factory SnPollMetric.fromJson(Map<String, Object?> json)
|
||||||
|
@ -344,7 +344,9 @@ SnPollMetric _$SnPollMetricFromJson(Map<String, dynamic> json) {
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnPollMetric {
|
mixin _$SnPollMetric {
|
||||||
int get totalAnswer => throw _privateConstructorUsedError;
|
int get totalAnswer => throw _privateConstructorUsedError;
|
||||||
dynamic get byOptions => throw _privateConstructorUsedError;
|
Map<String, int> get byOptions => throw _privateConstructorUsedError;
|
||||||
|
Map<String, int> get byOptionsPercentage =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this SnPollMetric to a JSON map.
|
/// Serializes this SnPollMetric to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@ -362,7 +364,10 @@ abstract class $SnPollMetricCopyWith<$Res> {
|
|||||||
SnPollMetric value, $Res Function(SnPollMetric) then) =
|
SnPollMetric value, $Res Function(SnPollMetric) then) =
|
||||||
_$SnPollMetricCopyWithImpl<$Res, SnPollMetric>;
|
_$SnPollMetricCopyWithImpl<$Res, SnPollMetric>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({int totalAnswer, dynamic byOptions});
|
$Res call(
|
||||||
|
{int totalAnswer,
|
||||||
|
Map<String, int> byOptions,
|
||||||
|
Map<String, int> byOptionsPercentage});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -381,17 +386,22 @@ class _$SnPollMetricCopyWithImpl<$Res, $Val extends SnPollMetric>
|
|||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? totalAnswer = null,
|
Object? totalAnswer = null,
|
||||||
Object? byOptions = freezed,
|
Object? byOptions = null,
|
||||||
|
Object? byOptionsPercentage = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
totalAnswer: null == totalAnswer
|
totalAnswer: null == totalAnswer
|
||||||
? _value.totalAnswer
|
? _value.totalAnswer
|
||||||
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
byOptions: freezed == byOptions
|
byOptions: null == byOptions
|
||||||
? _value.byOptions
|
? _value.byOptions
|
||||||
: byOptions // ignore: cast_nullable_to_non_nullable
|
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as Map<String, int>,
|
||||||
|
byOptionsPercentage: null == byOptionsPercentage
|
||||||
|
? _value.byOptionsPercentage
|
||||||
|
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, int>,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -404,7 +414,10 @@ abstract class _$$SnPollMetricImplCopyWith<$Res>
|
|||||||
__$$SnPollMetricImplCopyWithImpl<$Res>;
|
__$$SnPollMetricImplCopyWithImpl<$Res>;
|
||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({int totalAnswer, dynamic byOptions});
|
$Res call(
|
||||||
|
{int totalAnswer,
|
||||||
|
Map<String, int> byOptions,
|
||||||
|
Map<String, int> byOptionsPercentage});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -421,17 +434,22 @@ class __$$SnPollMetricImplCopyWithImpl<$Res>
|
|||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? totalAnswer = null,
|
Object? totalAnswer = null,
|
||||||
Object? byOptions = freezed,
|
Object? byOptions = null,
|
||||||
|
Object? byOptionsPercentage = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$SnPollMetricImpl(
|
return _then(_$SnPollMetricImpl(
|
||||||
totalAnswer: null == totalAnswer
|
totalAnswer: null == totalAnswer
|
||||||
? _value.totalAnswer
|
? _value.totalAnswer
|
||||||
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
byOptions: freezed == byOptions
|
byOptions: null == byOptions
|
||||||
? _value.byOptions
|
? _value._byOptions
|
||||||
: byOptions // ignore: cast_nullable_to_non_nullable
|
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as Map<String, int>,
|
||||||
|
byOptionsPercentage: null == byOptionsPercentage
|
||||||
|
? _value._byOptionsPercentage
|
||||||
|
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, int>,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -440,19 +458,39 @@ class __$$SnPollMetricImplCopyWithImpl<$Res>
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$SnPollMetricImpl implements _SnPollMetric {
|
class _$SnPollMetricImpl implements _SnPollMetric {
|
||||||
const _$SnPollMetricImpl(
|
const _$SnPollMetricImpl(
|
||||||
{required this.totalAnswer, required this.byOptions});
|
{required this.totalAnswer,
|
||||||
|
final Map<String, int> byOptions = const {},
|
||||||
|
final Map<String, int> byOptionsPercentage = const {}})
|
||||||
|
: _byOptions = byOptions,
|
||||||
|
_byOptionsPercentage = byOptionsPercentage;
|
||||||
|
|
||||||
factory _$SnPollMetricImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$SnPollMetricImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$SnPollMetricImplFromJson(json);
|
_$$SnPollMetricImplFromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int totalAnswer;
|
final int totalAnswer;
|
||||||
|
final Map<String, int> _byOptions;
|
||||||
@override
|
@override
|
||||||
final dynamic byOptions;
|
@JsonKey()
|
||||||
|
Map<String, int> get byOptions {
|
||||||
|
if (_byOptions is EqualUnmodifiableMapView) return _byOptions;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_byOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, int> _byOptionsPercentage;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<String, int> get byOptionsPercentage {
|
||||||
|
if (_byOptionsPercentage is EqualUnmodifiableMapView)
|
||||||
|
return _byOptionsPercentage;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_byOptionsPercentage);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnPollMetric(totalAnswer: $totalAnswer, byOptions: $byOptions)';
|
return 'SnPollMetric(totalAnswer: $totalAnswer, byOptions: $byOptions, byOptionsPercentage: $byOptionsPercentage)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -462,13 +500,19 @@ class _$SnPollMetricImpl implements _SnPollMetric {
|
|||||||
other is _$SnPollMetricImpl &&
|
other is _$SnPollMetricImpl &&
|
||||||
(identical(other.totalAnswer, totalAnswer) ||
|
(identical(other.totalAnswer, totalAnswer) ||
|
||||||
other.totalAnswer == totalAnswer) &&
|
other.totalAnswer == totalAnswer) &&
|
||||||
const DeepCollectionEquality().equals(other.byOptions, byOptions));
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._byOptions, _byOptions) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._byOptionsPercentage, _byOptionsPercentage));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
runtimeType, totalAnswer, const DeepCollectionEquality().hash(byOptions));
|
runtimeType,
|
||||||
|
totalAnswer,
|
||||||
|
const DeepCollectionEquality().hash(_byOptions),
|
||||||
|
const DeepCollectionEquality().hash(_byOptionsPercentage));
|
||||||
|
|
||||||
/// Create a copy of SnPollMetric
|
/// Create a copy of SnPollMetric
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -489,7 +533,8 @@ class _$SnPollMetricImpl implements _SnPollMetric {
|
|||||||
abstract class _SnPollMetric implements SnPollMetric {
|
abstract class _SnPollMetric implements SnPollMetric {
|
||||||
const factory _SnPollMetric(
|
const factory _SnPollMetric(
|
||||||
{required final int totalAnswer,
|
{required final int totalAnswer,
|
||||||
required final dynamic byOptions}) = _$SnPollMetricImpl;
|
final Map<String, int> byOptions,
|
||||||
|
final Map<String, int> byOptionsPercentage}) = _$SnPollMetricImpl;
|
||||||
|
|
||||||
factory _SnPollMetric.fromJson(Map<String, dynamic> json) =
|
factory _SnPollMetric.fromJson(Map<String, dynamic> json) =
|
||||||
_$SnPollMetricImpl.fromJson;
|
_$SnPollMetricImpl.fromJson;
|
||||||
@ -497,7 +542,9 @@ abstract class _SnPollMetric implements SnPollMetric {
|
|||||||
@override
|
@override
|
||||||
int get totalAnswer;
|
int get totalAnswer;
|
||||||
@override
|
@override
|
||||||
dynamic get byOptions;
|
Map<String, int> get byOptions;
|
||||||
|
@override
|
||||||
|
Map<String, int> get byOptionsPercentage;
|
||||||
|
|
||||||
/// Create a copy of SnPollMetric
|
/// Create a copy of SnPollMetric
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@ -34,13 +34,22 @@ Map<String, dynamic> _$$SnPollImplToJson(_$SnPollImpl instance) =>
|
|||||||
_$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) =>
|
_$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) =>
|
||||||
_$SnPollMetricImpl(
|
_$SnPollMetricImpl(
|
||||||
totalAnswer: (json['total_answer'] as num).toInt(),
|
totalAnswer: (json['total_answer'] as num).toInt(),
|
||||||
byOptions: json['by_options'],
|
byOptions: (json['by_options'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||||
|
) ??
|
||||||
|
const {},
|
||||||
|
byOptionsPercentage:
|
||||||
|
(json['by_options_percentage'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||||
|
) ??
|
||||||
|
const {},
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$SnPollMetricImplToJson(_$SnPollMetricImpl instance) =>
|
Map<String, dynamic> _$$SnPollMetricImplToJson(_$SnPollMetricImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'total_answer': instance.totalAnswer,
|
'total_answer': instance.totalAnswer,
|
||||||
'by_options': instance.byOptions,
|
'by_options': instance.byOptions,
|
||||||
|
'by_options_percentage': instance.byOptionsPercentage,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SnPollOptionImpl _$$SnPollOptionImplFromJson(Map<String, dynamic> json) =>
|
_$SnPollOptionImpl _$$SnPollOptionImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
@ -36,6 +36,7 @@ import 'package:surface/widgets/markdown_content.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||||
|
import 'package:surface/widgets/post/post_poll.dart';
|
||||||
import 'package:surface/widgets/post/post_reaction.dart';
|
import 'package:surface/widgets/post/post_reaction.dart';
|
||||||
import 'package:surface/widgets/post/publisher_popover.dart';
|
import 'package:surface/widgets/post/publisher_popover.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
@ -218,7 +219,7 @@ class PostItem extends StatelessWidget {
|
|||||||
).padding(bottom: 8),
|
).padding(bottom: 8),
|
||||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8),
|
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8),
|
||||||
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
||||||
_PostFeaturedComment(data: data).padding(),
|
_PostFeaturedComment(data: data),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
data: data,
|
data: data,
|
||||||
showComments: true,
|
showComments: true,
|
||||||
@ -389,6 +390,7 @@ class PostItem extends StatelessWidget {
|
|||||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
|
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4),
|
||||||
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||||
LinkPreviewWidget(
|
LinkPreviewWidget(
|
||||||
text: data.body['content'],
|
text: data.body['content'],
|
||||||
|
@ -1,201 +1,116 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/poll.dart';
|
import 'package:surface/types/poll.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
class PollEditorDialog extends StatefulWidget {
|
class PostPoll extends StatefulWidget {
|
||||||
final SnPoll? poll;
|
final SnPoll poll;
|
||||||
|
|
||||||
const PollEditorDialog({super.key, this.poll});
|
const PostPoll({super.key, required this.poll});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PollEditorDialog> createState() => _PollEditorDialogState();
|
State<PostPoll> createState() => _PostPollState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PollEditorDialogState extends State<PollEditorDialog> {
|
class _PostPollState extends State<PostPoll> {
|
||||||
final TextEditingController _linkController = TextEditingController();
|
|
||||||
final List<SnPollOption> _pollOptions = List.empty(growable: true);
|
|
||||||
|
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
late SnPoll _poll;
|
||||||
Future<void> _fetchPoll() async {
|
|
||||||
if (_linkController.text.isEmpty) return;
|
|
||||||
try {
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
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<void> _applyPost() async {
|
|
||||||
try {
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
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<void> _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<SnNetworkProvider>();
|
|
||||||
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
_poll = widget.poll;
|
||||||
|
_fetchAnswer();
|
||||||
super.initState();
|
super.initState();
|
||||||
_pollOptions.addAll(widget.poll?.options ?? []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
String? _answeredChoice;
|
||||||
void dispose() {
|
|
||||||
_linkController.dispose();
|
Future<void> _fetchAnswer() async {
|
||||||
super.dispose();
|
final ua = context.read<UserProvider>();
|
||||||
|
if (!ua.isAuthorized) return;
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/co/polls/${widget.poll.id}/answer');
|
||||||
|
_answeredChoice = resp.data?['answer'];
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {});
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _voteForOption(SnPollOption option) async {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
if (!ua.isAuthorized) return;
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post('/cgi/co/polls/${widget.poll.id}/answer', data: {
|
||||||
|
'answer': option.id,
|
||||||
|
});
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('pollAnswered'.tr());
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return Card(
|
||||||
title: Text(widget.poll == null ? 'pollEditorNew' : 'pollEditorEdit').tr(),
|
margin: EdgeInsets.zero,
|
||||||
content: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
spacing: 16,
|
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll == null)
|
for (final option in _poll.options)
|
||||||
TextField(
|
Stack(
|
||||||
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: [
|
children: [
|
||||||
for (int i = 0; i < _pollOptions.length; i++)
|
Container(
|
||||||
ListTile(
|
height: 60,
|
||||||
leading: const Icon(Symbols.circle),
|
width: MediaQuery.of(context).size.width * (_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble(),
|
||||||
title: TextFormField(
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
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(
|
ListTile(
|
||||||
leading: const Icon(Symbols.add),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
title: Text('pollOptionAdd').tr(),
|
minTileHeight: 60,
|
||||||
onTap: () {
|
leading: _answeredChoice == option.id ? const Icon(Symbols.circle, fill: 1) : const Icon(Symbols.circle),
|
||||||
setState(
|
title: Text(option.name),
|
||||||
() => _pollOptions.add(
|
subtitle: Column(
|
||||||
SnPollOption(id: const Uuid().v4(), icon: '', name: '', description: ''),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('pollVotes'.plural(_poll.metric.byOptions[option.id] ?? 0)),
|
||||||
|
Text(' · ').padding(horizontal: 4),
|
||||||
|
Text(
|
||||||
|
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
if (option.description.isNotEmpty) Text(option.description),
|
||||||
},
|
],
|
||||||
|
),
|
||||||
|
onTap: _isBusy ? null : () => _voteForOption(option),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
),
|
|
||||||
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()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
201
lib/widgets/post/post_poll_editor.dart
Normal file
201
lib/widgets/post/post_poll_editor.dart
Normal file
@ -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<PollEditorDialog> createState() => _PollEditorDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PollEditorDialogState extends State<PollEditorDialog> {
|
||||||
|
final TextEditingController _linkController = TextEditingController();
|
||||||
|
final List<SnPollOption> _pollOptions = List.empty(growable: true);
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _fetchPoll() async {
|
||||||
|
if (_linkController.text.isEmpty) return;
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
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<void> _applyPost() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
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<void> _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<SnNetworkProvider>();
|
||||||
|
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()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user