From 1c62f956c70a8e1cfd342636ae36750ee553bdad Mon Sep 17 00:00:00 2001 From: MohammadTaha Basiri Date: Sat, 19 Feb 2022 13:42:48 +0330 Subject: [PATCH] D1APP-101 chat waveform changed to slider --- lib/models/chat_room/chat_room.dart | 4 +- lib/models/message_data/message_data.dart | 19 - lib/pages/home/direct/direct.dart | 22 +- lib/pages/home/direct/direct_state.dart | 18 +- .../home/direct/widgets/audio_widget.dart | 36 ++ lib/pages/home/direct/widgets/message.dart | 36 +- .../home/direct/widgets/message_box.dart | 81 ++- .../home/news/news_details/news_details.dart | 5 +- .../radar/radar_details/radar_details.dart | 6 +- .../settings/direct_list/direct_list.dart | 2 +- .../{chat_room_item.dart => direct_item.dart} | 21 +- lib/pages/home/widgets/audio_slider.dart | 39 ++ lib/pages/home/widgets/audio_visualizer.dart | 590 +++++++++--------- .../home/widgets/floating_navigation_bar.dart | 44 +- .../widgets/player_controller_button.dart | 34 + lib/providers/server_data_provider.dart | 5 +- lib/services/app_initalizer.dart | 2 + lib/services/media/media.dart | 30 +- pubspec.lock | 14 +- pubspec.yaml | 2 +- 20 files changed, 590 insertions(+), 420 deletions(-) create mode 100644 lib/pages/home/direct/widgets/audio_widget.dart rename lib/pages/home/settings/direct_list/widgets/{chat_room_item.dart => direct_item.dart} (84%) create mode 100644 lib/pages/home/widgets/audio_slider.dart create mode 100644 lib/pages/home/widgets/player_controller_button.dart diff --git a/lib/models/chat_room/chat_room.dart b/lib/models/chat_room/chat_room.dart index a741cc7..99457eb 100644 --- a/lib/models/chat_room/chat_room.dart +++ b/lib/models/chat_room/chat_room.dart @@ -4,10 +4,10 @@ class ChatRoom { final int id; final String type; final String updatedAt; - final int unread; + int unread; final LastMessage lastMessage; - const ChatRoom({ + ChatRoom({ required this.id, required this.type, required this.updatedAt, diff --git a/lib/models/message_data/message_data.dart b/lib/models/message_data/message_data.dart index 6324a34..1778abc 100644 --- a/lib/models/message_data/message_data.dart +++ b/lib/models/message_data/message_data.dart @@ -1,8 +1,5 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; - -import 'package:just_waveform/just_waveform.dart'; import 'radar_attachment.dart'; @@ -15,7 +12,6 @@ class MessageData { final String createdAt; final RadarAttachment? radar; final File? audioFile; - final Waveform? waveform; final int? audioDuration; const MessageData({ @@ -26,7 +22,6 @@ class MessageData { this.text, this.audio, this.radar, - this.waveform, this.audioFile, this.audioDuration, }); @@ -41,20 +36,6 @@ class MessageData { audioDuration: json['waveform'] == null ? null : jsonDecode(json['waveform'])['duration'] ?? 0, - waveform: json['waveform'] != null - ? Waveform( - version: 1, - flags: 0, - sampleRate: 44100, - samplesPerPixel: 441, - length: jsonDecode(json['waveform'])['length'], - data: Int16List.fromList( - List.from( - jsonDecode(json['waveform'])['data'], - ), - ), - ) - : null, radar: json['radar'] == null ? null : RadarAttachment.fromJson(json['radar']), diff --git a/lib/pages/home/direct/direct.dart b/lib/pages/home/direct/direct.dart index 2478f71..2cf46aa 100644 --- a/lib/pages/home/direct/direct.dart +++ b/lib/pages/home/direct/direct.dart @@ -22,6 +22,7 @@ class _DirectState extends State { @override void initState() { final state = context.read(); + state.replyRadar = widget.pageData['radarAttachment']; final typeId = ServerDataProvider.labelToTypeId(widget.pageData['type']); state.typeId = typeId; Future.delayed(Duration.zero, () { @@ -48,18 +49,23 @@ class _DirectState extends State { appBarData: AppBarData( hasBack: true, subtitle: 'ارتباط با سردبیر', - title: widget.pageData['type'].substring(7), + title: widget.pageData['type'] ?? 'پشتیبانی اپلیکیشن', ), slivers: [ if (state.appState != AppState.busy) - SliverStateHandler( - itemPadding: const EdgeInsets.only(bottom: 12), - state: state, - builder: (context, state, index) => Message( - message: state.messages[index], + SliverPadding( + padding: state.replyRadar == null + ? EdgeInsets.zero + : const EdgeInsets.only(bottom: 68), + sliver: SliverStateHandler( + itemPadding: const EdgeInsets.only(bottom: 12), + state: state, + builder: (context, state, index) => Message( + message: state.messages[index], + ), + childCount: state.messages.length, + onRetry: state.getMessages, ), - childCount: state.messages.length, - onRetry: state.getMessages, ), ], children: [ diff --git a/lib/pages/home/direct/direct_state.dart b/lib/pages/home/direct/direct_state.dart index ec9e16d..86cf3a9 100644 --- a/lib/pages/home/direct/direct_state.dart +++ b/lib/pages/home/direct/direct_state.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'package:didvan/models/enums.dart'; @@ -9,7 +8,6 @@ import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; -import 'package:just_waveform/just_waveform.dart'; import 'package:record/record.dart'; class DirectState extends CoreProvier { @@ -21,7 +19,7 @@ class DirectState extends CoreProvier { String? text; RadarAttachment? replyRadar; File? recordedFile; - Waveform? waveform; + int? audioDuration; bool isRecording = false; @@ -90,9 +88,7 @@ class DirectState extends CoreProvier { Future sendMessage() async { if ((text == null || text!.isEmpty) && recordedFile == null) return; - if (recordedFile != null) { - while (waveform == null) {} - } + replyRadar = null; messages.insert( 0, MessageData( @@ -105,8 +101,7 @@ class DirectState extends CoreProvier { audio: null, audioFile: recordedFile, radar: replyRadar, - waveform: waveform, - audioDuration: waveform != null ? waveform!.duration.inSeconds : null, + audioDuration: audioDuration, ), ); _addToDailyGrouped(); @@ -126,13 +121,6 @@ class DirectState extends CoreProvier { if (uploadFile == null) { service.post(); } else { - body.addAll({ - 'waveform': jsonEncode({ - 'data': waveform!.data, - 'length': waveform!.length, - 'duration': waveform!.duration, - }) - }); service.multipart( file: uploadFile, method: 'POST', diff --git a/lib/pages/home/direct/widgets/audio_widget.dart b/lib/pages/home/direct/widgets/audio_widget.dart new file mode 100644 index 0000000..588f293 --- /dev/null +++ b/lib/pages/home/direct/widgets/audio_widget.dart @@ -0,0 +1,36 @@ +import 'dart:io'; + +import 'package:didvan/models/message_data/message_data.dart'; +import 'package:didvan/pages/home/widgets/audio_slider.dart'; +import 'package:didvan/pages/home/widgets/player_controller_button.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:flutter/material.dart'; + +class AudioWidget extends StatelessWidget { + final String? audioUrl; + final File? audioFile; + const AudioWidget({Key? key, this.audioUrl, this.audioFile}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: MediaService.audioPlayer.playingStream, + builder: (context, snapshot) { + return Row( + children: [ + Expanded( + child: AudioSlider( + tag: audioUrl ?? audioFile!.path, + ), + ), + AudioControllerButton( + audioFile: audioFile, + audioUrl: audioUrl, + ), + ], + ); + }, + ); + } +} diff --git a/lib/pages/home/direct/widgets/message.dart b/lib/pages/home/direct/widgets/message.dart index 4d75af1..3225052 100644 --- a/lib/pages/home/direct/widgets/message.dart +++ b/lib/pages/home/direct/widgets/message.dart @@ -1,8 +1,12 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/message_data/message_data.dart'; import 'package:didvan/pages/home/direct/direct_state.dart'; -import 'package:didvan/pages/home/widgets/audio_visualizer.dart'; +import 'package:didvan/pages/home/direct/widgets/audio_widget.dart'; +import 'package:didvan/pages/home/widgets/audio_slider.dart'; +import 'package:didvan/pages/home/widgets/player_controller_button.dart'; +import 'package:didvan/services/media/media.dart'; import 'package:didvan/utils/date_time.dart'; import 'package:didvan/widgets/didvan/divider.dart'; import 'package:didvan/widgets/didvan/text.dart'; @@ -60,13 +64,9 @@ class Message extends StatelessWidget { children: [ if (message.text != null) DidvanText(message.text!), if (message.audio != null || message.audioFile != null) - SizedBox( - height: 50, - child: AudioVisualizer( - audioUrl: message.audio, - waveform: message.waveform, - backgroundColor: Colors.transparent, - ), + AudioWidget( + audioFile: message.audioFile, + audioUrl: message.audio, ), if (message.radar != null) const DidvanDivider(), if (message.radar != null) const SizedBox(height: 4), @@ -77,10 +77,22 @@ class Message extends StatelessWidget { ), ), const SizedBox(height: 4), - DidvanText( - DateTimeUtils.timeWithAmPm(message.createdAt), - style: Theme.of(context).textTheme.overline, - color: Theme.of(context).colorScheme.caption, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + DidvanText( + DateTimeUtils.timeWithAmPm(message.createdAt), + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + if (!message.writedByAdmin) + Icon( + message.readed + ? DidvanIcons.check_double_light + : DidvanIcons.check_light, + size: 16, + ) + ], ), ], ), diff --git a/lib/pages/home/direct/widgets/message_box.dart b/lib/pages/home/direct/widgets/message_box.dart index 8583234..582446c 100644 --- a/lib/pages/home/direct/widgets/message_box.dart +++ b/lib/pages/home/direct/widgets/message_box.dart @@ -2,7 +2,7 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/pages/home/direct/direct_state.dart'; -import 'package:didvan/pages/home/widgets/audio_visualizer.dart'; +import 'package:didvan/pages/home/direct/widgets/audio_widget.dart'; import 'package:didvan/widgets/didvan/icon_button.dart'; import 'package:didvan/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; @@ -11,10 +11,72 @@ import 'package:provider/provider.dart'; class MessageBox extends StatelessWidget { const MessageBox({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + return Column( + children: [ + Consumer( + builder: (context, state, child) => state.replyRadar != null + ? _MessageBoxContainer( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DidvanText( + 'لینک به مطلب:', + ), + DidvanText( + state.replyRadar!.title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + color: Theme.of(context).colorScheme.primary, + ), + ], + ), + ), + DidvanIconButton( + icon: DidvanIcons.close_regular, + gestureSize: 24, + onPressed: () { + state.replyRadar = null; + state.update(); + }, + ), + ], + ), + ), + ) + : const SizedBox(), + ), + _MessageBoxContainer( + child: Consumer( + builder: (context, state, child) { + if (state.isRecording) { + return const _Recording(); + } else if (!state.isRecording && state.recordedFile != null) { + return const _RecordChecking(); + } + return const _Typing(); + }, + ), + ), + ], + ); + } +} + +class _MessageBoxContainer extends StatelessWidget { + final Widget child; + const _MessageBoxContainer({Key? key, required this.child}) : super(key: key); + @override Widget build(BuildContext context) { return Container( - height: 56, + height: 68, decoration: BoxDecoration( border: Border( top: BorderSide( @@ -23,16 +85,7 @@ class MessageBox extends StatelessWidget { ), color: Theme.of(context).colorScheme.surface, ), - child: Consumer( - builder: (context, state, child) { - if (state.isRecording) { - return const _Recording(); - } else if (!state.isRecording && state.recordedFile != null) { - return const _RecordChecking(); - } - return const _Typing(); - }, - ), + child: child, ); } } @@ -153,9 +206,7 @@ class _RecordChecking extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), - child: AudioVisualizer( - audioFile: state.recordedFile!, - ), + child: AudioWidget(audioFile: state.recordedFile!), ), ), DidvanIconButton( diff --git a/lib/pages/home/news/news_details/news_details.dart b/lib/pages/home/news/news_details/news_details.dart index 3604be9..692ecc4 100644 --- a/lib/pages/home/news/news_details/news_details.dart +++ b/lib/pages/home/news/news_details/news_details.dart @@ -64,10 +64,7 @@ class _NewsDetailsState extends State { hasUnmarkConfirmation: widget.pageData['hasUnmarkConfirmation'], scrollController: _scrollController, - comments: state.currentNews.comments, - id: state.currentNews.id, - marked: state.currentNews.marked, - title: state.currentNews.title, + item: state.currentNews, onCommentsChanged: state.onCommentsChanged, onMarkChanged: (value) => widget.pageData['onMarkChanged']( state.currentNews.id, diff --git a/lib/pages/home/radar/radar_details/radar_details.dart b/lib/pages/home/radar/radar_details/radar_details.dart index 478a90a..c4ac5f0 100644 --- a/lib/pages/home/radar/radar_details/radar_details.dart +++ b/lib/pages/home/radar/radar_details/radar_details.dart @@ -63,18 +63,14 @@ class _RadarDetailsState extends State { child: FloatingNavigationBar( hasUnmarkConfirmation: widget.pageData['hasUnmarkConfirmation'], - comments: state.currentRadar.comments, - id: state.currentRadar.id, isRadar: true, - marked: state.currentRadar.marked, - title: state.currentRadar.title, - categories: state.currentRadar.categories, scrollController: _scrollController, onMarkChanged: (value) => widget.pageData['onMarkChanged']?.call( state.currentRadar.id, value, ), + item: state.currentRadar, onCommentsChanged: (count) { state.onCommentsChanged(count); widget.pageData['onCommentsChanged']?.call( diff --git a/lib/pages/home/settings/direct_list/direct_list.dart b/lib/pages/home/settings/direct_list/direct_list.dart index 5a13fc4..03cc32e 100644 --- a/lib/pages/home/settings/direct_list/direct_list.dart +++ b/lib/pages/home/settings/direct_list/direct_list.dart @@ -1,7 +1,7 @@ import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/pages/home/settings/direct_list/direct_list_state.dart'; -import 'package:didvan/pages/home/settings/direct_list/widgets/chat_room_item.dart'; +import 'package:didvan/pages/home/settings/direct_list/widgets/direct_item.dart'; import 'package:didvan/widgets/didvan/badge.dart'; import 'package:didvan/widgets/didvan/divider.dart'; import 'package:didvan/widgets/didvan/scaffold.dart'; diff --git a/lib/pages/home/settings/direct_list/widgets/chat_room_item.dart b/lib/pages/home/settings/direct_list/widgets/direct_item.dart similarity index 84% rename from lib/pages/home/settings/direct_list/widgets/chat_room_item.dart rename to lib/pages/home/settings/direct_list/widgets/direct_item.dart index 5fc6f34..088106e 100644 --- a/lib/pages/home/settings/direct_list/widgets/chat_room_item.dart +++ b/lib/pages/home/settings/direct_list/widgets/direct_item.dart @@ -15,10 +15,13 @@ class ChatRoomItem extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => Navigator.of(context).pushNamed( - Routes.direct, - arguments: {'type': chatRoom.type}, - ), + onTap: () { + Navigator.of(context).pushNamed( + Routes.direct, + arguments: {'type': chatRoom.type}, + ); + chatRoom.unread = 0; + }, child: Container( color: Colors.transparent, child: Column( @@ -62,10 +65,12 @@ class ChatRoomItem extends StatelessWidget { DidvanIcons.mic_light, size: 18, ), - DidvanText( - chatRoom.lastMessage.text ?? 'پیام صوتی', - maxLines: 1, - overflow: TextOverflow.ellipsis, + Expanded( + child: DidvanText( + chatRoom.lastMessage.text ?? 'پیام صوتی', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ], ), diff --git a/lib/pages/home/widgets/audio_slider.dart b/lib/pages/home/widgets/audio_slider.dart new file mode 100644 index 0000000..b9edf70 --- /dev/null +++ b/lib/pages/home/widgets/audio_slider.dart @@ -0,0 +1,39 @@ +import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:flutter/material.dart'; + +class AudioSlider extends StatelessWidget { + final String tag; + const AudioSlider({Key? key, required this.tag}) : super(key: key); + + bool get _isPlaying => MediaService.audioPlayerTag == tag; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: MediaService.audioPlayerTag != tag, + child: Directionality( + textDirection: TextDirection.ltr, + child: StreamBuilder( + stream: _isPlaying ? MediaService.audioPlayer.positionStream : null, + builder: (context, snapshot) { + return ProgressBar( + total: MediaService.audioPlayer.duration ?? Duration.zero, + progress: snapshot.data ?? Duration.zero, + buffered: + _isPlaying ? MediaService.audioPlayer.bufferedPosition : null, + thumbRadius: 6, + barHeight: 3, + timeLabelTextStyle: const TextStyle(fontSize: 0), + onSeek: (value) => _onSeek(value.inMilliseconds), + ); + }, + ), + ), + ); + } + + void _onSeek(int value) { + MediaService.audioPlayer.seek(Duration(milliseconds: value)); + } +} diff --git a/lib/pages/home/widgets/audio_visualizer.dart b/lib/pages/home/widgets/audio_visualizer.dart index 2c4b525..c39ca3d 100644 --- a/lib/pages/home/widgets/audio_visualizer.dart +++ b/lib/pages/home/widgets/audio_visualizer.dart @@ -1,314 +1,314 @@ -import 'dart:io'; -import 'dart:math'; +// import 'dart:io'; +// import 'dart:math'; -import 'package:didvan/config/design_config.dart'; -import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/app_icons.dart'; -import 'package:didvan/constants/assets.dart'; -import 'package:didvan/pages/home/direct/direct_state.dart'; -import 'package:didvan/services/media/media.dart'; -import 'package:didvan/services/storage/storage.dart'; -import 'package:didvan/utils/date_time.dart'; -import 'package:didvan/widgets/didvan/icon_button.dart'; -import 'package:didvan/widgets/didvan/text.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:just_waveform/just_waveform.dart'; -import 'package:provider/provider.dart'; +// import 'package:didvan/config/design_config.dart'; +// import 'package:didvan/config/theme_data.dart'; +// import 'package:didvan/constants/app_icons.dart'; +// import 'package:didvan/constants/assets.dart'; +// import 'package:didvan/pages/home/direct/direct_state.dart'; +// import 'package:didvan/services/media/media.dart'; +// import 'package:didvan/services/storage/storage.dart'; +// import 'package:didvan/utils/date_time.dart'; +// import 'package:didvan/widgets/didvan/icon_button.dart'; +// import 'package:didvan/widgets/didvan/text.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_svg/flutter_svg.dart'; +// import 'package:just_waveform/just_waveform.dart'; +// import 'package:provider/provider.dart'; -class AudioVisualizer extends StatefulWidget { - final File? audioFile; - final Waveform? waveform; - final String? audioUrl; - final int? duration; - final Color? backgroundColor; +// class AudioVisualizer extends StatefulWidget { +// final File? audioFile; +// final Waveform? waveform; +// final String? audioUrl; +// final int? duration; +// final Color? backgroundColor; - const AudioVisualizer({ - Key? key, - this.audioFile, - this.waveform, - this.audioUrl, - this.duration, - this.backgroundColor, - }) : super(key: key); +// const AudioVisualizer({ +// Key? key, +// this.audioFile, +// this.waveform, +// this.audioUrl, +// this.duration, +// this.backgroundColor, +// }) : super(key: key); - @override - State createState() => _AudioVisualizerState(); -} +// @override +// State createState() => _AudioVisualizerState(); +// } -class _AudioVisualizerState extends State { - Stream? waveDataStream; +// class _AudioVisualizerState extends State { +// Stream? waveDataStream; - @override - void initState() { - if (!kIsWeb && widget.audioFile != null) { - waveDataStream = JustWaveform.extract( - audioInFile: widget.audioFile!, - waveOutFile: File(StorageService.appTempsDir + '/rec-wave.wave'), - zoom: const WaveformZoom.pixelsPerSecond(100), - ); - } - super.initState(); - } +// @override +// void initState() { +// if (!kIsWeb && widget.audioFile != null) { +// waveDataStream = JustWaveform.extract( +// audioInFile: widget.audioFile!, +// waveOutFile: File(StorageService.appTempsDir + '/rec-wave.wave'), +// zoom: const WaveformZoom.pixelsPerSecond(100), +// ); +// } +// super.initState(); +// } - bool get _nowPlaying => - MediaService.lastAudioPath == widget.audioFile || - MediaService.lastAudioPath == widget.audioUrl; +// bool get _nowPlaying => +// MediaService.lastAudioPath == widget.audioFile || +// MediaService.lastAudioPath == widget.audioUrl; - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: widget.backgroundColor ?? - (DesignConfig.isDark - ? Theme.of(context).colorScheme.black - : Theme.of(context).colorScheme.background), - borderRadius: DesignConfig.mediumBorderRadius, - ), - child: Row( - children: [ - const SizedBox(width: 12), - StreamBuilder( - stream: - _nowPlaying ? MediaService.audioPlayer.positionStream : null, - builder: (context, snapshot) { - String text = ''; - if (MediaService.audioPlayer.duration == null) { - Future.delayed(Duration.zero, () { - if (mounted) { - setState(() {}); - } - }); - } - if (snapshot.data == null || snapshot.data == Duration.zero) { - text = DateTimeUtils.normalizeTimeDuration( - MediaService.audioPlayer.duration ?? - widget.waveform?.duration ?? - Duration.zero); - } else { - text = DateTimeUtils.normalizeTimeDuration(snapshot.data!); - } - return DidvanText( - text, - color: Theme.of(context).colorScheme.focusedBorder, - isEnglishFont: true, - ); - }, - ), - const SizedBox(width: 12), - Expanded( - child: Builder( - builder: (context) { - if (kIsWeb) { - return SvgPicture.asset(Assets.record); - } - if (widget.audioFile != null) { - return StreamBuilder( - stream: waveDataStream, - builder: (context, snapshot) { - if (snapshot.data == null || - snapshot.data!.waveform == null) { - return const SizedBox(); - } - final waveform = snapshot.data!.waveform!; - context.read().waveform = waveform; - return _waveWidget(waveform); - }, - ); - } - if (widget.waveform == null && waveDataStream == null) { - return SvgPicture.asset(Assets.record); - } - return _waveWidget(widget.waveform!); - }, - ), - ), - StreamBuilder( - stream: _nowPlaying ? MediaService.audioPlayer.playingStream : null, - builder: (context, snapshot) { - return DidvanIconButton( - icon: snapshot.data == true - ? DidvanIcons.pause_circle_solid - : DidvanIcons.play_circle_solid, - color: Theme.of(context).colorScheme.focusedBorder, - onPressed: () { - MediaService.handleAudioPlayback( - audioSource: widget.audioFile ?? widget.audioUrl, - isNetworkAudio: widget.audioFile == null, - ); - setState(() {}); - }, - ); - }, - ), - ], - ), - ); - } +// @override +// Widget build(BuildContext context) { +// return Container( +// decoration: BoxDecoration( +// color: widget.backgroundColor ?? +// (DesignConfig.isDark +// ? Theme.of(context).colorScheme.black +// : Theme.of(context).colorScheme.background), +// borderRadius: DesignConfig.mediumBorderRadius, +// ), +// child: Row( +// children: [ +// const SizedBox(width: 12), +// StreamBuilder( +// stream: +// _nowPlaying ? MediaService.audioPlayer.positionStream : null, +// builder: (context, snapshot) { +// String text = ''; +// if (MediaService.audioPlayer.duration == null) { +// Future.delayed(Duration.zero, () { +// if (mounted) { +// setState(() {}); +// } +// }); +// } +// if (snapshot.data == null || snapshot.data == Duration.zero) { +// text = DateTimeUtils.normalizeTimeDuration( +// MediaService.audioPlayer.duration ?? +// widget.waveform?.duration ?? +// Duration.zero); +// } else { +// text = DateTimeUtils.normalizeTimeDuration(snapshot.data!); +// } +// return DidvanText( +// text, +// color: Theme.of(context).colorScheme.focusedBorder, +// isEnglishFont: true, +// ); +// }, +// ), +// const SizedBox(width: 12), +// Expanded( +// child: Builder( +// builder: (context) { +// if (kIsWeb) { +// return SvgPicture.asset(Assets.record); +// } +// if (widget.audioFile != null) { +// return StreamBuilder( +// stream: waveDataStream, +// builder: (context, snapshot) { +// if (snapshot.data == null || +// snapshot.data!.waveform == null) { +// return const SizedBox(); +// } +// final waveform = snapshot.data!.waveform!; +// context.read().waveform = waveform; +// return _waveWidget(waveform); +// }, +// ); +// } +// if (widget.waveform == null && waveDataStream == null) { +// return SvgPicture.asset(Assets.record); +// } +// return _waveWidget(widget.waveform!); +// }, +// ), +// ), +// StreamBuilder( +// stream: _nowPlaying ? MediaService.audioPlayer.playingStream : null, +// builder: (context, snapshot) { +// return DidvanIconButton( +// icon: snapshot.data == true +// ? DidvanIcons.pause_circle_solid +// : DidvanIcons.play_circle_solid, +// color: Theme.of(context).colorScheme.focusedBorder, +// onPressed: () { +// MediaService.handleAudioPlayback( +// audioSource: widget.audioFile ?? widget.audioUrl, +// isNetworkAudio: widget.audioFile == null, +// ); +// setState(() {}); +// }, +// ); +// }, +// ), +// ], +// ), +// ); +// } - Widget _waveWidget(Waveform waveform) => IgnorePointer( - ignoring: !_nowPlaying, - child: GestureDetector( - onHorizontalDragUpdate: _changePosition, - onTapDown: _changePosition, - child: SizedBox( - height: double.infinity, - width: double.infinity, - child: _AudioWaveformWidget( - waveform: waveform, - start: Duration.zero, - scale: 2, - strokeWidth: 3, - nowPlaying: _nowPlaying, - duration: waveform.duration, - waveColor: Theme.of(context).colorScheme.focusedBorder, - ), - ), - ), - ); +// Widget _waveWidget(Waveform waveform) => IgnorePointer( +// ignoring: !_nowPlaying, +// child: GestureDetector( +// onHorizontalDragUpdate: _changePosition, +// onTapDown: _changePosition, +// child: SizedBox( +// height: double.infinity, +// width: double.infinity, +// child: _AudioWaveformWidget( +// waveform: waveform, +// start: Duration.zero, +// scale: 2, +// strokeWidth: 3, +// nowPlaying: _nowPlaying, +// duration: waveform.duration, +// waveColor: Theme.of(context).colorScheme.focusedBorder, +// ), +// ), +// ), +// ); - void _changePosition(details) { - if (MediaService.audioPlayer.audioSource == null) return; - double posper = - details.localPosition.dx / (MediaQuery.of(context).size.width - 200); - if (posper >= 1 || posper < 0) return; - final position = MediaService.audioPlayer.duration!.inMilliseconds; - MediaService.audioPlayer.seek( - Duration(milliseconds: (posper * position).toInt()), - ); - } -} +// void _changePosition(details) { +// if (MediaService.audioPlayer.audioSource == null) return; +// double posper = +// details.localPosition.dx / (MediaQuery.of(context).size.width - 200); +// if (posper >= 1 || posper < 0) return; +// final position = MediaService.audioPlayer.duration!.inMilliseconds; +// MediaService.audioPlayer.seek( +// Duration(milliseconds: (posper * position).toInt()), +// ); +// } +// } -class _AudioWaveformWidget extends StatelessWidget { - final Color waveColor; - final double scale; - final double strokeWidth; - final double pixelsPerStep; - final Waveform waveform; - final Duration start; - final bool nowPlaying; - final Duration duration; +// class _AudioWaveformWidget extends StatelessWidget { +// final Color waveColor; +// final double scale; +// final double strokeWidth; +// final double pixelsPerStep; +// final Waveform waveform; +// final Duration start; +// final bool nowPlaying; +// final Duration duration; - const _AudioWaveformWidget({ - Key? key, - required this.waveform, - required this.start, - required this.duration, - required this.nowPlaying, - this.waveColor = Colors.blue, - this.scale = 1.0, - this.strokeWidth = 5.0, - this.pixelsPerStep = 8.0, - }) : super(key: key); +// const _AudioWaveformWidget({ +// Key? key, +// required this.waveform, +// required this.start, +// required this.duration, +// required this.nowPlaying, +// this.waveColor = Colors.blue, +// this.scale = 1.0, +// this.strokeWidth = 5.0, +// this.pixelsPerStep = 8.0, +// }) : super(key: key); - @override - Widget build(BuildContext context) { - return ClipRect( - child: StreamBuilder( - stream: nowPlaying ? MediaService.audioPlayer.positionStream : null, - builder: (context, snapshot) { - double progress = 0; - if (snapshot.data == null || - MediaService.audioPlayer.duration == null) { - progress = 0; - } else { - progress = snapshot.data!.inMilliseconds / - MediaService.audioPlayer.duration!.inMilliseconds * - 100; - } - if (progress >= 100) { - progress = 0; - MediaService.audioPlayer.stop(); - MediaService.audioPlayer.seek(Duration.zero); - } - return CustomPaint( - painter: _AudioWaveformPainter( - waveColor: waveColor, - waveform: waveform, - start: start, - duration: duration, - scale: scale, - strokeWidth: strokeWidth, - pixelsPerStep: pixelsPerStep, - progressPercentage: progress, - progressColor: Theme.of(context).colorScheme.focusedBorder, - color: Theme.of(context).colorScheme.border, - ), - ); - }), - ); - } -} +// @override +// Widget build(BuildContext context) { +// return ClipRect( +// child: StreamBuilder( +// stream: nowPlaying ? MediaService.audioPlayer.positionStream : null, +// builder: (context, snapshot) { +// double progress = 0; +// if (snapshot.data == null || +// MediaService.audioPlayer.duration == null) { +// progress = 0; +// } else { +// progress = snapshot.data!.inMilliseconds / +// MediaService.audioPlayer.duration!.inMilliseconds * +// 100; +// } +// if (progress >= 100) { +// progress = 0; +// MediaService.audioPlayer.stop(); +// MediaService.audioPlayer.seek(Duration.zero); +// } +// return CustomPaint( +// painter: _AudioWaveformPainter( +// waveColor: waveColor, +// waveform: waveform, +// start: start, +// duration: duration, +// scale: scale, +// strokeWidth: strokeWidth, +// pixelsPerStep: pixelsPerStep, +// progressPercentage: progress, +// progressColor: Theme.of(context).colorScheme.focusedBorder, +// color: Theme.of(context).colorScheme.border, +// ), +// ); +// }), +// ); +// } +// } -class _AudioWaveformPainter extends CustomPainter { - final double scale; - final double strokeWidth; - final double pixelsPerStep; - final Waveform waveform; - final Duration start; - final Duration duration; - final double progressPercentage; - final Color progressColor; - final Color color; +// class _AudioWaveformPainter extends CustomPainter { +// final double scale; +// final double strokeWidth; +// final double pixelsPerStep; +// final Waveform waveform; +// final Duration start; +// final Duration duration; +// final double progressPercentage; +// final Color progressColor; +// final Color color; - _AudioWaveformPainter({ - required this.waveform, - required this.start, - required this.duration, - required this.progressPercentage, - required this.color, - required this.progressColor, - Color waveColor = Colors.blue, - this.scale = 1.0, - this.strokeWidth = 5.0, - this.pixelsPerStep = 8.0, - }); +// _AudioWaveformPainter({ +// required this.waveform, +// required this.start, +// required this.duration, +// required this.progressPercentage, +// required this.color, +// required this.progressColor, +// Color waveColor = Colors.blue, +// this.scale = 1.0, +// this.strokeWidth = 5.0, +// this.pixelsPerStep = 8.0, +// }); - @override - void paint(Canvas canvas, Size size) { - if (duration == Duration.zero) return; - double width = size.width; - double height = size.height; +// @override +// void paint(Canvas canvas, Size size) { +// if (duration == Duration.zero) return; +// double width = size.width; +// double height = size.height; - final waveformPixelsPerWindow = waveform.positionToPixel(duration).toInt(); - final waveformPixelsPerDevicePixel = waveformPixelsPerWindow / width; - final waveformPixelsPerStep = waveformPixelsPerDevicePixel * pixelsPerStep; - final sampleOffset = waveform.positionToPixel(start); - final sampleStart = -sampleOffset % waveformPixelsPerStep; - final totalLength = waveformPixelsPerWindow; - final wavePaintB = Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = strokeWidth - ..strokeCap = StrokeCap.round - ..color = progressColor; - final wavePaintA = Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = strokeWidth - ..strokeCap = StrokeCap.round - ..color = color; - for (var i = sampleStart.toDouble(); - i <= waveformPixelsPerWindow + 1.0; - i += waveformPixelsPerStep) { - final sampleIdx = (sampleOffset + i).toInt(); - final x = i / waveformPixelsPerDevicePixel; - final minY = normalise(waveform.getPixelMin(sampleIdx), height); - final maxY = normalise(waveform.getPixelMax(sampleIdx), height); - canvas.drawLine( - Offset(x + strokeWidth / 2, max(strokeWidth * 0.75, minY)), - Offset(x + strokeWidth / 2, min(height - strokeWidth * 0.75, maxY)), - i / totalLength < progressPercentage / 100 ? wavePaintB : wavePaintA, - ); - } - } +// final waveformPixelsPerWindow = waveform.positionToPixel(duration).toInt(); +// final waveformPixelsPerDevicePixel = waveformPixelsPerWindow / width; +// final waveformPixelsPerStep = waveformPixelsPerDevicePixel * pixelsPerStep; +// final sampleOffset = waveform.positionToPixel(start); +// final sampleStart = -sampleOffset % waveformPixelsPerStep; +// final totalLength = waveformPixelsPerWindow; +// final wavePaintB = Paint() +// ..style = PaintingStyle.stroke +// ..strokeWidth = strokeWidth +// ..strokeCap = StrokeCap.round +// ..color = progressColor; +// final wavePaintA = Paint() +// ..style = PaintingStyle.stroke +// ..strokeWidth = strokeWidth +// ..strokeCap = StrokeCap.round +// ..color = color; +// for (var i = sampleStart.toDouble(); +// i <= waveformPixelsPerWindow + 1.0; +// i += waveformPixelsPerStep) { +// final sampleIdx = (sampleOffset + i).toInt(); +// final x = i / waveformPixelsPerDevicePixel; +// final minY = normalise(waveform.getPixelMin(sampleIdx), height); +// final maxY = normalise(waveform.getPixelMax(sampleIdx), height); +// canvas.drawLine( +// Offset(x + strokeWidth / 2, max(strokeWidth * 0.75, minY)), +// Offset(x + strokeWidth / 2, min(height - strokeWidth * 0.75, maxY)), +// i / totalLength < progressPercentage / 100 ? wavePaintB : wavePaintA, +// ); +// } +// } - @override - bool shouldRepaint(covariant _AudioWaveformPainter oldDelegate) { - return oldDelegate.progressPercentage != progressPercentage; - } +// @override +// bool shouldRepaint(covariant _AudioWaveformPainter oldDelegate) { +// return oldDelegate.progressPercentage != progressPercentage; +// } - double normalise(int s, double height) { - final y = 32768 + (scale * s).clamp(-32768.0, 32767.0).toDouble(); - return height - 1 - y * height / 65536; - } -} +// double normalise(int s, double height) { +// final y = 32768 + (scale * s).clamp(-32768.0, 32767.0).toDouble(); +// return height - 1 - y * height / 65536; +// } +// } diff --git a/lib/pages/home/widgets/floating_navigation_bar.dart b/lib/pages/home/widgets/floating_navigation_bar.dart index 86e7db1..6ed2f01 100644 --- a/lib/pages/home/widgets/floating_navigation_bar.dart +++ b/lib/pages/home/widgets/floating_navigation_bar.dart @@ -1,7 +1,7 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; -import 'package:didvan/models/category.dart'; +import 'package:didvan/models/message_data/radar_attachment.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/pages/home/widgets/menu_item.dart'; import 'package:didvan/routes/routes.dart'; @@ -16,14 +16,10 @@ import 'package:flutter/material.dart'; class FloatingNavigationBar extends StatefulWidget { final ScrollController scrollController; + final dynamic item; final bool hasUnmarkConfirmation; final void Function(int count) onCommentsChanged; final bool isRadar; - final bool marked; - final int comments; - final int id; - final String title; - final List? categories; final void Function(bool value) onMarkChanged; const FloatingNavigationBar({ @@ -32,11 +28,7 @@ class FloatingNavigationBar extends StatefulWidget { required this.onCommentsChanged, required this.onMarkChanged, required this.isRadar, - required this.marked, - required this.comments, - required this.id, - required this.title, - this.categories, + required this.item, this.hasUnmarkConfirmation = false, }) : super(key: key); @@ -50,7 +42,7 @@ class _FloatingNavigationBarState extends State { @override void didUpdateWidget(covariant FloatingNavigationBar oldWidget) { - _comments = widget.comments; + _comments = widget.item.comments; _isScrolled = false; _handleScroll(); super.didUpdateWidget(oldWidget); @@ -60,7 +52,7 @@ class _FloatingNavigationBarState extends State { void initState() { _handleScroll(); _isScrolled = false; - _comments = widget.comments; + _comments = widget.item.comments; super.initState(); } @@ -113,7 +105,7 @@ class _FloatingNavigationBarState extends State { if (widget.isRadar) BookmarkButton( askForConfirmation: widget.hasUnmarkConfirmation, - value: widget.marked, + value: widget.item.marked, onMarkChanged: (value) { widget.onMarkChanged(value); if (widget.hasUnmarkConfirmation && !value) { @@ -137,9 +129,9 @@ class _FloatingNavigationBarState extends State { onPressed: () => Navigator.of(context).pushNamed( Routes.comments, arguments: { - 'id': widget.id, + 'id': widget.item.id, 'isRadar': widget.isRadar, - 'title': widget.title, + 'title': widget.item.title, 'onCommentsChanged': widget.onCommentsChanged, }, ), @@ -152,7 +144,7 @@ class _FloatingNavigationBarState extends State { if (!widget.isRadar) BookmarkButton( askForConfirmation: widget.hasUnmarkConfirmation, - value: widget.marked, + value: widget.item.marked, onMarkChanged: (value) { widget.onMarkChanged(value); if (widget.hasUnmarkConfirmation && !value) { @@ -191,7 +183,7 @@ class _FloatingNavigationBarState extends State { } void _showMoreOptions() { - final categories = widget.categories!; + final categories = widget.item.categories!; ActionSheetUtils.showBottomSheet( data: ActionSheetData( content: Column( @@ -211,7 +203,19 @@ class _FloatingNavigationBarState extends State { Navigator.of(context).pop(); Navigator.of(context).pushNamed( Routes.direct, - arguments: categories[i].id, + arguments: { + 'radarAttachment': RadarAttachment( + id: widget.item.id, + title: widget.item.title, + description: widget.item.contents.first.text, + timeToRead: widget.item.timeToRead, + image: widget.item.image, + forManagers: widget.item.forManagers, + categories: widget.item.categories, + createdAt: widget.item.createdAt, + ), + 'type': categories[i].label, + }, ); }, ), @@ -229,7 +233,7 @@ class _FloatingNavigationBarState extends State { Navigator.of(context).pop(); Navigator.of(context).pushNamed( Routes.direct, - arguments: 0, + arguments: {}, ); }, icon: DidvanIcons.description_regular, diff --git a/lib/pages/home/widgets/player_controller_button.dart b/lib/pages/home/widgets/player_controller_button.dart new file mode 100644 index 0000000..029387d --- /dev/null +++ b/lib/pages/home/widgets/player_controller_button.dart @@ -0,0 +1,34 @@ +import 'dart:io'; + +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/widgets/didvan/icon_button.dart'; +import 'package:flutter/material.dart'; + +class AudioControllerButton extends StatelessWidget { + final String? audioUrl; + final File? audioFile; + + const AudioControllerButton({Key? key, this.audioUrl, this.audioFile}) + : super(key: key); + + bool get _nowPlaying => + MediaService.audioPlayerTag == audioUrl || + audioFile != null && MediaService.audioPlayerTag == audioFile!.path; + + @override + Widget build(BuildContext context) { + return DidvanIconButton( + icon: MediaService.audioPlayer.playing == true && _nowPlaying + ? DidvanIcons.pause_circle_solid + : DidvanIcons.play_circle_solid, + color: Theme.of(context).colorScheme.focusedBorder, + onPressed: () { + MediaService.handleAudioPlayback( + audioSource: audioFile ?? audioUrl, + ); + }, + ); + } +} diff --git a/lib/providers/server_data_provider.dart b/lib/providers/server_data_provider.dart index 7e564a2..e9b02a5 100644 --- a/lib/providers/server_data_provider.dart +++ b/lib/providers/server_data_provider.dart @@ -8,8 +8,9 @@ class ServerDataProvider { await _getDirectTypes(); } - static int labelToTypeId(String label) => - directTypes.firstWhere((element) => element.value == label).key; + static int labelToTypeId(String? label) => label == null + ? 7 + : directTypes.firstWhere((element) => element.value.contains(label)).key; static Future _getDirectTypes() async { final service = RequestService(RequestHelper.directTypes); diff --git a/lib/services/app_initalizer.dart b/lib/services/app_initalizer.dart index 74c1e6c..236fc19 100644 --- a/lib/services/app_initalizer.dart +++ b/lib/services/app_initalizer.dart @@ -1,3 +1,4 @@ +import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/storage/storage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -9,6 +10,7 @@ class AppInitializer { StorageService.appDocsDir = (await getApplicationDocumentsDirectory()).path; StorageService.appTempsDir = (await getTemporaryDirectory()).path; + MediaService.init(); } } diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index a2c83da..7e173cf 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -6,18 +6,36 @@ import 'package:just_audio/just_audio.dart'; class MediaService { static final AudioPlayer audioPlayer = AudioPlayer(); - static dynamic lastAudioPath; + static String? audioPlayerTag; - static Future handleAudioPlayback( - {required dynamic audioSource, required bool isNetworkAudio}) async { - if (lastAudioPath == audioSource) { + static void init() { + audioPlayer.positionStream.listen((event) { + if (audioPlayer.duration != null && audioPlayer.duration! < event) { + audioPlayer.stop(); + audioPlayer.seek(const Duration(seconds: 0)); + } + }); + } + + static Future handleAudioPlayback({ + required dynamic audioSource, + }) async { + bool isNetworkAudio = audioSource.runtimeType == String; + String tag; + if (isNetworkAudio) { + tag = audioSource; + } else { + tag = audioSource.path; + } + if (audioPlayerTag == tag) { if (audioPlayer.playing) { await audioPlayer.pause(); } else { await audioPlayer.play(); } } else { - lastAudioPath = audioSource; + await audioPlayer.stop(); + audioPlayerTag = tag; if (isNetworkAudio) { await audioPlayer.setUrl( RequestHelper.baseUrl + @@ -32,7 +50,7 @@ class MediaService { await audioPlayer.setFilePath(audioSource.path); } } - await audioPlayer.play(); + audioPlayer.play(); } } diff --git a/pubspec.lock b/pubspec.lock index 7f8137a..21c01a3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.6+1" + audio_video_progress_bar: + dependency: "direct main" + description: + name: audio_video_progress_bar + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.0" boolean_selector: dependency: transitive description: @@ -357,13 +364,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.2" - just_waveform: - dependency: "direct main" - description: - name: just_waveform - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.1" lints: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b15b912..0d9b80b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,7 +52,6 @@ dependencies: record: ^3.0.2 just_audio: ^0.9.18 record_web: ^0.2.1 - just_waveform: ^0.0.1 persian_datetime_picker: ^2.4.0 persian_number_utility: ^1.1.1 bot_toast: ^4.0.1 @@ -61,6 +60,7 @@ dependencies: crop: ^0.5.2 url_launcher: ^6.0.18 transparent_image: ^2.0.0 + audio_video_progress_bar: ^0.10.0 dev_dependencies: