发现页面
This commit is contained in:
150
lib/controllers/moment_controller.dart
Normal file
150
lib/controllers/moment_controller.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
149
lib/controllers/publish_controller.dart
Normal file
149
lib/controllers/publish_controller.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -23,18 +26,34 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetMaterialApp(
|
||||
title: 'ZH-CHAT',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: Themes.light,
|
||||
darkTheme: Themes.dark,
|
||||
initialRoute: AppRoutes.transit,
|
||||
defaultTransition: Transition.cupertino,
|
||||
getPages: AppRouter.getPages,
|
||||
builder: EasyLoading.init(),
|
||||
initialBinding: BindingsBuilder(() {
|
||||
title: 'ZH-CHAT',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: Themes.light,
|
||||
darkTheme: Themes.dark,
|
||||
initialRoute: AppRoutes.transit,
|
||||
defaultTransition: Transition.cupertino,
|
||||
getPages: AppRouter.getPages,
|
||||
builder: EasyLoading.init(),
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
91
lib/models/moment/moment_model.dart
Normal file
91
lib/models/moment/moment_model.dart
Normal 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'],
|
||||
);
|
||||
}
|
||||
23
lib/models/page_model.dart
Normal file
23
lib/models/page_model.dart
Normal 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'],
|
||||
);
|
||||
}
|
||||
20
lib/models/upload_model.dart
Normal file
20
lib/models/upload_model.dart
Normal 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'],
|
||||
);
|
||||
}
|
||||
23
lib/models/user_info_model.dart
Normal file
23
lib/models/user_info_model.dart
Normal 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'],
|
||||
);
|
||||
}
|
||||
46
lib/routes/moments_routes.dart
Normal file
46
lib/routes/moments_routes.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
123
lib/services/moment_service.dart
Normal file
123
lib/services/moment_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
51
lib/views/moments/detail/detail_page.dart
Normal file
51
lib/views/moments/detail/detail_page.dart
Normal 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),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
63
lib/views/moments/index/widgets/future_button.dart
Normal file
63
lib/views/moments/index/widgets/future_button.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
143
lib/views/moments/index/widgets/grid_media.dart
Normal file
143
lib/views/moments/index/widgets/grid_media.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
107
lib/views/moments/index/widgets/media_preview.dart
Normal file
107
lib/views/moments/index/widgets/media_preview.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
30
lib/views/moments/index/widgets/moment_avatar.dart
Normal file
30
lib/views/moments/index/widgets/moment_avatar.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
184
lib/views/moments/index/widgets/moment_header.dart
Normal file
184
lib/views/moments/index/widgets/moment_header.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
204
lib/views/moments/index/widgets/moment_list_item.dart
Normal file
204
lib/views/moments/index/widgets/moment_list_item.dart
Normal 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 ?? ''}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
200
lib/views/moments/index/widgets/moment_list_reply.dart
Normal file
200
lib/views/moments/index/widgets/moment_list_reply.dart
Normal 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,
|
||||
// )
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
101
lib/views/moments/index/widgets/quick_reply_bar.dart
Normal file
101
lib/views/moments/index/widgets/quick_reply_bar.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/views/moments/publish/preview_page.dart
Normal file
89
lib/views/moments/publish/preview_page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
146
lib/views/moments/publish/publish_page.dart
Normal file
146
lib/views/moments/publish/publish_page.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
20
lib/views/moments/publish/widgets/delete_dialog.dart
Normal file
20
lib/views/moments/publish/widgets/delete_dialog.dart
Normal 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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
19
lib/views/moments/user/index_page.dart
Normal file
19
lib/views/moments/user/index_page.dart
Normal 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
pubspec.lock
56
pubspec.lock
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user