opt: create dynamic panel

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2024-12-30 11:59:12 +08:00
parent bef7a28229
commit 991ae8518a
5 changed files with 437 additions and 306 deletions

View File

@@ -149,6 +149,7 @@ class MsgHttp {
List? pics, List? pics,
int? publishTime, int? publishTime,
ReplyOption replyOption = ReplyOption.allow, ReplyOption replyOption = ReplyOption.allow,
int? privatePub,
}) async { }) async {
String csrf = await Request.getCsrf(); String csrf = await Request.getCsrf();
var res = await Request().post( var res = await Request().post(
@@ -181,6 +182,10 @@ class MsgHttp {
: pics != null : pics != null
? 2 ? 2
: 1, : 1,
if (privatePub != null)
'create_option': {
'private_pub': privatePub,
},
if (pics != null) 'pics': pics, if (pics != null) 'pics': pics,
"attach_card": null, "attach_card": null,
"upload_id": "upload_id":

View File

@@ -97,7 +97,7 @@ class SearchHttp {
if (pubEnd != null) 'pubtime_end_s': pubEnd, if (pubEnd != null) 'pubtime_end_s': pubEnd,
}; };
var res = await Request().get(Api.searchByType, queryParameters: reqData); var res = await Request().get(Api.searchByType, queryParameters: reqData);
if (res.data['code'] == 0) { if (res.data['code'] is int && res.data['code'] == 0) {
dynamic data; dynamic data;
try { try {
switch (searchType) { switch (searchType) {

View File

@@ -14,6 +14,7 @@ import 'package:PiliPalaX/utils/feed_back.dart';
import 'package:PiliPalaX/utils/storage.dart'; import 'package:PiliPalaX/utils/storage.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:nil/nil.dart'; import 'package:nil/nil.dart';
import 'controller.dart'; import 'controller.dart';
@@ -23,6 +24,12 @@ enum ReplyOption { allow, close, choose }
extension ReplyOptionExtension on ReplyOption { extension ReplyOptionExtension on ReplyOption {
String get title => ['允许评论', '关闭评论', '精选评论'][index]; String get title => ['允许评论', '关闭评论', '精选评论'][index];
IconData get iconData => [
MdiIcons.commentTextOutline,
MdiIcons.commentOffOutline,
MdiIcons.commentProcessingOutline,
][index];
} }
class DynamicsPage extends StatefulWidget { class DynamicsPage extends StatefulWidget {
@@ -241,21 +248,18 @@ class CreatePanel extends StatefulWidget {
class _CreatePanelState extends State<CreatePanel> { class _CreatePanelState extends State<CreatePanel> {
final _ctr = TextEditingController(); final _ctr = TextEditingController();
bool _isEnable = false;
final _isEnableStream = StreamController<bool>();
late final _imagePicker = ImagePicker(); late final _imagePicker = ImagePicker();
late final _pathList = <String>[]; late final int _limit = 18;
late final _pathStream = StreamController<List<String>>();
final RxBool _isEnablePub = false.obs;
late final RxList<String> _pathList = <String>[].obs;
bool _isPrivate = false;
DateTime? _publishTime; DateTime? _publishTime;
ReplyOption _replyOption = ReplyOption.allow; ReplyOption _replyOption = ReplyOption.allow;
late final int _limit = 18;
@override @override
void dispose() { void dispose() {
_isEnableStream.close();
_pathStream.close();
_ctr.dispose(); _ctr.dispose();
super.dispose(); super.dispose();
} }
@@ -298,6 +302,7 @@ class _CreatePanelState extends State<CreatePanel> {
} }
} }
} }
SmartDialog.showLoading(msg: '正在发布');
dynamic result = await MsgHttp.createDynamic( dynamic result = await MsgHttp.createDynamic(
mid: GStorage.userInfo.get('userInfoCache')?.mid, mid: GStorage.userInfo.get('userInfoCache')?.mid,
rawText: _ctr.text, rawText: _ctr.text,
@@ -306,11 +311,14 @@ class _CreatePanelState extends State<CreatePanel> {
? _publishTime!.millisecondsSinceEpoch ~/ 1000 ? _publishTime!.millisecondsSinceEpoch ~/ 1000
: null, : null,
replyOption: _replyOption, replyOption: _replyOption,
privatePub: _isPrivate ? 1 : null,
); );
if (result['status']) { if (result['status']) {
Get.back(); Get.back();
SmartDialog.dismiss();
SmartDialog.showToast('发布成功'); SmartDialog.showToast('发布成功');
} else { } else {
SmartDialog.dismiss();
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
// } // }
@@ -318,12 +326,14 @@ class _CreatePanelState extends State<CreatePanel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Scaffold(
mainAxisSize: MainAxisSize.max, backgroundColor: Colors.transparent,
crossAxisAlignment: CrossAxisAlignment.start, resizeToAvoidBottomInset: true,
children: [ appBar: PreferredSize(
const SizedBox(height: 16), preferredSize: Size.fromHeight(66),
Row( child: Padding(
padding: const EdgeInsets.only(top: 16, bottom: 16),
child: Row(
children: [ children: [
const SizedBox(width: 16), const SizedBox(width: 16),
SizedBox( SizedBox(
@@ -353,26 +363,30 @@ class _CreatePanelState extends State<CreatePanel> {
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
), ),
const Spacer(), const Spacer(),
StreamBuilder( Obx(
initialData: false, () => FilledButton.tonal(
stream: _isEnableStream.stream, onPressed: _isEnablePub.value ? _onCreate : null,
builder: (context, snapshot) => FilledButton.tonal(
onPressed: snapshot.data == true ? _onCreate : null,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 20, vertical: 10), horizontal: 20, vertical: 10),
visualDensity: const VisualDensity( visualDensity: const VisualDensity(
horizontal: -2, horizontal: -2,
vertical: -2, vertical: -2,
), ),
), ),
child: const Text('发布'), child: Text(_publishTime == null ? '发布' : '定时发布'),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
], ],
), ),
const SizedBox(width: 10), ),
),
body: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextField( child: TextField(
@@ -381,14 +395,11 @@ class _CreatePanelState extends State<CreatePanel> {
maxLines: 8, maxLines: 8,
autofocus: true, autofocus: true,
onChanged: (value) { onChanged: (value) {
bool isEmpty = bool isEmpty = value.trim().isEmpty && _pathList.isEmpty;
value.replaceAll('\n', '').isEmpty && _pathList.isEmpty; if (!isEmpty && !_isEnablePub.value) {
if (!isEmpty && !_isEnable) { _isEnablePub.value = true;
_isEnable = true; } else if (isEmpty && _isEnablePub.value) {
_isEnableStream.add(true); _isEnablePub.value = false;
} else if (isEmpty && _isEnable) {
_isEnable = false;
_isEnableStream.add(false);
} }
}, },
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -397,10 +408,11 @@ class _CreatePanelState extends State<CreatePanel> {
borderSide: BorderSide.none, borderSide: BorderSide.none,
gapPadding: 0, gapPadding: 0,
), ),
contentPadding: EdgeInsets.symmetric(vertical: 10), contentPadding: EdgeInsets.zero,
), ),
), ),
), ),
const SizedBox(height: 16),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row( child: Row(
@@ -418,7 +430,9 @@ class _CreatePanelState extends State<CreatePanel> {
vertical: -2, vertical: -2,
), ),
), ),
onPressed: () { onPressed: _isPrivate
? null
: () {
DateTime nowDate = DateTime.now(); DateTime nowDate = DateTime.now();
showDatePicker( showDatePicker(
context: context, context: context,
@@ -431,30 +445,35 @@ class _CreatePanelState extends State<CreatePanel> {
), ),
).then( ).then(
(selectedDate) { (selectedDate) {
if (selectedDate != null && context.mounted) { if (selectedDate != null &&
if (selectedDate.day == nowDate.day) { context.mounted) {
SmartDialog.showToast('至少选择10分钟之后');
}
TimeOfDay nowTime = TimeOfDay.now(); TimeOfDay nowTime = TimeOfDay.now();
showTimePicker( showTimePicker(
context: context, context: context,
initialTime: nowTime.replacing( initialTime: nowTime.replacing(
hour: nowTime.minute + 10 >= 60 hour: nowTime.minute + 6 >= 60
? (nowTime.hour + 1) % 24 ? (nowTime.hour + 1) % 24
: nowTime.hour, : nowTime.hour,
minute: (nowTime.minute + 10) % 60, minute: (nowTime.minute + 6) % 60,
), ),
).then((selectedTime) { ).then((selectedTime) {
if (selectedTime != null) { if (selectedTime != null) {
if (selectedDate.day == nowDate.day) { if (selectedDate.day ==
if (selectedTime.hour < nowTime.hour) { nowDate.day) {
SmartDialog.showToast('时间设置错误'); if (selectedTime.hour <
nowTime.hour) {
SmartDialog.showToast(
'时间设置错误至少选择6分钟之后');
return; return;
} else if (selectedTime.hour == } else if (selectedTime.hour ==
nowTime.hour) { nowTime.hour) {
if (selectedTime.minute < if (selectedTime.minute <
nowTime.minute + 10) { nowTime.minute + 6) {
SmartDialog.showToast('时间设置错误'); if (selectedDate.day ==
nowDate.day) {
SmartDialog.showToast(
'时间设置错误至少选择6分钟之后');
}
return; return;
} }
} }
@@ -492,11 +511,87 @@ class _CreatePanelState extends State<CreatePanel> {
_publishTime = null; _publishTime = null;
}); });
}, },
label: Text( label: Text(DateFormat('yyyy-MM-dd HH:mm')
DateFormat('yyyy-MM-dd HH:mm').format(_publishTime!)), .format(_publishTime!)),
icon: Icon(Icons.clear, size: 20), icon: Icon(Icons.clear, size: 20),
iconAlignment: IconAlignment.end, iconAlignment: IconAlignment.end,
), ),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PopupMenuButton(
enabled: _publishTime == null,
initialValue: _isPrivate,
onSelected: (value) {
setState(() {
_isPrivate = value;
});
},
itemBuilder: (context) => List.generate(
2,
(index) => PopupMenuItem<bool>(
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
? Icons.visibility_off
: Icons.visibility,
color: _publishTime == null
? _isPrivate
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
const SizedBox(width: 4),
Text(
_isPrivate ? '仅自己可见' : '所有人可见',
style: TextStyle(
height: 1,
color: _publishTime == null
? _isPrivate
? Theme.of(context).colorScheme.error
: Theme.of(context)
.colorScheme
.primary
: Theme.of(context).colorScheme.outline,
),
strutStyle: StrutStyle(leading: 0, height: 1),
),
Icon(
size: 20,
Icons.keyboard_arrow_right,
color: _publishTime == null
? _isPrivate
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
],
),
),
),
const SizedBox(height: 5),
PopupMenuButton( PopupMenuButton(
initialValue: _replyOption, initialValue: _replyOption,
onSelected: (item) { onSelected: (item) {
@@ -505,57 +600,85 @@ class _CreatePanelState extends State<CreatePanel> {
}); });
}, },
itemBuilder: (context) => ReplyOption.values itemBuilder: (context) => ReplyOption.values
.map((item) => PopupMenuItem<ReplyOption>( .map(
(item) => PopupMenuItem<ReplyOption>(
value: item, value: item,
child: Text(item.title),
))
.toList(),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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.iconData,
color: _replyOption == ReplyOption.close
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 4),
Text( Text(
_replyOption.title, _replyOption.title,
style: TextStyle( style: TextStyle(
height: 1, height: 1,
color: Theme.of(context).colorScheme.primary, color: _replyOption == ReplyOption.close
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
), ),
strutStyle: StrutStyle(leading: 0, height: 1), strutStyle: StrutStyle(leading: 0, height: 1),
), ),
Icon( Icon(
size: 20, size: 20,
Icons.keyboard_arrow_right, Icons.keyboard_arrow_right,
color: Theme.of(context).colorScheme.primary, color: _replyOption == ReplyOption.close
) ? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
),
],
),
),
),
], ],
), ),
)
], ],
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
StreamBuilder( Obx(
initialData: const [], () => SizedBox(
stream: _pathStream.stream, height: 100,
builder: (context, snapshot) => SizedBox(
height: 75,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const AlwaysScrollableScrollPhysics( physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics(), parent: BouncingScrollPhysics(),
), ),
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: itemCount: _pathList.length == _limit
_pathList.length == _limit ? _limit : _pathList.length + 1, ? _limit
: _pathList.length + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (_pathList.length != _limit && index == _pathList.length) { if (_pathList.length != _limit &&
index == _pathList.length) {
return Material( return Material(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
onTap: () { onTap: () {
EasyThrottle.throttle( EasyThrottle.throttle('imagePicker',
'imagePicker', const Duration(milliseconds: 500), const Duration(milliseconds: 500), () async {
() async {
try { try {
List<XFile> pickedFiles = List<XFile> pickedFiles =
await _imagePicker.pickMultiImage( await _imagePicker.pickMultiImage(
@@ -566,20 +689,14 @@ class _CreatePanelState extends State<CreatePanel> {
for (int i = 0; i < pickedFiles.length; i++) { for (int i = 0; i < pickedFiles.length; i++) {
if (_pathList.length == _limit) { if (_pathList.length == _limit) {
SmartDialog.showToast('最多选择$_limit张图片'); SmartDialog.showToast('最多选择$_limit张图片');
if (i != 0) {
_pathStream.add(_pathList);
}
break; break;
} else { } else {
_pathList.add(pickedFiles[i].path); _pathList.add(pickedFiles[i].path);
if (i == pickedFiles.length - 1) {
_pathStream.add(_pathList);
} }
} }
} if (_pathList.isNotEmpty &&
if (_pathList.isNotEmpty && !_isEnable) { !_isEnablePub.value) {
_isEnable = true; _isEnablePub.value = true;
_isEnableStream.add(true);
} }
} }
} catch (e) { } catch (e) {
@@ -588,14 +705,15 @@ class _CreatePanelState extends State<CreatePanel> {
}); });
}, },
child: Ink( child: Ink(
width: 75, width: 100,
height: 75, height: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
color: color: Theme.of(context)
Theme.of(context).colorScheme.secondaryContainer, .colorScheme
.secondaryContainer,
), ),
child: Center(child: Icon(Icons.add)), child: Center(child: Icon(Icons.add, size: 35)),
), ),
), ),
); );
@@ -603,15 +721,12 @@ class _CreatePanelState extends State<CreatePanel> {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_pathList.removeAt(index); _pathList.removeAt(index);
_pathStream.add(_pathList); if (_pathList.isEmpty && _ctr.text.trim().isEmpty) {
if (_pathList.isEmpty && _isEnablePub.value = false;
_ctr.text.replaceAll('\n', '').isEmpty) {
_isEnable = false;
_isEnableStream.add(false);
} }
}, },
child: Image( child: Image(
height: 75, height: 100,
fit: BoxFit.fitHeight, fit: BoxFit.fitHeight,
filterQuality: FilterQuality.low, filterQuality: FilterQuality.low,
image: FileImage(File(_pathList[index])), image: FileImage(File(_pathList[index])),
@@ -619,11 +734,17 @@ class _CreatePanelState extends State<CreatePanel> {
); );
} }
}, },
separatorBuilder: (context, index) => const SizedBox(width: 10), separatorBuilder: (context, index) =>
const SizedBox(width: 10),
), ),
), ),
), ),
SizedBox(
height: MediaQuery.paddingOf(context).bottom + 25,
),
], ],
),
),
); );
} }
} }

View File

@@ -283,7 +283,7 @@ class _ReplyPageState extends State<ReplyPage>
autofocus: false, autofocus: false,
readOnly: snapshot.data ?? false, readOnly: snapshot.data ?? false,
onChanged: (value) { onChanged: (value) {
bool isEmpty = value.replaceAll('\n', '').isEmpty; bool isEmpty = value.trim().isEmpty;
if (!isEmpty && !_enablePublish) { if (!isEmpty && !_enablePublish) {
_enablePublish = true; _enablePublish = true;
_publishStream.add(true); _publishStream.add(true);

View File

@@ -239,8 +239,14 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
Widget _buildInputView() { Widget _buildInputView() {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface, color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
@@ -273,7 +279,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
minLines: 1, minLines: 1,
maxLines: 4, maxLines: 4,
onChanged: (value) { onChanged: (value) {
bool isNotEmpty = value.replaceAll('\n', '').isNotEmpty; bool isNotEmpty = value.trim().isNotEmpty;
if (isNotEmpty && !_visibleSend) { if (isNotEmpty && !_visibleSend) {
_visibleSend = true; _visibleSend = true;
_enableSend.add(true); _enableSend.add(true);
@@ -289,7 +295,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
fillColor: Theme.of(context).colorScheme.surface, fillColor: Theme.of(context).colorScheme.surface,
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide.none, borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(6),
gapPadding: 0, gapPadding: 0,
), ),
contentPadding: const EdgeInsets.all(10), contentPadding: const EdgeInsets.all(10),
@@ -364,8 +370,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
height = max(200, keyboardHeight); height = max(200, keyboardHeight);
} }
return Container( return SizedBox(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
height: height, height: height,
child: EmotePanel( child: EmotePanel(
onChoose: onChooseEmote, onChoose: onChooseEmote,