import 'dart:convert'; import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:just_audio/just_audio.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), ); }, ), ], ), ), commonIconButton("assets/icons/download_aya.svg", () async { if (await _requestPermission()) { if (await _saveAya()) { Utils.showToast("تم حفظ الآية بنجاح"); } else { Utils.showToast("خطأ في حفظ الآية"); } } 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; } Future _saveAya() async { Utils.showLoading(context); try { RenderRepaintBoundary boundary = widget.globalKey.currentContext.findRenderObject(); ui.Image image = await boundary.toImage(pixelRatio: 3.0); ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png); final result = await ImageGallerySaver.saveImage(byteData.buffer.asUint8List(), quality: 100); Utils.hideLoading(context); return result["isSuccess"]; } catch (ex) { Future.delayed(Duration(seconds: 1), () { Utils.hideLoading(context); }); return false; } } }