mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-25 03:26:22 +08:00
feat: create vote (#871)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -48,7 +48,9 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
if (Platform.isAndroid) {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
initPubState();
|
||||
|
||||
@@ -68,7 +70,9 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
}
|
||||
focusNode.dispose();
|
||||
editController.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
if (Platform.isAndroid) {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -83,6 +87,7 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
if (mounted &&
|
||||
widget.autofocus &&
|
||||
panelType.value == PanelType.keyboard) {
|
||||
controller.restoreChatPanel();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
|
||||
@@ -66,9 +66,9 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
controller.keepChatPanel();
|
||||
context.imageView(
|
||||
await context.imageView(
|
||||
imgList: pathList
|
||||
.map((path) => SourceModel(
|
||||
url: path,
|
||||
@@ -77,6 +77,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
.toList(),
|
||||
initialPage: index,
|
||||
);
|
||||
controller.restoreChatPanel();
|
||||
},
|
||||
onLongPress: onClear,
|
||||
child: ClipRRect(
|
||||
@@ -193,31 +194,47 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
|
||||
List<Map<String, dynamic>>? getRichContent() {
|
||||
if (editController.items.isEmpty) return null;
|
||||
return editController.items.map((e) {
|
||||
return switch (e.type) {
|
||||
RichTextType.text || RichTextType.composing => <String, dynamic>{
|
||||
final list = <Map<String, dynamic>>[];
|
||||
for (var e in editController.items) {
|
||||
switch (e.type) {
|
||||
case RichTextType.text || RichTextType.composing:
|
||||
list.add({
|
||||
"raw_text": e.text,
|
||||
"type": 1,
|
||||
"biz_id": "",
|
||||
},
|
||||
RichTextType.at => <String, dynamic>{
|
||||
});
|
||||
case RichTextType.at:
|
||||
list.add({
|
||||
"raw_text": '@${e.rawText}',
|
||||
"type": 2,
|
||||
"biz_id": e.uid,
|
||||
},
|
||||
RichTextType.emoji => <String, dynamic>{
|
||||
"biz_id": e.id,
|
||||
});
|
||||
case RichTextType.emoji:
|
||||
list.add({
|
||||
"raw_text": e.rawText,
|
||||
"type": 9,
|
||||
"biz_id": "",
|
||||
},
|
||||
};
|
||||
}).toList();
|
||||
});
|
||||
case RichTextType.vote:
|
||||
list.add({
|
||||
"raw_text": e.rawText,
|
||||
"type": 4,
|
||||
"biz_id": e.id,
|
||||
});
|
||||
list.add({
|
||||
"raw_text": ' ',
|
||||
"type": 1,
|
||||
"biz_id": "",
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
double _mentionOffset = 0;
|
||||
void onMention([bool fromClick = false]) {
|
||||
Future<void> onMention([bool fromClick = false]) async {
|
||||
controller.keepChatPanel();
|
||||
DynMentionPanel.onDynMention(
|
||||
await DynMentionPanel.onDynMention(
|
||||
context,
|
||||
offset: _mentionOffset,
|
||||
callback: (offset) => _mentionOffset = offset,
|
||||
@@ -227,11 +244,12 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
'@${res.name} ',
|
||||
RichTextType.at,
|
||||
rawText: res.name,
|
||||
uid: res.uid,
|
||||
id: res.uid,
|
||||
fromClick: fromClick,
|
||||
);
|
||||
}
|
||||
});
|
||||
controller.restoreChatPanel();
|
||||
}
|
||||
|
||||
void onInsertText(
|
||||
@@ -239,7 +257,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
RichTextType type, {
|
||||
String? rawText,
|
||||
Emote? emote,
|
||||
String? uid,
|
||||
String? id,
|
||||
bool? fromClick,
|
||||
}) {
|
||||
if (text.isEmpty) {
|
||||
@@ -268,7 +286,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
rawText: rawText,
|
||||
type: type,
|
||||
emote: emote,
|
||||
uid: uid,
|
||||
id: id,
|
||||
);
|
||||
} else {
|
||||
delta = RichTextEditingDeltaInsertion(
|
||||
@@ -282,7 +300,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
rawText: rawText,
|
||||
type: type,
|
||||
emote: emote,
|
||||
uid: uid,
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -297,7 +315,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
rawText: rawText,
|
||||
type: type,
|
||||
emote: emote,
|
||||
uid: uid,
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -308,8 +326,8 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
}
|
||||
|
||||
editController
|
||||
..value = newValue
|
||||
..syncRichText(delta);
|
||||
..syncRichText(delta)
|
||||
..value = newValue.copyWith(selection: editController.newSelection);
|
||||
} else {
|
||||
editController.value = TextEditingValue(
|
||||
text: text,
|
||||
@@ -327,7 +345,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
end: text.length,
|
||||
),
|
||||
emote: emote,
|
||||
uid: uid,
|
||||
id: id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,14 +146,14 @@ class _VotePanelState extends State<VotePanel> {
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final opt = _voteInfo.options[index];
|
||||
final selected = groupValue.contains(opt.optidx);
|
||||
final selected = groupValue.contains(opt.optIdx);
|
||||
return PercentageChip(
|
||||
label: opt.optdesc!,
|
||||
label: opt.optDesc!,
|
||||
percentage: _showPercentage ? _percentage[index] : null,
|
||||
selected: selected,
|
||||
onSelected: !_enabled || (groupValue.length >= _maxCnt && !selected)
|
||||
? null
|
||||
: (value) => _onSelected(context, value, opt.optidx!),
|
||||
: (value) => _onSelected(context, value, opt.optIdx!),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,12 +5,15 @@ import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/draggable_sheet/draggable_scrollable_sheet_dyn.dart'
|
||||
as dyn_sheet;
|
||||
import 'package:PiliPlus/common/widgets/pair.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/text_field.dart';
|
||||
import 'package:PiliPlus/http/dynamics.dart';
|
||||
import 'package:PiliPlus/models/common/publish_panel_type.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_option_type.dart';
|
||||
import 'package:PiliPlus/models/dynamics/vote_model.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
|
||||
import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_create_vote/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_select_topic/view.dart';
|
||||
@@ -341,7 +344,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
return PopupMenuButton<bool>(
|
||||
requestFocus: false,
|
||||
initialValue: _isPrivate.value,
|
||||
onOpened: controller.keepChatPanel,
|
||||
onSelected: (value) => _isPrivate.value = value,
|
||||
itemBuilder: (context) => List.generate(
|
||||
2,
|
||||
@@ -398,7 +400,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
return PopupMenuButton<ReplyOptionType>(
|
||||
requestFocus: false,
|
||||
initialValue: _replyOption.value,
|
||||
onOpened: controller.keepChatPanel,
|
||||
onSelected: (item) => _replyOption.value = item,
|
||||
itemBuilder: (context) => ReplyOptionType.values
|
||||
.map(
|
||||
@@ -549,6 +550,55 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
tooltip: '@',
|
||||
selected: false,
|
||||
),
|
||||
ToolbarIconButton(
|
||||
onPressed: () async {
|
||||
controller.keepChatPanel();
|
||||
RichTextItem? voteItem = editController.items
|
||||
.firstWhereOrNull((e) => e.type == RichTextType.vote);
|
||||
VoteInfo? voteInfo = await Navigator.of(context).push(
|
||||
GetPageRoute(
|
||||
page: () => CreateVotePage(
|
||||
voteId: voteItem?.id == null
|
||||
? null
|
||||
: int.parse(voteItem!.id!))),
|
||||
);
|
||||
if (voteInfo != null) {
|
||||
if (voteItem != null) {
|
||||
final delta = RichTextEditingDeltaReplacement(
|
||||
oldText: editController.text,
|
||||
replacementText: ' ${voteInfo.title} ',
|
||||
replacedRange: voteItem.range,
|
||||
selection: editController.selection,
|
||||
composing: TextRange.empty,
|
||||
type: RichTextType.vote,
|
||||
id: voteInfo.voteId.toString(),
|
||||
rawText: voteInfo.title,
|
||||
);
|
||||
final newValue = delta.apply(editController.value);
|
||||
editController
|
||||
..syncRichText(delta)
|
||||
..value = newValue.copyWith(
|
||||
selection: editController.newSelection,
|
||||
);
|
||||
} else {
|
||||
onInsertText(
|
||||
'我发起了一个投票',
|
||||
RichTextType.text,
|
||||
);
|
||||
onInsertText(
|
||||
' ${voteInfo.title} ',
|
||||
RichTextType.vote,
|
||||
rawText: voteInfo.title,
|
||||
id: voteInfo.voteId.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
controller.restoreChatPanel();
|
||||
},
|
||||
icon: const Icon(Icons.bar_chart_rounded, size: 22),
|
||||
tooltip: '投票',
|
||||
selected: false,
|
||||
),
|
||||
// if (kDebugMode)
|
||||
// ToolbarIconButton(
|
||||
// onPressed: editController.clear,
|
||||
|
||||
126
lib/pages/dynamics_create_vote/controller.dart
Normal file
126
lib/pages/dynamics_create_vote/controller.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
||||
import 'package:PiliPlus/http/dynamics.dart';
|
||||
import 'package:PiliPlus/http/msg.dart';
|
||||
import 'package:PiliPlus/models/dynamics/vote_model.dart';
|
||||
import 'package:PiliPlus/models_new/upload_bfs/data.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class CreateVoteController extends GetxController {
|
||||
CreateVoteController(this.voteId);
|
||||
final int? voteId;
|
||||
|
||||
String key = Utils.generateRandomString(6);
|
||||
final RxString title = ''.obs;
|
||||
final RxString desc = ''.obs;
|
||||
final RxInt type = 0.obs;
|
||||
final RxList<Option> options = <Option>[
|
||||
Option(optDesc: '', imgUrl: ''),
|
||||
Option(optDesc: '', imgUrl: ''),
|
||||
].obs;
|
||||
final RxInt choiceCnt = 1.obs;
|
||||
final now = DateTime.now();
|
||||
late final end = now.copyWith(day: now.day + 90);
|
||||
late Rx<DateTime> endtime;
|
||||
final RxBool canCreate = false.obs;
|
||||
|
||||
void updateCanCreate() {
|
||||
if (type.value == 0) {
|
||||
canCreate.value = title.value.isNotEmpty &&
|
||||
options.every((e) => e.optDesc?.isNotEmpty == true);
|
||||
} else {
|
||||
canCreate.value = title.value.isNotEmpty &&
|
||||
options.every(
|
||||
(e) =>
|
||||
e.optDesc?.isNotEmpty == true && e.imgUrl?.isNotEmpty == true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
endtime = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day + 1,
|
||||
now.hour,
|
||||
now.minute,
|
||||
).obs;
|
||||
if (voteId != null) {
|
||||
queryData();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> queryData() async {
|
||||
var res = await DynamicsHttp.voteInfo(voteId);
|
||||
if (res.isSuccess) {
|
||||
key = Utils.generateRandomString(6);
|
||||
final VoteInfo data = res.data;
|
||||
title.value = data.title!;
|
||||
desc.value = data.desc ?? '';
|
||||
type.value = data.options.first.imgUrl?.isNotEmpty == true ? 1 : 0;
|
||||
options.value = data.options;
|
||||
choiceCnt.value = data.choiceCnt!;
|
||||
endtime.value = DateTime.fromMillisecondsSinceEpoch(data.endTime! * 1000);
|
||||
canCreate.value = true;
|
||||
} else {
|
||||
showConfirmDialog(
|
||||
context: Get.context!,
|
||||
title: res.toString(),
|
||||
onConfirm: Get.back,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onDel(int i) {
|
||||
options.removeAt(i);
|
||||
updateCanCreate();
|
||||
if (choiceCnt.value > options.length) {
|
||||
choiceCnt.value = options.length;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onCreate() async {
|
||||
final voteInfo = VoteInfo(
|
||||
title: title.value,
|
||||
desc: desc.value,
|
||||
type: type.value,
|
||||
duration: endtime.value.difference(now).inSeconds,
|
||||
options: options,
|
||||
onlyFansLevel: 0,
|
||||
choiceCnt: choiceCnt.value,
|
||||
votePublisher: Accounts.main.mid,
|
||||
voteId: voteId,
|
||||
);
|
||||
var res = voteId == null
|
||||
? await DynamicsHttp.createVote(voteInfo)
|
||||
: await DynamicsHttp.updateVote(voteInfo);
|
||||
if (res.isSuccess) {
|
||||
voteInfo.voteId = res.data;
|
||||
Get.back(result: voteInfo);
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onUpload(int index, XFile pickedFile) async {
|
||||
var res = await MsgHttp.uploadBfs(
|
||||
path: pickedFile.path,
|
||||
category: 'daily',
|
||||
biz: 'vote',
|
||||
);
|
||||
if (res['status']) {
|
||||
UploadBfsResData data = res['data'];
|
||||
options
|
||||
..[index].imgUrl = data.imageUrl
|
||||
..refresh();
|
||||
updateCanCreate();
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
}
|
||||
412
lib/pages/dynamics_create_vote/view.dart
Normal file
412
lib/pages/dynamics_create_vote/view.dart
Normal file
@@ -0,0 +1,412 @@
|
||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/dynamics/vote_model.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_create_vote/controller.dart';
|
||||
import 'package:PiliPlus/utils/date_util.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class CreateVotePage extends StatefulWidget {
|
||||
const CreateVotePage({super.key, this.voteId});
|
||||
|
||||
final int? voteId;
|
||||
|
||||
@override
|
||||
State<CreateVotePage> createState() => _CreateVotePageState();
|
||||
}
|
||||
|
||||
class _CreateVotePageState extends State<CreateVotePage> {
|
||||
late final _controller = Get.put(CreateVoteController(widget.voteId),
|
||||
tag: Utils.generateRandomString(8));
|
||||
late final imagePicker = ImagePicker();
|
||||
|
||||
late Divider _divider;
|
||||
late TextStyle _leadingStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
_divider = Divider(
|
||||
height: 1,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||
);
|
||||
_leadingStyle = TextStyle(
|
||||
fontSize: 15,
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.9),
|
||||
);
|
||||
final padding = MediaQuery.paddingOf(context);
|
||||
final divider = [
|
||||
const SizedBox(height: 10),
|
||||
_divider,
|
||||
const SizedBox(height: 10),
|
||||
];
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${_controller.voteId != null ? '' : '发起'}投票'),
|
||||
),
|
||||
body: ListView(
|
||||
padding: EdgeInsets.only(
|
||||
left: padding.left + 16,
|
||||
right: padding.right + 16,
|
||||
bottom: padding.bottom + 80,
|
||||
),
|
||||
children: [
|
||||
const Text(
|
||||
'投票类型',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildType(theme),
|
||||
const SizedBox(height: 40),
|
||||
Obx(
|
||||
() => _buildInput(
|
||||
theme,
|
||||
key: ValueKey('${_controller.key}title'),
|
||||
initialValue: _controller.title.value,
|
||||
onChanged: (value) => _controller
|
||||
..title.value = value
|
||||
..updateCanCreate(),
|
||||
desc: '投票标题',
|
||||
hintText: '请填写标题',
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(32)],
|
||||
),
|
||||
),
|
||||
...divider,
|
||||
Obx(
|
||||
() => _buildInput(
|
||||
theme,
|
||||
key: ValueKey('${_controller.key}desc'),
|
||||
initialValue: _controller.desc.value,
|
||||
onChanged: (value) => _controller.desc.value = value,
|
||||
desc: '投票说明',
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(100)],
|
||||
),
|
||||
),
|
||||
...divider,
|
||||
const SizedBox(height: 40),
|
||||
Obx(
|
||||
() {
|
||||
final showImg = _controller.type.value == 1;
|
||||
final showDel = _controller.options.length > 2;
|
||||
List<Widget> children = [];
|
||||
for (int i = 0; i < _controller.options.length; i++) {
|
||||
final e = _controller.options[i];
|
||||
children
|
||||
..add(_buildInput(
|
||||
theme,
|
||||
key: ValueKey(e.hashCode),
|
||||
showDel: showDel,
|
||||
onDel: () {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
_controller.onDel(i);
|
||||
},
|
||||
showImg: showImg,
|
||||
imgUrl: e.imgUrl,
|
||||
onPickImg: () => EasyThrottle.throttle(
|
||||
'picImg',
|
||||
const Duration(milliseconds: 500),
|
||||
() => _onPickImg(i),
|
||||
),
|
||||
initialValue: e.optDesc,
|
||||
onChanged: (value) => _controller
|
||||
..options[i].optDesc = value
|
||||
..updateCanCreate(),
|
||||
desc: '选项${i + 1}',
|
||||
hintText: '选项内容,最多20字',
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(20)],
|
||||
))
|
||||
..addAll(divider);
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...children,
|
||||
if (_controller.options.length < 20)
|
||||
FilledButton(
|
||||
onPressed: () => _controller
|
||||
..options.add(Option(optDesc: '', imgUrl: ''))
|
||||
..updateCanCreate(),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: Size.zero,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10, right: 14, top: 4, bottom: 4),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
foregroundColor: theme.colorScheme.onSurfaceVariant,
|
||||
backgroundColor: theme.colorScheme.onInverseSurface,
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add, size: 16),
|
||||
Text(
|
||||
' 添加选项',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text('单选/多选', style: _leadingStyle),
|
||||
),
|
||||
Obx(() {
|
||||
final choiceCnt = _controller.choiceCnt.value;
|
||||
final choices =
|
||||
List.generate(_controller.options.length, (i) => i + 1);
|
||||
return Listener(
|
||||
onPointerDown: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
child: PopupMenuButton<int>(
|
||||
initialValue: choiceCnt,
|
||||
requestFocus: false,
|
||||
child:
|
||||
Text(choiceCnt == 1 ? '单选 ' : '最多选$choiceCnt项'),
|
||||
onSelected: (value) => _controller.choiceCnt.value = value,
|
||||
itemBuilder: (context) {
|
||||
return choices
|
||||
.map((e) => PopupMenuItem(
|
||||
value: e,
|
||||
child: Text(e == 1 ? '单选' : '最多选$e项'),
|
||||
))
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
...divider,
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text('投票截止时间', style: _leadingStyle),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
DateTime? newDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _controller.endtime.value,
|
||||
firstDate: _controller.now,
|
||||
lastDate: _controller.end,
|
||||
);
|
||||
if (newDate != null && context.mounted) {
|
||||
TimeOfDay? newTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime:
|
||||
TimeOfDay.fromDateTime(_controller.endtime.value),
|
||||
);
|
||||
if (newTime != null) {
|
||||
final newEndtime = DateTime(
|
||||
newDate.year,
|
||||
newDate.month,
|
||||
newDate.day,
|
||||
newTime.hour,
|
||||
newTime.minute,
|
||||
);
|
||||
if (newEndtime.difference(DateTime.now()) >=
|
||||
const Duration(minutes: 5)) {
|
||||
_controller.endtime.value = newEndtime;
|
||||
} else {
|
||||
SmartDialog.showToast('至少选择5分钟之后');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Obx(() => Text(
|
||||
DateUtil.longFormatD.format(_controller.endtime.value),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
...divider,
|
||||
const SizedBox(height: 40),
|
||||
Obx(() {
|
||||
final canCreate = _controller.canCreate.value;
|
||||
return FilledButton.tonal(
|
||||
onPressed: canCreate ? _controller.onCreate : null,
|
||||
child: const Text('发起投票'),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInput(
|
||||
ThemeData theme, {
|
||||
Key? key,
|
||||
String? initialValue,
|
||||
required ValueChanged<String> onChanged,
|
||||
required String desc,
|
||||
String? hintText,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
bool showDel = false,
|
||||
bool showImg = false,
|
||||
String? imgUrl,
|
||||
VoidCallback? onDel,
|
||||
VoidCallback? onPickImg,
|
||||
}) {
|
||||
return Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 65,
|
||||
child: Text(
|
||||
desc,
|
||||
style: _leadingStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
key: key,
|
||||
initialValue: initialValue,
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: hintText ?? desc,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
inputFormatters: inputFormatters,
|
||||
),
|
||||
),
|
||||
if (showImg)
|
||||
GestureDetector(
|
||||
onTap: onPickImg,
|
||||
child: NetworkImgLayer(
|
||||
src: imgUrl,
|
||||
width: 40,
|
||||
height: 40,
|
||||
radius: 6,
|
||||
),
|
||||
),
|
||||
if (showDel)
|
||||
iconButton(
|
||||
size: 26,
|
||||
iconSize: 18,
|
||||
tooltip: '移除',
|
||||
context: context,
|
||||
icon: Icons.clear,
|
||||
onPressed: onDel,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildType(ThemeData theme) => Obx(
|
||||
() {
|
||||
return Row(
|
||||
spacing: 16,
|
||||
children: List.generate(
|
||||
2,
|
||||
(index) {
|
||||
final isEnable = index == _controller.type.value;
|
||||
final style = TextButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 17),
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: isEnable
|
||||
? theme.colorScheme.secondary
|
||||
: theme.colorScheme.outline,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
backgroundColor: isEnable
|
||||
? theme.colorScheme.secondaryContainer
|
||||
: Colors.transparent,
|
||||
foregroundColor: isEnable
|
||||
? theme.colorScheme.onSecondaryContainer
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
);
|
||||
Widget child = TextButton(
|
||||
style: style,
|
||||
onPressed: () => _controller
|
||||
..type.value = index
|
||||
..updateCanCreate(),
|
||||
child: Text(
|
||||
'${const ['文字', '图片'][index]}投票',
|
||||
strutStyle: const StrutStyle(forceStrutHeight: true),
|
||||
),
|
||||
);
|
||||
if (isEnable) {
|
||||
child = Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
child,
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(1),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
bottomRight: Radius.circular(6),
|
||||
),
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
child: Icon(
|
||||
size: 10,
|
||||
Icons.check,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
void _onPickImg(int index) {
|
||||
EasyThrottle.throttle('imagePicker', const Duration(milliseconds: 500),
|
||||
() async {
|
||||
try {
|
||||
XFile? pickedFile = await imagePicker.pickImage(
|
||||
imageQuality: 100,
|
||||
source: ImageSource.gallery,
|
||||
);
|
||||
if (pickedFile != null) {
|
||||
_controller.onUpload(index, pickedFile);
|
||||
}
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -200,5 +200,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<LiveSendDmPanel> {
|
||||
}
|
||||
|
||||
@override
|
||||
void onMention([bool fromClick = false]) {}
|
||||
Future<void> onMention([bool fromClick = false]) {
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
|
||||
Future<void> onCustomPublish({List? pictures}) async {
|
||||
Map<String, int> atNameToMid = {
|
||||
for (var e in editController.items)
|
||||
if (e.type == RichTextType.at) e.rawText: int.parse(e.uid!),
|
||||
if (e.type == RichTextType.at) e.rawText: int.parse(e.id!),
|
||||
};
|
||||
String message = editController.rawText;
|
||||
var result = await VideoHttp.replyAdd(
|
||||
|
||||
@@ -426,9 +426,9 @@ class _SendDanmakuPanelState extends CommonTextPubPageState<SendDanmakuPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showColorPicker() {
|
||||
Future<void> _showColorPicker() async {
|
||||
controller.keepChatPanel();
|
||||
showDialog(
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
@@ -445,6 +445,7 @@ class _SendDanmakuPanelState extends CommonTextPubPageState<SendDanmakuPanel> {
|
||||
),
|
||||
),
|
||||
);
|
||||
controller.restoreChatPanel();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -335,7 +335,9 @@ class _WhisperDetailPageState
|
||||
}
|
||||
|
||||
@override
|
||||
void onMention([bool fromClick = false]) {}
|
||||
Future<void> onMention([bool fromClick = false]) {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
void onSave() {}
|
||||
|
||||
Reference in New Issue
Block a user