diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 14fe98e..aa4c8cd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,8 @@ CreateStoryScreen( + targetType: AmityStoryTargetTypeExtension.enumOf( + state.queryParams['targetType']!), + targetId: state.queryParams['targetId'], + isVideoType: state.queryParams['isTypeVideo'] == 'true', + ), + + ), GoRoute( name: AppRoute.commentList, diff --git a/lib/core/widget/story_widget.dart b/lib/core/widget/story_widget.dart new file mode 100644 index 0000000..54d3ea6 --- /dev/null +++ b/lib/core/widget/story_widget.dart @@ -0,0 +1,387 @@ +import 'dart:io'; + +import 'package:amity_sdk/amity_sdk.dart'; +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_social_sample_app/core/utils/extension/date_extension.dart'; +import 'package:flutter_social_sample_app/core/widget/common_snackbar.dart'; +import 'package:flutter_social_sample_app/core/widget/shadow_container_widget.dart'; +import 'package:flutter_social_sample_app/core/widget/user_profile_info_row_widget.dart'; +import 'package:flutter_social_sample_app/presentation/screen/video_player/full_screen_video_player.dart'; +import 'package:video_player/video_player.dart'; + +class StoryWidget extends StatelessWidget { + final AmityStory story; + final AmityStoryTargetType targetType; + final String targetId; + final bool disableAction; + const StoryWidget( + {super.key, + required this.story, + required this.targetType, + required this.targetId, + this.disableAction = false}); + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + return Stack( + fit: StackFit.loose, + children: [ + ShadowContainerWidget( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UserProfileInfoRowWidget( + userId: story.creatorPublicId!, + userAvatar: + story.creator?.avatarUrl ?? story.creator?.avatarCustomUrl, + userName: story.creator!.displayName!, + options: disableAction + ? null + : [ + PopupMenuButton( + itemBuilder: (context) { + return [ + const PopupMenuItem( + value: 0, + enabled: true, + child: Text("Delete (Hard)"), + ), + const PopupMenuItem( + value: 1, + enabled: true, + child: Text("Delete (Soft)"), + ), + ]; + }, + child: const Icon( + Icons.more_vert, + size: 18, + ), + onSelected: (index) { + if (index == 0) { + AmitySocialClient.newStoryRepository() + .hardDeleteStory(storyId: story.storyId!) + .onError((error, stackTrace) { + print(error.toString()); + print(stackTrace.toString()); + CommonSnackbar.showNagativeSnackbar( + context, 'Error', error.toString()); + }); + } + if (index == 1) { + AmitySocialClient.newStoryRepository() + .softDeleteStory(storyId: story.storyId!) + .onError((error, stackTrace) { + print(error.toString()); + print(stackTrace.toString()); + CommonSnackbar.showNagativeSnackbar( + context, 'Error', error.toString()); + }); + } + }, + ), + ], + ), + Container( + margin: const EdgeInsets.symmetric(vertical: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Created At - ${story.createdAt!.format()}', + style: themeData.textTheme.bodySmall, + ), + Text( + 'Updated At - ${story.updatedAt?.format()}', + style: themeData.textTheme.bodySmall, + ), + + SelectableText( + 'Target Type -> ${story.targetType?.value}', + style: themeData.textTheme.bodySmall, + ), + + SelectableText( + 'Target Type -> ${story.targetId}', + style: themeData.textTheme.bodySmall, + ), + + SelectableText( + 'Story ID - ${story.storyId!}', + style: themeData.textTheme.bodySmall, + ), + + SelectableText( + 'Sync State -> ${story.syncState?.value}', + style: themeData.textTheme.bodySmall, + ), + + SelectableText( + 'Date Type -> ${story.dataType?.value}', + style: themeData.textTheme.bodySmall, + ), + + SelectableText( + 'Story Item Count -> ${story.storyItems.length}', + style: themeData.textTheme.bodySmall, + ), + + story.storyItems.length > 0 + ? const Text("Story Items") + : Container(), + for (var storyItem in story.storyItems) + SelectableText( + 'HyperLink text -> ${storyItem.toJson()}', + style: themeData.textTheme.bodySmall, + ), + + // if (story.targetType is AmityStoryTargetType.COMMUNITY) + // Text( + // 'Posted On : ${(value.target as UserTarget).targetUser?.displayName ?? 'No name'}', + // style: themeData.textTheme.bodySmall, + // ), + // if (value.target is CommunityTarget) + // Text( + // 'Posted On : ${(value.target as CommunityTarget).targetCommunity?.displayName ?? 'No name'} Community', + // style: themeData.textTheme.bodySmall, + // ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (story.data != null) + StoryContentWidget( + story: story, + storyData: story.data!, + ), + const SizedBox(height: 8), + ], + ), + ), + ], + ), + ), + if (story.isDeleted ?? false) + Positioned.fill( + child: Container( + margin: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.4), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.red.shade400, + borderRadius: BorderRadius.circular(12)), + child: Text( + 'Soft Deleted Amity Post', + style: themeData.textTheme.bodyLarge! + .copyWith(color: Colors.white), + ), + ), + ), + ), + ) + ], + ); + ; + } +} + +class StoryContentWidget extends StatelessWidget { + final AmityStory story; + final AmityStoryData storyData; + const StoryContentWidget( + {super.key, required this.story, required this.storyData}); + + @override + Widget build(BuildContext context) { + if (storyData is ImageStoryData) { + final data = storyData as ImageStoryData; + if (data.image.hasLocalPreview != null) { + return Column( + children: [ + Text("Image Display Mode ${data.imageDisplayMode.value}"), + SizedBox( + width: 100, + height: 100, + child: (data.image.hasLocalPreview!) + ? Image.file( + File(data.image.getFilePath!), + fit: BoxFit.cover, + ) + : (data.image != null) + ? Image.network( + data.image!.getUrl(AmityImageSize.MEDIUM), + fit: BoxFit.cover, + ) + : Text("MEDIA DELETED"), + ), + ], + ); + } + } + if (storyData is VideoStoryData) { + final data = storyData as VideoStoryData; + return Column( + children: [ + SelectableText( + 'Thumbnail -> ${data.thumbnail.fileUrl ?? 'No Thumbnail'}', + ), + SelectableText( + 'Video Resolutions -> ${data.video.getResolutions() ?? 'No Resolution'}', + ), + (data.video.hasLocalPreview != null) + ? (data.video.hasLocalPreview!) + ? SizedBox( + width: 200, + height: 200, + child: MiniVideoPlayer(uri: data.video.getFilePath!), + ) + : SizedBox( + width: 100, + height: 100, + // color: Colors.red, + child: (data.thumbnail != null && + data.video.fileId != null) + ? Stack( + children: [ + Positioned.fill( + child: Image.network( + data.thumbnail + ?.getUrl(AmityImageSize.MEDIUM) ?? + '', + fit: BoxFit.cover, + ), + ), + Align( + alignment: Alignment.center, + child: IconButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + FullScreenVideoPlayer( + title: data.video.fileName!, + url: data.video.fileUrl!, + ), + ), + ); + }, + icon: const Icon( + Icons.play_circle_fill_rounded, + color: Colors.white, + ), + ), + ) + ], + ) + : Text("MEDIA DELETED"), + ) + : (data.video != null) + ? SizedBox( + width: 100, + height: 100, + // color: Colors.red, + child: (data.thumbnail != null && + data.video.fileId != null) + ? Stack( + children: [ + Positioned.fill( + child: Image.network( + data.thumbnail + ?.getUrl(AmityImageSize.MEDIUM) ?? + '', + fit: BoxFit.cover, + ), + ), + Align( + alignment: Alignment.center, + child: IconButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + FullScreenVideoPlayer( + title: data.video.fileName!, + url: data.video.fileUrl!, + ), + ), + ); + }, + icon: const Icon( + Icons.play_circle_fill_rounded, + color: Colors.white, + ), + ), + ) + ], + ) + : Text("MEDIA DELETED"), + ) + : Container( + width: 20, + height: 20, + color: Colors.amber, + ) + ], + ); + } + if (storyData is UnknownStoryData) {} + + return const Placeholder(); + } +} + +class MiniVideoPlayer extends StatefulWidget { + final String uri; + const MiniVideoPlayer({super.key, required this.uri}); + + @override + State createState() => _MiniVideoPlayerState(); +} + +class _MiniVideoPlayerState extends State { + late ChewieController chewieController; + @override + void initState() { + print("MiniVideoPlaye ---> URI ${widget.uri}"); + setUpChewie(); + super.initState(); + } + + void setUpChewie() { + final videoPlayerController = VideoPlayerController.file(File(widget.uri)); + videoPlayerController.initialize(); + chewieController = ChewieController( + videoPlayerController: videoPlayerController, + autoPlay: true, + looping: true, + ); + } + + @override + Widget build(BuildContext context) { + return chewieController != null && + chewieController!.videoPlayerController.value.isInitialized + ? Chewie( + controller: chewieController!, + ) + : const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 20), + Text('Loading'), + ], + ); + } +} diff --git a/lib/presentation/screen/community_feed/community_feed_screen.dart b/lib/presentation/screen/community_feed/community_feed_screen.dart index c78fd9d..a3cad34 100644 --- a/lib/presentation/screen/community_feed/community_feed_screen.dart +++ b/lib/presentation/screen/community_feed/community_feed_screen.dart @@ -40,7 +40,7 @@ class _CommunityFeedScreenState extends State { .then((value) { _amityCommunity = value; _amityCommunity! - .subscription(AmityCommunityEvents.values[1]) + .subscription(AmityCommunityEvents.POSTS_AND_COMMENTS) .subscribeTopic() .then((value) {}) .onError((error, stackTrace) {}); diff --git a/lib/presentation/screen/community_profile/community_profile_screen.dart b/lib/presentation/screen/community_profile/community_profile_screen.dart index 9194df7..b5f6db9 100644 --- a/lib/presentation/screen/community_profile/community_profile_screen.dart +++ b/lib/presentation/screen/community_profile/community_profile_screen.dart @@ -9,31 +9,32 @@ import 'package:flutter_social_sample_app/presentation/screen/community_feed/com import 'package:flutter_social_sample_app/presentation/screen/community_member/community_member_banned_screen.dart'; import 'package:flutter_social_sample_app/presentation/screen/community_member/community_member_screen.dart'; import 'package:flutter_social_sample_app/presentation/screen/create_poll_post/create_poll_post_screen.dart'; +import 'package:flutter_social_sample_app/presentation/screen/story_list/story_list_screen.dart'; import 'package:go_router/go_router.dart'; class CommunityProfileScreen extends StatefulWidget { - const CommunityProfileScreen({Key? key, required this.communityId}) : super(key: key); + const CommunityProfileScreen({Key? key, required this.communityId}) + : super(key: key); final String communityId; @override State createState() => _CommunityProfileScreenState(); } -class _CommunityProfileScreenState extends State with TickerProviderStateMixin { +class _CommunityProfileScreenState extends State + with TickerProviderStateMixin { late TabController _tabController; late AmityCommunity _amityCommunity; Future? _future; - - - GlobalKey memberList = GlobalKey(); + GlobalKey memberList = + GlobalKey(); @override - void initState() { - _tabController = TabController(length: 3, vsync: this); - + void initState() { + _tabController = TabController(length: 4, vsync: this); - _future = AmitySocialClient.newCommunityRepository().getCommunity(widget.communityId); + _future = AmitySocialClient.newCommunityRepository() + .getCommunity(widget.communityId); super.initState(); - } @override @@ -77,32 +78,40 @@ class _CommunityProfileScreenState extends State with Ti onSelected: (index) { if (index == 1) { //Open Edit Community - GoRouter.of(context) - .goNamed(AppRoute.updateCommunity, queryParams: {'communityId': widget.communityId}); + GoRouter.of(context).goNamed(AppRoute.updateCommunity, + queryParams: {'communityId': widget.communityId}); } if (index == 2) { //Delete Community - AmitySocialClient.newCommunityRepository().deleteCommunity(widget.communityId); + AmitySocialClient.newCommunityRepository() + .deleteCommunity(widget.communityId); } if (index == 4) { EditTextDialog.show(context, title: 'Check my permission in this community', hintText: 'Enter permission name', onPress: (value) { - final permissions = AmityPermission.values.where((v) => v.value == value); + final permissions = + AmityPermission.values.where((v) => v.value == value); if (permissions.isEmpty) { - ErrorDialog.show(context, title: 'Error', message: 'permission does not exist'); + ErrorDialog.show(context, + title: 'Error', message: 'permission does not exist'); } else { final hasPermission = - AmityCoreClient.hasPermission(permissions.first).atCommunity(widget.communityId).check(); + AmityCoreClient.hasPermission(permissions.first) + .atCommunity(widget.communityId) + .check(); PositiveDialog.show(context, - title: 'Permission', message: 'The permission "$value" is valid = $hasPermission'); + title: 'Permission', + message: + 'The permission "$value" is valid = $hasPermission'); } }); } if (index == 5) { //Open RTE event for community - GoRouter.of(context).pushNamed(AppRoute.communityRTE, queryParams: {'communityId': widget.communityId}); + GoRouter.of(context).pushNamed(AppRoute.communityRTE, + queryParams: {'communityId': widget.communityId}); } }, ), @@ -122,17 +131,21 @@ class _CommunityProfileScreenState extends State with Ti headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverToBoxAdapter( - child: _CommunityProfileHeaderWidget(amityCommunity: _amityCommunity), + child: _CommunityProfileHeaderWidget( + amityCommunity: _amityCommunity), ), SliverToBoxAdapter( child: DefaultTabController( - length: 3, + length: 4, child: TabBar( controller: _tabController, tabs: const [ Tab( text: 'Feed', ), + Tab( + text: 'Stories', + ), Tab( text: 'Members', ), @@ -154,6 +167,12 @@ class _CommunityProfileScreenState extends State with Ti showAppBar: false, isPublic: _amityCommunity.isPublic ?? true, ), + StoryListScreen( + targetType: AmityStoryTargetType.COMMUNITY, + targetId: widget.communityId, + amityCommunity: _amityCommunity, + showAppBar: false, + ), CommunityMemberScreen( key: memberList, communityId: _amityCommunity.communityId!, @@ -189,7 +208,8 @@ class _CommunityProfileScreenState extends State with Ti onPressed: () { Navigator.of(context).pop(); //show create post for community - GoRouter.of(context).pushNamed(AppRoute.createPost, queryParams: { + GoRouter.of(context) + .pushNamed(AppRoute.createPost, queryParams: { 'communityId': _amityCommunity.communityId, 'isPublic': _amityCommunity.isPublic.toString(), }); @@ -205,7 +225,8 @@ class _CommunityProfileScreenState extends State with Ti //show create post for community Navigator.of(context).push(MaterialPageRoute( builder: (context) { - return CreatePollPostScreen(communityId: widget.communityId); + return CreatePollPostScreen( + communityId: widget.communityId); }, )); }, @@ -214,13 +235,66 @@ class _CommunityProfileScreenState extends State with Ti ), ], ), - actions: [ElevatedButton(onPressed: () {}, child: const Text('Cancel'))], + actions: [ + ElevatedButton(onPressed: () {}, child: const Text('Cancel')) + ], + ), + ); + } else if (_tabController.index == 1) { + //show add member action + // Add Stories + + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Please Select Story Type'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 200, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + //show create post for community + GoRouter.of(context) + .pushNamed(AppRoute.createStory, queryParams: { + 'targetId': _amityCommunity.communityId, + 'targetType': AmityStoryTargetType.COMMUNITY.value, + 'isTypeVideo': false.toString() + }); + }, + child: const Text('Image Story'), + ), + ), + SizedBox( + width: 200, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + //show create post for community + GoRouter.of(context) + .pushNamed(AppRoute.createStory, queryParams: { + 'targetId': _amityCommunity.communityId, + 'targetType': AmityStoryTargetType.COMMUNITY.value, + 'isTypeVideo': true.toString() + }); + }, + child: const Text('Video Story'), + ), + ), + ], + ), + actions: [ + ElevatedButton(onPressed: () {}, child: const Text('Cancel')) + ], ), ); } else { //show add member action - EditTextDialog.show(context, title: 'Add Member', hintText: 'Enter Comma seperated user Ids', - onPress: (value) { + EditTextDialog.show(context, + title: 'Add Member', + hintText: 'Enter Comma seperated user Ids', onPress: (value) { AmitySocialClient.newCommunityRepository() .membership(widget.communityId) .addMembers(value.split(',')) @@ -230,7 +304,8 @@ class _CommunityProfileScreenState extends State with Ti } // }).onError((error, stackTrace) { - ErrorDialog.show(context, title: 'Error', message: error.toString()); + ErrorDialog.show(context, + title: 'Error', message: error.toString()); }); }); } @@ -242,25 +317,28 @@ class _CommunityProfileScreenState extends State with Ti } class _CommunityProfileHeaderWidget extends StatefulWidget { - const _CommunityProfileHeaderWidget({Key? key, required this.amityCommunity}) : super(key: key); + const _CommunityProfileHeaderWidget({Key? key, required this.amityCommunity}) + : super(key: key); final AmityCommunity amityCommunity; @override - State<_CommunityProfileHeaderWidget> createState() => _CommunityProfileHeaderWidgetState(); + State<_CommunityProfileHeaderWidget> createState() => + _CommunityProfileHeaderWidgetState(); } -class _CommunityProfileHeaderWidgetState extends State<_CommunityProfileHeaderWidget> { +class _CommunityProfileHeaderWidgetState + extends State<_CommunityProfileHeaderWidget> { Future?>? _rolesFuture; @override void initState() { - if(widget.amityCommunity.communityId!=null){ - _rolesFuture = AmitySocialClient.newCommunityRepository().getCurrentUserRoles(widget.amityCommunity.communityId!); + if (widget.amityCommunity.communityId != null) { + _rolesFuture = AmitySocialClient.newCommunityRepository() + .getCurrentUserRoles(widget.amityCommunity.communityId!); } super.initState(); } - @override Widget build(BuildContext context) { final themeData = Theme.of(context); @@ -274,11 +352,15 @@ class _CommunityProfileHeaderWidgetState extends State<_CommunityProfileHeaderWi Container( width: 64, height: 64, - decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.grey.withOpacity(.3)), + decoration: BoxDecoration( + shape: BoxShape.circle, color: Colors.grey.withOpacity(.3)), clipBehavior: Clip.antiAliasWithSaveLayer, - child: widget.amityCommunity.avatarImage?.getUrl(AmityImageSize.MEDIUM) != null + child: widget.amityCommunity.avatarImage + ?.getUrl(AmityImageSize.MEDIUM) != + null ? Image.network( - widget.amityCommunity.avatarImage!.getUrl(AmityImageSize.MEDIUM), + widget.amityCommunity.avatarImage! + .getUrl(AmityImageSize.MEDIUM), fit: BoxFit.fill, ) : Image.asset('assets/user_placeholder.png'), @@ -294,9 +376,12 @@ class _CommunityProfileHeaderWidgetState extends State<_CommunityProfileHeaderWi children: [ TextSpan( text: '${widget.amityCommunity.postsCount}\n', - style: themeData.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold), + style: themeData.textTheme.titleMedium! + .copyWith(fontWeight: FontWeight.bold), ), - TextSpan(text: 'Posts', style: themeData.textTheme.bodyMedium), + TextSpan( + text: 'Posts', + style: themeData.textTheme.bodyMedium), ], ), ), @@ -306,9 +391,12 @@ class _CommunityProfileHeaderWidgetState extends State<_CommunityProfileHeaderWi children: [ TextSpan( text: '${widget.amityCommunity.membersCount}\n', - style: themeData.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold), + style: themeData.textTheme.titleMedium! + .copyWith(fontWeight: FontWeight.bold), ), - TextSpan(text: 'Members', style: themeData.textTheme.bodyMedium), + TextSpan( + text: 'Members', + style: themeData.textTheme.bodyMedium), ], ), ) @@ -332,7 +420,8 @@ class _CommunityProfileHeaderWidgetState extends State<_CommunityProfileHeaderWi future: _rolesFuture, builder: (context, snapshot) { var currentUserRoles = snapshot.data; - return Text("Current User Roles: ${currentUserRoles ?? "Not A member"}"); + return Text( + "Current User Roles: ${currentUserRoles ?? "Not A member"}"); }, ), const SizedBox(height: 18), @@ -346,32 +435,39 @@ class _CommunityProfileHeaderWidgetState extends State<_CommunityProfileHeaderWi .joinCommunity(widget.amityCommunity.communityId!) .then((value) {}) .onError((error, stackTrace) { - ErrorDialog.show(context, title: 'Error', message: error.toString()); + ErrorDialog.show(context, + title: 'Error', message: error.toString()); }); } else { AmitySocialClient.newCommunityRepository() .leaveCommunity(widget.amityCommunity.communityId!) .then((value) {}) .onError((error, stackTrace) { - ErrorDialog.show(context, title: 'Error', message: error.toString()); + ErrorDialog.show(context, + title: 'Error', message: error.toString()); }); } }, - child: Text(!(widget.amityCommunity.isJoined ?? true) ? 'Join' : 'Leave'), + child: Text(!(widget.amityCommunity.isJoined ?? true) + ? 'Join' + : 'Leave'), ), ), ), - if (widget.amityCommunity.hasPermission(AmityPermission.REVIEW_COMMUNITY_POST) && + if (widget.amityCommunity + .hasPermission(AmityPermission.REVIEW_COMMUNITY_POST) && widget.amityCommunity.isPostReviewEnabled!) Center( child: SizedBox( width: 260, child: ElevatedButton( onPressed: () { - GoRouter.of(context).pushNamed(AppRoute.communityInReviewPost, queryParams: { - 'communityId': widget.amityCommunity.communityId, - 'isPublic': widget.amityCommunity.isPublic!.toString() - }); + GoRouter.of(context).pushNamed( + AppRoute.communityInReviewPost, + queryParams: { + 'communityId': widget.amityCommunity.communityId, + 'isPublic': widget.amityCommunity.isPublic!.toString() + }); }, child: const Text('Review Post'), ), @@ -383,7 +479,8 @@ class _CommunityProfileHeaderWidgetState extends State<_CommunityProfileHeaderWi width: 260, child: ElevatedButton( onPressed: () { - GoRouter.of(context).pushNamed(AppRoute.communityPendingPost, queryParams: { + GoRouter.of(context) + .pushNamed(AppRoute.communityPendingPost, queryParams: { 'communityId': widget.amityCommunity.communityId, 'isPublic': widget.amityCommunity.isPublic!.toString() }); diff --git a/lib/presentation/screen/create_story/create_story_screen.dart b/lib/presentation/screen/create_story/create_story_screen.dart new file mode 100644 index 0000000..cc8fbc0 --- /dev/null +++ b/lib/presentation/screen/create_story/create_story_screen.dart @@ -0,0 +1,445 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:amity_sdk/amity_sdk.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_social_sample_app/core/widget/common_snackbar.dart'; +import 'package:flutter_social_sample_app/core/widget/user_suggestion_overlay.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path/path.dart'; + +class CreateStoryScreen extends StatefulWidget { + final AmityStoryTargetType? targetType; + final String? targetId; + final bool isVideoType; + + const CreateStoryScreen( + {Key? key, this.targetType, this.targetId, this.isVideoType = false}) + : super(key: key); + + @override + State createState() => _CreateStoryScreenState(); +} + +class _CreateStoryScreenState extends State { + late BuildContext _context; + final _targetuserTextEditController = TextEditingController(); + final _customTextEditController = TextEditingController(); + final _hyperLinkEditController = TextEditingController(); + + List list = ['fit', 'fill']; + AmityStoryImageDisplayMode amityStoryImageDisplayMode = + AmityStoryImageDisplayMode.FIT; + + List files = []; + + final _postTextTextFieldKey = GlobalKey(); + + final mentionUsers = []; + + @override + void initState() { + if (widget.targetId != null) { + _targetuserTextEditController.text = widget.targetId!; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + final isCommunityPost = widget.targetId != null; + var targetLabel = ''; + if (isCommunityPost) { + targetLabel = 'Target community'; + } else { + targetLabel = 'Target user'; + } + return Scaffold( + appBar: AppBar( + title: + Text('Create ${widget.isVideoType ? "Video" : "Image"} Stroy')), + body: Builder(builder: (context) { + _context = context; + return Container( + margin: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _targetuserTextEditController, + enabled: !isCommunityPost, + decoration: InputDecoration( + label: Text(targetLabel), + ), + ), + const SizedBox(height: 20), + TextFormField( + key: _postTextTextFieldKey, + controller: _customTextEditController, + decoration: const InputDecoration( + label: Text('Custom text'), + ), + onChanged: (value) { + UserSuggesionOverlay.instance.hideOverLay(); + + if (widget.targetId == null || widget.targetId!.isEmpty) { + UserSuggesionOverlay.instance.updateOverLay( + context, + UserSuggestionType.global, + _postTextTextFieldKey, + value, (keyword, user) { + mentionUsers.add(user); + if (keyword.isNotEmpty) { + final length = _customTextEditController.text.length; + _customTextEditController.text = + _customTextEditController.text.replaceRange( + length - keyword.length, + length, + user.displayName ?? ''); + } else { + _customTextEditController.text = + (_customTextEditController.text + + user.displayName!); + } + + _customTextEditController.selection = + TextSelection.fromPosition(TextPosition( + offset: _customTextEditController.text.length)); + }, postion: UserSuggestionPostion.bottom); + } else { + UserSuggesionOverlay.instance.updateOverLay( + context, + UserSuggestionType.community, + _postTextTextFieldKey, + value, (keyword, user) { + mentionUsers.add(user); + + if (keyword.isNotEmpty) { + final length = _customTextEditController.text.length; + _customTextEditController.text = + _customTextEditController.text.replaceRange( + length - keyword.length, + length, + user.displayName ?? ''); + } else { + _customTextEditController.text = + (_customTextEditController.text + + user.displayName!); + } + + _customTextEditController.selection = + TextSelection.fromPosition(TextPosition( + offset: _customTextEditController.text.length)); + }, + communityId: widget.targetId, + postion: UserSuggestionPostion.bottom); + } + }, + ), + TextFormField( + controller: _hyperLinkEditController, + decoration: const InputDecoration( + label: Text('Hyperlink'), + ), + ), + const SizedBox(height: 20), + if (!widget.isVideoType) + Row( + children: [ + const Text('Image Display Mode'), + const SizedBox(width: 20), + Expanded( + child: DropdownButton( + value: amityStoryImageDisplayMode.value, + elevation: 16, + style: const TextStyle(color: Colors.blue), + underline: Container( + height: 2, + color: Colors.blue, + ), + onChanged: (String? value) { + // This is called when the user selects an item. + setState(() { + amityStoryImageDisplayMode = + AmityStoryImageDisplayModeExtension.enumOf( + value!); + }); + }, + items: + list.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + ], + ), + const SizedBox(height: 20), + Column( + children: List.generate(files.length, (index) { + final file = files[index]; + return TextButton.icon( + onPressed: () {}, + icon: Icon(!widget.isVideoType + ? Icons.image + : Icons.attach_file), + label: Text(basename(file.path)), + style: + TextButton.styleFrom(foregroundColor: Colors.blue)); + }), + ), + const SizedBox(height: 20), + const Spacer(), + const SizedBox(height: 20), + widget.isVideoType + ? TextButton.icon( + onPressed: () async { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Please Select source'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 200, + child: ElevatedButton( + onPressed: () async { + files.clear(); + final image = await ImagePicker() + .pickVideo( + source: ImageSource.gallery); + if (image != null) { + files.add(File(image.path)); + } + setState(() { + Navigator.of(context).pop(); + }); + }, + child: const Text('Gallery'), + ), + ), + SizedBox( + width: 200, + child: ElevatedButton( + onPressed: () async { + files.clear(); + final image = await ImagePicker() + .pickVideo( + source: ImageSource.camera); + if (image != null) { + files.add(File(image.path)); + } + setState(() { + Navigator.of(context).pop(); + }); + }, + child: const Text('Camera'), + ), + ), + SizedBox( + width: 200, + child: ElevatedButton( + onPressed: () async { + files.clear(); + files.clear(); + + FilePickerResult? result = + await FilePicker.platform.pickFiles( + allowMultiple: false,); + + if (result != null) { + files.addAll(result.paths + .map((path) => File(path!)) + .toList()); + } + setState(() { + Navigator.of(context).pop(); + }); + }, + child: const Text('File'), + ), + ), + ], + ), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')) + ], + ), + ); + }, + icon: const Icon(Icons.video_file), + label: const Text('Attach Video'), + style: TextButton.styleFrom(foregroundColor: Colors.blue)) + : TextButton.icon( + onPressed: () async { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Please Select source'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 200, + child: ElevatedButton( + onPressed: () async { + files.clear(); + final image = await ImagePicker() + .pickImage( + source: ImageSource.gallery); + if (image != null) { + files.add(File(image.path)); + } + setState(() { + Navigator.of(context).pop(); + }); + }, + child: const Text('Gallery'), + ), + ), + SizedBox( + width: 200, + child: ElevatedButton( + onPressed: () async { + files.clear(); + final image = await ImagePicker() + .pickImage( + source: ImageSource.camera); + if (image != null) { + files.add(File(image.path)); + } + setState(() { + Navigator.of(context).pop(); + }); + }, + child: const Text('Camera'), + ), + ), + ], + ), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')) + ], + ), + ); + }, + icon: const Icon(Icons.add_a_photo), + label: const Text('Attach Image'), + style: + TextButton.styleFrom(foregroundColor: Colors.blue)), + const SizedBox(height: 48), + Center( + child: TextButton( + onPressed: () async { + _createStory(context).then((value) { + // Navigator.of(context).pop(); + }).onError((error, stackTrace) { + print(error.toString()); + print(stackTrace.toString()); + Fluttertoast.showToast( + msg: error.toString(), + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0); + }); + Navigator.of(context).pop(); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.blue, + padding: const EdgeInsets.all(12), + ), + child: Container( + width: 200, + alignment: Alignment.center, + child: RichText( + text: TextSpan(children: [ + const TextSpan(text: 'Create'), + (widget.isVideoType) + ? const TextSpan(text: ' Video') + : const TextSpan(text: ' Image'), + const TextSpan(text: ' Story'), + ])), + ), + ), + ) + ], + ), + ); + }), + ); + } + + Future _createStory(BuildContext context) async { + FocusManager.instance.primaryFocus?.unfocus(); + if (files.isEmpty) { + throw Exception( + 'Please attach a ${widget.isVideoType ? "Video" : "Image"}'); + } + + AmityStoryItem? storyItem; + + if (_hyperLinkEditController.text.isNotEmpty) { + storyItem = HyperLink( + url: _hyperLinkEditController.text, + customText: _customTextEditController.text); + } + + if (storyItem == null) { + if (widget.isVideoType) { + return AmitySocialClient.newStoryRepository().createVideoStory( + targetType: widget.targetType!, + targetId: widget.targetId!, + videoFile: files[0], + storyItems: [], + ).onError((error, stackTrace) { + CommonSnackbar.showNagativeSnackbar( + context, 'Error', error.toString()); + }); + } else { + return AmitySocialClient.newStoryRepository().createImageStory( + targetType: widget.targetType!, + targetId: widget.targetId!, + imageFile: files[0], + imageDisplayMode: amityStoryImageDisplayMode, + storyItems: []); + } + } else { + if (widget.isVideoType) { + return AmitySocialClient.newStoryRepository().createVideoStory( + targetType: widget.targetType!, + targetId: widget.targetId!, + videoFile: files[0], + storyItems: [storyItem], + ).onError((error, stackTrace) { + CommonSnackbar.showNagativeSnackbar( + context, 'Error', error.toString()); + }); + } else { + return AmitySocialClient.newStoryRepository().createImageStory( + targetType: widget.targetType!, + targetId: widget.targetId!, + imageFile: files[0], + imageDisplayMode: amityStoryImageDisplayMode, + storyItems: [storyItem]); + } + } + } +} diff --git a/lib/presentation/screen/story_list/story_list_screen.dart b/lib/presentation/screen/story_list/story_list_screen.dart new file mode 100644 index 0000000..94f5304 --- /dev/null +++ b/lib/presentation/screen/story_list/story_list_screen.dart @@ -0,0 +1,162 @@ +import 'package:amity_sdk/amity_sdk.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_social_sample_app/core/widget/story_widget.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +class StoryListScreen extends StatefulWidget { + final AmityStoryTargetType targetType; + final String targetId; + final bool showAppBar; + final AmityCommunity amityCommunity; + const StoryListScreen( + {super.key, + required this.targetType, + required this.targetId, + this.showAppBar = true, + required this.amityCommunity}); + + @override + State createState() => _StoryListScreenState(); +} + +class _StoryListScreenState extends State { + List amityStories = []; + // late StoryLive + late StoryLiveCollection storyLiveCollection; + AmityStorySortingOrder _sortOption = AmityStorySortingOrder.LAST_CREATED; + // StoryLiveCollection + + @override + void initState() { + storyLiveCollectionInit(); + widget.amityCommunity + .subscription(AmityCommunityEvents.STORIES_AND_COMMENTS) + .subscribeTopic() + .then((value) {}) + .onError((error, stackTrace) {}); + + super.initState(); + } + + @override + void dispose() { + widget.amityCommunity + .subscription(AmityCommunityEvents.STORIES_AND_COMMENTS) + .unsubscribeTopic() + .then((value) {}) + .onError((error, stackTrace) {}); + super.dispose(); + } + + void storyLiveCollectionInit() { + storyLiveCollection = StoryLiveCollection( + request: ()=> AmitySocialClient.newStoryRepository() + .getActiveStories( + targetId: widget.targetId, + targetType: widget.targetType, + orderBy: _sortOption) + .build()); + + storyLiveCollection.getStreamController().stream.listen((event) { + if (mounted) { + setState(() { + amityStories = event; + }); + } + }); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + storyLiveCollection.getData(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: widget.showAppBar + ? AppBar( + title: Text('${widget.targetType.value} - ${widget.targetId}')) + : null, + body: Column( + children: [ + Text("Size of posts: ${amityStories.length}"), + Container( + padding: const EdgeInsets.all(8), + child: PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + value: 2, + child: Text(AmityStorySortingOrder.FIRST_CREATED.name), + ), + PopupMenuItem( + value: 3, + child: Text(AmityStorySortingOrder.LAST_CREATED.name), + ) + ]; + }, + child: const Icon( + Icons.sort_rounded, + size: 18, + ), + onSelected: (index) { + if (index == 2) { + _sortOption = AmityStorySortingOrder.FIRST_CREATED; + } + if (index == 3) { + _sortOption = AmityStorySortingOrder.LAST_CREATED; + } + + storyLiveCollection.reset(); + storyLiveCollectionInit(); + }, + ), + ), + Expanded( + child: amityStories.isNotEmpty + ? RefreshIndicator( + onRefresh: () async { + storyLiveCollection.reset(); + storyLiveCollectionInit(); + }, + child: ListView.builder( + itemCount: amityStories.length, + itemBuilder: (context, index) { + final amityStory = amityStories[index]; + print("amityStory: ${amityStory.createdAt?.hour}:${amityStory.createdAt?.minute}:${amityStory.createdAt?.second}"); + var uniqueKey = UniqueKey(); + return VisibilityDetector( + key: uniqueKey, + onVisibilityChanged: (VisibilityInfo info) { + if (info.visibleFraction > 0) { + // amityPost.analytics().markPostAsViewed(); + } + }, + child: StoryWidget( + key: uniqueKey, + story: amityStory, + disableAction: false , + targetType: widget.targetType, + targetId: widget.targetId, + ), + ); + }, + ), + ) + : Container( + alignment: Alignment.center, + child: storyLiveCollection.isFetching + ? const CircularProgressIndicator() + : const Text('No Post'), + ), + ), + if (storyLiveCollection.isFetching && amityStories.isNotEmpty) + Container( + alignment: Alignment.center, + child: const CircularProgressIndicator(), + ) + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 5b0fea9..fadf8b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_social_sample_app description: Demonstrates how to use the flutter_application_1 plugin. -version: 1.1.18+33 +version: 1.1.24+39 environment: sdk: ">=3.0.0 <4.0.0" @@ -16,7 +16,7 @@ dependencies: path: ../Amity-Social-Cloud-SDK-Flutter-Internal # amity_video_player: # path: ../amity_social_cloud_flutter_video_player - amity_video_player: ^0.0.1 + amity_video_player: ^0.0.2 cached_network_image: ^3.2.0 chewie: ^1.3.1 cupertino_icons: ^1.0.2 @@ -34,7 +34,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.15 permission_handler: ^10.3.0 - + fluttertoast: ^8.2.4 universal_html: ^2.2.3 url_launcher: ^6.2.3 uuid: ^4.3.3