327 lines
11 KiB
Dart
327 lines
11 KiB
Dart
// ignore_for_file: deprecated_member_use_from_same_package
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:audioplayers/audioplayers.dart';
|
|
import 'package:cross_file/cross_file.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:hoshan/core/gen/assets.gen.dart';
|
|
import 'package:hoshan/core/services/api/dio_service.dart';
|
|
import 'package:hoshan/core/utils/date_time.dart';
|
|
import 'package:hoshan/ui/theme/colors.dart';
|
|
import 'package:hoshan/ui/theme/text.dart';
|
|
import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart';
|
|
import 'package:string_validator/string_validator.dart';
|
|
|
|
class Player extends StatefulWidget {
|
|
final String fileUrl;
|
|
final Function()? onDelete;
|
|
final bool inMessages;
|
|
const Player(
|
|
{super.key,
|
|
required this.fileUrl,
|
|
this.onDelete,
|
|
this.inMessages = false});
|
|
|
|
@override
|
|
State<Player> createState() => _PlayerState();
|
|
}
|
|
|
|
enum DownloadAudioState { onDownloading, downloaded, initial }
|
|
|
|
class _PlayerState extends State<Player> {
|
|
final AudioPlayer audioPlayer = AudioPlayer();
|
|
bool isPlaying = false;
|
|
bool isPaused = false;
|
|
Duration duration = Duration.zero;
|
|
DownloadAudioState downloadState = DownloadAudioState.initial;
|
|
StreamSubscription? durationSubscription;
|
|
StreamSubscription? positionSubscription;
|
|
StreamSubscription? playerStateChangeSubscription;
|
|
String? _audioFilePath;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
try {
|
|
setAudio();
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
print("Audio Error is: $e");
|
|
}
|
|
}
|
|
|
|
playerStateChangeSubscription = audioPlayer.onPlayerStateChanged.listen(
|
|
(state) {
|
|
if (kDebugMode) {
|
|
print("Player state changed: $state");
|
|
}
|
|
setState(() {
|
|
isPlaying = state == PlayerState.playing;
|
|
isPaused = state == PlayerState.paused;
|
|
});
|
|
},
|
|
);
|
|
|
|
durationSubscription = audioPlayer.onDurationChanged.listen(
|
|
(newDuration) {
|
|
if (kDebugMode) {
|
|
print("Duration changed: $newDuration");
|
|
}
|
|
setState(() {
|
|
duration = newDuration;
|
|
});
|
|
},
|
|
);
|
|
|
|
audioPlayer.onPlayerComplete.listen((event) {
|
|
if (kDebugMode) {
|
|
print("Player completed");
|
|
}
|
|
});
|
|
|
|
audioPlayer.onLog.listen((msg) {
|
|
if (kDebugMode) {
|
|
print("AudioPlayer log: $msg");
|
|
}
|
|
});
|
|
}
|
|
|
|
Future setAudio() async {
|
|
try {
|
|
audioPlayer.setReleaseMode(ReleaseMode.stop);
|
|
|
|
if (widget.fileUrl.isNotEmpty) {
|
|
XFile? file;
|
|
if (widget.fileUrl.isURL()) {
|
|
setState(() {
|
|
downloadState = DownloadAudioState.onDownloading;
|
|
});
|
|
file = await DioService.downloadFile(
|
|
widget.fileUrl,
|
|
);
|
|
} else {
|
|
file = XFile(widget.fileUrl);
|
|
}
|
|
if (file != null) {
|
|
final fileExists = await File(file.path).exists();
|
|
if (!fileExists) {
|
|
if (kDebugMode) {
|
|
print("Audio file does not exist: ${file.path}");
|
|
}
|
|
setState(() {
|
|
downloadState = DownloadAudioState.initial;
|
|
});
|
|
return;
|
|
}
|
|
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
|
|
_audioFilePath = file.path;
|
|
if (kDebugMode) {
|
|
print("Setting audio source: ${file.path}");
|
|
final fileSize = await File(file.path).length();
|
|
print("File size: $fileSize bytes");
|
|
}
|
|
try {
|
|
await audioPlayer.setSource(DeviceFileSource(file.path));
|
|
setState(() {
|
|
downloadState = DownloadAudioState.downloaded;
|
|
});
|
|
if (kDebugMode) {
|
|
print("Audio source set successfully");
|
|
}
|
|
} catch (error) {
|
|
if (kDebugMode) {
|
|
print("Error setting audio source: $error");
|
|
}
|
|
setState(() {
|
|
downloadState = DownloadAudioState.initial;
|
|
});
|
|
}
|
|
} else {
|
|
setState(() {
|
|
downloadState = DownloadAudioState.initial;
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
print("Error in setAudio: $e");
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
playerStateChangeSubscription?.cancel();
|
|
durationSubscription?.cancel();
|
|
positionSubscription?.cancel();
|
|
audioPlayer.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
child: Row(
|
|
children: [
|
|
if (widget.onDelete != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: GestureDetector(
|
|
onTap: widget.onDelete,
|
|
child: Assets.icon.outline.trash.svg(),
|
|
),
|
|
),
|
|
),
|
|
widget.inMessages
|
|
? downloadState == DownloadAudioState.initial
|
|
? CircleIconBtn(
|
|
icon: Assets.icon.outline.download,
|
|
size: 32,
|
|
iconPadding: const EdgeInsets.all(6),
|
|
iconColor: AppColors.secondryColor.defaultShade,
|
|
onTap: () async {
|
|
await setAudio();
|
|
},
|
|
)
|
|
: downloadState == DownloadAudioState.onDownloading
|
|
? Stack(
|
|
children: [
|
|
Transform.rotate(
|
|
angle: pi / 4,
|
|
child: CircleIconBtn(
|
|
icon: Assets.icon.outline.add,
|
|
size: 32,
|
|
iconPadding: const EdgeInsets.all(6),
|
|
iconColor: AppColors.secondryColor.defaultShade,
|
|
onTap: () async {},
|
|
),
|
|
),
|
|
Positioned.fill(child: Builder(builder: (context) {
|
|
return const CircularProgressIndicator();
|
|
}))
|
|
],
|
|
)
|
|
: CircleIconBtn(
|
|
icon: isPlaying
|
|
? Assets.icon.outline.pause
|
|
: Assets.icon.outline.play,
|
|
size: 32,
|
|
iconPadding: const EdgeInsets.all(6),
|
|
iconColor: AppColors.secondryColor.defaultShade,
|
|
onTap: () async {
|
|
try {
|
|
if (isPlaying) {
|
|
await audioPlayer.pause();
|
|
} else if (isPaused) {
|
|
await audioPlayer.resume();
|
|
} else {
|
|
if (_audioFilePath != null) {
|
|
await audioPlayer
|
|
.play(DeviceFileSource(_audioFilePath!));
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
print("Error playing audio: $e");
|
|
}
|
|
}
|
|
},
|
|
)
|
|
: SizedBox(
|
|
width: 28,
|
|
height: 28,
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
try {
|
|
if (isPlaying) {
|
|
await audioPlayer.pause();
|
|
} else if (isPaused) {
|
|
await audioPlayer.resume();
|
|
} else {
|
|
if (_audioFilePath != null) {
|
|
await audioPlayer
|
|
.play(DeviceFileSource(_audioFilePath!));
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
print("Error playing audio: $e");
|
|
}
|
|
}
|
|
},
|
|
child: (isPlaying
|
|
? Assets.icon.bold.pause
|
|
: Assets.icon.bold.play)
|
|
.svg(color: AppColors.primaryColor.defaultShade),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: StreamBuilder<Duration>(
|
|
stream: audioPlayer.onPositionChanged,
|
|
builder: (context, snapshot) {
|
|
Duration position = Duration.zero;
|
|
if (snapshot.hasData && snapshot.data != null) {
|
|
position = snapshot.data!;
|
|
}
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Slider(
|
|
activeColor: widget.inMessages
|
|
? AppColors.secondryColor[200]
|
|
: null,
|
|
inactiveColor: AppColors.secondryColor[50],
|
|
thumbColor: widget.inMessages
|
|
? AppColors.secondryColor.defaultShade
|
|
: null,
|
|
min: 0,
|
|
max: position < duration
|
|
? duration.inMilliseconds.toDouble()
|
|
: 100,
|
|
value: position < duration
|
|
? position.inMilliseconds.toDouble()
|
|
: 10,
|
|
onChanged: (value) async {
|
|
if (downloadState !=
|
|
DownloadAudioState.downloaded) {
|
|
return;
|
|
}
|
|
final position =
|
|
Duration(milliseconds: value.toInt());
|
|
await audioPlayer.seek(position);
|
|
// await audioPlayer.resume();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
DateTimeUtils.getTimeFromDuration(
|
|
(isPlaying || isPaused ? position : duration)
|
|
.inSeconds,
|
|
),
|
|
style: AppTextStyles.body4.copyWith(
|
|
color: widget.inMessages
|
|
? Colors.white
|
|
: Theme.of(context).colorScheme.onSurface),
|
|
),
|
|
],
|
|
);
|
|
})),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|