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 createState() => _MusicPlayerState(); } class _MusicPlayerState extends State { 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( 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( 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( 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), ), ); }) ], ); } }