import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; import 'dart:typed_data'; 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:tangheem/classes/colors.dart'; import 'package:tangheem/classes/consts.dart'; import 'package:tangheem/classes/utils.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 Container( margin: EdgeInsets.only(top: 8, bottom: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), padding: EdgeInsets.all(8), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ if (widget.voiceNoteList.length > 1) PopupMenuButton( padding: EdgeInsets.fromLTRB(4, 4, 0, 4), onSelected: (int index) { _player.seek(Duration.zero, index: index); }, child: Transform.rotate( angle: 180 * math.pi / 180, child: SvgPicture.asset( "assets/icons/drop_menu.svg", width: 16, ), ), 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?.length ?? 0) > 0) StreamBuilder( stream: _player.currentIndexStream, builder: (context, snapshot) { int state = snapshot.data; if (state == null) return SizedBox(); return Container( width: 50.0, margin: EdgeInsets.only(left: 8, right: 8), height: 50.0, decoration: BoxDecoration( image: widget.voiceNoteList.length < 1 ? null : getDecodedImage(state), borderRadius: BorderRadius.all( Radius.circular(30.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, ); }, ), Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.surahName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: ColorConsts.primaryBlack, height: 1), ), SizedBox(height: 4), if ((widget.voiceNoteList?.length ?? 0) > 0) StreamBuilder( stream: _player.currentIndexStream, builder: (context, snapshot) { final state = snapshot.data; return Text( (state == null || widget.voiceNoteList.isEmpty) ? "" : widget.voiceNoteList?.elementAt(state)?.userName ?? "", style: TextStyle(fontSize: 10, color: ColorConsts.textGrey1, height: 1), ); }, ), ], ), ), if (widget.voiceNoteList?.isNotEmpty ?? false) commonIconButton("assets/icons/download_aya.svg", () async { if (await _requestPermission()) { saveToPhoneStorage(widget.voiceNoteList[_player.currentIndex].exposeFilePath, widget.voiceNoteList[_player.currentIndex].userName ?? ""); } else { Utils.showToast("يجب اعطاء الاذن لتنزيل الآية"); } }), ], ), SizedBox(height: 8), if ((widget.voiceNoteList?.length ?? 0) > 0) Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ commonIconButton("assets/icons/next_aya.svg", () { _player.seekToNext(); }), SizedBox(width: 4), 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(); }); }, ), SizedBox(width: 4), commonIconButton("assets/icons/previous_aya.svg", () { _player.seekToPrevious(); }), SizedBox(width: 16), Expanded( child: StreamBuilder( stream: _player.positionStream, builder: (context, snapshot) { final state = snapshot.data; return SliderTheme( data: SliderTheme.of(context).copyWith( inactiveTrackColor: ColorConsts.sliderBackground, activeTrackColor: ColorConsts.secondaryOrange, trackHeight: 8.0, thumbColor: ColorConsts.primaryBlack, thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10.0), overlayColor: ColorConsts.primaryBlack.withAlpha(32), overlayShape: RoundSliderOverlayShape(overlayRadius: 12.0), ), 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())); }, ), ), ); }, ), ), SizedBox(width: 8), StreamBuilder( stream: _player.positionStream, builder: (context, snapshot) { final state = snapshot.data; return Text( _durationTime(state) ?? "", style: TextStyle(color: ColorConsts.textGrey1, height: 1.1, fontFamily: "Roboto"), ); }, ), ], ) ], ), ); } 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.only(right: 2), icon: SvgPicture.asset(icon, height: 16, width: 16), 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")); } storagePath += "/Download/Tangheem/"; 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); 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); }); } }