发现页面
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/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_router.dart';
|
||||||
import 'package:chat/routes/app_routes.dart';
|
import 'package:chat/routes/app_routes.dart';
|
||||||
import 'package:chat/services/auth_service.dart';
|
import 'package:chat/services/auth_service.dart';
|
||||||
@@ -23,18 +26,34 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetMaterialApp(
|
return GetMaterialApp(
|
||||||
title: 'ZH-CHAT',
|
title: 'ZH-CHAT',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: Themes.light,
|
theme: Themes.light,
|
||||||
darkTheme: Themes.dark,
|
darkTheme: Themes.dark,
|
||||||
initialRoute: AppRoutes.transit,
|
initialRoute: AppRoutes.transit,
|
||||||
defaultTransition: Transition.cupertino,
|
defaultTransition: Transition.cupertino,
|
||||||
getPages: AppRouter.getPages,
|
getPages: AppRouter.getPages,
|
||||||
builder: EasyLoading.init(),
|
builder: EasyLoading.init(),
|
||||||
initialBinding: BindingsBuilder(() {
|
initialBinding: BindingsBuilder(
|
||||||
|
() {
|
||||||
Get.put(AuthService());
|
Get.put(AuthService());
|
||||||
Get.put(TabbarService());
|
Get.put(TabbarService());
|
||||||
Get.put(TimService());
|
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/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);
|
const MomentsPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
|
||||||
_MomentsPageState createState() => _MomentsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MomentsPageState extends State<MomentsPage> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ctrl = MomentController.to;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
body: Padding(
|
||||||
title: const Text('发现'),
|
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"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
chewie:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: chewie
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.5"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -571,6 +578,13 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
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:
|
pinput:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -758,6 +772,13 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.0"
|
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:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -821,6 +842,41 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.12"
|
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:
|
wechat_assets_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ dependencies:
|
|||||||
scroll_to_index: ^2.1.1
|
scroll_to_index: ^2.1.1
|
||||||
dart_date: ^1.1.1
|
dart_date: ^1.1.1
|
||||||
permission_handler: ^10.2.0
|
permission_handler: ^10.2.0
|
||||||
|
tuple: ^2.0.1
|
||||||
|
photo_view: ^0.14.0
|
||||||
|
chewie: ^1.3.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user