diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index aadf776..be5acdf 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,7 +5,8 @@
+ android:icon="@mipmap/ic_launcher"
+ android:usesCleartextTraffic="true">
json) => MessageData(
@@ -34,19 +38,26 @@ class MessageData {
writedByAdmin: json['writedByAdmin'],
readed: json['readed'],
createdAt: json['createdAt'],
- waveform: json['waveForm'] != null
+ audioDuration: json['waveform'] == null
+ ? null
+ : jsonDecode(json['waveform'])['duration'] ?? 0,
+ waveform: json['waveform'] != null
? Waveform(
- version: json['version'],
- flags: json['flags'],
- sampleRate: json['sampleRate'],
- samplesPerPixel: json['samplesPerPixel'],
- length: json['length'],
- data: json['data'],
+ 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'] as Map),
+ : RadarAttachment.fromJson(json['radar']),
);
Map toJson() => {
diff --git a/lib/pages/home/direct/direct_state.dart b/lib/pages/home/direct/direct_state.dart
index bfe16f2..ec9e16d 100644
--- a/lib/pages/home/direct/direct_state.dart
+++ b/lib/pages/home/direct/direct_state.dart
@@ -1,3 +1,4 @@
+import 'dart:convert';
import 'dart:io';
import 'package:didvan/models/enums.dart';
@@ -89,37 +90,57 @@ class DirectState extends CoreProvier {
Future sendMessage() async {
if ((text == null || text!.isEmpty) && recordedFile == null) return;
+ if (recordedFile != null) {
+ while (waveform == null) {}
+ }
+ messages.insert(
+ 0,
+ MessageData(
+ id: 0,
+ writedByAdmin: false,
+ readed: false,
+ createdAt:
+ DateTime.now().subtract(const Duration(minutes: 210)).toString(),
+ text: text,
+ audio: null,
+ audioFile: recordedFile,
+ radar: replyRadar,
+ waveform: waveform,
+ audioDuration: waveform != null ? waveform!.duration.inSeconds : null,
+ ),
+ );
+ _addToDailyGrouped();
final body = {};
if (text != null) {
body.addAll({'text': text});
- messages.insert(
- 0,
- MessageData(
- id: 0,
- writedByAdmin: false,
- readed: false,
- createdAt: DateTime.now().toString(),
- text: text,
- audio: null,
- audioFile: recordedFile,
- radar: replyRadar,
- waveform: waveform,
- ),
- );
}
- _addToDailyGrouped();
if (replyRadar != null) {
body.addAll({'radarId': replyRadar!.id});
}
+ final uploadFile = recordedFile;
text = null;
recordedFile = null;
notifyListeners();
final service =
RequestService(RequestHelper.sendDirectMessage(typeId), body: body);
- if (recordedFile == null) {
- await service.post();
+ if (uploadFile == null) {
+ service.post();
} else {
- await service.multipart(recordedFile, 'POST');
+ body.addAll({
+ 'waveform': jsonEncode({
+ 'data': waveform!.data,
+ 'length': waveform!.length,
+ 'duration': waveform!.duration,
+ })
+ });
+ service.multipart(
+ file: uploadFile,
+ method: 'POST',
+ fieldName: 'audio',
+ fileName: 'voice-message',
+ mediaExtension: 'm4a',
+ mediaFormat: 'audio',
+ );
}
}
}
diff --git a/lib/pages/home/direct/widgets/message.dart b/lib/pages/home/direct/widgets/message.dart
index 9c4171c..4d75af1 100644
--- a/lib/pages/home/direct/widgets/message.dart
+++ b/lib/pages/home/direct/widgets/message.dart
@@ -59,10 +59,14 @@ class Message extends StatelessWidget {
child: Column(
children: [
if (message.text != null) DidvanText(message.text!),
- if (message.audio != null)
- AudioVisualizer(
- audioUrl: message.audio,
- waveform: message.waveform,
+ if (message.audio != null || message.audioFile != null)
+ SizedBox(
+ height: 50,
+ child: AudioVisualizer(
+ audioUrl: message.audio,
+ waveform: message.waveform,
+ backgroundColor: Colors.transparent,
+ ),
),
if (message.radar != null) const DidvanDivider(),
if (message.radar != null) const SizedBox(height: 4),
@@ -156,7 +160,7 @@ class _MessageContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
- padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
+ padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
borderRadius: DesignConfig.mediumBorderRadius.copyWith(
bottomLeft: writedByAdmin ? Radius.zero : null,
diff --git a/lib/pages/home/widgets/audio_visualizer.dart b/lib/pages/home/widgets/audio_visualizer.dart
index f4c77dc..2c4b525 100644
--- a/lib/pages/home/widgets/audio_visualizer.dart
+++ b/lib/pages/home/widgets/audio_visualizer.dart
@@ -6,6 +6,7 @@ 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';
@@ -13,7 +14,6 @@ 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_audio/just_audio.dart';
import 'package:just_waveform/just_waveform.dart';
import 'package:provider/provider.dart';
@@ -21,12 +21,16 @@ 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);
@override
@@ -34,8 +38,6 @@ class AudioVisualizer extends StatefulWidget {
}
class _AudioVisualizerState extends State {
- final AudioPlayer _audioPlayer = AudioPlayer();
-
Stream? waveDataStream;
@override
@@ -47,34 +49,43 @@ class _AudioVisualizerState extends State {
zoom: const WaveformZoom.pixelsPerSecond(100),
);
}
- _setupAudioPlayer();
super.initState();
}
+ bool get _nowPlaying =>
+ MediaService.lastAudioPath == widget.audioFile ||
+ MediaService.lastAudioPath == widget.audioUrl;
+
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
- color: DesignConfig.isDark
- ? Theme.of(context).colorScheme.black
- : Theme.of(context).colorScheme.background,
+ 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: _audioPlayer.positionStream,
+ stream:
+ _nowPlaying ? MediaService.audioPlayer.positionStream : null,
builder: (context, snapshot) {
String text = '';
- if (_audioPlayer.duration == null) {
+ if (MediaService.audioPlayer.duration == null) {
Future.delayed(Duration.zero, () {
- setState(() {});
+ if (mounted) {
+ setState(() {});
+ }
});
}
if (snapshot.data == null || snapshot.data == Duration.zero) {
text = DateTimeUtils.normalizeTimeDuration(
- _audioPlayer.duration ?? Duration.zero);
+ MediaService.audioPlayer.duration ??
+ widget.waveform?.duration ??
+ Duration.zero);
} else {
text = DateTimeUtils.normalizeTimeDuration(snapshot.data!);
}
@@ -92,15 +103,12 @@ class _AudioVisualizerState extends State {
if (kIsWeb) {
return SvgPicture.asset(Assets.record);
}
-
if (widget.audioFile != null) {
return StreamBuilder(
stream: waveDataStream,
builder: (context, snapshot) {
- if (snapshot.data == null) {
- return const SizedBox();
- }
- if (snapshot.data!.waveform == null) {
+ if (snapshot.data == null ||
+ snapshot.data!.waveform == null) {
return const SizedBox();
}
final waveform = snapshot.data!.waveform!;
@@ -109,19 +117,28 @@ class _AudioVisualizerState extends State {
},
);
}
+ if (widget.waveform == null && waveDataStream == null) {
+ return SvgPicture.asset(Assets.record);
+ }
return _waveWidget(widget.waveform!);
},
),
),
StreamBuilder(
- stream: _audioPlayer.playingStream,
+ 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: _playAndPouse,
+ onPressed: () {
+ MediaService.handleAudioPlayback(
+ audioSource: widget.audioFile ?? widget.audioUrl,
+ isNetworkAudio: widget.audioFile == null,
+ );
+ setState(() {});
+ },
);
},
),
@@ -130,127 +147,96 @@ class _AudioVisualizerState extends State {
);
}
- Widget _waveWidget(Waveform waveform) => GestureDetector(
- onHorizontalDragUpdate: _changePosition,
- onTapDown: _changePosition,
- child: SizedBox(
- height: double.infinity,
- width: double.infinity,
- child: _AudioWaveformWidget(
- waveform: waveform,
- audioPlayer: _audioPlayer,
- start: Duration.zero,
- scale: 2,
- strokeWidth: 3,
- 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 = _audioPlayer.duration!.inMilliseconds;
- _audioPlayer.seek(
+ final position = MediaService.audioPlayer.duration!.inMilliseconds;
+ MediaService.audioPlayer.seek(
Duration(milliseconds: (posper * position).toInt()),
);
- setState(() {});
- }
-
- Future _setupAudioPlayer() async {
- if (kIsWeb || widget.audioFile == null) {
- await _audioPlayer.setUrl(
- kIsWeb
- ? widget.audioFile!.uri.path.replaceAll('%3A', ':')
- : widget.audioUrl!,
- );
- } else {
- await _audioPlayer.setFilePath(widget.audioFile!.path);
- }
- }
-
- Future _playAndPouse() async {
- if (_audioPlayer.playing) {
- _audioPlayer.pause();
- return;
- }
- await _audioPlayer.play();
- }
-
- @override
- void dispose() {
- _audioPlayer.dispose();
- super.dispose();
}
}
-class _AudioWaveformWidget extends StatefulWidget {
+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;
- final AudioPlayer audioPlayer;
const _AudioWaveformWidget({
Key? key,
required this.waveform,
required this.start,
required this.duration,
- required this.audioPlayer,
+ required this.nowPlaying,
this.waveColor = Colors.blue,
this.scale = 1.0,
this.strokeWidth = 5.0,
this.pixelsPerStep = 8.0,
}) : super(key: key);
- @override
- __AudioWaveformWidgetState createState() => __AudioWaveformWidgetState();
-}
-
-class __AudioWaveformWidgetState extends State<_AudioWaveformWidget>
- with SingleTickerProviderStateMixin {
- double progress = 0;
-
- @override
- void initState() {
- widget.audioPlayer.positionStream.listen((event) {
- if (widget.audioPlayer.duration == null) return;
- setState(() {
- progress = event.inMilliseconds /
- widget.audioPlayer.duration!.inMilliseconds *
- 100;
- if (progress >= 100) {
- progress = 0;
- widget.audioPlayer.stop();
- widget.audioPlayer.seek(Duration.zero);
- }
- });
- });
- super.initState();
- }
-
@override
Widget build(BuildContext context) {
return ClipRect(
- child: CustomPaint(
- painter: _AudioWaveformPainter(
- waveColor: widget.waveColor,
- waveform: widget.waveform,
- start: widget.start,
- duration: widget.duration,
- scale: widget.scale,
- strokeWidth: widget.strokeWidth,
- pixelsPerStep: widget.pixelsPerStep,
- progressPercentage: progress,
- progressColor: Theme.of(context).colorScheme.focusedBorder,
- color: Theme.of(context).colorScheme.border,
- ),
- ),
+ 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,
+ ),
+ );
+ }),
);
}
}
diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart
index 9d47e85..77aa17d 100644
--- a/lib/providers/user_provider.dart
+++ b/lib/providers/user_provider.dart
@@ -37,7 +37,14 @@ class UserProvider extends CoreProvier {
appState = AppState.isolatedBusy;
final RequestService service =
RequestService(RequestHelper.updateProfilePhoto);
- await service.multipart(file, 'PUT');
+ await service.multipart(
+ file: file,
+ method: 'PUT',
+ fileName: 'user-profile',
+ fieldName: 'photo',
+ mediaExtension: 'jpg',
+ mediaFormat: 'image',
+ );
if (service.isSuccess) {
user = user.copyWith(photo: service.result['photo']);
appState = AppState.idle;
diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart
index de0dba3..a2c83da 100644
--- a/lib/services/media/media.dart
+++ b/lib/services/media/media.dart
@@ -1,6 +1,41 @@
+import 'package:didvan/services/network/request.dart';
+import 'package:didvan/services/network/request_helper.dart';
+import 'package:flutter/foundation.dart';
import 'package:image_picker/image_picker.dart';
+import 'package:just_audio/just_audio.dart';
class MediaService {
+ static final AudioPlayer audioPlayer = AudioPlayer();
+ static dynamic lastAudioPath;
+
+ static Future handleAudioPlayback(
+ {required dynamic audioSource, required bool isNetworkAudio}) async {
+ if (lastAudioPath == audioSource) {
+ if (audioPlayer.playing) {
+ await audioPlayer.pause();
+ } else {
+ await audioPlayer.play();
+ }
+ } else {
+ lastAudioPath = audioSource;
+ if (isNetworkAudio) {
+ await audioPlayer.setUrl(
+ RequestHelper.baseUrl +
+ audioSource +
+ '?accessToken=${RequestService.token}',
+ );
+ } else {
+ if (kIsWeb) {
+ await audioPlayer
+ .setUrl(audioSource!.uri.path.replaceAll('%3A', ':'));
+ } else {
+ await audioPlayer.setFilePath(audioSource.path);
+ }
+ }
+ await audioPlayer.play();
+ }
+ }
+
static Future pickImage({required ImageSource source}) async {
final imagePicker = ImagePicker();
final XFile? pickedFile = await imagePicker.pickImage(source: source);
diff --git a/lib/services/network/request.dart b/lib/services/network/request.dart
index 037fc71..1614cbc 100644
--- a/lib/services/network/request.dart
+++ b/lib/services/network/request.dart
@@ -99,7 +99,14 @@ class RequestService {
}
}
- Future multipart(dynamic file, String method) async {
+ Future multipart({
+ required dynamic file,
+ required String method,
+ required String fileName,
+ required String fieldName,
+ required String mediaFormat,
+ required String mediaExtension,
+ }) async {
try {
final request = http.MultipartRequest(method, Uri.parse(url));
_headers.update('Content-Type', (_) => 'multipart/form-data');
@@ -112,11 +119,11 @@ class RequestService {
}
request.files.add(
http.MultipartFile(
- 'photo',
+ fieldName,
file.readAsBytes().asStream(),
length,
- filename: 'profile-photo',
- contentType: parser.MediaType('image', 'jpg'),
+ filename: fileName + '.' + mediaExtension,
+ contentType: parser.MediaType(mediaFormat, mediaExtension),
),
);
final streamedResponse = await request