Files
PiliPlus/lib/pages/dynamics_create/view.dart
bggRGjQaUbCoE 5ff6ef8801 opt reply hint
opt mention list header

tweak

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-07 13:40:48 +08:00

700 lines
26 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/button/toolbar_icon_button.dart';
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';
import 'package:PiliPlus/pages/emote/controller.dart';
import 'package:PiliPlus/pages/emote/view.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/date_util.dart';
import 'package:PiliPlus/utils/request_utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart' hide DraggableScrollableSheet;
import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class CreateDynPanel extends CommonRichTextPubPage {
const CreateDynPanel({
super.key,
super.imageLengthLimit = 18,
this.scrollController,
this.topic,
});
final ScrollController? scrollController;
final Pair<int, String>? topic;
@override
State<CreateDynPanel> createState() => _CreateDynPanelState();
static void onCreateDyn(BuildContext context, {Pair<int, String>? topic}) =>
showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
builder: (context) => dyn_sheet.DraggableScrollableSheet(
snap: true,
expand: false,
initialChildSize: 1,
minChildSize: 0,
maxChildSize: 1,
snapSizes: const [1],
builder: (context, scrollController) => CreateDynPanel(
scrollController: scrollController,
topic: topic,
),
),
);
}
class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
final RxBool _isPrivate = false.obs;
final Rx<DateTime?> _publishTime = Rx<DateTime?>(null);
final Rx<ReplyOptionType> _replyOption = ReplyOptionType.allow.obs;
final _titleEditCtr = TextEditingController();
final Rx<Pair<int, String>?> topic = Rx<Pair<int, String>?>(null);
@override
void initState() {
super.initState();
topic.value = widget.topic;
}
@override
void dispose() {
_titleEditCtr.dispose();
Get
..delete<EmotePanelController>()
..delete<SelectTopicController>()
..delete<DynMentionController>();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAppBar(theme),
Expanded(
child: ListView(
padding: EdgeInsets.zero,
controller: widget.scrollController,
physics: const ClampingScrollPhysics(),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Obx(
() {
final hasTopic = topic.value != null;
return Row(
spacing: 10,
children: [
TextButton(
style: TextButton.styleFrom(
overlayColor: hasTopic ? Colors.transparent : null,
splashFactory:
hasTopic ? NoSplash.splashFactory : null,
shape: hasTopic
? null
: RoundedRectangleBorder(
side: BorderSide(
color: hasTopic
? Colors.transparent
: theme.colorScheme.outline
.withValues(alpha: 0.2),
),
borderRadius: const BorderRadius.all(
Radius.circular(25),
),
),
minimumSize: Size.zero,
padding: hasTopic
? const EdgeInsets.symmetric(vertical: 12)
: const EdgeInsets.all(12),
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: _onSelectTopic,
child: Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(right: 5),
child: Icon(
CustomIcon.topic_tag,
size: 18,
color: hasTopic
? null
: theme.colorScheme.outline,
),
),
),
TextSpan(
text: hasTopic ? topic.value!.second : '选择话题',
style: TextStyle(
color: hasTopic
? null
: theme.colorScheme.outline,
),
),
],
),
),
),
if (hasTopic)
iconButton(
size: 22,
iconSize: 16,
context: context,
icon: Icons.clear,
bgColor: theme.colorScheme.onInverseSurface,
iconColor: theme.colorScheme.onSurfaceVariant,
onPressed: () => topic.value = null,
),
],
);
},
),
),
const SizedBox(height: 5),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextField(
controller: _titleEditCtr,
style: const TextStyle(fontWeight: FontWeight.bold),
decoration: InputDecoration(
hintText: '标题选填20字',
isDense: true,
contentPadding: EdgeInsets.zero,
border: const OutlineInputBorder(
gapPadding: 0,
borderSide: BorderSide.none,
),
hintStyle: TextStyle(
fontWeight: FontWeight.bold,
color: theme.colorScheme.outline.withValues(alpha: 0.7),
),
),
inputFormatters: [LengthLimitingTextInputFormatter(20)],
),
),
const SizedBox(height: 5),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _buildEditWidget(theme),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(() => _buildPubtimeWidget),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() => _buildReplyOptionWidget(theme)),
const SizedBox(height: 5),
Obx(() => _buildPrivateWidget(theme)),
],
),
],
),
),
const SizedBox(height: 10),
_buildImageList(theme),
],
),
),
_buildToolbar,
buildPanelContainer(Colors.transparent),
],
);
}
Widget _buildImageList(ThemeData theme) => SizedBox(
height: 100,
width: double.infinity,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Obx(
() => Row(
spacing: 10,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...List.generate(
pathList.length, (index) => buildImage(index, 100)),
if (pathList.length != limit)
Builder(
builder: (context) {
const borderRadius = StyleString.mdRadius;
return Material(
borderRadius: borderRadius,
child: InkWell(
borderRadius: borderRadius,
onTap: () => onPickImage(() {
if (pathList.isNotEmpty && !enablePublish.value) {
enablePublish.value = true;
}
}),
child: Ink(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: borderRadius,
color: theme.colorScheme.secondaryContainer,
),
child:
const Center(child: Icon(Icons.add, size: 35)),
),
),
);
},
),
],
),
),
),
);
PreferredSizeWidget _buildAppBar(ThemeData theme) => PreferredSize(
preferredSize: const Size.fromHeight(66),
child: Padding(
padding: const EdgeInsets.all(16),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 34,
height: 34,
child: IconButton(
tooltip: '返回',
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero),
backgroundColor: WidgetStateProperty.resolveWith(
(states) {
return theme.colorScheme.secondaryContainer;
},
),
),
onPressed: Get.back,
icon: Icon(
Icons.arrow_back_outlined,
size: 18,
color: theme.colorScheme.onSecondaryContainer,
),
),
),
),
const Center(
child: Text(
'发布动态',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
),
Align(
alignment: Alignment.centerRight,
child: Obx(
() => FilledButton.tonal(
onPressed: enablePublish.value ? onPublish : null,
style: FilledButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
visualDensity: VisualDensity.compact,
),
child: Text(_publishTime.value == null ? '发布' : '定时发布'),
),
),
),
],
),
),
);
Widget _buildPrivateWidget(ThemeData theme) {
final color = _isPrivate.value
? theme.colorScheme.error
: theme.colorScheme.secondary;
return PopupMenuButton<bool>(
requestFocus: false,
initialValue: _isPrivate.value,
onSelected: (value) => _isPrivate.value = value,
itemBuilder: (context) => List.generate(
2,
(index) => PopupMenuItem<bool>(
enabled: _publishTime.value != null && index == 1 ? false : true,
value: index == 0 ? false : true,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 19,
index == 0 ? Icons.visibility : Icons.visibility_off,
),
const SizedBox(width: 4),
Text(index == 0 ? '所有人可见' : '仅自己可见'),
],
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 19,
_isPrivate.value ? Icons.visibility_off : Icons.visibility,
color: color,
),
const SizedBox(width: 4),
Text(
_isPrivate.value ? '仅自己可见' : '所有人可见',
style: TextStyle(
height: 1,
color: color,
),
strutStyle: const StrutStyle(leading: 0, height: 1),
),
Icon(
size: 20,
Icons.keyboard_arrow_right,
color: color,
),
],
),
),
);
}
Widget _buildReplyOptionWidget(ThemeData theme) {
final color = _replyOption.value == ReplyOptionType.close
? theme.colorScheme.error
: theme.colorScheme.secondary;
return PopupMenuButton<ReplyOptionType>(
requestFocus: false,
initialValue: _replyOption.value,
onSelected: (item) => _replyOption.value = item,
itemBuilder: (context) => ReplyOptionType.values
.map(
(item) => PopupMenuItem<ReplyOptionType>(
value: item,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 19,
item.iconData,
),
const SizedBox(width: 4),
Text(item.title),
],
),
),
)
.toList(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 19,
_replyOption.value.iconData,
color: color,
),
const SizedBox(width: 4),
Text(
_replyOption.value.title,
style: TextStyle(
height: 1,
color: color,
),
strutStyle: const StrutStyle(leading: 0, height: 1),
),
Icon(
size: 20,
Icons.keyboard_arrow_right,
color: color,
),
],
),
),
);
}
Widget get _buildPubtimeWidget => _publishTime.value == null
? FilledButton.tonal(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
visualDensity: VisualDensity.compact,
),
onPressed: _isPrivate.value
? null
: () {
DateTime nowDate = DateTime.now();
showDatePicker(
context: context,
initialDate: nowDate,
firstDate: nowDate,
lastDate: DateTime(
nowDate.year,
nowDate.month,
nowDate.day + 7,
),
).then(
(selectedDate) {
if (selectedDate != null && mounted) {
TimeOfDay nowTime = TimeOfDay.now();
showTimePicker(
context: context,
initialTime: nowTime.replacing(
hour: nowTime.minute + 6 >= 60
? (nowTime.hour + 1) % 24
: nowTime.hour,
minute: (nowTime.minute + 6) % 60,
),
).then((selectedTime) {
if (selectedTime != null) {
if (selectedDate.day == nowDate.day) {
if (selectedTime.hour < nowTime.hour) {
SmartDialog.showToast('时间设置错误至少选择6分钟之后');
return;
} else if (selectedTime.hour == nowTime.hour) {
if (selectedTime.minute < nowTime.minute + 6) {
if (selectedDate.day == nowDate.day) {
SmartDialog.showToast('时间设置错误至少选择6分钟之后');
}
return;
}
}
}
_publishTime.value = DateTime(
selectedDate.year,
selectedDate.month,
selectedDate.day,
selectedTime.hour,
selectedTime.minute,
);
}
});
}
},
);
},
child: const Text('定时发布'),
)
: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
visualDensity: VisualDensity.compact,
),
onPressed: () => _publishTime.value = null,
label: Text(DateUtil.longFormatD.format(_publishTime.value!)),
icon: const Icon(Icons.clear, size: 20),
iconAlignment: IconAlignment.end,
);
Widget get _buildToolbar => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
spacing: 16,
children: [
Obx(
() => ToolbarIconButton(
onPressed: () => updatePanelType(
panelType.value == PanelType.emoji
? PanelType.keyboard
: PanelType.emoji,
),
icon: const Icon(Icons.emoji_emotions, size: 22),
tooltip: '表情',
selected: panelType.value == PanelType.emoji,
),
),
ToolbarIconButton(
onPressed: () => onMention(true),
icon: const Icon(Icons.alternate_email, size: 22),
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 range = voteItem.range;
final text = ' ${voteInfo.title} ';
final selection = TextSelection.collapsed(
offset: range.start + text.length);
final delta = RichTextEditingDeltaReplacement(
oldText: editController.text,
replacementText: text,
replacedRange: range,
selection: 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;
} 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: 24),
tooltip: '投票',
selected: false,
),
// if (kDebugMode)
// ToolbarIconButton(
// onPressed: editController.clear,
// icon: const Icon(Icons.clear, size: 22),
// selected: false,
// ),
],
),
);
Widget _buildEditWidget(ThemeData theme) => Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Listener(
onPointerUp: (event) {
if (readOnly.value) {
updatePanelType(PanelType.keyboard);
}
},
child: Obx(
() => RichTextField(
key: key,
controller: editController,
minLines: 4,
maxLines: null,
focusNode: focusNode,
readOnly: readOnly.value,
onChanged: onChanged,
decoration: InputDecoration(
hintText: '说点什么吧',
hintStyle: TextStyle(color: theme.colorScheme.outline),
border: const OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0,
),
contentPadding: EdgeInsets.zero,
),
// inputFormatters: [LengthLimitingTextInputFormatter(1000)],
),
),
),
);
@override
Widget? get customPanel => EmotePanel(onChoose: onChooseEmote);
@override
Future<void> onCustomPublish({List? pictures}) async {
SmartDialog.showLoading(msg: '正在发布');
List<Map<String, dynamic>>? extraContent = getRichContent();
final hasRichText = extraContent != null;
var result = await DynamicsHttp.createDynamic(
mid: Accounts.main.mid,
rawText: hasRichText ? null : editController.text,
pics: pictures,
publishTime: _publishTime.value != null
? _publishTime.value!.millisecondsSinceEpoch ~/ 1000
: null,
replyOption: _replyOption.value,
privatePub: _isPrivate.value ? 1 : null,
title: _titleEditCtr.text,
topic: topic.value,
extraContent: extraContent,
);
SmartDialog.dismiss();
if (result['status']) {
hasPub = true;
Get.back();
SmartDialog.showToast('发布成功');
var id = result['data']?['dyn_id'];
RequestUtils.insertCreatedDyn(id);
RequestUtils.checkCreatedDyn(
id: id,
dynText: editController.rawText,
);
} else {
SmartDialog.showToast(result['msg']);
if (kDebugMode) debugPrint('failed to publish: ${result['msg']}');
}
}
double _topicOffset = 0;
void _onSelectTopic() {
SelectTopicPanel.onSelectTopic(
context,
offset: _topicOffset,
callback: (offset) => _topicOffset = offset,
).then((TopicItem? res) {
if (res != null) {
topic.value = Pair(first: res.id, second: res.name);
}
});
}
@override
void onSave() {}
}