D1APP-101 chat waveform changed to slider

This commit is contained in:
MohammadTaha Basiri 2022-02-19 13:42:48 +03:30
parent abe153d1ff
commit 1c62f956c7
20 changed files with 590 additions and 420 deletions

View File

@ -4,10 +4,10 @@ class ChatRoom {
final int id; final int id;
final String type; final String type;
final String updatedAt; final String updatedAt;
final int unread; int unread;
final LastMessage lastMessage; final LastMessage lastMessage;
const ChatRoom({ ChatRoom({
required this.id, required this.id,
required this.type, required this.type,
required this.updatedAt, required this.updatedAt,

View File

@ -1,8 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:just_waveform/just_waveform.dart';
import 'radar_attachment.dart'; import 'radar_attachment.dart';
@ -15,7 +12,6 @@ class MessageData {
final String createdAt; final String createdAt;
final RadarAttachment? radar; final RadarAttachment? radar;
final File? audioFile; final File? audioFile;
final Waveform? waveform;
final int? audioDuration; final int? audioDuration;
const MessageData({ const MessageData({
@ -26,7 +22,6 @@ class MessageData {
this.text, this.text,
this.audio, this.audio,
this.radar, this.radar,
this.waveform,
this.audioFile, this.audioFile,
this.audioDuration, this.audioDuration,
}); });
@ -41,20 +36,6 @@ class MessageData {
audioDuration: json['waveform'] == null audioDuration: json['waveform'] == null
? null ? null
: jsonDecode(json['waveform'])['duration'] ?? 0, : 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<int>.from(
jsonDecode(json['waveform'])['data'],
),
),
)
: null,
radar: json['radar'] == null radar: json['radar'] == null
? null ? null
: RadarAttachment.fromJson(json['radar']), : RadarAttachment.fromJson(json['radar']),

View File

@ -22,6 +22,7 @@ class _DirectState extends State<Direct> {
@override @override
void initState() { void initState() {
final state = context.read<DirectState>(); final state = context.read<DirectState>();
state.replyRadar = widget.pageData['radarAttachment'];
final typeId = ServerDataProvider.labelToTypeId(widget.pageData['type']); final typeId = ServerDataProvider.labelToTypeId(widget.pageData['type']);
state.typeId = typeId; state.typeId = typeId;
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
@ -48,18 +49,23 @@ class _DirectState extends State<Direct> {
appBarData: AppBarData( appBarData: AppBarData(
hasBack: true, hasBack: true,
subtitle: 'ارتباط با سردبیر', subtitle: 'ارتباط با سردبیر',
title: widget.pageData['type'].substring(7), title: widget.pageData['type'] ?? 'پشتیبانی اپلیکیشن',
), ),
slivers: [ slivers: [
if (state.appState != AppState.busy) if (state.appState != AppState.busy)
SliverStateHandler<DirectState>( SliverPadding(
itemPadding: const EdgeInsets.only(bottom: 12), padding: state.replyRadar == null
state: state, ? EdgeInsets.zero
builder: (context, state, index) => Message( : const EdgeInsets.only(bottom: 68),
message: state.messages[index], sliver: SliverStateHandler<DirectState>(
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: [ children: [

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:didvan/models/enums.dart'; 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:didvan/services/network/request_helper.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart';
import 'package:just_waveform/just_waveform.dart';
import 'package:record/record.dart'; import 'package:record/record.dart';
class DirectState extends CoreProvier { class DirectState extends CoreProvier {
@ -21,7 +19,7 @@ class DirectState extends CoreProvier {
String? text; String? text;
RadarAttachment? replyRadar; RadarAttachment? replyRadar;
File? recordedFile; File? recordedFile;
Waveform? waveform; int? audioDuration;
bool isRecording = false; bool isRecording = false;
@ -90,9 +88,7 @@ class DirectState extends CoreProvier {
Future<void> sendMessage() async { Future<void> sendMessage() async {
if ((text == null || text!.isEmpty) && recordedFile == null) return; if ((text == null || text!.isEmpty) && recordedFile == null) return;
if (recordedFile != null) { replyRadar = null;
while (waveform == null) {}
}
messages.insert( messages.insert(
0, 0,
MessageData( MessageData(
@ -105,8 +101,7 @@ class DirectState extends CoreProvier {
audio: null, audio: null,
audioFile: recordedFile, audioFile: recordedFile,
radar: replyRadar, radar: replyRadar,
waveform: waveform, audioDuration: audioDuration,
audioDuration: waveform != null ? waveform!.duration.inSeconds : null,
), ),
); );
_addToDailyGrouped(); _addToDailyGrouped();
@ -126,13 +121,6 @@ class DirectState extends CoreProvier {
if (uploadFile == null) { if (uploadFile == null) {
service.post(); service.post();
} else { } else {
body.addAll({
'waveform': jsonEncode({
'data': waveform!.data,
'length': waveform!.length,
'duration': waveform!.duration,
})
});
service.multipart( service.multipart(
file: uploadFile, file: uploadFile,
method: 'POST', method: 'POST',

View File

@ -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<bool>(
stream: MediaService.audioPlayer.playingStream,
builder: (context, snapshot) {
return Row(
children: [
Expanded(
child: AudioSlider(
tag: audioUrl ?? audioFile!.path,
),
),
AudioControllerButton(
audioFile: audioFile,
audioUrl: audioUrl,
),
],
);
},
);
}
}

View File

@ -1,8 +1,12 @@
import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.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/models/message_data/message_data.dart';
import 'package:didvan/pages/home/direct/direct_state.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/utils/date_time.dart';
import 'package:didvan/widgets/didvan/divider.dart'; import 'package:didvan/widgets/didvan/divider.dart';
import 'package:didvan/widgets/didvan/text.dart'; import 'package:didvan/widgets/didvan/text.dart';
@ -60,13 +64,9 @@ class Message extends StatelessWidget {
children: [ children: [
if (message.text != null) DidvanText(message.text!), if (message.text != null) DidvanText(message.text!),
if (message.audio != null || message.audioFile != null) if (message.audio != null || message.audioFile != null)
SizedBox( AudioWidget(
height: 50, audioFile: message.audioFile,
child: AudioVisualizer( audioUrl: message.audio,
audioUrl: message.audio,
waveform: message.waveform,
backgroundColor: Colors.transparent,
),
), ),
if (message.radar != null) const DidvanDivider(), if (message.radar != null) const DidvanDivider(),
if (message.radar != null) const SizedBox(height: 4), if (message.radar != null) const SizedBox(height: 4),
@ -77,10 +77,22 @@ class Message extends StatelessWidget {
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
DidvanText( Row(
DateTimeUtils.timeWithAmPm(message.createdAt), mainAxisSize: MainAxisSize.min,
style: Theme.of(context).textTheme.overline, children: [
color: Theme.of(context).colorScheme.caption, 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,
)
],
), ),
], ],
), ),

View File

@ -2,7 +2,7 @@ import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart'; import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/pages/home/direct/direct_state.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/icon_button.dart';
import 'package:didvan/widgets/didvan/text.dart'; import 'package:didvan/widgets/didvan/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -11,10 +11,72 @@ import 'package:provider/provider.dart';
class MessageBox extends StatelessWidget { class MessageBox extends StatelessWidget {
const MessageBox({Key? key}) : super(key: key); const MessageBox({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Consumer<DirectState>(
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<DirectState>(
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 56, height: 68,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
@ -23,16 +85,7 @@ class MessageBox extends StatelessWidget {
), ),
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
), ),
child: Consumer<DirectState>( child: child,
builder: (context, state, child) {
if (state.isRecording) {
return const _Recording();
} else if (!state.isRecording && state.recordedFile != null) {
return const _RecordChecking();
}
return const _Typing();
},
),
); );
} }
} }
@ -153,9 +206,7 @@ class _RecordChecking extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: AudioVisualizer( child: AudioWidget(audioFile: state.recordedFile!),
audioFile: state.recordedFile!,
),
), ),
), ),
DidvanIconButton( DidvanIconButton(

View File

@ -64,10 +64,7 @@ class _NewsDetailsState extends State<NewsDetails> {
hasUnmarkConfirmation: hasUnmarkConfirmation:
widget.pageData['hasUnmarkConfirmation'], widget.pageData['hasUnmarkConfirmation'],
scrollController: _scrollController, scrollController: _scrollController,
comments: state.currentNews.comments, item: state.currentNews,
id: state.currentNews.id,
marked: state.currentNews.marked,
title: state.currentNews.title,
onCommentsChanged: state.onCommentsChanged, onCommentsChanged: state.onCommentsChanged,
onMarkChanged: (value) => widget.pageData['onMarkChanged']( onMarkChanged: (value) => widget.pageData['onMarkChanged'](
state.currentNews.id, state.currentNews.id,

View File

@ -63,18 +63,14 @@ class _RadarDetailsState extends State<RadarDetails> {
child: FloatingNavigationBar( child: FloatingNavigationBar(
hasUnmarkConfirmation: hasUnmarkConfirmation:
widget.pageData['hasUnmarkConfirmation'], widget.pageData['hasUnmarkConfirmation'],
comments: state.currentRadar.comments,
id: state.currentRadar.id,
isRadar: true, isRadar: true,
marked: state.currentRadar.marked,
title: state.currentRadar.title,
categories: state.currentRadar.categories,
scrollController: _scrollController, scrollController: _scrollController,
onMarkChanged: (value) => onMarkChanged: (value) =>
widget.pageData['onMarkChanged']?.call( widget.pageData['onMarkChanged']?.call(
state.currentRadar.id, state.currentRadar.id,
value, value,
), ),
item: state.currentRadar,
onCommentsChanged: (count) { onCommentsChanged: (count) {
state.onCommentsChanged(count); state.onCommentsChanged(count);
widget.pageData['onCommentsChanged']?.call( widget.pageData['onCommentsChanged']?.call(

View File

@ -1,7 +1,7 @@
import 'package:didvan/constants/assets.dart'; import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/view/app_bar_data.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/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/badge.dart';
import 'package:didvan/widgets/didvan/divider.dart'; import 'package:didvan/widgets/didvan/divider.dart';
import 'package:didvan/widgets/didvan/scaffold.dart'; import 'package:didvan/widgets/didvan/scaffold.dart';

View File

@ -15,10 +15,13 @@ class ChatRoomItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () => Navigator.of(context).pushNamed( onTap: () {
Routes.direct, Navigator.of(context).pushNamed(
arguments: {'type': chatRoom.type}, Routes.direct,
), arguments: {'type': chatRoom.type},
);
chatRoom.unread = 0;
},
child: Container( child: Container(
color: Colors.transparent, color: Colors.transparent,
child: Column( child: Column(
@ -62,10 +65,12 @@ class ChatRoomItem extends StatelessWidget {
DidvanIcons.mic_light, DidvanIcons.mic_light,
size: 18, size: 18,
), ),
DidvanText( Expanded(
chatRoom.lastMessage.text ?? 'پیام صوتی', child: DidvanText(
maxLines: 1, chatRoom.lastMessage.text ?? 'پیام صوتی',
overflow: TextOverflow.ellipsis, maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
], ],
), ),

View File

@ -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<Duration>(
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));
}
}

View File

@ -1,314 +1,314 @@
import 'dart:io'; // import 'dart:io';
import 'dart:math'; // import 'dart:math';
import 'package:didvan/config/design_config.dart'; // import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart'; // import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart'; // import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/constants/assets.dart'; // import 'package:didvan/constants/assets.dart';
import 'package:didvan/pages/home/direct/direct_state.dart'; // import 'package:didvan/pages/home/direct/direct_state.dart';
import 'package:didvan/services/media/media.dart'; // import 'package:didvan/services/media/media.dart';
import 'package:didvan/services/storage/storage.dart'; // import 'package:didvan/services/storage/storage.dart';
import 'package:didvan/utils/date_time.dart'; // import 'package:didvan/utils/date_time.dart';
import 'package:didvan/widgets/didvan/icon_button.dart'; // import 'package:didvan/widgets/didvan/icon_button.dart';
import 'package:didvan/widgets/didvan/text.dart'; // import 'package:didvan/widgets/didvan/text.dart';
import 'package:flutter/foundation.dart'; // import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; // import 'package:flutter_svg/flutter_svg.dart';
import 'package:just_waveform/just_waveform.dart'; // import 'package:just_waveform/just_waveform.dart';
import 'package:provider/provider.dart'; // import 'package:provider/provider.dart';
class AudioVisualizer extends StatefulWidget { // class AudioVisualizer extends StatefulWidget {
final File? audioFile; // final File? audioFile;
final Waveform? waveform; // final Waveform? waveform;
final String? audioUrl; // final String? audioUrl;
final int? duration; // final int? duration;
final Color? backgroundColor; // final Color? backgroundColor;
const AudioVisualizer({ // const AudioVisualizer({
Key? key, // Key? key,
this.audioFile, // this.audioFile,
this.waveform, // this.waveform,
this.audioUrl, // this.audioUrl,
this.duration, // this.duration,
this.backgroundColor, // this.backgroundColor,
}) : super(key: key); // }) : super(key: key);
@override // @override
State<AudioVisualizer> createState() => _AudioVisualizerState(); // State<AudioVisualizer> createState() => _AudioVisualizerState();
} // }
class _AudioVisualizerState extends State<AudioVisualizer> { // class _AudioVisualizerState extends State<AudioVisualizer> {
Stream<WaveformProgress>? waveDataStream; // Stream<WaveformProgress>? waveDataStream;
@override // @override
void initState() { // void initState() {
if (!kIsWeb && widget.audioFile != null) { // if (!kIsWeb && widget.audioFile != null) {
waveDataStream = JustWaveform.extract( // waveDataStream = JustWaveform.extract(
audioInFile: widget.audioFile!, // audioInFile: widget.audioFile!,
waveOutFile: File(StorageService.appTempsDir + '/rec-wave.wave'), // waveOutFile: File(StorageService.appTempsDir + '/rec-wave.wave'),
zoom: const WaveformZoom.pixelsPerSecond(100), // zoom: const WaveformZoom.pixelsPerSecond(100),
); // );
} // }
super.initState(); // super.initState();
} // }
bool get _nowPlaying => // bool get _nowPlaying =>
MediaService.lastAudioPath == widget.audioFile || // MediaService.lastAudioPath == widget.audioFile ||
MediaService.lastAudioPath == widget.audioUrl; // MediaService.lastAudioPath == widget.audioUrl;
@override // @override
Widget build(BuildContext context) { // Widget build(BuildContext context) {
return Container( // return Container(
decoration: BoxDecoration( // decoration: BoxDecoration(
color: widget.backgroundColor ?? // color: widget.backgroundColor ??
(DesignConfig.isDark // (DesignConfig.isDark
? Theme.of(context).colorScheme.black // ? Theme.of(context).colorScheme.black
: Theme.of(context).colorScheme.background), // : Theme.of(context).colorScheme.background),
borderRadius: DesignConfig.mediumBorderRadius, // borderRadius: DesignConfig.mediumBorderRadius,
), // ),
child: Row( // child: Row(
children: [ // children: [
const SizedBox(width: 12), // const SizedBox(width: 12),
StreamBuilder<Duration>( // StreamBuilder<Duration>(
stream: // stream:
_nowPlaying ? MediaService.audioPlayer.positionStream : null, // _nowPlaying ? MediaService.audioPlayer.positionStream : null,
builder: (context, snapshot) { // builder: (context, snapshot) {
String text = ''; // String text = '';
if (MediaService.audioPlayer.duration == null) { // if (MediaService.audioPlayer.duration == null) {
Future.delayed(Duration.zero, () { // Future.delayed(Duration.zero, () {
if (mounted) { // if (mounted) {
setState(() {}); // setState(() {});
} // }
}); // });
} // }
if (snapshot.data == null || snapshot.data == Duration.zero) { // if (snapshot.data == null || snapshot.data == Duration.zero) {
text = DateTimeUtils.normalizeTimeDuration( // text = DateTimeUtils.normalizeTimeDuration(
MediaService.audioPlayer.duration ?? // MediaService.audioPlayer.duration ??
widget.waveform?.duration ?? // widget.waveform?.duration ??
Duration.zero); // Duration.zero);
} else { // } else {
text = DateTimeUtils.normalizeTimeDuration(snapshot.data!); // text = DateTimeUtils.normalizeTimeDuration(snapshot.data!);
} // }
return DidvanText( // return DidvanText(
text, // text,
color: Theme.of(context).colorScheme.focusedBorder, // color: Theme.of(context).colorScheme.focusedBorder,
isEnglishFont: true, // isEnglishFont: true,
); // );
}, // },
), // ),
const SizedBox(width: 12), // const SizedBox(width: 12),
Expanded( // Expanded(
child: Builder( // child: Builder(
builder: (context) { // builder: (context) {
if (kIsWeb) { // if (kIsWeb) {
return SvgPicture.asset(Assets.record); // return SvgPicture.asset(Assets.record);
} // }
if (widget.audioFile != null) { // if (widget.audioFile != null) {
return StreamBuilder<WaveformProgress>( // return StreamBuilder<WaveformProgress>(
stream: waveDataStream, // stream: waveDataStream,
builder: (context, snapshot) { // builder: (context, snapshot) {
if (snapshot.data == null || // if (snapshot.data == null ||
snapshot.data!.waveform == null) { // snapshot.data!.waveform == null) {
return const SizedBox(); // return const SizedBox();
} // }
final waveform = snapshot.data!.waveform!; // final waveform = snapshot.data!.waveform!;
context.read<DirectState>().waveform = waveform; // context.read<DirectState>().waveform = waveform;
return _waveWidget(waveform); // return _waveWidget(waveform);
}, // },
); // );
} // }
if (widget.waveform == null && waveDataStream == null) { // if (widget.waveform == null && waveDataStream == null) {
return SvgPicture.asset(Assets.record); // return SvgPicture.asset(Assets.record);
} // }
return _waveWidget(widget.waveform!); // return _waveWidget(widget.waveform!);
}, // },
), // ),
), // ),
StreamBuilder<bool>( // StreamBuilder<bool>(
stream: _nowPlaying ? MediaService.audioPlayer.playingStream : null, // stream: _nowPlaying ? MediaService.audioPlayer.playingStream : null,
builder: (context, snapshot) { // builder: (context, snapshot) {
return DidvanIconButton( // return DidvanIconButton(
icon: snapshot.data == true // icon: snapshot.data == true
? DidvanIcons.pause_circle_solid // ? DidvanIcons.pause_circle_solid
: DidvanIcons.play_circle_solid, // : DidvanIcons.play_circle_solid,
color: Theme.of(context).colorScheme.focusedBorder, // color: Theme.of(context).colorScheme.focusedBorder,
onPressed: () { // onPressed: () {
MediaService.handleAudioPlayback( // MediaService.handleAudioPlayback(
audioSource: widget.audioFile ?? widget.audioUrl, // audioSource: widget.audioFile ?? widget.audioUrl,
isNetworkAudio: widget.audioFile == null, // isNetworkAudio: widget.audioFile == null,
); // );
setState(() {}); // setState(() {});
}, // },
); // );
}, // },
), // ),
], // ],
), // ),
); // );
} // }
Widget _waveWidget(Waveform waveform) => IgnorePointer( // Widget _waveWidget(Waveform waveform) => IgnorePointer(
ignoring: !_nowPlaying, // ignoring: !_nowPlaying,
child: GestureDetector( // child: GestureDetector(
onHorizontalDragUpdate: _changePosition, // onHorizontalDragUpdate: _changePosition,
onTapDown: _changePosition, // onTapDown: _changePosition,
child: SizedBox( // child: SizedBox(
height: double.infinity, // height: double.infinity,
width: double.infinity, // width: double.infinity,
child: _AudioWaveformWidget( // child: _AudioWaveformWidget(
waveform: waveform, // waveform: waveform,
start: Duration.zero, // start: Duration.zero,
scale: 2, // scale: 2,
strokeWidth: 3, // strokeWidth: 3,
nowPlaying: _nowPlaying, // nowPlaying: _nowPlaying,
duration: waveform.duration, // duration: waveform.duration,
waveColor: Theme.of(context).colorScheme.focusedBorder, // waveColor: Theme.of(context).colorScheme.focusedBorder,
), // ),
), // ),
), // ),
); // );
void _changePosition(details) { // void _changePosition(details) {
if (MediaService.audioPlayer.audioSource == null) return; // if (MediaService.audioPlayer.audioSource == null) return;
double posper = // double posper =
details.localPosition.dx / (MediaQuery.of(context).size.width - 200); // details.localPosition.dx / (MediaQuery.of(context).size.width - 200);
if (posper >= 1 || posper < 0) return; // if (posper >= 1 || posper < 0) return;
final position = MediaService.audioPlayer.duration!.inMilliseconds; // final position = MediaService.audioPlayer.duration!.inMilliseconds;
MediaService.audioPlayer.seek( // MediaService.audioPlayer.seek(
Duration(milliseconds: (posper * position).toInt()), // Duration(milliseconds: (posper * position).toInt()),
); // );
} // }
} // }
class _AudioWaveformWidget extends StatelessWidget { // class _AudioWaveformWidget extends StatelessWidget {
final Color waveColor; // final Color waveColor;
final double scale; // final double scale;
final double strokeWidth; // final double strokeWidth;
final double pixelsPerStep; // final double pixelsPerStep;
final Waveform waveform; // final Waveform waveform;
final Duration start; // final Duration start;
final bool nowPlaying; // final bool nowPlaying;
final Duration duration; // final Duration duration;
const _AudioWaveformWidget({ // const _AudioWaveformWidget({
Key? key, // Key? key,
required this.waveform, // required this.waveform,
required this.start, // required this.start,
required this.duration, // required this.duration,
required this.nowPlaying, // required this.nowPlaying,
this.waveColor = Colors.blue, // this.waveColor = Colors.blue,
this.scale = 1.0, // this.scale = 1.0,
this.strokeWidth = 5.0, // this.strokeWidth = 5.0,
this.pixelsPerStep = 8.0, // this.pixelsPerStep = 8.0,
}) : super(key: key); // }) : super(key: key);
@override // @override
Widget build(BuildContext context) { // Widget build(BuildContext context) {
return ClipRect( // return ClipRect(
child: StreamBuilder<Duration?>( // child: StreamBuilder<Duration?>(
stream: nowPlaying ? MediaService.audioPlayer.positionStream : null, // stream: nowPlaying ? MediaService.audioPlayer.positionStream : null,
builder: (context, snapshot) { // builder: (context, snapshot) {
double progress = 0; // double progress = 0;
if (snapshot.data == null || // if (snapshot.data == null ||
MediaService.audioPlayer.duration == null) { // MediaService.audioPlayer.duration == null) {
progress = 0; // progress = 0;
} else { // } else {
progress = snapshot.data!.inMilliseconds / // progress = snapshot.data!.inMilliseconds /
MediaService.audioPlayer.duration!.inMilliseconds * // MediaService.audioPlayer.duration!.inMilliseconds *
100; // 100;
} // }
if (progress >= 100) { // if (progress >= 100) {
progress = 0; // progress = 0;
MediaService.audioPlayer.stop(); // MediaService.audioPlayer.stop();
MediaService.audioPlayer.seek(Duration.zero); // MediaService.audioPlayer.seek(Duration.zero);
} // }
return CustomPaint( // return CustomPaint(
painter: _AudioWaveformPainter( // painter: _AudioWaveformPainter(
waveColor: waveColor, // waveColor: waveColor,
waveform: waveform, // waveform: waveform,
start: start, // start: start,
duration: duration, // duration: duration,
scale: scale, // scale: scale,
strokeWidth: strokeWidth, // strokeWidth: strokeWidth,
pixelsPerStep: pixelsPerStep, // pixelsPerStep: pixelsPerStep,
progressPercentage: progress, // progressPercentage: progress,
progressColor: Theme.of(context).colorScheme.focusedBorder, // progressColor: Theme.of(context).colorScheme.focusedBorder,
color: Theme.of(context).colorScheme.border, // color: Theme.of(context).colorScheme.border,
), // ),
); // );
}), // }),
); // );
} // }
} // }
class _AudioWaveformPainter extends CustomPainter { // class _AudioWaveformPainter extends CustomPainter {
final double scale; // final double scale;
final double strokeWidth; // final double strokeWidth;
final double pixelsPerStep; // final double pixelsPerStep;
final Waveform waveform; // final Waveform waveform;
final Duration start; // final Duration start;
final Duration duration; // final Duration duration;
final double progressPercentage; // final double progressPercentage;
final Color progressColor; // final Color progressColor;
final Color color; // final Color color;
_AudioWaveformPainter({ // _AudioWaveformPainter({
required this.waveform, // required this.waveform,
required this.start, // required this.start,
required this.duration, // required this.duration,
required this.progressPercentage, // required this.progressPercentage,
required this.color, // required this.color,
required this.progressColor, // required this.progressColor,
Color waveColor = Colors.blue, // Color waveColor = Colors.blue,
this.scale = 1.0, // this.scale = 1.0,
this.strokeWidth = 5.0, // this.strokeWidth = 5.0,
this.pixelsPerStep = 8.0, // this.pixelsPerStep = 8.0,
}); // });
@override // @override
void paint(Canvas canvas, Size size) { // void paint(Canvas canvas, Size size) {
if (duration == Duration.zero) return; // if (duration == Duration.zero) return;
double width = size.width; // double width = size.width;
double height = size.height; // double height = size.height;
final waveformPixelsPerWindow = waveform.positionToPixel(duration).toInt(); // final waveformPixelsPerWindow = waveform.positionToPixel(duration).toInt();
final waveformPixelsPerDevicePixel = waveformPixelsPerWindow / width; // final waveformPixelsPerDevicePixel = waveformPixelsPerWindow / width;
final waveformPixelsPerStep = waveformPixelsPerDevicePixel * pixelsPerStep; // final waveformPixelsPerStep = waveformPixelsPerDevicePixel * pixelsPerStep;
final sampleOffset = waveform.positionToPixel(start); // final sampleOffset = waveform.positionToPixel(start);
final sampleStart = -sampleOffset % waveformPixelsPerStep; // final sampleStart = -sampleOffset % waveformPixelsPerStep;
final totalLength = waveformPixelsPerWindow; // final totalLength = waveformPixelsPerWindow;
final wavePaintB = Paint() // final wavePaintB = Paint()
..style = PaintingStyle.stroke // ..style = PaintingStyle.stroke
..strokeWidth = strokeWidth // ..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round // ..strokeCap = StrokeCap.round
..color = progressColor; // ..color = progressColor;
final wavePaintA = Paint() // final wavePaintA = Paint()
..style = PaintingStyle.stroke // ..style = PaintingStyle.stroke
..strokeWidth = strokeWidth // ..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round // ..strokeCap = StrokeCap.round
..color = color; // ..color = color;
for (var i = sampleStart.toDouble(); // for (var i = sampleStart.toDouble();
i <= waveformPixelsPerWindow + 1.0; // i <= waveformPixelsPerWindow + 1.0;
i += waveformPixelsPerStep) { // i += waveformPixelsPerStep) {
final sampleIdx = (sampleOffset + i).toInt(); // final sampleIdx = (sampleOffset + i).toInt();
final x = i / waveformPixelsPerDevicePixel; // final x = i / waveformPixelsPerDevicePixel;
final minY = normalise(waveform.getPixelMin(sampleIdx), height); // final minY = normalise(waveform.getPixelMin(sampleIdx), height);
final maxY = normalise(waveform.getPixelMax(sampleIdx), height); // final maxY = normalise(waveform.getPixelMax(sampleIdx), height);
canvas.drawLine( // canvas.drawLine(
Offset(x + strokeWidth / 2, max(strokeWidth * 0.75, minY)), // Offset(x + strokeWidth / 2, max(strokeWidth * 0.75, minY)),
Offset(x + strokeWidth / 2, min(height - strokeWidth * 0.75, maxY)), // Offset(x + strokeWidth / 2, min(height - strokeWidth * 0.75, maxY)),
i / totalLength < progressPercentage / 100 ? wavePaintB : wavePaintA, // i / totalLength < progressPercentage / 100 ? wavePaintB : wavePaintA,
); // );
} // }
} // }
@override // @override
bool shouldRepaint(covariant _AudioWaveformPainter oldDelegate) { // bool shouldRepaint(covariant _AudioWaveformPainter oldDelegate) {
return oldDelegate.progressPercentage != progressPercentage; // return oldDelegate.progressPercentage != progressPercentage;
} // }
double normalise(int s, double height) { // double normalise(int s, double height) {
final y = 32768 + (scale * s).clamp(-32768.0, 32767.0).toDouble(); // final y = 32768 + (scale * s).clamp(-32768.0, 32767.0).toDouble();
return height - 1 - y * height / 65536; // return height - 1 - y * height / 65536;
} // }
} // }

View File

@ -1,7 +1,7 @@
import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart'; import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.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/models/view/action_sheet_data.dart';
import 'package:didvan/pages/home/widgets/menu_item.dart'; import 'package:didvan/pages/home/widgets/menu_item.dart';
import 'package:didvan/routes/routes.dart'; import 'package:didvan/routes/routes.dart';
@ -16,14 +16,10 @@ import 'package:flutter/material.dart';
class FloatingNavigationBar extends StatefulWidget { class FloatingNavigationBar extends StatefulWidget {
final ScrollController scrollController; final ScrollController scrollController;
final dynamic item;
final bool hasUnmarkConfirmation; final bool hasUnmarkConfirmation;
final void Function(int count) onCommentsChanged; final void Function(int count) onCommentsChanged;
final bool isRadar; final bool isRadar;
final bool marked;
final int comments;
final int id;
final String title;
final List<CategoryData>? categories;
final void Function(bool value) onMarkChanged; final void Function(bool value) onMarkChanged;
const FloatingNavigationBar({ const FloatingNavigationBar({
@ -32,11 +28,7 @@ class FloatingNavigationBar extends StatefulWidget {
required this.onCommentsChanged, required this.onCommentsChanged,
required this.onMarkChanged, required this.onMarkChanged,
required this.isRadar, required this.isRadar,
required this.marked, required this.item,
required this.comments,
required this.id,
required this.title,
this.categories,
this.hasUnmarkConfirmation = false, this.hasUnmarkConfirmation = false,
}) : super(key: key); }) : super(key: key);
@ -50,7 +42,7 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
@override @override
void didUpdateWidget(covariant FloatingNavigationBar oldWidget) { void didUpdateWidget(covariant FloatingNavigationBar oldWidget) {
_comments = widget.comments; _comments = widget.item.comments;
_isScrolled = false; _isScrolled = false;
_handleScroll(); _handleScroll();
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
@ -60,7 +52,7 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
void initState() { void initState() {
_handleScroll(); _handleScroll();
_isScrolled = false; _isScrolled = false;
_comments = widget.comments; _comments = widget.item.comments;
super.initState(); super.initState();
} }
@ -113,7 +105,7 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
if (widget.isRadar) if (widget.isRadar)
BookmarkButton( BookmarkButton(
askForConfirmation: widget.hasUnmarkConfirmation, askForConfirmation: widget.hasUnmarkConfirmation,
value: widget.marked, value: widget.item.marked,
onMarkChanged: (value) { onMarkChanged: (value) {
widget.onMarkChanged(value); widget.onMarkChanged(value);
if (widget.hasUnmarkConfirmation && !value) { if (widget.hasUnmarkConfirmation && !value) {
@ -137,9 +129,9 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
onPressed: () => Navigator.of(context).pushNamed( onPressed: () => Navigator.of(context).pushNamed(
Routes.comments, Routes.comments,
arguments: { arguments: {
'id': widget.id, 'id': widget.item.id,
'isRadar': widget.isRadar, 'isRadar': widget.isRadar,
'title': widget.title, 'title': widget.item.title,
'onCommentsChanged': widget.onCommentsChanged, 'onCommentsChanged': widget.onCommentsChanged,
}, },
), ),
@ -152,7 +144,7 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
if (!widget.isRadar) if (!widget.isRadar)
BookmarkButton( BookmarkButton(
askForConfirmation: widget.hasUnmarkConfirmation, askForConfirmation: widget.hasUnmarkConfirmation,
value: widget.marked, value: widget.item.marked,
onMarkChanged: (value) { onMarkChanged: (value) {
widget.onMarkChanged(value); widget.onMarkChanged(value);
if (widget.hasUnmarkConfirmation && !value) { if (widget.hasUnmarkConfirmation && !value) {
@ -191,7 +183,7 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
} }
void _showMoreOptions() { void _showMoreOptions() {
final categories = widget.categories!; final categories = widget.item.categories!;
ActionSheetUtils.showBottomSheet( ActionSheetUtils.showBottomSheet(
data: ActionSheetData( data: ActionSheetData(
content: Column( content: Column(
@ -211,7 +203,19 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
Routes.direct, 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<FloatingNavigationBar> {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
Routes.direct, Routes.direct,
arguments: 0, arguments: {},
); );
}, },
icon: DidvanIcons.description_regular, icon: DidvanIcons.description_regular,

View File

@ -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,
);
},
);
}
}

View File

@ -8,8 +8,9 @@ class ServerDataProvider {
await _getDirectTypes(); await _getDirectTypes();
} }
static int labelToTypeId(String label) => static int labelToTypeId(String? label) => label == null
directTypes.firstWhere((element) => element.value == label).key; ? 7
: directTypes.firstWhere((element) => element.value.contains(label)).key;
static Future<void> _getDirectTypes() async { static Future<void> _getDirectTypes() async {
final service = RequestService(RequestHelper.directTypes); final service = RequestService(RequestHelper.directTypes);

View File

@ -1,3 +1,4 @@
import 'package:didvan/services/media/media.dart';
import 'package:didvan/services/storage/storage.dart'; import 'package:didvan/services/storage/storage.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,6 +10,7 @@ class AppInitializer {
StorageService.appDocsDir = StorageService.appDocsDir =
(await getApplicationDocumentsDirectory()).path; (await getApplicationDocumentsDirectory()).path;
StorageService.appTempsDir = (await getTemporaryDirectory()).path; StorageService.appTempsDir = (await getTemporaryDirectory()).path;
MediaService.init();
} }
} }

View File

@ -6,18 +6,36 @@ import 'package:just_audio/just_audio.dart';
class MediaService { class MediaService {
static final AudioPlayer audioPlayer = AudioPlayer(); static final AudioPlayer audioPlayer = AudioPlayer();
static dynamic lastAudioPath; static String? audioPlayerTag;
static Future<void> handleAudioPlayback( static void init() {
{required dynamic audioSource, required bool isNetworkAudio}) async { audioPlayer.positionStream.listen((event) {
if (lastAudioPath == audioSource) { if (audioPlayer.duration != null && audioPlayer.duration! < event) {
audioPlayer.stop();
audioPlayer.seek(const Duration(seconds: 0));
}
});
}
static Future<void> 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) { if (audioPlayer.playing) {
await audioPlayer.pause(); await audioPlayer.pause();
} else { } else {
await audioPlayer.play(); await audioPlayer.play();
} }
} else { } else {
lastAudioPath = audioSource; await audioPlayer.stop();
audioPlayerTag = tag;
if (isNetworkAudio) { if (isNetworkAudio) {
await audioPlayer.setUrl( await audioPlayer.setUrl(
RequestHelper.baseUrl + RequestHelper.baseUrl +
@ -32,7 +50,7 @@ class MediaService {
await audioPlayer.setFilePath(audioSource.path); await audioPlayer.setFilePath(audioSource.path);
} }
} }
await audioPlayer.play(); audioPlayer.play();
} }
} }

View File

@ -15,6 +15,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.6+1" 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: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -357,13 +364,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.2" 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: lints:
dependency: transitive dependency: transitive
description: description:

View File

@ -52,7 +52,6 @@ dependencies:
record: ^3.0.2 record: ^3.0.2
just_audio: ^0.9.18 just_audio: ^0.9.18
record_web: ^0.2.1 record_web: ^0.2.1
just_waveform: ^0.0.1
persian_datetime_picker: ^2.4.0 persian_datetime_picker: ^2.4.0
persian_number_utility: ^1.1.1 persian_number_utility: ^1.1.1
bot_toast: ^4.0.1 bot_toast: ^4.0.1
@ -61,6 +60,7 @@ dependencies:
crop: ^0.5.2 crop: ^0.5.2
url_launcher: ^6.0.18 url_launcher: ^6.0.18
transparent_image: ^2.0.0 transparent_image: ^2.0.0
audio_video_progress_bar: ^0.10.0
dev_dependencies: dev_dependencies: