168 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			168 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | |
| import 'package:gap/gap.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:island/pods/network.dart';
 | |
| import 'package:island/services/time.dart';
 | |
| import 'package:island/talker.dart';
 | |
| import 'package:material_symbols_icons/symbols.dart';
 | |
| import 'package:media_kit/media_kit.dart';
 | |
| import 'package:styled_widget/styled_widget.dart';
 | |
| 
 | |
| class UniversalAudio extends ConsumerStatefulWidget {
 | |
|   final String uri;
 | |
|   final String filename;
 | |
|   final bool autoplay;
 | |
|   const UniversalAudio({
 | |
|     super.key,
 | |
|     required this.uri,
 | |
|     required this.filename,
 | |
|     this.autoplay = false,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   ConsumerState<UniversalAudio> createState() => _UniversalAudioState();
 | |
| }
 | |
| 
 | |
| class _UniversalAudioState extends ConsumerState<UniversalAudio> {
 | |
|   Player? _player;
 | |
| 
 | |
|   Duration _duration = Duration(seconds: 1);
 | |
|   Duration _duartionBuffered = Duration(seconds: 1);
 | |
|   Duration _position = Duration(seconds: 0);
 | |
| 
 | |
|   bool _sliderWorking = false;
 | |
|   Duration _sliderPosition = Duration(seconds: 0);
 | |
| 
 | |
|   void _openAudio() async {
 | |
|     final url = widget.uri;
 | |
|     MediaKit.ensureInitialized();
 | |
| 
 | |
|     _player = Player();
 | |
|     _player!.stream.position.listen((value) {
 | |
|       _position = value;
 | |
|       if (!_sliderWorking) _sliderPosition = _position;
 | |
|       setState(() {});
 | |
|     });
 | |
|     _player!.stream.buffer.listen((value) {
 | |
|       _duartionBuffered = value;
 | |
|       setState(() {});
 | |
|     });
 | |
|     _player!.stream.duration.listen((value) {
 | |
|       _duration = value;
 | |
|       setState(() {});
 | |
|     });
 | |
| 
 | |
|     String? uri;
 | |
|     final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
 | |
|     if (inCacheInfo == null) {
 | |
|       talker.info('[MediaPlayer] Miss cache: $url');
 | |
|       final token = ref.watch(tokenProvider)?.token;
 | |
|       DefaultCacheManager().downloadFile(
 | |
|         url,
 | |
|         authHeaders: {'Authorization': 'AtField $token'},
 | |
|       );
 | |
|       uri = url;
 | |
|     } else {
 | |
|       uri = inCacheInfo.file.path;
 | |
|       talker.info('[MediaPlayer] Hit cache: $url');
 | |
|     }
 | |
| 
 | |
|     _player!.open(Media(uri), play: widget.autoplay);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _openAudio();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     super.dispose();
 | |
|     _player?.dispose();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     if (_player == null) {
 | |
|       return Center(child: CircularProgressIndicator());
 | |
|     }
 | |
| 
 | |
|     return Card(
 | |
|       color: Theme.of(context).colorScheme.surfaceContainerLowest,
 | |
|       child: Row(
 | |
|         children: [
 | |
|           IconButton.filled(
 | |
|             onPressed: () {
 | |
|               _player!.playOrPause().then((_) {
 | |
|                 if (mounted) setState(() {});
 | |
|               });
 | |
|             },
 | |
|             icon:
 | |
|                 _player!.state.playing
 | |
|                     ? const Icon(Symbols.pause, fill: 1, color: Colors.white)
 | |
|                     : const Icon(
 | |
|                       Symbols.play_arrow,
 | |
|                       fill: 1,
 | |
|                       color: Colors.white,
 | |
|                     ),
 | |
|           ),
 | |
|           const Gap(20),
 | |
|           Expanded(
 | |
|             child: Column(
 | |
|               mainAxisSize: MainAxisSize.min,
 | |
|               crossAxisAlignment: CrossAxisAlignment.start,
 | |
|               children: [
 | |
|                 AnimatedSwitcher(
 | |
|                   duration: const Duration(milliseconds: 300),
 | |
|                   child:
 | |
|                       (_player!.state.playing || _sliderWorking)
 | |
|                           ? SizedBox(
 | |
|                             width: double.infinity,
 | |
|                             key: const ValueKey('playing'),
 | |
|                             child: Text(
 | |
|                               '${_position.formatShortDuration()} / ${_duration.formatShortDuration()}',
 | |
|                             ),
 | |
|                           )
 | |
|                           : SizedBox(
 | |
|                             width: double.infinity,
 | |
|                             key: const ValueKey('filename'),
 | |
|                             child: Text(
 | |
|                               widget.filename.isEmpty
 | |
|                                   ? 'Audio'
 | |
|                                   : widget.filename,
 | |
|                               maxLines: 1,
 | |
|                               overflow: TextOverflow.ellipsis,
 | |
|                             ),
 | |
|                           ),
 | |
|                 ),
 | |
|                 Slider(
 | |
|                   value: _sliderPosition.inMilliseconds.toDouble(),
 | |
|                   secondaryTrackValue:
 | |
|                       _duartionBuffered.inMilliseconds.toDouble(),
 | |
|                   max: _duration.inMilliseconds.toDouble(),
 | |
|                   onChangeStart: (_) {
 | |
|                     _sliderWorking = true;
 | |
|                   },
 | |
|                   onChanged: (value) {
 | |
|                     _sliderPosition = Duration(milliseconds: value.toInt());
 | |
|                     setState(() {});
 | |
|                   },
 | |
|                   onChangeEnd: (value) {
 | |
|                     _sliderPosition = Duration(milliseconds: value.toInt());
 | |
|                     _sliderWorking = false;
 | |
|                     _player!.seek(_sliderPosition);
 | |
|                   },
 | |
|                   year2023: true,
 | |
|                   padding: EdgeInsets.zero,
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|         ],
 | |
|       ).padding(horizontal: 24, vertical: 16),
 | |
|     );
 | |
|   }
 | |
| }
 |