发现页面

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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