D1APP-53 chat

This commit is contained in:
MohammadTaha Basiri 2022-02-14 19:05:38 +03:30
parent 6d5548362c
commit c740a72025
8 changed files with 217 additions and 145 deletions

View File

@ -5,7 +5,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="Didvan"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"

View File

@ -1,4 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:just_waveform/just_waveform.dart';
@ -14,17 +16,19 @@ class MessageData {
final RadarAttachment? radar;
final File? audioFile;
final Waveform? waveform;
final int? audioDuration;
const MessageData({
required this.id,
required this.writedByAdmin,
required this.readed,
required this.createdAt,
required this.text,
required this.audio,
required this.radar,
required this.waveform,
this.text,
this.audio,
this.radar,
this.waveform,
this.audioFile,
this.audioDuration,
});
factory MessageData.fromJson(Map<String, dynamic> 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<int>.from(
jsonDecode(json['waveform'])['data'],
),
),
)
: null,
radar: json['radar'] == null
? null
: RadarAttachment.fromJson(json['radar'] as Map<String, dynamic>),
: RadarAttachment.fromJson(json['radar']),
);
Map<String, dynamic> toJson() => {

View File

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

View File

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

View File

@ -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<AudioVisualizer> {
final AudioPlayer _audioPlayer = AudioPlayer();
Stream<WaveformProgress>? waveDataStream;
@override
@ -47,34 +49,43 @@ class _AudioVisualizerState extends State<AudioVisualizer> {
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<Duration>(
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<AudioVisualizer> {
if (kIsWeb) {
return SvgPicture.asset(Assets.record);
}
if (widget.audioFile != null) {
return StreamBuilder<WaveformProgress>(
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<AudioVisualizer> {
},
);
}
if (widget.waveform == null && waveDataStream == null) {
return SvgPicture.asset(Assets.record);
}
return _waveWidget(widget.waveform!);
},
),
),
StreamBuilder<bool>(
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<AudioVisualizer> {
);
}
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<void> _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<void> _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<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,
),
);
}),
);
}
}

View File

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

View File

@ -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<void> 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<XFile?> pickImage({required ImageSource source}) async {
final imagePicker = ImagePicker();
final XFile? pickedFile = await imagePicker.pickImage(source: source);

View File

@ -99,7 +99,14 @@ class RequestService {
}
}
Future<void> multipart(dynamic file, String method) async {
Future<void> 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