import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'dart:math' as math; import 'dart:typed_data'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart' as http; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_storage/shared_storage.dart' as saf; import 'package:tangheem/classes/colors.dart'; import 'package:tangheem/classes/consts.dart'; import 'package:tangheem/classes/utils.dart'; import 'package:tangheem/extensions/int_extensions.dart'; import 'package:tangheem/extensions/string_extensions.dart'; import 'package:tangheem/extensions/widget_extensions.dart'; import 'package:tangheem/models/aya_tangheem_type_mapped.dart'; class AyaPlayerWidget extends StatefulWidget { final int surahNo; final int ayaNo; final String numberInSurah; final String surahName; final String ayaTangheemTypeId; final GlobalKey globalKey; final List voiceNoteList; AyaPlayerWidget({Key key, this.surahName, this.ayaTangheemTypeId, this.voiceNoteList, this.ayaNo, this.numberInSurah, this.surahNo, @required this.globalKey}) : super(key: key); @override _AyaPlayerWidgetState createState() { return _AyaPlayerWidgetState(); } } class _AyaPlayerWidgetState extends State { double sliderValue = 0.4; bool isPlaying = false; AudioPlayer _player; bool _isAudioHaveError = false; @override void initState() { super.initState(); _player = AudioPlayer(); setAudioSource(); } setAudioSource() { try { List voiceList = []; if ((widget.voiceNoteList?.length ?? 0) > 0) { voiceList = widget.voiceNoteList.map((e) => AudioSource.uri(Uri.parse(ApiConsts.baseUrl + e.exposeFilePath))).toList(); } final _playlist = ConcatenatingAudioSource(children: voiceList); _player.setAudioSource(_playlist, initialIndex: 0, initialPosition: Duration.zero).then((value) => () {}); } catch (e) { _isAudioHaveError = true; } } @override void didUpdateWidget(covariant AyaPlayerWidget oldWidget) { if (widget.voiceNoteList != oldWidget.voiceNoteList) { setAudioSource(); } super.didUpdateWidget(oldWidget); } @override void dispose() { _player.dispose(); super.dispose(); } int _tempIndex; Uint8List temp; // made this dedicated function to remove avatar flashing DecorationImage getDecodedImage(int index) { if (_tempIndex == null || _tempIndex != index) { _tempIndex = index; String encodedImage = widget.voiceNoteList.elementAt(index).profilePicture; if (encodedImage?.contains("data:image/png;base64,") ?? false) { encodedImage = encodedImage.replaceAll("data:image/png;base64,", ""); } if (encodedImage?.contains("data:image/jpeg;base64,") ?? false) { encodedImage = encodedImage.replaceAll("data:image/jpeg;base64,", ""); } if (encodedImage == null) return null; temp = base64Decode(encodedImage); } if (temp == null) return null; return DecorationImage( fit: BoxFit.cover, image: MemoryImage(temp), ); } @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ if ((widget.voiceNoteList?.length ?? 0) > 0) DottedBorder( borderType: BorderType.Circle, padding: EdgeInsets.all(5), color: Color(0xff91C1BC), dashPattern: [4, 4], child: StreamBuilder( stream: _player.currentIndexStream, builder: (context, snapshot) { int state = snapshot.data; if (state == null) return SizedBox(); return Container( width: 72.0, margin: EdgeInsets.only(left: 8, right: 8), height: 72.0, decoration: BoxDecoration( image: widget.voiceNoteList.length < 1 ? null : getDecodedImage(state), borderRadius: BorderRadius.all( Radius.circular(50.0), ), ), child: widget.voiceNoteList.length < 1 ? ClipRRect( borderRadius: BorderRadius.all( Radius.circular(30.0), ), child: SvgPicture.asset( "assets/icons/chat_user.svg", clipBehavior: Clip.antiAlias, ), ) : null, ); }, ), ), if ((widget.voiceNoteList?.length ?? 0) > 0) Container( height: 42, margin: EdgeInsets.only(top: 8, bottom: 12), padding: EdgeInsets.only(left: 15, right: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: Color(0xffE3E3E3), width: 1), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (widget.voiceNoteList.length > 1) PopupMenuButton( padding: EdgeInsets.fromLTRB(4, 4, 0, 4), onSelected: (int index) { _player.seek(Duration.zero, index: index); }, child: Icon( Icons.more_vert_rounded, color: Color(0xffB5B5B5), size: 18, ), itemBuilder: (context) { var _voiceList = widget.voiceNoteList; var length = _voiceList.length < 2 ? 0 : _voiceList.length; return List.generate(length, (index) { return PopupMenuItem( value: index, child: Text( '${_voiceList[index].userName}', style: TextStyle(color: ColorConsts.primaryBlack), ), ); }); }, ), if (widget.voiceNoteList?.isNotEmpty ?? false) commonIconButton("assets/icons/download_aya.svg", () async { if (Platform.isAndroid && (await DeviceInfoPlugin().androidInfo).version.sdkInt > 29) { String filePath = widget.voiceNoteList[_player.currentIndex].exposeFilePath; saveAudioToAboveAndroid32(filePath, "Tangheem-${widget.voiceNoteList[_player.currentIndex].userName ?? ""}-${widget.voiceNoteList[_player.currentIndex].fileName}", widget.voiceNoteList[_player.currentIndex].fileType); } else if (await _requestPermission()) { saveToPhoneStorage(widget.voiceNoteList[_player.currentIndex].exposeFilePath, widget.voiceNoteList[_player.currentIndex].userName ?? ""); } else { Utils.showToast("يجب اعطاء الاذن لتنزيل الآية"); } }), 4.width, StreamBuilder( stream: _player.volumeStream, builder: (context, snapshot) { final volumn = snapshot.data; return Icon( volumn == 0 ? Icons.volume_off_rounded : Icons.volume_up_rounded, color: Color(0xffB5B5B5), size: 18, ).onPress(() { _player.setVolume(volumn == 0 ? 1.0 : 0.0); }); }, ), SizedBox(width: 4), StreamBuilder( stream: _player.positionStream, builder: (context, snapshot) { final state = snapshot.data; return Directionality(textDirection: TextDirection.ltr, child: (_durationTime(state) + " / " + _durationTime(_player.duration)).toText(8, color: ColorConsts.greyB7Color)); }, ), SizedBox(width: 4), Expanded( child: StreamBuilder( stream: _player.positionStream, builder: (context, snapshot) { final state = snapshot.data; return SliderTheme( data: SliderTheme.of(context).copyWith( inactiveTrackColor: Color(0xffF5F5F5), activeTrackColor: Color(0xff91C1BC), trackHeight: 5.0, thumbColor: ColorConsts.primaryBlack, thumbShape: RoundSliderThumbShape(enabledThumbRadius: 5.0), overlayColor: ColorConsts.primaryBlack.withAlpha(32), overlayShape: SliderComponentShape.noOverlay), child: Directionality( textDirection: TextDirection.ltr, child: Slider( value: (state?.inSeconds ?? 0) + 0.0, min: 0, max: (_player?.duration?.inSeconds ?? 0) + 0.0, onChanged: (value) { _player.seek(Duration(seconds: value.round())); }, ), ), ); }, ), ), StreamBuilder( stream: _player.playerStateStream, builder: (context, snapshot) { final state = snapshot.data?.playing ?? false; if (state) { if (_player?.duration?.inSeconds == _player?.position?.inSeconds) { _player.pause(); _player.seek(Duration.zero); } } return commonIconButton(state ? "assets/icons/pause.svg" : "assets/icons/play_aya.svg", () { state ? _player.pause() : _isAudioHaveError ? Utils.showToast("خطأ في تحميل ملف الصوت") : _player.play(); }); }, ), ], ), ), Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ (widget.surahName).toText(13, color: ColorConsts.greyB7Color), SizedBox(width: 4), widget.numberInSurah.toText(13, color: ColorConsts.greyB7Color), SizedBox(width: 4), if ((widget.voiceNoteList?.length ?? 0) > 0) StreamBuilder( stream: _player.currentIndexStream, builder: (context, snapshot) { final state = snapshot.data; return ((state == null || widget.voiceNoteList.isEmpty) ? "" : widget.voiceNoteList?.elementAt(state)?.userName ?? "").toText(13, color: ColorConsts.greyB7Color); }, ), ], ), 8.height, ], ); } String _durationTime(Duration duration) { if (duration == null) { return "00:00"; } String twoDigits(int n) => n.toString().padLeft(2, "0"); String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); return "$twoDigitMinutes:$twoDigitSeconds"; } Widget commonIconButton(String icon, VoidCallback onPressed) { return IconButton( highlightColor: Colors.transparent, splashColor: Colors.transparent, constraints: BoxConstraints(), padding: EdgeInsets.zero, icon: SvgPicture.asset(icon, color: Color(0xff939393), height: 14, width: 14), onPressed: onPressed); } Future _requestPermission() async { Map statuses = await [Permission.storage].request(); return statuses[Permission.storage].isGranted; } void saveToPhoneStorage(String filePath, String name) async { Directory storageDirectory; if (Platform.isAndroid) { storageDirectory = await getExternalStorageDirectory(); } else if (Platform.isIOS) { storageDirectory = await getApplicationDocumentsDirectory(); } else { return; } String storagePath = storageDirectory.path; if (storagePath.contains("/Android/data")) { storagePath = storagePath.substring(0, storagePath.indexOf("/Android/data")); } if (Platform.isAndroid) { storagePath += "/Download/Tangheem/"; } else { storagePath += "/Recording/"; } var d = Directory(storagePath); if (!d.existsSync()) { d.createSync(recursive: true); } storagePath += "Tangheem-$name-${DateTime.now().millisecondsSinceEpoch}${filePath.substring(filePath.lastIndexOf('.'), filePath.length)}"; Utils.showLoading(context); Utils.downloadFile(ApiConsts.baseUrl + filePath, storagePath, onResponse: (isSuccess) { Utils.hideLoading(context); if (isSuccess) { Utils.showToast("تم حفظ الآية بنجاح"); } else { Utils.showToast("خطأ في حفظ الآية"); } }); } void downloadFile(String url, String path, {Function(bool) onResponse}) { var httpClient = http.Client(); var request = http.Request('GET', Uri.parse(url)); var response = httpClient.send(request); String dir = path; List> chunks = []; int downloaded = 0; int lastVal; response.asStream().listen((http.StreamedResponse r) { r.stream.listen((List chunk) { debugPrint('downloadPercentage: ${downloaded / r.contentLength * 100}'); lastVal = (downloaded / r.contentLength * 100).toInt(); chunks.add(chunk); downloaded += chunk.length; }, onDone: () async { debugPrint('downloadPercentage: ${downloaded / r.contentLength * 100}'); if (lastVal == 0) { onResponse(false); return; } // Save the file File file = new File('$dir'); final Uint8List bytes = Uint8List(r.contentLength); int offset = 0; for (List chunk in chunks) { bytes.setRange(offset, offset + chunk.length, chunk); offset += chunk.length; } await file.writeAsBytes(bytes); onResponse(true); return; }, onError: (ex) { debugPrint("onError:$ex"); }, cancelOnError: true); }).onError((ex) { debugPrint("onError:$ex"); onResponse(false); }); } void saveAudioToAboveAndroid32(String pathUrl, String fileName, String fileType) async { List _persistedPermissionUris = await saf.persistedUriPermissions(); if (_persistedPermissionUris.isNotEmpty) { saf.UriPermission permissionUri = _persistedPermissionUris.first; startFileDownloadAboveAndroid32(pathUrl, fileName, permissionUri.uri, fileType); } else { Uri selectedDocumentUris = await saf.openDocumentTree(); if (selectedDocumentUris == null) return; saveAudioToAboveAndroid32(pathUrl, fileName, fileType); } } void startFileDownloadAboveAndroid32(String url, String fileName, Uri persistentUri, String fileType) async { try { Utils.showLoading(context); //String path = await Utils.getStoragePath(fileName); Utils.downloadFileAboveAndroid32(ApiConsts.baseUrl + url, "path", onResponse: (bytes) async { Utils.hideLoading(context); if (bytes.isNotEmpty) { final documentFile = await persistentUri.toDocumentFile(); final child = await documentFile?.child(fileName); if (child == null) { documentFile?.createFileAsBytes( mimeType: fileType, bytes: bytes, displayName: fileName, ); } else { documentFile?.writeToFileAsBytes( bytes: bytes, mode: FileMode.write, ); } Utils.showToast("تم حفظ الملف بنجاح"); } else { Utils.showToast("فشل حفظ الملف ، حاول مرة أخرى"); } }); } catch (ex) { print(ex); Utils.hideLoading(context); } } }