//@dart=2.12 import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:matrix_link_text/link_text.dart'; import 'package:video_player/video_player.dart'; import 'package:fluffychat/pages/story/story_page.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; class StoryView extends StatelessWidget { final StoryPageController controller; const StoryView(this.controller, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { final currentEvent = controller.currentEvent; return Scaffold( backgroundColor: Colors.blueGrey.shade900, appBar: AppBar( titleSpacing: 0, leading: IconButton( icon: const Icon(Icons.close), onPressed: Navigator.of(context).pop, ), title: AnimatedOpacity( duration: const Duration(seconds: 1), opacity: controller.isHold ? 0 : 1, child: ListTile( contentPadding: EdgeInsets.zero, title: Text( controller.title, style: const TextStyle( color: Colors.white, shadows: [ Shadow( color: Colors.black, offset: Offset(0, 0), blurRadius: 5, ), ], ), ), subtitle: currentEvent != null ? Text( currentEvent.originServerTs.localizedTime(context), style: const TextStyle( color: Colors.white70, shadows: [ Shadow( color: Colors.black, offset: Offset(0, 0), blurRadius: 5, ), ], ), ) : null, leading: Avatar( mxContent: controller.avatar, name: controller.title, ), ), ), actions: currentEvent == null ? null : [ if (!controller.isOwnStory) AnimatedOpacity( duration: const Duration(seconds: 1), opacity: controller.isHold ? 0 : 1, child: IconButton( icon: Icon(Icons.adaptive.share_outlined), onPressed: controller.share, ), ), AnimatedOpacity( duration: const Duration(seconds: 1), opacity: controller.isHold ? 0 : 1, child: PopupMenuButton( onSelected: controller.onPopupStoryAction, itemBuilder: (context) => [ if (controller.currentEvent?.canRedact ?? false) PopupMenuItem( value: PopupStoryAction.delete, child: Text(L10n.of(context)!.delete), ), PopupMenuItem( value: PopupStoryAction.report, child: Text(L10n.of(context)!.reportMessage), ), ], )), ], systemOverlayStyle: SystemUiOverlayStyle.light, iconTheme: const IconThemeData(color: Colors.white), elevation: 0, backgroundColor: Colors.transparent, ), extendBodyBehindAppBar: true, body: FutureBuilder( future: controller.loadStory, builder: (context, snapshot) { final error = snapshot.error; if (error != null) { return Center(child: Text(error.toLocalizedString(context))); } final events = controller.events; if (snapshot.connectionState != ConnectionState.done) { return const Center( child: CircularProgressIndicator.adaptive( strokeWidth: 2, )); } if (events.isEmpty) { return Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Avatar( mxContent: controller.avatar, name: controller.title, size: 128, fontSize: 64, ), const SizedBox(height: 32), Text( L10n.of(context)!.thisUserHasNotPostedAnythingYet, textAlign: TextAlign.center, style: const TextStyle( fontSize: 20, color: Colors.white, ), ), ], ), ); } final event = events[controller.index]; final backgroundColor = event.content.tryGet('body')?.color ?? Theme.of(context).primaryColor; final backgroundColorDark = event.content.tryGet('body')?.darkColor ?? Theme.of(context).primaryColorDark; if (event.messageType == MessageTypes.Text) { controller.loadingModeOff(); } return GestureDetector( onTapDown: controller.hold, onTapUp: controller.unhold, child: Stack( children: [ if (event.messageType == MessageTypes.Video && PlatformInfos.isMobile) FutureBuilder( future: controller.loadVideoControllerFuture ??= controller.loadVideoController(event), builder: (context, snapshot) { final videoPlayerController = snapshot.data; if (videoPlayerController == null) { controller.loadingModeOn(); return Container(); } controller.loadingModeOff(); return Center(child: VideoPlayer(videoPlayerController)); }, ), if (event.messageType == MessageTypes.Image || (event.messageType == MessageTypes.Video && !PlatformInfos.isMobile)) Positioned( top: 0, bottom: 0, left: 0, right: 0, child: FutureBuilder( future: controller.downloadAndDecryptAttachment( event, event.messageType == MessageTypes.Video), builder: (context, snapshot) { final matrixFile = snapshot.data; if (matrixFile == null) { controller.loadingModeOn(); final hash = event.infoMap['xyz.amorgan.blurhash']; return hash is String ? BlurHash( hash: hash, imageFit: BoxFit.cover, ) : Container(); } controller.loadingModeOff(); return Center( child: Image.memory( matrixFile.bytes, fit: BoxFit.contain, ), ); }, ), ), AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( gradient: event.messageType == MessageTypes.Text ? LinearGradient( colors: [ backgroundColorDark, backgroundColor, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ) : null, ), alignment: Alignment.center, child: ListView( shrinkWrap: true, children: [ LinkText( text: controller.loadingMode ? L10n.of(context)!.loadingPleaseWait : event.content.tryGet('body') ?? '', textAlign: TextAlign.center, onLinkTap: (url) => UrlLauncher(context, url).launchUrl(), linkStyle: TextStyle( fontSize: 24, color: Colors.blue.shade50, decoration: TextDecoration.underline, backgroundColor: event.messageType == MessageTypes.Text ? null : Colors.black, ), textStyle: TextStyle( fontSize: 24, color: Colors.white, backgroundColor: event.messageType == MessageTypes.Text ? null : Colors.black, ), ), ], ), ), Positioned( top: 4, left: 4, right: 4, child: AnimatedOpacity( duration: const Duration(seconds: 1), opacity: controller.isHold ? 0 : 1, child: SafeArea( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (var i = 0; i < events.length; i++) Expanded( child: i == controller.index ? LinearProgressIndicator( color: Colors.white, minHeight: 2, backgroundColor: Colors.grey.shade600, value: controller.loadingMode ? null : controller.progress.inMilliseconds / StoryPageController .maxProgress.inMilliseconds, ) : Container( margin: const EdgeInsets.all(4), height: 2, color: i < controller.index ? Colors.white : Colors.grey.shade600, ), ), ], ), ), ), ), if (!controller.isOwnStory && currentEvent != null) Positioned( bottom: 16, left: 16, right: 16, child: AnimatedOpacity( duration: const Duration(seconds: 1), opacity: controller.isHold ? 0 : 1, child: SafeArea( child: TextField( focusNode: controller.replyFocus, controller: controller.replyController, minLines: 1, maxLines: 7, onSubmitted: controller.replyAction, textInputAction: TextInputAction.newline, readOnly: controller.replyLoading, decoration: InputDecoration( hintText: L10n.of(context)!.reply, prefixIcon: IconButton( onPressed: controller.replyEmojiAction, icon: const Icon(Icons.emoji_emotions_outlined), ), suffixIcon: controller.replyLoading ? const CircularProgressIndicator.adaptive( strokeWidth: 2) : IconButton( onPressed: controller.replyAction, icon: const Icon(Icons.send_outlined), ), ), ), ), ), ), if (controller.isOwnStory && controller.currentSeenByUsers.isNotEmpty) Positioned( bottom: 16, left: 16, right: 16, child: AnimatedOpacity( duration: const Duration(seconds: 1), opacity: controller.isHold ? 0 : 1, child: SafeArea( child: Center( child: OutlinedButton.icon( style: OutlinedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surface, ), onPressed: controller.displaySeenByUsers, icon: const Icon(Icons.visibility_outlined), label: Text(controller.seenByUsersTitle), ), ), ), ), ), ], ), ); }, ), ); } }