diff --git a/README.md b/README.md index 2d0cafda6..544522104 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ ## feat +- [x] 带图评论 - [x] 视频TAG - [x] 筛选搜索 - [x] 转发动态 diff --git a/lib/http/video.dart b/lib/http/video.dart index 63b9cf0fb..f05d7dca3 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:developer'; import 'package:PiliPalaX/http/loading_state.dart'; import 'package:dio/dio.dart'; @@ -515,18 +516,29 @@ class VideoHttp { required String message, int? root, int? parent, + List? pictures, }) async { if (message == '') { return {'status': false, 'data': [], 'msg': '请输入评论内容'}; } - var res = await Request().post(Api.replyAdd, queryParameters: { + Map data = { 'type': type.index, 'oid': oid, 'root': root == null || root == 0 ? '' : root, 'parent': parent == null || parent == 0 ? '' : parent, 'message': message, + if (pictures != null) 'pictures': jsonEncode(pictures), 'csrf': await Request.getCsrf(), - }); + }; + var res = await Request().post( + Api.replyAdd, + data: FormData.fromMap(data), + options: Options( + headers: { + 'Content-Type': Headers.formUrlEncodedContentType, + }, + ), + ); log(res.toString()); if (res.data['code'] == 0) { return {'status': true, 'data': res.data['data']}; diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index 2bc19ef36..8c7be0dd1 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -222,7 +222,6 @@ class _CreatePanelState extends State { bool _isEnable = false; final _isEnableStream = StreamController(); late final _imagePicker = ImagePicker(); - late final _pics = []; late final _pathList = []; late final _pathStream = StreamController>(); @@ -244,12 +243,13 @@ class _CreatePanelState extends State { SmartDialog.showToast(result['msg']); } } else { + final pics = []; for (int i = 0; i < _pathList.length; i++) { SmartDialog.showLoading(msg: '正在上传图片: ${i + 1}/${_pathList.length}'); dynamic result = await MsgHttp.uploadBfs(_pathList[i]); if (result['status']) { int imageSize = await File(_pathList[i]).length(); - _pics.add({ + pics.add({ 'img_width': result['data']['image_width'], 'img_height': result['data']['image_height'], 'img_size': imageSize / 1024, @@ -267,7 +267,7 @@ class _CreatePanelState extends State { dynamic result = await MsgHttp.createDynamic( mid: GStorage.userInfo.get('userInfoCache').mid, rawText: _ctr.text, - pics: _pics, + pics: pics, ); if (result['status']) { Get.back(); @@ -343,7 +343,8 @@ class _CreatePanelState extends State { maxLines: 8, autofocus: true, onChanged: (value) { - bool isEmpty = value.replaceAll('\n', '').isEmpty; + bool isEmpty = + value.replaceAll('\n', '').isEmpty && _pathList.isEmpty; if (!isEmpty && !_isEnable) { _isEnable = true; _isEnableStream.add(true); @@ -372,7 +373,7 @@ class _CreatePanelState extends State { physics: const AlwaysScrollableScrollPhysics( parent: BouncingScrollPhysics(), ), - padding: const EdgeInsets.symmetric(horizontal: 15), + padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _pathList.length == 9 ? 9 : _pathList.length + 1, itemBuilder: (context, index) { if (_pathList.length != 9 && index == _pathList.length) { @@ -418,7 +419,6 @@ class _CreatePanelState extends State { } else { return GestureDetector( onTap: () { - _pics.clear(); _pathList.removeAt(index); _pathStream.add(_pathList); if (_pathList.isEmpty && diff --git a/lib/pages/video/detail/reply_new/reply_page.dart b/lib/pages/video/detail/reply_new/reply_page.dart index 816dd11ba..9333861c0 100644 --- a/lib/pages/video/detail/reply_new/reply_page.dart +++ b/lib/pages/video/detail/reply_new/reply_page.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math'; +import 'package:PiliPalaX/http/msg.dart'; import 'package:chat_bottom_container/chat_bottom_container.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -14,6 +16,7 @@ import 'package:PiliPalaX/pages/emote/index.dart'; import 'package:PiliPalaX/utils/feed_back.dart'; import 'package:PiliPalaX/pages/emote/view.dart'; import 'package:PiliPalaX/pages/video/detail/reply_new/toolbar_icon_button.dart'; +import 'package:image_picker/image_picker.dart'; enum PanelType { none, keyboard, emoji } @@ -55,6 +58,9 @@ class _ReplyPageState extends State final _publishStream = StreamController(); bool _selectKeyboard = true; final _keyboardStream = StreamController.broadcast(); + late final _imagePicker = ImagePicker(); + late final _pathStream = StreamController>(); + late final _pathList = []; @override void initState() { @@ -74,6 +80,8 @@ class _ReplyPageState extends State @override void dispose() async { + _keyboardStream.close(); + _pathStream.close(); _publishStream.close(); _readOnlyStream.close(); _enableSend.close(); @@ -105,6 +113,7 @@ class _ReplyPageState extends State ), ), _buildInputView(), + _buildImagePreview(), _buildPanelContainer(), ], ), @@ -162,6 +171,45 @@ class _ReplyPageState extends State ); } + Widget _buildImagePreview() { + return StreamBuilder( + initialData: const [], + stream: _pathStream.stream, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + return Container( + height: 85, + color: Theme.of(context).colorScheme.surface, + padding: const EdgeInsets.only(bottom: 10), + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + padding: const EdgeInsets.symmetric(horizontal: 15), + itemCount: _pathList.length, + itemBuilder: (context, index) => GestureDetector( + onTap: () { + _pathList.removeAt(index); + _pathStream.add(_pathList); + }, + child: Image( + height: 75, + fit: BoxFit.fitHeight, + filterQuality: FilterQuality.low, + image: FileImage(File(_pathList[index])), + ), + ), + separatorBuilder: (_, index) => const SizedBox(width: 10), + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + } + Widget _buildInputView() { return Container( clipBehavior: Clip.hardEdge, @@ -200,10 +248,11 @@ class _ReplyPageState extends State autofocus: false, readOnly: snapshot.data ?? false, onChanged: (value) { - if (value.isNotEmpty && !_enablePublish) { + bool isEmpty = value.replaceAll('\n', '').isEmpty; + if (!isEmpty && !_enablePublish) { _enablePublish = true; _publishStream.add(true); - } else if (value.isEmpty && _enablePublish) { + } else if (isEmpty && _enablePublish) { _enablePublish = false; _publishStream.add(false); } @@ -265,6 +314,35 @@ class _ReplyPageState extends State selected: !snapshot.data!, ), ), + const SizedBox(width: 20), + ToolbarIconButton( + tooltip: '图片', + selected: false, + icon: const Icon(Icons.image, size: 22), + onPressed: () async { + List pickedFiles = await _imagePicker.pickMultiImage( + limit: 9, + imageQuality: 100, + ); + if (pickedFiles.isNotEmpty) { + for (int i = 0; i < pickedFiles.length; i++) { + if (_pathList.length == 9) { + SmartDialog.showToast('最多选择9张图片'); + if (i != 0) { + _pathStream.add(_pathList); + } + break; + } else { + _pathList.add(pickedFiles[i].path); + if (i == pickedFiles.length - 1) { + SmartDialog.dismiss(); + _pathStream.add(_pathList); + } + } + } + } + }, + ), const Spacer(), StreamBuilder( initialData: _enablePublish, @@ -350,6 +428,30 @@ class _ReplyPageState extends State Future submitReplyAdd() async { feedBack(); + List? pictures; + if (_pathList.isNotEmpty) { + pictures = []; + for (int i = 0; i < _pathList.length; i++) { + SmartDialog.showLoading(msg: '正在上传图片: ${i + 1}/${_pathList.length}'); + dynamic result = await MsgHttp.uploadBfs(_pathList[i]); + if (result['status']) { + int imageSize = await File(_pathList[i]).length(); + pictures.add({ + 'img_width': result['data']['image_width'], + 'img_height': result['data']['image_height'], + 'img_size': imageSize / 1024, + 'img_src': result['data']['image_url'], + }); + } else { + SmartDialog.dismiss(); + SmartDialog.showToast(result['msg']); + return; + } + if (i == _pathList.length - 1) { + SmartDialog.dismiss(); + } + } + } String message = _replyContentController.text; var result = await VideoHttp.replyAdd( type: widget.replyType ?? ReplyType.video, @@ -359,6 +461,7 @@ class _ReplyPageState extends State message: widget.replyItem != null && widget.replyItem!.root != 0 ? ' 回复 @${widget.replyItem!.member!.uname!} : $message' : message, + pictures: pictures, ); if (result['status']) { SmartDialog.showToast(result['data']['success_toast']);