diff --git a/lib/models/ai/bots_model.dart b/lib/models/ai/bots_model.dart index fa81daf..29e5836 100644 --- a/lib/models/ai/bots_model.dart +++ b/lib/models/ai/bots_model.dart @@ -2,6 +2,7 @@ class BotsModel { int? id; String? name; String? image; + String? responseType; String? description; List? attachmentType; int? attachment; @@ -22,6 +23,7 @@ class BotsModel { } attachment = json['attachment']; editable = json['editable']; + responseType = json['responseType']; } Map toJson() { @@ -35,6 +37,7 @@ class BotsModel { } data['attachment'] = attachment; data['editable'] = editable; + data['responseType'] = responseType; return data; } } diff --git a/lib/models/ai/files_model.dart b/lib/models/ai/files_model.dart index ecc13b9..ad028fb 100644 --- a/lib/models/ai/files_model.dart +++ b/lib/models/ai/files_model.dart @@ -12,6 +12,7 @@ class FilesModel { final bool isRecorded; final bool? audio; final bool? image; + final bool? video; final bool? network; final Uint8List? bytes; final Duration? duration; @@ -22,6 +23,7 @@ class FilesModel { this.isRecorded = false, this.audio, this.image, + this.video, this.network, this.bytes, this.duration, @@ -39,7 +41,7 @@ class FilesModel { bool isAudio() { return audio ?? (lookupMimeType(path)?.startsWith('audio/') ?? false) || - (lookupMimeType(path)?.startsWith('video/') ?? false); + path.contains(".mp3"); } bool isImage() { @@ -48,6 +50,12 @@ class FilesModel { false || path.contains(".png"); } + bool isVideo() { + return video ?? + lookupMimeType(path)?.startsWith('video/') ?? + false || path.contains(".mp4"); + } + bool isNetwork() { return network ?? path.startsWith('blob:') || path.startsWith('/uploads'); } diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index d5012d7..7e1e27c 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -1,22 +1,93 @@ import 'package:flutter/cupertino.dart'; extension NavigatorStateExtension on NavigatorState { - - void pushNamedIfNotCurrent( String routeName, {Object? arguments} ) { + void pushNamedIfNotCurrent(String routeName, {Object? arguments}) { if (!isCurrent(routeName)) { - pushNamed( routeName, arguments: arguments ); + pushNamed(routeName, arguments: arguments); } } - bool isCurrent( String routeName ) { + bool isCurrent(String routeName) { bool isCurrent = false; - popUntil( (route) { + popUntil((route) { if (route.settings.name == routeName) { isCurrent = true; } return true; - } ); + }); return isCurrent; } +} -} \ No newline at end of file +extension StringUrl on String { + bool startsWithEnglish() { + // Regular expression to check if the first character is an English letter + return RegExp(r'^[A-Za-z]').hasMatch(this); + } + + bool startsWithPersian() { + // Regular expression to check if the first character is a Persian letter + return RegExp(r'^[\u0600-\u06FF]').hasMatch(this); + } + + bool isImage() { + final extension = split('.').last.toLowerCase(); + const imageExtensions = [ + 'jpg', + 'jpeg', + 'png', + 'gif', + 'bmp', + 'webp', + 'tiff' + ]; + return imageExtensions.contains(extension); + } + + bool isDocument() { + final extension = split('.').last.toLowerCase(); + const documentExtensions = [ + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'txt' + ]; + return documentExtensions.contains(extension); + } + + bool isAudio() { + final extension = split('.').last.toLowerCase(); + const audioExtensions = ['mp3', 'wav', 'aac', 'ogg', 'flac']; + return audioExtensions.contains(extension); + } + + bool isVideo() { + final extension = split('.').last.toLowerCase(); + const videoExtensions = ['mp4', 'avi', 'mov', 'wmv', 'mkv', 'flv']; + return videoExtensions.contains(extension); + } + + String convertToEnglishNumber() { + final Map persianToEnglishMap = { + '۰': '0', + '۱': '1', + '۲': '2', + '۳': '3', + '۴': '4', + '۵': '5', + '۶': '6', + '۷': '7', + '۸': '8', + '۹': '9', + }; + + return split('').map((char) { + return persianToEnglishMap[char] ?? + char; // Replace with English number if exists, else keep original char + }).join(''); + } +} diff --git a/lib/views/ai/ai_chat_page.dart b/lib/views/ai/ai_chat_page.dart index 3ca8c41..e96c618 100644 --- a/lib/views/ai/ai_chat_page.dart +++ b/lib/views/ai/ai_chat_page.dart @@ -16,7 +16,6 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/models/view/alert_data.dart'; import 'package:didvan/routes/routes.dart'; -import 'package:didvan/services/app_initalizer.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -31,6 +30,7 @@ import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/marquee_text.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:didvan/views/widgets/video/chat_video_player.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -421,7 +421,7 @@ class _AiChatPageState extends State { FilesModel? file = message.fileLocal ?? (message.file == null ? null - : FilesModel(message.file.toString(), + : FilesModel(message.file.toString().replaceAll(' ', ''), duration: message.duration != null ? Duration(seconds: message.duration!) : null)); @@ -540,18 +540,30 @@ class _AiChatPageState extends State { totalDuration: file.duration, ), ) - : file.isImage() + : file.isVideo() ? Padding( - padding: const EdgeInsets.all(8.0), - child: messageImage(file), + padding: const EdgeInsets.fromLTRB( + 16, 16, 16, 0), + child: ClipRRect( + borderRadius: + DesignConfig.lowBorderRadius, + child: ChatVideoPlayer( + src: file.path, + )), ) - : Padding( - padding: const EdgeInsets.all( - 8.0, - ), - child: messageFile( - context, message, state), - ), + : file.isImage() + ? Padding( + padding: + const EdgeInsets.all(8.0), + child: messageImage(file), + ) + : Padding( + padding: const EdgeInsets.all( + 8.0, + ), + child: messageFile( + context, message, state), + ), if (message.text != null && message.text!.isNotEmpty && ((message.audio == null || diff --git a/lib/views/ai/ai_chat_state.dart b/lib/views/ai/ai_chat_state.dart index 3d27dbf..a240af9 100644 --- a/lib/views/ai/ai_chat_state.dart +++ b/lib/views/ai/ai_chat_state.dart @@ -179,7 +179,7 @@ class AiChatState extends CoreProvier { var str = utf8.decode(value); if (!kIsWeb) { - if (bot.id == 12) { + if (bot.responseType != 'text') { responseMessgae += str.split('{{{').first; } if (str.contains('{{{')) { @@ -202,7 +202,7 @@ class AiChatState extends CoreProvier { } catch (e) { e.printError(); } - if (bot.id == 12) { + if (bot.responseType != 'text') { responseMessgae = "${responseMessgae.split('.png').first}.png"; return; } @@ -243,8 +243,8 @@ class AiChatState extends CoreProvier { } messages.last.prompts.last = messages.last.prompts.last.copyWith( finished: true, - text: bot.id == 12 ? null : responseMessgae, - file: bot.id == 12 ? responseMessgae : null, + text: bot.responseType != 'text' ? null : responseMessgae, + file: bot.responseType != 'text' ? responseMessgae : null, id: aiMessageId); if (messages.last.prompts.length > 2) { messages.last.prompts[messages.last.prompts.length - 2] = messages diff --git a/lib/views/ai/widgets/ai_message_bar.dart b/lib/views/ai/widgets/ai_message_bar.dart index 663b2ce..856eb44 100644 --- a/lib/views/ai/widgets/ai_message_bar.dart +++ b/lib/views/ai/widgets/ai_message_bar.dart @@ -376,17 +376,19 @@ class _AiMessageBarState extends State { Expanded( child: state.file != null && state.file!.isRecorded ? audioContainer() - : Form( - child: TextFormField( + : TextFormField( textInputAction: TextInputAction.newline, style: Theme.of(context).textTheme.bodyMedium, minLines: 1, maxLines: 6, // Set this keyboardType: TextInputType.multiline, controller: state.message, + enabled: !(state.file != null && widget.bot.attachment == 1), decoration: InputDecoration( + contentPadding: + const EdgeInsets.fromLTRB(12, 12, 12, 0), border: InputBorder.none, hintText: 'بنویسید...', hintStyle: Theme.of(context) @@ -412,7 +414,7 @@ class _AiMessageBarState extends State { state.message.text = value; state.update(); }, - )), + ), ), Padding( padding: const EdgeInsets.only(bottom: 8.0), diff --git a/lib/views/widgets/video/chat_video_player.dart b/lib/views/widgets/video/chat_video_player.dart new file mode 100644 index 0000000..46856d9 --- /dev/null +++ b/lib/views/widgets/video/chat_video_player.dart @@ -0,0 +1,86 @@ +// ignore_for_file: deprecated_member_use + +import 'package:chewie/chewie.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:didvan/views/widgets/video/custome_controls.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:video_player/video_player.dart'; + +class ChatVideoPlayer extends StatefulWidget { + final String src; + const ChatVideoPlayer({Key? key, required this.src}) : super(key: key); + + @override + State createState() => _ChatVideoPlayerState(); +} + +class _ChatVideoPlayerState extends State { + late VideoPlayerController _videoPlayerController; + ChewieController? _chewieController; + + @override + void initState() { + super.initState(); + _handleVideoPlayback(); + } + + Future _handleVideoPlayback() async { + _videoPlayerController = VideoPlayerController.network( + RequestHelper.baseUrl + widget.src, + httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'}); + + await _videoPlayerController.initialize().then((_) { + setState(() { + _chewieController = ChewieController( + customControls: const CustomControls(), + videoPlayerController: _videoPlayerController, + autoPlay: false, + looping: true, + showOptions: false, + allowPlaybackSpeedChanging: false, + placeholder: const CircularProgressIndicator(), + aspectRatio: 16 / 9, + materialProgressColors: ChewieProgressColors( + playedColor: Theme.of(context).colorScheme.title, + handleColor: Theme.of(context).colorScheme.title), + ); + }); + }).catchError((e) { + setState(() {}); + }); + } + + @override + void dispose() { + _videoPlayerController.pause(); + _videoPlayerController.dispose(); + _chewieController?.dispose(); // Dispose of the ChewieController + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _chewieController == null + ? SizedBox( + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SpinKitThreeBounce( + color: Theme.of(context).colorScheme.primary, + size: 18, + ), + ), + ), + ) + : AspectRatio( + aspectRatio: _videoPlayerController.value.aspectRatio, + child: Chewie( + controller: _chewieController!, + ), + ); + } +} diff --git a/lib/views/widgets/video/custome_controls.dart b/lib/views/widgets/video/custome_controls.dart new file mode 100644 index 0000000..d756dfe --- /dev/null +++ b/lib/views/widgets/video/custome_controls.dart @@ -0,0 +1,197 @@ +import 'dart:async'; + +import 'package:chewie/chewie.dart'; +import 'package:didvan/views/widgets/video/play_btn_animation.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class CustomControls extends StatefulWidget { + const CustomControls({super.key}); + + @override + State createState() => _CustomControlsState(); +} + +class _CustomControlsState extends State { + late ChewieController chewieController; + bool isAnimating = false; + double opacity = 1; + Timer? _hideControlsTimer; + ValueNotifier position = ValueNotifier(Duration.zero); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + chewieController = ChewieController.of(context); + chewieController.videoPlayerController.addListener( + () { + position.value = chewieController.videoPlayerController.value.position; + }, + ); + } + + void _startHideControlsTimer() { + _hideControlsTimer?.cancel(); + _hideControlsTimer = Timer(const Duration(seconds: 3), () { + setState(() { + opacity = 0; + }); + }); + } + + @override + void dispose() { + _hideControlsTimer?.cancel(); // Clean up the timer + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 400), + opacity: opacity, + child: InkWell( + onTap: () { + setState(() { + opacity = 1; + }); + _startHideControlsTimer(); // Restart the timer on tap + }, + child: Stack( + children: [ + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.only(bottom: 12), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black, + Colors.black26, + Color.fromARGB(10, 0, 0, 0) + ])), + child: Row( + children: [ + // Positioned.fill( + // child: Container( + // decoration: BoxDecoration( + // color: !chewieController.isPlaying + // ? Colors.black.withOpacity(0.4) + // : Colors.transparent), + // )), + _buildPlayPause(), + + _buildProgressIndicator(), + _buildFullScreenToggle(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildPlayPause() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: InkWell( + onTap: () { + setState(() { + if (chewieController.isPlaying) { + chewieController.pause(); + opacity = 1; + } else { + chewieController.play(); + opacity = 0; + } + + isAnimating = true; + }); + _startHideControlsTimer(); // Restart the timer on tap + }, + child: PlayBtnAnimation( + alwaysAnimate: true, + isAnimating: isAnimating, + onEnd: () => setState( + () => isAnimating = false, + ), + child: Icon( + chewieController.isPlaying + ? CupertinoIcons.pause_fill + : CupertinoIcons.play_fill, + color: Colors.white, + size: 24, + ), + ), + ), + ); + } + + Widget _buildProgressIndicator() { + return Expanded( + child: ValueListenableBuilder( + valueListenable: position, + builder: (context, p, _) { + Duration duration = + chewieController.videoPlayerController.value.duration; + + return SliderTheme( + data: SliderThemeData( + trackHeight: 2, + // thumbColor: Colors.transparent, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + // elevation: 0, + // pressedElevation: 0, + enabledThumbRadius: 8)), + child: Slider( + min: 0, + max: duration.inMilliseconds.toDouble(), + value: p.inMilliseconds.toDouble(), + onChanged: (value) async { + await chewieController.pause(); + position.value = Duration(milliseconds: value.round()); + _startHideControlsTimer(); + }, + onChangeEnd: (value) async { + await chewieController + .seekTo(Duration(milliseconds: value.round())); + await chewieController.play(); + + setState(() {}); + }, + ), + ); + }, + ), + ); + } + + Widget _buildFullScreenToggle() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: InkWell( + onTap: () => setState(() { + chewieController.toggleFullScreen(); + _startHideControlsTimer(); // Restart the timer on tap + }), + child: Icon( + chewieController.isFullScreen + ? Icons.fullscreen_exit + : Icons.fullscreen, + color: Colors.white, + size: 30, + ), + ), + ); + } +} diff --git a/lib/views/widgets/video/play_btn_animation.dart b/lib/views/widgets/video/play_btn_animation.dart new file mode 100644 index 0000000..505dd96 --- /dev/null +++ b/lib/views/widgets/video/play_btn_animation.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +class PlayBtnAnimation extends StatefulWidget { + final Widget child; + final bool isAnimating; + final bool alwaysAnimate; + final Duration duration; + final Function()? onEnd; + const PlayBtnAnimation( + {Key? key, + required this.child, + required this.isAnimating, + this.duration = const Duration(milliseconds: 150), + this.onEnd, + this.alwaysAnimate = false}) + : super(key: key); + + @override + State createState() => _PlayBtnAnimationState(); +} + +class _PlayBtnAnimationState extends State + with SingleTickerProviderStateMixin { + late AnimationController controller; + late Animation scale; + + @override + void initState() { + super.initState(); + + final halfDuration = widget.duration.inMilliseconds ~/ 2; + controller = AnimationController( + vsync: this, duration: Duration(milliseconds: halfDuration)); + + scale = Tween(begin: 1, end: 1.2).animate(controller); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant PlayBtnAnimation oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.isAnimating != oldWidget.isAnimating) { + doAnimation(); + } + } + + Future doAnimation() async { + if (widget.isAnimating) { + await controller.forward(); + await controller.reverse(); + await Future.delayed(const Duration(milliseconds: 400)); + widget.onEnd?.call(); + } + } + + @override + Widget build(BuildContext context) { + return ScaleTransition(scale: scale, child: widget.child); + } +} diff --git a/pubspec.lock b/pubspec.lock index f8137b8..5f35698 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1374,10 +1374,10 @@ packages: dependency: "direct main" description: name: video_player - sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d + sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17" url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.9.2" video_player_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 37803a9..981d023 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -84,7 +84,7 @@ dependencies: get: ^4.6.6 # firebase_auth: ^4.19.6 just_audio: ^0.9.11 - video_player: ^2.8.7 + video_player: ^2.9.2 chewie: ^1.8.3 typewritertext: ^3.0.8 flutter_markdown: ^0.7.3+1