// ignore_for_file: prefer_const_literals_to_create_immutables, avoid_print import 'package:fip_parser_ui/globals.dart'; import 'package:fip_parser_ui/providers/home.dart'; import 'package:fip_parser_ui/providers/player.dart'; import 'package:flutter/material.dart'; import 'package:fip_parser_ui/queries.dart'; import 'package:graphql/client.dart'; import 'package:kplayer/kplayer.dart'; import 'package:miniplayer/miniplayer.dart'; import 'package:provider/provider.dart'; import 'package:retry/retry.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:http/http.dart' as http; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; PlayerController? player; Track? currentTrack; List trackList = []; String radio = 'groove'; int isDownloading = -1; class HomeScreen extends StatefulWidget { const HomeScreen({Key? key, required this.title}) : super(key: key); final String title; @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { @override Widget build(BuildContext context) { final MiniplayerController controller = MiniplayerController(); const hours = 8; return Stack( children: [ Scaffold( body: SingleChildScrollView( child: Column( children: [ Container( color: Colors.grey[900], height: 50, child: Center( child: DropdownButton( dropdownColor: Colors.grey[900], value: radio, style: TextStyle(fontSize: 15, color: Colors.grey[300]), underline: const SizedBox(), iconSize: 22, onChanged: (String? newRadio) { setState(() { radio = newRadio!; // getTracks(radio, hours); }); }, items: radioList), ), ), FutureBuilder>( future: getTracks(radio, hours), builder: ( BuildContext context, AsyncSnapshot> snapshot, ) { print(snapshot.connectionState); if (snapshot.connectionState == ConnectionState.waiting) { return Stack(children: [ Container( height: 10000, width: 10000, color: const Color(0xFF121212)), Center( child: Column(children: [ const SizedBox(height: 20), SizedBox( height: 30, width: 30, child: CircularProgressIndicator( color: Colors.amber[100], ), ), ]), ), ]); } else if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { return const Text('Error'); } else if (snapshot.hasData) { return Table( columnWidths: { 0: const FlexColumnWidth(4), 1: const FlexColumnWidth(2), 2: const FlexColumnWidth(1), }, defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: snapshot.data! .map((item) => _buildTableRow(item, context)) .toList() ..insert( 0, _buildTableRow( Track( number: -1, title: 'TITRE', artiste: 'ARTISTE', album: 'ALBUM', id: 'URL'), context), ), ); } else { return const Text('Empty data'); } } else { return Text('State: ${snapshot.connectionState}'); } }, ), ], ), ), ), Consumer(builder: (context, playerProvider, _) { TextEditingController trackTitle = TextEditingController(); TextEditingController trackArtiste = TextEditingController(); trackTitle.text = currentTrack?.title ?? ''; trackArtiste.text = currentTrack?.artiste ?? ''; return Miniplayer( controller: controller, backgroundColor: Colors.grey[900]!, minHeight: 70, maxHeight: 370, builder: (height, percentage) { return Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ currentTrack?.id != null ? Image.network( 'https://img.youtube.com/vi/${currentTrack?.id}/1.jpg', width: 93, ) : const SizedBox(width: 100), Expanded( child: Column(children: [ SizedBox( height: 20, child: TextField( enabled: false, maxLines: 1, controller: trackTitle, decoration: const InputDecoration( border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, disabledBorder: InputBorder.none, contentPadding: EdgeInsets.only(left: 15), ), style: TextStyle( fontSize: 14, color: Colors.grey[300], ), ), ), SizedBox( height: 20, child: TextField( enabled: false, maxLines: 1, controller: trackArtiste, decoration: const InputDecoration( border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, disabledBorder: InputBorder.none, contentPadding: EdgeInsets.only(left: 15), ), style: TextStyle( fontSize: 12, color: Colors.grey[500], ), ), ), ]), ), Column(children: [ const Spacer(), Row(children: [ Column(children: [ IconButton( icon: Icon(Icons.skip_previous, color: Colors.grey[500], size: 32), onPressed: () { if (currentTrack != null && currentTrack!.number > 1) { currentTrack = trackList.firstWhere((element) => element.number == currentTrack!.number - 1); playTrack(context, currentTrack!); } }), const SizedBox(height: 7), ]), const SizedBox(width: 8), ElevatedButton( onPressed: () { player?.playing ?? false ? player?.pause() : player?.play(); playerProvider.reload(); }, child: Icon( player?.playing ?? false ? Icons.pause : Icons.play_arrow, color: Colors.grey[900], size: 30), style: ElevatedButton.styleFrom( shape: const CircleBorder(), padding: const EdgeInsets.all(12), primary: Colors.grey[300], onPrimary: Colors.grey[900], ), ), Column(children: [ IconButton( icon: Icon(Icons.skip_next, color: Colors.grey[500], size: 32), onPressed: () { if (currentTrack != null && currentTrack!.number < trackList.last.number) { currentTrack = trackList.firstWhere((element) => element.number == currentTrack!.number + 1); playTrack(context, currentTrack!); } }), const SizedBox(height: 7), ]), ]), const Spacer(), Row(children: [ Text( timeFormat( player?.position ?? const Duration(seconds: 0)), style: TextStyle(color: Colors.grey[500], fontSize: 12), ), const SizedBox(width: 3), SliderTheme( data: const SliderThemeData( thumbShape: RoundSliderThumbShape(enabledThumbRadius: 2), overlayShape: RoundSliderThumbShape(enabledThumbRadius: 5), trackHeight: 2, ), child: SizedBox( width: 300, child: Slider( value: double.parse( player?.position.inSeconds.toString() ?? '0'), max: double.parse( player?.duration.inSeconds.toString() ?? '2000'), onChanged: (double value) { print(value); if (player?.position != null) { player!.position = Duration(seconds: value.toInt()); playerProvider.reload(); } }, activeColor: Colors.grey[400], inactiveColor: Colors.grey[700], ), ), ), const SizedBox(width: 3), Text( timeFormat( player?.duration ?? const Duration(seconds: 0)), style: TextStyle(color: Colors.grey[500], fontSize: 12), ), ]), const Spacer(), ]), const Spacer(), SliderTheme( data: const SliderThemeData( thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), overlayShape: RoundSliderThumbShape(enabledThumbRadius: 8), trackHeight: 2.5, ), child: SizedBox( width: 130, child: Slider( value: player?.volume ?? 1, max: 1, onChanged: (double value) { if (player?.volume != null) { player!.volume = value; playerProvider.reload(); } }, activeColor: Colors.grey[400], inactiveColor: Colors.grey[700], ), ), ), const SizedBox(width: 20) ], ); }, ); }) ], ); } } Widget trackLine(Track track) { return SizedBox( height: 30, child: Row( children: [Text(track.title + ' - ' + track.artiste)], ), ); } class Track { final int number; final String title; final String artiste; final String? album; String? id; Duration? duration; Track( {required this.number, required this.title, required this.artiste, this.album, this.id, this.duration}); } GraphQLClient initClient() { final _httpLink = HttpLink( 'https://openapi.radiofrance.fr/v1/graphql?x-token=$token', ); final GraphQLClient client = GraphQLClient( cache: GraphQLCache(), link: _httpLink, ); return client; } Future> getTracks(String radio, int hours) async { const int queryTimeout = 8; final client = initClient(); final now = (DateTime.now().millisecondsSinceEpoch ~/ 1000); final yesterday = now - (hours * 3600); final String radioQuery; if (radio == 'fip') { radioQuery = 'FIP'; } else { radioQuery = 'FIP_${radio.toUpperCase()}'; } final QueryOptions options = QueryOptions( document: gql(getSongs), pollInterval: const Duration(milliseconds: 50), variables: { 'start': yesterday, 'end': now, 'radio': radioQuery, }, ); QueryResult result; result = await retry( () async => await client .query(options) .timeout(const Duration(seconds: queryTimeout)), onRetry: (_) => print('Timeout, retry...'), ); if (result.hasException) { print(result.exception.toString()); } final List data = result.data?['grid'] ?? []; print(data.length.toString() + ' songs'); // ####### int trackNbr = 0; trackList.clear(); // final yt = YoutubeExplode(); // List resultUrlPool = []; for (Map track in data) { track = track['track']; trackNbr++; final String title = track['title']; final String artiste; if (track['mainArtists'].isNotEmpty) { artiste = track['mainArtists'].first; } else { artiste = ''; } final String album = track['albumTitle']; final thisTrack = Track(number: trackNbr, title: title, artiste: artiste, album: album); trackList.add(thisTrack); // final secondMatch = artiste == '' ? album : artiste; // final resultUrl = yt.search.search(title + ' ' + secondMatch); // resultUrlPool.add(resultUrl); // resultUrl.then((value) { // try { // trackList[trackNbr - 1].id = value.first.id.value; // } catch (e) { // print( // 'Error: ' + trackList[trackNbr - 1].title + ' -> ' + e.toString()); // } // }); } // trackList.sort((a, b) => a.number.compareTo(b.number)); // final secondMatch = // trackList[0].artiste == '' ? trackList[0].album : trackList[0].artiste; // yt.search.search(trackList[0].title + ' ' + secondMatch!).then((resultUrl) { // trackList[0].id = resultUrl.first.id.value; // player = Player.network( // "https://invidious.fdn.fr/embed/${trackList[0].id}?raw=1&?listen=1"); // currentTrack = trackList[0]; // }); return trackList; } TableRow _buildTableRow(Track track, BuildContext context) { DownloadProvider downloadProvider = Provider.of(context, listen: false); final textStyle = TextStyle( fontWeight: track.number == -1 ? FontWeight.w200 : FontWeight.normal, color: Colors.grey[500], fontSize: track.number == -1 ? 15 : 14); const rowPadding = EdgeInsets.all(10); final yt = YoutubeExplode(); return TableRow( decoration: const BoxDecoration( color: Color(0xFF121212), ), children: [ TableCell( child: Padding( padding: rowPadding, child: InkWell( onTap: () async { if (track.number != -1) { playTrack(context, track); } }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Consumer( builder: (context, playerProvider, _) { return Text(track.title, style: TextStyle( fontWeight: FontWeight.normal, color: currentTrack == track ? Colors.green : Colors.grey[350], fontSize: 14)); }), const SizedBox(height: 5), if (track.number != -1) Text(track.artiste, style: TextStyle( fontWeight: FontWeight.normal, color: Colors.grey[500], fontSize: 13)) ]), )), ), TableCell( child: Padding( padding: rowPadding, child: Text(track.album ?? '', style: textStyle), ), ), TableCell( child: Padding( padding: rowPadding, child: InkWell( onTap: () async { isDownloading = track.number; downloadProvider.reload(); if (track.id == null) { final secondMatch = track.artiste == '' ? track.album : track.artiste; final resultUrl = await yt.search .search(track.title + ' ' + secondMatch!); track.id = resultUrl.first.id.value; } if (track.id != null) { final response = await http.get(Uri.parse( 'https://ytdl.p2p.legal/ytdl.sh?${track.id}')); final dlLink = response.body; if (await canLaunchUrl(Uri.parse(dlLink))) { launchUrl(Uri.parse(dlLink)); } } isDownloading = -1; downloadProvider.reload(); }, child: track.number == -1 ? Text('TÉLÉCHARGER', style: textStyle) : Consumer( builder: (context, playerProvider, _) { return Row(children: [ const SizedBox(width: 37), isDownloading == track.number ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.grey[500], ), ) : Icon(Icons.download, color: Colors.grey[500]), ]); }))), ), ]); } Future playTrack(BuildContext context, Track track) async { var yt = YoutubeExplode(); PlayerProvider playerProvider = Provider.of(context, listen: false); HomeProvider homeProvider = Provider.of(context, listen: false); track = trackList[track.number - 1]; currentTrack = track; if (track.id == null) { final secondMatch = track.artiste == '' ? track.album : track.artiste; final resultUrl = await yt.search.search(track.title + ' ' + secondMatch!); track.id = resultUrl.first.id.value; } final currentVolume = player?.volume ?? 1; player?.dispose(); Future.delayed(const Duration(milliseconds: 5)); player = Player.network( "https://invidious.fdn.fr/embed/${track.id}?raw=1&?listen=1"); print(track.id); player!.volume = currentVolume; try { player!.play(); } catch (e) { print('Play error: ' + e.toString()); } Future.delayed(const Duration(milliseconds: 500)); player!.callback = (PlayerEvent event) { if (event.name == 'position') { currentTrack!.duration = player!.duration; playerProvider.reload(); } if (event.name == 'status') { var nextTrack = trackList.firstWhere((element) => element.number == track.number + 1); playTrack(context, nextTrack); } }; playerProvider.reload(); homeProvider.reload(); } // final radioList = [ // 'fip', // 'electro', // 'groove', // 'rock', // 'jazz', // 'pop', // 'reggae', // 'world', // 'nouveautes', // ]; List> get radioList { List> menuItems = [ const DropdownMenuItem(child: Text("FIP"), value: "fip"), const DropdownMenuItem(child: Text("Electro"), value: "electro"), const DropdownMenuItem(child: Text("Groove"), value: "groove"), const DropdownMenuItem(child: Text("Rock"), value: "rock"), const DropdownMenuItem(child: Text("Jazz"), value: "jazz"), const DropdownMenuItem(child: Text("Pop"), value: "pop"), const DropdownMenuItem(child: Text("Reggae"), value: "reggae"), const DropdownMenuItem(child: Text("World"), value: "world"), const DropdownMenuItem(child: Text("Nouveautes"), value: "nouveautes"), ]; return menuItems; } String timeFormat(Duration d) { String dd = d.toString().split('.').first; String minutes = dd.toString().split(':')[1]; String seconds = dd.toString().split(':')[2]; return '$minutes:$seconds'; }