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 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,

View File

@ -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<int>.from(
jsonDecode(json['waveform'])['data'],
),
),
)
: null,
radar: json['radar'] == null
? null
: RadarAttachment.fromJson(json['radar']),

View File

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

View File

@ -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<void> 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',

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/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(
AudioWidget(
audioFile: message.audioFile,
audioUrl: message.audio,
waveform: message.waveform,
backgroundColor: Colors.transparent,
),
),
if (message.radar != null) const DidvanDivider(),
if (message.radar != null) const SizedBox(height: 4),
@ -77,11 +77,23 @@ class Message extends StatelessWidget {
),
),
const SizedBox(height: 4),
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,
)
],
),
],
),
),

View File

@ -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';
@ -13,16 +13,46 @@ class MessageBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 56,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.cardBorder,
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,
),
],
),
),
color: Theme.of(context).colorScheme.surface,
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) {
@ -33,6 +63,29 @@ class MessageBox extends StatelessWidget {
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: 68,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.cardBorder,
),
),
color: Theme.of(context).colorScheme.surface,
),
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(

View File

@ -64,10 +64,7 @@ class _NewsDetailsState extends State<NewsDetails> {
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,

View File

@ -63,18 +63,14 @@ class _RadarDetailsState extends State<RadarDetails> {
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(

View File

@ -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';

View File

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

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: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<AudioVisualizer> createState() => _AudioVisualizerState();
}
// @override
// State<AudioVisualizer> createState() => _AudioVisualizerState();
// }
class _AudioVisualizerState extends State<AudioVisualizer> {
Stream<WaveformProgress>? waveDataStream;
// class _AudioVisualizerState extends State<AudioVisualizer> {
// Stream<WaveformProgress>? 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<Duration>(
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<WaveformProgress>(
stream: waveDataStream,
builder: (context, snapshot) {
if (snapshot.data == null ||
snapshot.data!.waveform == null) {
return const SizedBox();
}
final waveform = snapshot.data!.waveform!;
context.read<DirectState>().waveform = waveform;
return _waveWidget(waveform);
},
);
}
if (widget.waveform == null && waveDataStream == null) {
return SvgPicture.asset(Assets.record);
}
return _waveWidget(widget.waveform!);
},
),
),
StreamBuilder<bool>(
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<Duration>(
// 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<WaveformProgress>(
// stream: waveDataStream,
// builder: (context, snapshot) {
// if (snapshot.data == null ||
// snapshot.data!.waveform == null) {
// return const SizedBox();
// }
// final waveform = snapshot.data!.waveform!;
// context.read<DirectState>().waveform = waveform;
// return _waveWidget(waveform);
// },
// );
// }
// if (widget.waveform == null && waveDataStream == null) {
// return SvgPicture.asset(Assets.record);
// }
// return _waveWidget(widget.waveform!);
// },
// ),
// ),
// StreamBuilder<bool>(
// 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<Duration?>(
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<Duration?>(
// 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;
// }
// }

View File

@ -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<CategoryData>? 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<FloatingNavigationBar> {
@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<FloatingNavigationBar> {
void initState() {
_handleScroll();
_isScrolled = false;
_comments = widget.comments;
_comments = widget.item.comments;
super.initState();
}
@ -113,7 +105,7 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
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<FloatingNavigationBar> {
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<FloatingNavigationBar> {
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<FloatingNavigationBar> {
}
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<FloatingNavigationBar> {
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<FloatingNavigationBar> {
Navigator.of(context).pop();
Navigator.of(context).pushNamed(
Routes.direct,
arguments: 0,
arguments: {},
);
},
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();
}
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<void> _getDirectTypes() async {
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: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();
}
}

View File

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

View File

@ -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:

View File

@ -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: