You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tangheem/lib/widgets/aya_player_widget.dart

395 lines
14 KiB
Dart

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<VoiceNote> 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<AyaPlayerWidget> {
double sliderValue = 0.4;
bool isPlaying = false;
AudioPlayer _player;
bool _isAudioHaveError = false;
@override
void initState() {
super.initState();
_player = AudioPlayer();
setAudioSource();
}
setAudioSource() {
try {
List<AudioSource> 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<int>(
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<int>(
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<PlayerState>(
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<Duration>(
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<Duration>(
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<bool> _requestPermission() async {
Map<Permission, PermissionStatus> 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);
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<List<int>> chunks = [];
int downloaded = 0;
int lastVal;
response.asStream().listen((http.StreamedResponse r) {
r.stream.listen((List<int> 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<int> 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);
});
}
}