Big improvements, usable and pretty app

This commit is contained in:
poka 2022-04-28 21:24:26 +02:00
parent 1b288c2655
commit 9fefb780d1
7 changed files with 588 additions and 303 deletions

4
lib/globals.dart Normal file
View File

@ -0,0 +1,4 @@
import 'package:flutter/material.dart';
late String token;
TextStyle globalTextStyle = TextStyle(color: Colors.grey[350]);

View File

@ -1,22 +1,13 @@
// ignore_for_file: prefer_const_literals_to_create_immutables, avoid_print
import 'dart:convert';
import 'package:fip_parser_ui/queries.dart';
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:fip_parser_ui/screens/home.dart';
import 'package:flutter/material.dart';
import 'package:graphql/client.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';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import 'package:kplayer/kplayer.dart';
const int queryTimeout = 8;
late String token;
const hours = 3;
const String radio = 'groove';
PlayerController? player;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
Player.boot();
@ -30,294 +21,17 @@ class FipyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// final radioList = [
// 'fip',
// 'electro',
// 'groove',
// 'rock',
// 'jazz',
// 'pop',
// 'reggae',
// 'world',
// 'nouveautes',
// ];
// #######
return MaterialApp(
title: 'Fipy',
theme: ThemeData(
primarySwatch: Colors.blue, backgroundColor: Colors.grey[900]),
home: const HomePage(title: 'Fip moi ça'),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
color: Colors.grey[900],
height: 50,
child: Center(
child: Text(
'Radio: ' + radio + '\nHistorique: ' + hours.toString() + 'h',
style: globalTextStyle,
),
),
),
FutureBuilder<List<Track>>(
future: getTracks(),
builder: (
BuildContext context,
AsyncSnapshot<List<Track>> snapshot,
) {
print(snapshot.connectionState);
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Table(
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: snapshot.data!
.map((item) => _buildTableRow(item))
.toList()
..insert(
0,
_buildTableRow(Track(
number: -1,
title: 'Titre',
artiste: 'Artiste',
album: 'Album',
id: 'URL')),
),
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
},
),
],
),
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => PlayerProvider()),
ChangeNotifierProvider(create: (_) => HomeProvider()),
],
child: MaterialApp(
title: 'Fipy',
theme: ThemeData(
primarySwatch: Colors.blue, backgroundColor: Colors.grey[900]),
home: const HomeScreen(title: 'Fipy'),
),
);
}
}
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;
Track(
{required this.number,
required this.title,
required this.artiste,
this.album,
this.id});
}
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<List<Track>> getTracks() async {
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: <String, dynamic>{
'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');
// #######
List<Track> songsList = [];
int trackNbr = 0;
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 thisSong =
Track(number: trackNbr, title: title, artiste: artiste, album: album);
songsList.add(thisSong);
// resultUrl = yt.search.search(title + ' ' + artiste);
// resultUrlPool.add(resultUrl);
// resultUrl.then((value) {
// try {
// thisSong.url = value.first.url;
// } catch (e) {
// thisSong.url = '';
// }
// songsList.add(thisSong);
// });
}
// for (var searchTrack in resultUrlPool) {
// await searchTrack;
// }
songsList.sort((a, b) => a.number.compareTo(b.number));
return songsList;
}
TableRow _buildTableRow(Track track) {
final textStyle = TextStyle(
fontWeight: track.number == -1 ? FontWeight.w700 : FontWeight.normal,
color: Colors.grey[350]);
const rowPadding = EdgeInsets.all(10);
var yt = YoutubeExplode();
return TableRow(
decoration: BoxDecoration(
color: Colors.grey[850],
),
children: [
TableCell(
child: Padding(
padding: rowPadding,
child: InkWell(
onTap: () async {
if (track.id == null) {
final resultUrl = await yt.search
.search(track.title + ' ' + track.artiste);
track.id = resultUrl.first.id.value;
}
if (track.id != null) {
// Map<String, String> header = {
// 'Access-Control-Allow-Origin': '*',
// };
final response = await http.get(
Uri.parse('https://ytdl.p2p.legal/ytdl.sh?${track.id}'),
);
final dlLink = response.body.replaceFirst('/dl/', '/play/');
print(dlLink);
player?.dispose();
Future.delayed(const Duration(milliseconds: 5));
player = Player.network(dlLink, autoPlay: true);
// player = Player.asset("assets/uka.mp3");
player!.play();
}
},
child: Text(track.title, style: textStyle),
)),
),
TableCell(
child: Padding(
padding: rowPadding,
child: Text(track.artiste, style: textStyle),
),
),
TableCell(
child: Padding(
padding: rowPadding,
child: Text(track.album ?? '', style: textStyle),
),
),
TableCell(
child: Padding(
padding: rowPadding,
child: InkWell(
onTap: () async {
if (track.id == null) {
final resultUrl = await yt.search
.search(track.title + ' ' + track.artiste);
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));
}
Clipboard.setData(ClipboardData(text: dlLink));
}
},
child: Text('Télécharger', style: textStyle),
)),
),
]);
}
TextStyle globalTextStyle = TextStyle(color: Colors.grey[350]);

7
lib/providers/home.dart Normal file
View File

@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
class HomeProvider with ChangeNotifier {
void reload() {
notifyListeners();
}
}

View File

@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
class PlayerProvider with ChangeNotifier {
void reload() {
notifyListeners();
}
}

530
lib/screens/home.dart Normal file
View File

@ -0,0 +1,530 @@
// 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<Track> trackList = [];
String radio = 'groove';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final MiniplayerController controller = MiniplayerController();
const hours = 8;
return Stack(
children: <Widget>[
Scaffold(
body: SingleChildScrollView(
child: Column(
children: <Widget>[
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<List<Track>>(
future: getTracks(radio, hours),
builder: (
BuildContext context,
AsyncSnapshot<List<Track>> 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(
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<PlayerProvider>(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],
),
),
),
]),
),
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!);
}
}),
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: 32),
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(12),
primary: Colors.grey[300],
onPrimary: Colors.grey[900],
),
),
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 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<List<Track>> 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: <String, dynamic>{
'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) {
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<PlayerProvider>(
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 {
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));
}
}
},
child: Text('TÉLÉCHARGER', style: textStyle),
)),
),
]);
}
Future playTrack(BuildContext context, Track track) async {
var yt = YoutubeExplode();
PlayerProvider playerProvider =
Provider.of<PlayerProvider>(context, listen: false);
HomeProvider homeProvider = Provider.of<HomeProvider>(context, listen: false);
track = trackList[track.number - 1];
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;
}
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);
try {
player!.play();
} catch (e) {
print('Play error: ' + e.toString());
}
Future.delayed(const Duration(milliseconds: 50));
player!.callback = (PlayerEvent event) {
if (event.name == 'status') {
var nextTrack =
trackList.firstWhere((element) => element.number == track.number + 1);
playTrack(context, nextTrack);
}
};
currentTrack = track;
playerProvider.reload();
homeProvider.reload();
}
// final radioList = [
// 'fip',
// 'electro',
// 'groove',
// 'rock',
// 'jazz',
// 'pop',
// 'reggae',
// 'world',
// 'nouveautes',
// ];
List<DropdownMenuItem<String>> get radioList {
List<DropdownMenuItem<String>> 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;
}

View File

@ -317,6 +317,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
miniplayer:
dependency: "direct main"
description:
path: "/home/poka/dev/flutter/miniplayer"
relative: false
source: path
version: "1.0.1"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
normalize:
dependency: transitive
description:
@ -408,6 +422,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.4"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.2"
retry:
dependency: "direct main"
description:

View File

@ -12,13 +12,15 @@ dependencies:
sdk: flutter
cupertino_icons: ^1.0.2
graphql: ^5.0.0
# youtube_explode_dart: ^1.10.9+1
youtube_explode_dart:
youtube_explode_dart: #^1.10.9+1
path: /home/poka/dev/flutter/youtube_explode_dart
retry: ^3.1.0
url_launcher: ^6.1.0
http: ^0.13.4
kplayer: ^0.1.12
miniplayer: # ^1.0.1
path: /home/poka/dev/flutter/miniplayer
provider: ^6.0.1
dev_dependencies:
flutter_test: