Houshan-Basa/lib/ui/widgets/components/audio/music_player.dart

386 lines
16 KiB
Dart

import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
import 'package:background_downloader/background_downloader.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/services/downloader/downloader_service.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:path_provider/path_provider.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
import 'package:string_validator/string_validator.dart';
class MusicPlayer extends StatefulWidget {
final String url;
const MusicPlayer({super.key, required this.url});
@override
State<MusicPlayer> createState() => _MusicPlayerState();
}
class _MusicPlayerState extends State<MusicPlayer> {
final player = AudioPlayer();
dynamic fileSource;
bool isReady = false;
Duration? duration;
final DownloaderService _downloadFileService = DownloaderService();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
if (widget.url.isNotEmpty) {
if (!widget.url.isURL()) {
XFile? file = XFile(widget.url);
fileSource = DeviceFileSource(file.path);
} else {
var fileName = widget.url.split('/').last;
if (!fileName.endsWith('.mp3')) {
fileName = '$fileName.mp3';
}
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$fileName');
if (await directory.exists() && await file.exists()) {
fileSource = DeviceFileSource(file.path);
}
}
if (fileSource != null) {
try {
await player.setSource(fileSource!);
} catch (e) {
if (kDebugMode) {
print('Error in setSource: $e');
}
}
duration = await player.getDuration();
setState(() {
isReady = true;
});
}
}
} catch (e) {
if (kDebugMode) {
print('Error in initState: $e');
}
}
});
}
@override
void dispose() {
player.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.url.isEmpty) {
return const Text('Audio not found');
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ValueListenableBuilder(
valueListenable: _downloadFileService.onStatus,
builder: (context, status, _) {
return status == TaskStatus.complete || isReady
? StreamBuilder<PlayerState>(
stream: player.onPlayerStateChanged,
builder: (context, snapshot) {
return CircleIconBtn(
icon: (snapshot.hasData ||
!snapshot.hasError) &&
snapshot.data == PlayerState.playing
? Assets.icon.bold.pause
: Assets.icon.bold.play,
color: Theme.of(context).colorScheme.primary,
iconColor: Colors.white,
iconPadding: const EdgeInsets.all(6),
size: 32,
onTap: () async {
if (snapshot.data == PlayerState.playing) {
await player.pause();
} else if (snapshot.data ==
PlayerState.paused) {
await player.resume();
} else {
if (fileSource != null) {
await player.play(fileSource!);
}
}
},
);
})
: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary),
child: InkWell(
onTap: () async {
if (status == null) {
final path = await _downloadFileService
.downloadFile(widget.url);
if (path != null) {
XFile? file = XFile(path);
fileSource = DeviceFileSource(file.path);
}
if (fileSource != null) {
try {
await player.setSource(fileSource!);
} catch (e) {
if (kDebugMode) {
print('Error in setSource: $e');
}
}
duration = await player.getDuration();
setState(() {
isReady = true;
});
}
} else if (status == TaskStatus.running) {
_downloadFileService.pauseDownload();
} else {
_downloadFileService.resumeDownload();
}
},
child: ValueListenableBuilder(
valueListenable:
_downloadFileService.progressPer,
builder: (context, progress, _) => progress ==
0 &&
status != null &&
status == TaskStatus.running ||
progress < 0
? const SizedBox(
width: 28,
height: 28,
child: Padding(
padding: EdgeInsets.all(2.0),
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.download_rounded,
color: Colors.white,
size: 16,
),
Positioned.fill(
child:
CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
],
),
),
)
: CircularPercentIndicator(
radius: 14.0,
animation: true,
animationDuration: 100,
lineWidth: 2.0,
percent: progress,
animateFromLastPercent: true,
center: status != null &&
status == TaskStatus.running
? Text(
(progress * 100)
.abs()
.round()
.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 10.0),
)
: const Icon(
Icons.download,
color: Colors.white,
size: 14,
),
circularStrokeCap:
CircularStrokeCap.round,
backgroundColor: AppColors.gray[300],
progressColor:
AppColors.green.defaultShade,
),
),
),
);
}),
const SizedBox(
width: 8,
),
InkWell(
onTap: () async {
final pos = await player.getCurrentPosition();
if (pos != null) {
final targetPosition =
Duration(seconds: pos.inSeconds - 5);
await player.seek(targetPosition);
} else {
if (kDebugMode) {
print('Failed to get current position');
}
}
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary),
child: const Icon(
Icons.replay_5_rounded,
color: Colors.white,
size: 22,
),
),
),
const SizedBox(
width: 8,
),
InkWell(
onTap: () async {
final pos = await player.getCurrentPosition();
if (pos != null) {
final targetPosition =
Duration(seconds: pos.inSeconds + 5);
await player.seek(targetPosition);
} else {
if (kDebugMode) {
print('Failed to get current position');
}
}
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary),
child: const Icon(
Icons.forward_5_rounded,
color: Colors.white,
size: 22,
),
),
),
],
),
InkWell(
onTap: () {
setState(() {
if (player.playbackRate == 2) {
player.setPlaybackRate(1);
} else {
player.setPlaybackRate(2);
}
});
},
child: Text(
player.playbackRate == 2 ? '2X' : '1X',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Theme.of(context).colorScheme.primary),
),
)
],
),
const SizedBox(
height: 16,
),
if (isReady)
Column(
mainAxisSize: MainAxisSize.min,
children: [
if (duration != null)
SizedBox(
child: StreamBuilder<Duration>(
stream: player.onPositionChanged,
builder: (context, position) {
return Slider(
value: position.hasData && !position.hasError
? position.data!.inMilliseconds.toDouble()
: 0,
onChanged: (value) {
player.seek(Duration(milliseconds: value.round()));
},
min: 0,
max: duration!.inMilliseconds.toDouble(),
);
}),
),
const SizedBox(
height: 8,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StreamBuilder<Duration>(
stream: player.onPositionChanged,
builder: (context, snapshot) {
return Text(
snapshot.hasError ||
!snapshot.hasData && snapshot.data == null
? '00:00'
: '${snapshot.data!.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(snapshot.data!.inSeconds.remainder(60)).toString().padLeft(2, '0')}',
style: AppTextStyles.body6.copyWith(
color: Theme.of(context).colorScheme.onSurface),
);
}),
Text(
duration == null
? '00:00'
: '${duration!.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(duration!.inSeconds.remainder(60)).toString().padLeft(2, '0')}',
style: AppTextStyles.body6.copyWith(
color: Theme.of(context).colorScheme.onSurface),
),
],
)
],
),
if (widget.url.isURL())
FutureBuilder(
future: DioService.getFileSize(widget.url),
builder: (context, size) {
if (!size.hasData || size.hasError) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(16)),
child: Text(
'${size.data} MB',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold),
),
);
})
],
);
}
}