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.
448 lines
17 KiB
Dart
448 lines
17 KiB
Dart
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<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 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<int>(
|
|
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<double>(
|
|
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<Duration>(
|
|
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<Duration>(
|
|
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<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();
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
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<int>(
|
|
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<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);
|
|
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<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);
|
|
});
|
|
}
|
|
|
|
void saveAudioToAboveAndroid32(String pathUrl, String fileName, String fileType) async {
|
|
List<saf.UriPermission> _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);
|
|
}
|
|
}
|
|
}
|