发现页面

This commit is contained in:
2022-10-20 14:33:16 +08:00
parent 42ba10ec61
commit 36b860752a
25 changed files with 2150 additions and 19 deletions

View File

@@ -0,0 +1,150 @@
import 'package:chat/models/moment/moment_model.dart';
import 'package:chat/routes/moments_routes.dart';
import 'package:chat/services/moment_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:chat/views/moments/index/widgets/quick_reply_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:get/get.dart';
class MomentController extends GetxController {
static MomentController get to => Get.find<MomentController>();
final EasyRefreshController refreshController = EasyRefreshController();
final ScrollController scrollController = ScrollController();
// 连接通知器
final LinkHeaderNotifier headerNotifier = LinkHeaderNotifier();
final momentData = Rx<MomentModel?>(null);
// 当前选中的moment id
int currentMomentIndex = 0;
static const defaultHitText = '回复';
final replyBarHitTest = defaultHitText.obs;
@override
void onReady() {
super.onReady();
callRefresh();
}
@override
void onClose() {
refreshController.dispose();
scrollController.dispose();
headerNotifier.dispose();
super.onClose();
}
void callRefresh() {
refreshController.callRefresh();
}
Future<void> refreshList() async {
final res = await MomentService.fetchMomentList();
if (res != null) {
momentData.value = res;
}
refreshController.resetLoadState();
}
Future<void> loadMoreList() async {
final res = await MomentService.fetchMomentList(
momentData.value?.data?.last.createdAt);
if (res != null) {
final data = res.data ?? [];
if (data.isEmpty || res.page?.hasMore == true) {
refreshController.finishLoad(noMore: true);
}
momentData.value?.data?.addAll(data);
momentData.refresh();
}
}
void pushToDetail(int index) {
currentMomentIndex = index;
Get.toNamed(MomentsRoutes.detail, arguments: {'index': index});
}
// 点赞
Future<void> likeMoment(MomentItemModel item) async {
final result = await MomentService.likeMoment(item.dynamicId!);
if (result != null) {
if (item.isLike != result && result && item.likerCount != null) {
item.likerCount = item.likerCount! + 1;
} else {
item.likerCount = item.likerCount! - 1;
}
item.isLike = result;
momentData.refresh();
}
}
// 删除动态
Future<void> delMoment(MomentItemModel item) async {
final result = await MomentService.delMoment(item.dynamicId!);
if (result == true) {
final moment = momentData.value?.data?.indexWhere(
(e) => e.dynamicId == item.dynamicId,
);
momentData.value?.data!.removeAt(moment!);
momentData.refresh();
Get.back();
}
}
// 删除评论
Future<void> delReply(
int index,
int dynamicId, [
Comment? comment,
]) async {
final result = await MomentService.delComment(
dynamicId,
comment?.id,
);
if (result == true) {
final moment = momentData.value?.data?.firstWhere(
(e) => e.dynamicId == dynamicId,
orElse: () => MomentItemModel(),
);
if (moment?.dynamicId == null) return;
moment?.comments?.removeAt(index);
momentData.refresh();
UiTools.toast('删除成功');
}
}
// 发送回复
Future<void> sendReply(
int dynamicId,
String content, [
Comment? comment,
]) async {
final result = await MomentService.replyComment(
dynamicId,
content,
comment?.id,
);
if (result != null) {
final moment = momentData.value?.data?.firstWhere(
(e) => e.dynamicId == dynamicId,
orElse: () => MomentItemModel(),
);
if (moment?.dynamicId == null) return;
moment?.comments?.add(result);
momentData.refresh();
Get.back();
// UiTools.toast('回复成功');
}
}
// 弹出QuickReplyBar
Future<void> showReplyBar(int dynamicId, [Comment? comment]) async {
Get.bottomSheet(
QuickReplyBar(dynamicId: dynamicId, comment: comment),
);
}
}

View File

@@ -0,0 +1,149 @@
import 'package:chat/controllers/moment_controller.dart';
import 'package:chat/models/upload_model.dart';
import 'package:chat/routes/moments_routes.dart';
import 'package:chat/services/moment_service.dart';
import 'package:chat/utils/ui_tools.dart';
import 'package:chat/views/moments/publish/widgets/delete_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:tuple/tuple.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class PublishController extends GetxController {
static PublishController get to => Get.find<PublishController>();
static const fileMaxLength = 9;
/// 发布"发现"的文件列表
final publishFileList = <AssetEntity>[].obs;
/// 已上传的文件获得的url
///
/// [Tuple(hashCode, UploadModel)]
final uploadedFileList = <Tuple2<int, UploadModel?>>[];
/// preview页显示的下标
final publistFileIndex = 0.obs;
/// 发布的文本内容
final publishContent = ''.obs;
/// 退出确认
Future<bool> exitConfirmation() async {
if (publishFileList.isEmpty && publishContent.isEmpty) return true;
final result = await Get.defaultDialog<bool?>(
title: '确认',
middleText: '退出后将不会保存内容',
onConfirm: () {
Get.back(result: true);
},
);
return result == true;
}
Future<void> publish() async {
FocusScope.of(Get.context!).requestFocus(FocusNode());
EasyLoading.show(status: '上传中', maskType: EasyLoadingMaskType.black);
final result = await uploadAllFile();
if (!result) return;
final res = await MomentService.publishMoment(
description: publishContent.value,
pictures: uploadedFileList.map((e) => e.item2!.url).toList(),
);
EasyLoading.dismiss();
if (res != null) {
UiTools.toast('发表成功');
Get.back();
MomentController.to.refreshList();
}
}
Future<bool> uploadAllFile() async {
try {
for (var i = 0; i < publishFileList.length; i++) {
final file = publishFileList[i];
final exists = uploadedFileList.any(
(e) => (e.item1 == file.hashCode && e.item2?.url != null),
);
if (!exists) {
final res = await MomentService.uploadFile((await file.file)!.path);
if (res == null) throw Exception('上传失败');
for (var index = 0; index < uploadedFileList.length; index++) {
final uploaded = uploadedFileList[index];
if (uploaded.item1 == file.hashCode) {
uploadedFileList[index] = uploaded.withItem2(res);
}
}
}
}
return true;
} catch (e) {
UiTools.toast('上传失败');
return false;
}
}
Future<void> deleteImageOrVideo(int index) async {
final result = await Get.dialog<bool?>(const PublishDeleteDialog());
if (result == true) {
removeFileByIndex(index);
}
}
/// 选择文件并添加到[publishFileList]里
Future<void> pickImageOrVideo() async {
FocusScope.of(Get.context!).requestFocus(FocusNode());
final result = await AssetPicker.pickAssets(
Get.context!,
pickerConfig: AssetPickerConfig(
maxAssets: fileMaxLength - publishFileList.length,
),
);
if (result == null) return;
bool videoAlreadyExists =
publishFileList.any((e) => e.type == AssetType.video);
final list = result.where(
(e) {
if (e.type == AssetType.image) return true;
if (videoAlreadyExists) return false;
final authorized = e.videoDuration.inSeconds <= 30;
if (authorized) videoAlreadyExists = true;
return authorized;
},
).toList();
if (videoAlreadyExists && list.length != result.length) {
UiTools.toast('最多能选择一个视频, 且视频时长不能超过30秒');
} else if (list.length != result.length) {
UiTools.toast('视频时长不能超过30秒');
}
// ignore: todo
// TODO: 限制视频或者图片大小
publishFileList.addAll(list);
list.asMap().forEach((index, file) {
uploadedFileList.add(Tuple2(file.hashCode, null));
});
}
/// preview[publishFileList]里的文件
void previewFiles(int index) {
FocusScope.of(Get.context!).requestFocus(FocusNode());
publistFileIndex.value = index;
Get.toNamed(MomentsRoutes.publishPreview);
}
/// 按index删除file
void removeFileByIndex(int index) {
uploadedFileList.removeAt(index);
publishFileList.removeAt(index);
if (publishFileList.isEmpty &&
Get.currentRoute == MomentsRoutes.publishPreview) Get.back();
if (publistFileIndex.value >= publishFileList.length) {
publistFileIndex.value = publishFileList.length - 1;
}
}
/// 删除preview当前显示的file
void removeFileByCurrentIndex() {
removeFileByIndex(publistFileIndex.value);
}
}

View File

@@ -1,4 +1,7 @@
import 'package:chat/configs/themes.dart';
import 'package:chat/controllers/group_controller.dart';
import 'package:chat/controllers/moment_controller.dart';
import 'package:chat/controllers/private_controller.dart';
import 'package:chat/routes/app_router.dart';
import 'package:chat/routes/app_routes.dart';
import 'package:chat/services/auth_service.dart';
@@ -31,10 +34,26 @@ class MyApp extends StatelessWidget {
defaultTransition: Transition.cupertino,
getPages: AppRouter.getPages,
builder: EasyLoading.init(),
initialBinding: BindingsBuilder(() {
initialBinding: BindingsBuilder(
() {
Get.put(AuthService());
Get.put(TabbarService());
Get.put(TimService());
}));
Get.lazyPut(
() => GroupController(),
fenix: true,
);
Get.lazyPut(
() => PrivateController(),
fenix: true,
);
Get.lazyPut(
() => MomentController(),
fenix: true,
);
},
),
);
}
}

View File

@@ -0,0 +1,91 @@
import 'package:chat/models/page_model.dart';
import 'package:chat/models/user_info_model.dart';
class MomentModel {
MomentModel({
this.data,
this.page,
});
List<MomentItemModel>? data;
PageModel? page;
factory MomentModel.fromMap(Map<String, dynamic> json) => MomentModel(
data: List<MomentItemModel>.from(
json['data'].map(
(x) => MomentItemModel.fromMap(x),
),
),
page: PageModel.fromJson(json['page']),
);
}
class MomentItemModel {
MomentItemModel({
this.dynamicId,
this.user,
this.description,
this.pictures,
this.isLike,
this.isMe,
this.likerCount,
this.liker,
this.comments,
this.createdAt,
this.time,
});
int? dynamicId;
UserInfoModel? user;
String? description;
bool? isLike;
bool? isMe;
int? likerCount;
List<String>? pictures;
List<UserInfoModel>? liker;
List<Comment>? comments;
String? createdAt;
String? time;
factory MomentItemModel.fromMap(Map<String, dynamic> json) => MomentItemModel(
dynamicId: json['dynamic_id'],
user: UserInfoModel.fromJson(json['user']),
description: json['description'],
pictures: List<String>.from(json['pictures'].map((x) => x)),
isLike: json['is_like'],
isMe: json['is_me'],
likerCount: json['liker_count'],
liker: List<UserInfoModel>.from(
json['liker'].map((x) => UserInfoModel.fromJson(x))),
comments:
List<Comment>.from(json['comments'].map((x) => Comment.fromMap(x))),
createdAt: json['created_at'],
time: json['time'],
);
}
class Comment {
Comment({
this.id,
this.user,
this.parent,
this.content,
required this.isMe,
});
int? id;
UserInfoModel? user;
UserInfoModel? parent;
String? content;
bool isMe;
factory Comment.fromMap(Map<String, dynamic> json) => Comment(
id: json['comment_id'],
parent: json['parent'] == null
? null
: UserInfoModel.fromJson(json['parent']),
user: UserInfoModel.fromJson(json['user']),
content: json['content'],
isMe: json['is_me'],
);
}

View File

@@ -0,0 +1,23 @@
class PageModel {
PageModel({
required this.current,
required this.totalPage,
required this.perPage,
required this.hasMore,
required this.total,
});
final int current;
final int totalPage;
final int perPage;
final bool hasMore;
final int total;
factory PageModel.fromJson(Map<String, dynamic> json) => PageModel(
current: json['current'],
totalPage: json['total_page'],
perPage: json['per_page'],
hasMore: json['has_more'],
total: json['total'],
);
}

View File

@@ -0,0 +1,20 @@
class UploadModel {
final bool exists;
final int size;
final String path;
final String url;
UploadModel({
required this.exists,
required this.size,
required this.path,
required this.url,
});
factory UploadModel.fromJson(Map<String, dynamic> json) => UploadModel(
exists: json['exists'],
size: json['size'],
path: json['path'],
url: json['url'],
);
}

View File

@@ -0,0 +1,23 @@
class UserInfoModel {
UserInfoModel({
required this.userId,
required this.username,
required this.nickname,
required this.avatar,
required this.address,
});
int userId;
String username;
String nickname;
String avatar;
String? address;
factory UserInfoModel.fromJson(Map<String, dynamic> json) => UserInfoModel(
userId: json['user_id'],
username: json['username'],
nickname: json['nickname'],
avatar: json['avatar'],
address: json['address'],
);
}

View File

@@ -0,0 +1,46 @@
import 'package:chat/middleware/auth_middleware.dart';
import 'package:chat/views/moments/detail/detail_page.dart';
import 'package:chat/views/moments/index/index_page.dart';
import 'package:chat/views/moments/publish/preview_page.dart';
import 'package:chat/views/moments/publish/publish_page.dart';
import 'package:chat/views/moments/user/index_page.dart';
import 'package:get/get.dart';
abstract class MomentsRoutes {
static const String index = '/moments';
static const String publish = '/moments/publish';
static const String publishPreview = '/moments/publish/preview';
static const String detail = '/moments/detail';
static const String user = '/moments/user';
static GetPage router = GetPage(
name: MomentsRoutes.index,
middlewares: [
EnsureAuthMiddleware(),
],
page: () => const MomentsPage(),
children: [
GetPage(
name: '/publish',
page: () => const MomentsPublishPage(),
children: [
GetPage(
name: '/preview',
page: () => const PublishPreviewPage(),
),
],
),
GetPage(
name: '/detail',
page: () => const MomentDetailPage(),
),
GetPage(
name: '/user',
page: () => const MomentsUserPage(),
),
],
);
}

View File

@@ -0,0 +1,123 @@
import 'package:chat/models/moment/moment_model.dart';
import 'package:chat/models/upload_model.dart';
import 'package:chat/utils/request/http.dart';
import 'package:chat/utils/ui_tools.dart';
class MomentService {
MomentService._();
/// 获取“发现”列表数据
///
/// [createAt] 最后一列的create_at
static Future<MomentModel?> fetchMomentList([String? createAt]) async {
try {
final result = await Http.get(
'user/dynamics',
params: {'created_at': createAt},
);
return MomentModel.fromMap(result);
} catch (e) {
UiTools.toast('获取发现列表失败');
}
return null;
}
/// 点赞
///
/// [dynamicId]
static Future<bool?> likeMoment(int dynamicId) async {
try {
final result = await Http.post('user/dynamics/$dynamicId/like');
return result['is_like'];
} catch (e) {
UiTools.toast('点赞失败');
}
return null;
}
/// 删除
///
/// [dynamicId]
static Future<bool?> delMoment(int dynamicId) async {
try {
final result = await Http.delete('user/dynamics/$dynamicId');
return result;
} catch (e) {
UiTools.toast('删除失败');
}
return null;
}
/// 回复或评论
/// [dynamicId]
/// [content] 评论内容
/// [parentId] 回复的评论id
static Future<Comment?> replyComment(
int dynamicId,
String content, [
int? parentId,
]) async {
try {
final result = await Http.post(
'user/dynamics/$dynamicId/comment',
data: {
'content': content,
'parent_id': parentId,
},
);
return Comment.fromMap(result);
} catch (e) {
UiTools.toast('评论失败');
}
return null;
}
/// 删除评论
/// [dynamicId]
/// [comment] 评论id
static Future<bool?> delComment(
int dynamicId, [
int? commentId,
]) async {
try {
final result = await Http.delete(
'user/dynamics/$dynamicId/comment/$commentId',
);
return result;
} catch (e) {
UiTools.toast('删除评论失败');
}
return null;
}
/// 上传文件
static Future<UploadModel?> uploadFile(String filePath) async {
try {
final result = await Http.upload('storage/upload', filePath: filePath);
return UploadModel.fromJson(result);
} catch (e) {
UiTools.toast('上传失败');
}
return null;
}
/// 发布“发现”内容
///
/// [description] 文本内容
/// [pictures] 媒体内容
static Future<dynamic> publishMoment({
required String description,
required List<String> pictures,
}) async {
try {
final result = await Http.post('user/dynamics', data: {
'description': description,
'pictures': pictures,
});
return result;
} catch (e) {
UiTools.toast('发布失败');
}
return null;
}
}

View File

@@ -0,0 +1,51 @@
import 'package:chat/controllers/moment_controller.dart';
import 'package:chat/views/moments/index/widgets/moment_list_item.dart';
import 'package:chat/views/moments/index/widgets/moment_list_reply.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MomentDetailPage extends StatelessWidget {
const MomentDetailPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final ctrl = MomentController.to;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: const Text('动态详情'),
// actions: [
// PopupMenuButton(
// itemBuilder: (context) => [
// const PopupMenuItem(
// child: Text(''),
// ),
// ],
// )
// ],
),
body: SafeArea(
child: SingleChildScrollView(
child: Obx(() {
final item =
ctrl.momentData.value!.data![ctrl.currentMomentIndex];
return Column(
children: [
MomentListItem(item: item),
MomentListItemReplay(
index: 0,
item: item,
reply: (value) => MomentController.to
.showReplyBar(item.dynamicId!, value),
),
const SizedBox(height: 16),
],
);
}),
),
),
),
);
}
}

View File

@@ -1,19 +1,90 @@
import 'package:chat/controllers/moment_controller.dart';
import 'package:chat/models/moment/moment_model.dart';
import 'package:chat/views/moments/index/widgets/moment_header.dart';
import 'package:chat/views/moments/index/widgets/moment_list_item.dart';
import 'package:chat/views/moments/index/widgets/moment_list_reply.dart';
import 'package:chat/widgets/custom_easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:get/get.dart';
class MomentsPage extends StatefulWidget {
class MomentsPage extends StatelessWidget {
const MomentsPage({Key? key}) : super(key: key);
@override
_MomentsPageState createState() => _MomentsPageState();
}
class _MomentsPageState extends State<MomentsPage> {
@override
Widget build(BuildContext context) {
final ctrl = MomentController.to;
return Scaffold(
appBar: AppBar(
title: const Text('发现'),
body: Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewPadding.bottom),
child: EasyRefresh.custom(
scrollController: ctrl.scrollController,
controller: ctrl.refreshController,
header: LinkHeader(
ctrl.headerNotifier,
extent: 70.0,
triggerDistance: 70.0,
completeDuration: const Duration(milliseconds: 500),
),
footer: CustomEasyRefresh.footer,
onRefresh: () => ctrl.refreshList(),
onLoad: () => ctrl.loadMoreList(),
slivers: [
MomentHeader(
linkNotifier: ctrl.headerNotifier,
onTitleDoubleTap: () {
ctrl.scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.fastOutSlowIn,
);
},
),
Obx(() {
final momentList = ctrl.momentData.value?.data ?? [];
if (momentList.isEmpty) {
return SliverFillRemaining(
child: CustomEasyRefresh.empty(text: '暂无动态内容'),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index.isEven) {
final i = (index / 2).round();
return momentItemWidget(i, momentList[i]);
} else {
return const Divider();
}
},
childCount: (momentList.length * 2) - 1,
),
);
})
],
),
),
);
}
Widget momentItemWidget(int index, MomentItemModel item) {
return Column(children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => MomentController.to.pushToDetail(index),
child: MomentListItem(item: item),
),
Padding(
padding: const EdgeInsets.only(left: 50),
child: MomentListItemReplay(
index: index,
item: item,
maxDisplayCount: 3,
maxLine: 2,
reply: (value) =>
MomentController.to.showReplyBar(item.dynamicId!, value),
),
),
]);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
typedef VoidFutureCallBack = Future<void> Function();
class FutureTextButton extends StatefulWidget {
const FutureTextButton({
Key? key,
this.onLongPress,
this.onHover,
this.onFocusChange,
this.style,
this.focusNode,
this.autofocus = false,
this.clipBehavior = Clip.none,
required this.child,
required this.onPressed,
}) : super(key: key);
final VoidFutureCallBack? onPressed;
final VoidCallback? onLongPress;
final ValueChanged<bool>? onHover;
final ValueChanged<bool>? onFocusChange;
final ButtonStyle? style;
final Clip clipBehavior;
final FocusNode? focusNode;
final bool autofocus;
final Widget child;
@override
State<FutureTextButton> createState() => _FutureTextButtonState();
}
class _FutureTextButtonState extends State<FutureTextButton> {
bool _isBusy = false;
Future<void> onPressed() async {
if (_isBusy) return;
setState(() => _isBusy = true);
try {
await widget.onPressed?.call();
} catch (e) {
rethrow;
} finally {
if (mounted) setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return TextButton(
key: widget.key,
onPressed: _isBusy || widget.onPressed == null ? null : () => onPressed(),
onLongPress: widget.onLongPress,
onHover: widget.onHover,
onFocusChange: widget.onFocusChange,
style: widget.style,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
clipBehavior: widget.clipBehavior,
child: widget.child,
);
}
}

View File

@@ -0,0 +1,143 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:chat/views/moments/index/widgets/media_preview.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class GridMedia extends StatelessWidget {
final List<String>? mediaList;
const GridMedia(this.mediaList, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
if (mediaList?.length == 1) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: Get.width * 0.5,
maxWidth: Get.width * 0.5,
minHeight: 50,
minWidth: 50,
),
child: mediaList!.first.split('?').first.isImageFileName
? imageWidget(mediaList![0], 0)
: videoWidget(mediaList![0], 0),
);
} else if (mediaList?.length == 2 || mediaList?.length == 4) {
return GridView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1,
),
itemCount: mediaList?.length,
itemBuilder: (context, index) {
final source = mediaList![index];
if (source.split('?').first.isImageFileName) {
return imageWidget(source, index);
}
return videoWidget(source, index);
},
);
} else {
return GridView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1,
),
itemCount: mediaList?.length ?? 0,
itemBuilder: (context, index) {
final source = mediaList![index];
if (source.split('?').first.isImageFileName) {
return imageWidget(source, index);
}
return videoWidget(source, index);
},
);
}
}
Widget videoWidget(String sourse, int index) {
return GestureDetector(
onTap: () {
pushToPreview(index);
},
child: const ColoredBox(
color: AppColors.black,
child: Center(
child: ClipOval(
child: ColoredBox(
color: AppColors.white,
child: SizedBox.square(
dimension: 30,
child: Icon(Icons.play_arrow),
),
),
),
),
),
// ignore: todo
// TODO: 缩略图太耗流量
// child: FutureBuilder<Uint8List?>(
// // future: getVideoThumbnail(sourse),
// builder: (context, snapshot) {
// if (snapshot.data == null) {
// return Container(color: Colors.white);
// } else {
// return DecoratedBox(
// decoration: BoxDecoration(
// image: DecorationImage(
// image: MemoryImage(snapshot.data!),
// fit: BoxFit.cover,
// ),
// ),
// child: const Center(
// child: Icon(
// Icons.play_arrow_rounded,
// color: Colors.white,
// size: 40,
// ),
// ),
// );
// }
// },
// ),
);
}
Widget imageWidget(String source, int index) {
return GestureDetector(
onTap: () {
pushToPreview(index);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: source,
alignment: Alignment.center,
fit: BoxFit.cover,
),
),
);
}
void pushToPreview(int index) {
Get.dialog(
MomentMediaPreview(
mediaSourceList: mediaList!,
initialPage: index,
),
useSafeArea: false,
);
}
}

View File

@@ -0,0 +1,107 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:video_player/video_player.dart';
class MomentMediaPreview extends StatelessWidget {
final List<String> mediaSourceList;
final int? initialPage;
const MomentMediaPreview({
Key? key,
required this.mediaSourceList,
this.initialPage,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.pop(context),
child: Material(
color: AppColors.black,
child: SafeArea(
child: Stack(children: [
Positioned.fill(
child: PhotoViewGallery.builder(
pageController: PageController(initialPage: initialPage ?? 0),
itemCount: mediaSourceList.length,
builder: (context, index) {
final source = mediaSourceList[index];
if (source.split('?').first.isImageFileName) {
return PhotoViewGalleryPageOptions(
imageProvider: CachedNetworkImageProvider(source),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
);
} else if (source.split('?').first.isVideoFileName) {
return PhotoViewGalleryPageOptions.customChild(
disableGestures: true,
child: _VideoPreview(source: source),
);
} else {
return PhotoViewGalleryPageOptions.customChild(
child: const Center(
child: Text(
'格式不支持',
style: TextStyle(color: AppColors.white),
),
),
);
}
},
),
),
const SafeArea(
child: Padding(
padding: EdgeInsets.all(4.0),
child: BackButton(color: AppColors.white),
),
)
]),
),
),
);
}
}
class _VideoPreview extends StatefulWidget {
final String source;
const _VideoPreview({Key? key, required this.source}) : super(key: key);
@override
State<_VideoPreview> createState() => __VideoPreviewState();
}
class __VideoPreviewState extends State<_VideoPreview> {
late final videoCtrl = VideoPlayerController.network(widget.source);
@override
void dispose() {
videoCtrl.dispose();
super.dispose();
}
Future<void> initVideo() async {
await videoCtrl.initialize();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: initVideo(),
builder: (context, snapshot) {
return Chewie(
controller: ChewieController(
showOptions: false,
autoPlay: true,
aspectRatio: videoCtrl.value.aspectRatio,
videoPlayerController: videoCtrl,
),
);
},
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:flutter/material.dart';
class MomentAvatar extends StatelessWidget {
final String imageUrl;
const MomentAvatar({Key? key, String? imageUrl})
: imageUrl = imageUrl ?? '',
super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: 40,
height: 40,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: AppColors.white,
image: imageUrl.isNotEmpty
? DecorationImage(
image: CachedNetworkImageProvider(imageUrl),
fit: BoxFit.cover,
)
: null,
),
),
);
}
}

View File

@@ -0,0 +1,184 @@
import 'package:chat/configs/app_colors.dart';
import 'package:chat/routes/moments_routes.dart';
import 'package:chat/services/tabbar_service.dart';
import 'package:chat/widgets/custom_avatar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:get/get.dart';
class MomentHeader extends StatelessWidget {
final LinkHeaderNotifier linkNotifier;
final VoidCallback? onTitleDoubleTap;
const MomentHeader({
Key? key,
required this.linkNotifier,
this.onTitleDoubleTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SliverAppBar(
systemOverlayStyle: SystemUiOverlayStyle.light,
pinned: true,
expandedHeight: 260,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.white,
titleTextStyle: const TextStyle(color: AppColors.white),
leading: CircleHeader(linkNotifier),
actions: [
IconButton(
onPressed: () {
Get.toNamed(MomentsRoutes.publish);
},
icon: const Icon(
Icons.camera_alt_rounded,
),
),
],
flexibleSpace: LayoutBuilder(
builder: (context, constraints) {
final FlexibleSpaceBarSettings settings = context
.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
return FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
centerTitle: true,
title: Visibility(
visible: constraints.maxHeight <= settings.minExtent,
child: GestureDetector(
onDoubleTap: () {
onTitleDoubleTap?.call();
},
child: Text(
'发现',
style: Theme.of(context)
.appBarTheme
.titleTextStyle
?.copyWith(color: AppColors.white),
),
),
),
background: const _HeaderBackground(),
);
},
),
);
}
}
class _HeaderBackground extends StatelessWidget {
const _HeaderBackground({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Theme.of(context).scaffoldBackgroundColor,
child: Stack(
children: [
Positioned.fill(
bottom: 32,
child: GestureDetector(
child: Image.asset(
'assets/backgrounds/moment_3.jpg',
fit: BoxFit.cover,
),
),
),
Positioned(
right: 16,
bottom: 0,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: 52,
child: Obx(() {
return const Text(
"UserController.to.userInfo.value?.nickname ?? ''",
style: TextStyle(
color: AppColors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
);
}),
),
const SizedBox(width: 8),
Obx(() {
return GestureDetector(
onTap: () => TabbarService.to.index = 4,
child: CustomAvatar(
'',
size: 64,
),
);
}),
],
),
)
],
),
);
}
}
// 圆形Header
class CircleHeader extends StatefulWidget {
final LinkHeaderNotifier linkNotifier;
const CircleHeader(this.linkNotifier, {Key? key}) : super(key: key);
@override
CircleHeaderState createState() {
return CircleHeaderState();
}
}
class CircleHeaderState extends State<CircleHeader> {
// 指示器值
double? _indicatorValue = 0.0;
RefreshMode get _refreshState => widget.linkNotifier.refreshState;
double get _pulledExtent => widget.linkNotifier.pulledExtent;
@override
void initState() {
super.initState();
widget.linkNotifier.addListener(onLinkNotify);
}
void onLinkNotify() {
if (!mounted) return;
setState(() {
if (_refreshState == RefreshMode.armed ||
_refreshState == RefreshMode.refresh) {
_indicatorValue = null;
} else if (_refreshState == RefreshMode.refreshed ||
_refreshState == RefreshMode.done) {
_indicatorValue = 1.0;
} else {
if (_refreshState == RefreshMode.inactive) {
_indicatorValue = 0.0;
} else {
double indicatorValue = _pulledExtent / 70.0 * 0.8;
_indicatorValue = indicatorValue < 0.8 ? indicatorValue : 0.8;
}
}
});
}
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox.square(
dimension: 24.0,
child: CircularProgressIndicator(
value: _indicatorValue,
valueColor: const AlwaysStoppedAnimation(AppColors.white),
strokeWidth: 2.4,
),
),
);
}
}

View File

@@ -0,0 +1,204 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:chat/configs/app_size.dart';
import 'package:chat/controllers/moment_controller.dart';
import 'package:chat/models/moment/moment_model.dart';
import 'package:chat/views/moments/index/widgets/future_button.dart';
import 'package:chat/views/moments/index/widgets/grid_media.dart';
import 'package:chat/views/moments/index/widgets/moment_avatar.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MomentListItem extends StatelessWidget {
final MomentItemModel item;
MomentListItem({Key? key, required this.item}) : super(key: key);
final actionStyle = ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(vertical: 4, horizontal: 8)),
minimumSize: MaterialStateProperty.all(Size.zero),
foregroundColor: MaterialStateProperty.all(AppColors.deep.withOpacity(0.6)),
textStyle: MaterialStateProperty.all(
const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
);
// Future<Uint8List?> getVideoThumbnail(String source) async {
// return await VideoThumbnail.thumbnailData(
// video: source,
// imageFormat: ImageFormat.JPEG,
// maxWidth: 128,
// quality: 25,
// );
// }
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSize.horizontalLargePadding,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MomentAvatar(imageUrl: item.user?.avatar),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.user?.nickname ?? '',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: AppSize.fontSize + 1,
color: AppColors.darkBlue,
),
),
const SizedBox(height: 2),
Text(
item.description ?? '',
style: const TextStyle(
fontSize: AppSize.fontSize,
),
),
const SizedBox(height: 8),
Visibility(
visible: item.pictures?.isNotEmpty ?? false,
child: GridMedia(item.pictures),
),
timeAndAction()
],
),
)
],
),
);
}
// Widget gridMedia() {
// if (item.pictures?.length == 1) {
// return ConstrainedBox(
// constraints: BoxConstraints(
// maxHeight: Get.width * 0.5,
// maxWidth: Get.width * 0.5,
// minHeight: 50,
// minWidth: 50,
// ),
// child: imageWidget(item.pictures![0], 0),
// );
// } else {
// return GridView.builder(
// padding: EdgeInsets.zero,
// shrinkWrap: true,
// physics: const NeverScrollableScrollPhysics(),
// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
// crossAxisCount: 3,
// mainAxisSpacing: 8,
// crossAxisSpacing: 8,
// childAspectRatio: 1,
// ),
// itemCount: item.pictures?.length ?? 0,
// itemBuilder: (context, index) {
// final source = item.pictures![index];
// if (source.isImageFileName) {
// return imageWidget(source, index);
// }
// return videoWidget(source, index);
// },
// );
// }
// }
Widget timeAndAction() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Text(
item.time ?? '',
style: const TextStyle(
fontSize: AppSize.fontSize - 2,
color: AppColors.unactive,
),
),
item.isMe == true
? InkWell(
onTap: () async {
OkCancelResult result = await showOkCancelAlertDialog(
style: AdaptiveStyle.iOS,
context: Get.context!,
title: '系统提示',
message: '删除后无法撤回',
okLabel: '确定',
cancelLabel: '取消',
defaultType: OkCancelAlertDefaultType.cancel,
);
if (result == OkCancelResult.ok) {
MomentController.to.delMoment(item);
}
},
child: const Padding(
padding: EdgeInsets.all(10.0),
child: Text(
'删除',
style: TextStyle(
fontSize: AppSize.fontSize - 2,
color: AppColors.darkBlue,
),
),
),
)
: const SizedBox(),
],
),
),
FutureTextButton(
style: actionStyle,
onPressed: () => MomentController.to.likeMoment(item),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Visibility(
visible: item.isLike ?? false,
child: const Icon(Icons.favorite, size: 14),
replacement: const Icon(Icons.favorite_border, size: 14),
),
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Text('${item.likerCount ?? ''}'),
),
],
),
),
const SizedBox(width: 6),
TextButton(
style: actionStyle,
onPressed: () => MomentController.to.showReplyBar(item.dynamicId!),
child: Row(
children: [
const Icon(Icons.message, size: 14),
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Text('${item.comments?.length ?? ''}'),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,200 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:chat/configs/app_colors.dart';
import 'package:chat/configs/app_size.dart';
import 'package:chat/controllers/moment_controller.dart';
import 'package:chat/models/moment/moment_model.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MomentListItemReplay extends StatelessWidget {
final int? maxDisplayCount;
final int? maxLine;
final int index;
final MomentItemModel item;
final void Function(Comment value)? reply;
const MomentListItemReplay({
Key? key,
required this.index,
this.maxDisplayCount,
this.reply,
this.maxLine,
required this.item,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final listLength = item.comments?.length ?? 0;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSize.horizontalLargePadding),
child: DecoratedBox(
decoration: BoxDecoration(
color: AppColors.unactive.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: AppSize.horizontalPadding,
),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount:
listLength > 3 ? maxDisplayCount ?? listLength : listLength,
itemBuilder: (context, index) {
final comment = item.comments![index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (comment.isMe) return;
reply?.call(comment);
},
onLongPress: () async {
if (comment.isMe) {
OkCancelResult result = await showOkCancelAlertDialog(
style: AdaptiveStyle.iOS,
context: Get.context!,
title: '系统提示',
message: '删除后无法恢复',
okLabel: '确定',
cancelLabel: '取消',
defaultType: OkCancelAlertDefaultType.cancel,
);
if (result == OkCancelResult.ok) {
MomentController.to
.delReply(index, item.dynamicId!, comment);
}
} else {
reply?.call(comment);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: AppSize.verticalPadding,
),
child: replayItem(comment),
),
);
},
separatorBuilder: (context, index) => const Divider(height: 0.4),
),
if (maxDisplayCount != null && listLength > 3)
Padding(
padding: const EdgeInsets.all(
AppSize.verticalPadding,
),
child: GestureDetector(
onTap: () {
MomentController.to.pushToDetail(index);
},
child: Row(
children: [
Text(
'查看全部$listLength条回复',
style: const TextStyle(
color: AppColors.tTextColor999,
fontSize: 12,
),
),
const Padding(
padding: EdgeInsets.only(
top: 2.0,
),
child: Icon(
Icons.keyboard_arrow_down,
color: AppColors.tTextColor999,
size: 16,
),
),
],
),
),
),
],
),
),
);
}
Widget replayItem(Comment comment) {
final name = comment.user?.nickname ?? '';
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// MomentAvatar(imageUrl: comment.user?.avatar),
// const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (comment.parent == null)
Text.rich(TextSpan(children: [
TextSpan(
text: '$name:',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: AppColors.darkBlue,
),
),
TextSpan(
text: comment.content ?? '',
style: const TextStyle(
fontWeight: FontWeight.normal,
color: AppColors.tTextColor333,
fontSize: 13,
),
),
]))
else
Text.rich(
TextSpan(children: [
TextSpan(
text: name,
style: const TextStyle(
fontSize: 13,
),
),
const TextSpan(
text: ' 回复 ',
style: TextStyle(
fontWeight: FontWeight.normal,
color: AppColors.tTextColor333,
fontSize: 13,
),
),
TextSpan(
text: '${comment.parent?.nickname}: ',
style: const TextStyle(
fontSize: 13,
),
),
TextSpan(
text: comment.content ?? '',
style: const TextStyle(
fontWeight: FontWeight.normal,
color: AppColors.tTextColor333,
fontSize: 13,
),
),
]),
style: const TextStyle(
fontWeight: FontWeight.bold,
// fontSize: AppSize.titleFontSize,
color: AppColors.darkBlue,
),
),
// Text(
// comment.content ?? '',
// maxLines: maxLine ?? 99,
// overflow: TextOverflow.ellipsis,
// )
],
),
)
],
);
}
}

View File

@@ -0,0 +1,101 @@
import 'package:chat/configs/app_colors.dart';
import 'package:chat/controllers/moment_controller.dart';
import 'package:chat/models/moment/moment_model.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class QuickReplyBar extends StatefulWidget {
final bool? autofocus;
final int dynamicId;
final Comment? comment;
const QuickReplyBar({
Key? key,
this.autofocus,
required this.dynamicId,
this.comment,
}) : super(key: key);
static const _border = OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(6)),
);
@override
State<QuickReplyBar> createState() => _QuickReplyBarState();
}
class _QuickReplyBarState extends State<QuickReplyBar> {
final content = ''.obs;
@override
Widget build(BuildContext context) {
final ctrl = MomentController.to;
return ColoredBox(
color: AppColors.page,
child: SafeArea(
child: Row(
children: [
const SizedBox(width: 16),
Expanded(
child: SizedBox(
height: 36,
child: TextField(
onChanged: (value) => content.value = value,
autofocus: widget.autofocus ?? true,
decoration: InputDecoration(
hintStyle: const TextStyle(
fontSize: 13,
),
border: QuickReplyBar._border,
focusedBorder: QuickReplyBar._border,
disabledBorder: QuickReplyBar._border,
hintText: widget.comment?.user?.nickname.isNotEmpty ?? false
? '回复:${widget.comment?.user?.nickname}'
: '评论',
filled: true,
fillColor: AppColors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
constraints: const BoxConstraints(),
),
),
),
),
const SizedBox(width: 8),
// IconButton(
// onPressed: () {
// print('emoji picker');
// },
// icon: const Icon(Icons.emoji_emotions_outlined),
// color: Colors.grey,
// ),
Obx(() {
return ElevatedButton(
style: ButtonStyle(
elevation: MaterialStateProperty.all(0),
backgroundColor: MaterialStateProperty.resolveWith(
(states) {
if (states.contains(MaterialState.disabled)) {
return AppColors.tTextColor999.withAlpha(128);
} else {
return AppColors.primary;
}
},
),
),
onPressed: content.isEmpty
? null
: () => ctrl.sendReply(
widget.dynamicId,
content.value,
widget.comment,
),
child: const Text('发送'),
);
}),
const SizedBox(width: 16),
],
),
),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'dart:io';
import 'package:chat/configs/app_colors.dart';
import 'package:chat/controllers/publish_controller.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:video_player/video_player.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class PublishPreviewPage extends StatelessWidget {
const PublishPreviewPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final ctrl = PublishController.to;
return Scaffold(
appBar: AppBar(
title: Obx(() {
return Text(
'${(ctrl.publistFileIndex.value + 1)}/${ctrl.publishFileList.length}',
);
}),
actions: [
IconButton(
onPressed: () => ctrl.removeFileByCurrentIndex(),
icon: const Icon(Icons.delete),
)
],
),
body: Obx(() {
final assetList = ctrl.publishFileList;
return PhotoViewGallery.builder(
onPageChanged: (index) => ctrl.publistFileIndex.value = index,
pageController: PageController(
initialPage: ctrl.publistFileIndex.value,
),
itemCount: assetList.length,
builder: (context, index) {
final source = assetList[index];
if (source.type == AssetType.image) {
return PhotoViewGalleryPageOptions(
imageProvider: AssetEntityImageProvider(source),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
);
} else if (source.type == AssetType.video) {
return PhotoViewGalleryPageOptions.customChild(
disableGestures: true,
child: FutureBuilder<File?>(
initialData: null,
future: source.originFile,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const Center(child: CircularProgressIndicator());
}
return Chewie(
controller: ChewieController(
showOptions: false,
aspectRatio: source.orientatedSize.width /
source.orientatedSize.height,
autoInitialize: true,
autoPlay: true,
videoPlayerController: VideoPlayerController.file(
snapshot.data!,
),
),
);
},
),
);
} else {
return PhotoViewGalleryPageOptions.customChild(
child: const Center(
child: Text(
'格式不支持',
style: TextStyle(color: AppColors.white),
),
),
);
}
},
);
}),
);
}
}

View File

@@ -0,0 +1,146 @@
import 'package:chat/configs/app_colors.dart';
import 'package:chat/configs/app_size.dart';
import 'package:chat/controllers/publish_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class MomentsPublishPage extends StatelessWidget {
const MomentsPublishPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final ctrl = Get.put(PublishController());
return GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
child: Scaffold(
appBar: AppBar(
title: const Text('发布动态'),
actions: [
Center(
child: Padding(
padding: const EdgeInsets.only(
right: AppSize.horizontalLargePadding,
),
child: Obx(() {
return ElevatedButton(
style: ButtonStyle(
elevation: MaterialStateProperty.all(0),
backgroundColor: MaterialStateProperty.resolveWith(
(states) {
if (states.contains(MaterialState.disabled)) {
return AppColors.tTextColor999.withAlpha(128);
} else {
return AppColors.primary;
}
},
),
minimumSize: MaterialStateProperty.all(Size.zero),
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(
vertical: 6,
horizontal: 16,
),
),
),
onPressed: ctrl.publishContent.isNotEmpty
? () => ctrl.publish()
: null,
child: const Text('发布'),
);
}),
),
)
],
),
body: CustomScrollView(
slivers: [
const SizedBox(height: 10).sliverBox,
TextField(
decoration: const InputDecoration(
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
focusedBorder: InputBorder.none,
disabledBorder: InputBorder.none,
fillColor: AppColors.transparent,
filled: true,
hintText: '这一刻的想法...',
hintStyle: TextStyle(
fontSize: 14,
),
),
onChanged: (value) => ctrl.publishContent.value = value,
minLines: 7,
maxLines: 14,
).sliverBox.contentPadding,
const SizedBox(height: 8).sliverBox,
Obx(() {
return GridView.count(
crossAxisCount: 3,
childAspectRatio: 1,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
...ctrl.publishFileList
.map((e) =>
showImageWidget(ctrl.publishFileList.indexOf(e)))
.toList(),
if (ctrl.publishFileList.length < 9) pickImageWidget(),
],
).sliverBox.contentPadding;
}),
],
),
),
);
}
Widget showImageWidget(int index) {
final ctrl = PublishController.to;
return GestureDetector(
onLongPress: () => ctrl.deleteImageOrVideo(index),
onTap: () => ctrl.previewFiles(index),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image(
image: AssetEntityImageProvider(
ctrl.publishFileList[index],
isOriginal: false,
),
fit: BoxFit.cover,
alignment: Alignment.center,
),
),
);
}
Widget pickImageWidget() {
return InkWell(
onTap: () => PublishController.to.pickImageOrVideo(),
child: Container(
decoration: BoxDecoration(
color: AppColors.unactive.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.add_rounded,
size: 40,
color: AppColors.active,
),
),
);
}
}
extension on Widget {
Widget get contentPadding {
return SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: AppSize.horizontalLargePadding,
),
sliver: this,
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:chat/configs/app_colors.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class PublishDeleteDialog extends StatelessWidget {
const PublishDeleteDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dialog(
child: ListTile(
textColor: AppColors.red,
title: const Text('删除'),
onTap: () {
Get.back(result: true);
},
),
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
class MomentsUserPage extends StatefulWidget {
const MomentsUserPage({Key? key}) : super(key: key);
@override
State<MomentsUserPage> createState() => _MomentsUserPageState();
}
class _MomentsUserPageState extends State<MomentsUserPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Moments'),
),
);
}
}

View File

@@ -94,6 +94,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
chewie:
dependency: "direct main"
description:
name: chewie
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.5"
clock:
dependency: transitive
description:
@@ -571,6 +578,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
photo_view:
dependency: "direct main"
description:
name: photo_view
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.14.0"
pinput:
dependency: "direct main"
description:
@@ -758,6 +772,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.0"
tuple:
dependency: "direct main"
description:
name: tuple
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
typed_data:
dependency: transitive
description:
@@ -821,6 +842,41 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.12"
wakelock:
dependency: transitive
description:
name: wakelock
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.2"
wakelock_macos:
dependency: transitive
description:
name: wakelock_macos
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.4.0"
wakelock_platform_interface:
dependency: transitive
description:
name: wakelock_platform_interface
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.0"
wakelock_web:
dependency: transitive
description:
name: wakelock_web
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.4.0"
wakelock_windows:
dependency: transitive
description:
name: wakelock_windows
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.0"
wechat_assets_picker:
dependency: "direct main"
description:

View File

@@ -44,6 +44,9 @@ dependencies:
scroll_to_index: ^2.1.1
dart_date: ^1.1.1
permission_handler: ^10.2.0
tuple: ^2.0.1
photo_view: ^0.14.0
chewie: ^1.3.5
dev_dependencies:
flutter_test: