freezer/lib/ui/importer_screen.dart

690 lines
22 KiB
Dart
Raw Normal View History

2023-07-29 02:17:26 +00:00
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
2023-07-29 02:17:26 +00:00
import 'package:freezer/api/definitions.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/api/importer.dart';
import 'package:freezer/api/spotify.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:spotify/spotify.dart' as spotify;
import 'package:url_launcher/url_launcher.dart';
import 'dart:async';
class SpotifyImporterV1 extends StatefulWidget {
const SpotifyImporterV1({super.key});
2023-07-29 02:17:26 +00:00
@override
State<SpotifyImporterV1> createState() => _SpotifyImporterV1State();
2023-07-29 02:17:26 +00:00
}
class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
late String _url;
bool _error = false;
bool _loading = false;
SpotifyPlaylist? _data;
//Load URL
Future _load() async {
setState(() {
_error = false;
_loading = true;
});
try {
String? uri = await SpotifyScrapper.resolveUrl(_url);
//Error/NonPlaylist
if (uri == null || uri.split(':')[1] != 'playlist') {
throw Exception();
}
//Load
SpotifyPlaylist data = await SpotifyScrapper.playlist(uri);
setState(() => _data = data);
return;
} catch (e) {
2023-07-29 02:17:26 +00:00
setState(() {
_error = true;
_loading = false;
});
return;
}
}
//Start importing
Future _start() async {
List<ImporterTrack> tracks = _data!.toImporter();
await importer.start(context, _data!.name, _data!.description, tracks);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Importer'.i18n)),
body: ListView(
children: <Widget>[
ListTile(
title: Text(
'Currently supporting only Spotify, with 100 tracks limit'
.i18n),
subtitle: Text('Due to API limitations'.i18n),
leading: const Icon(
2023-07-29 02:17:26 +00:00
Icons.warning,
color: Colors.deepOrangeAccent,
),
),
const FreezerDivider(),
2023-07-29 02:17:26 +00:00
Container(
height: 16.0,
),
Text(
'Enter your playlist link below'.i18n,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20.0),
2023-07-29 02:17:26 +00:00
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
2023-07-29 02:17:26 +00:00
child: Row(
children: <Widget>[
Expanded(
child: TextField(
onChanged: (String s) => _url = s,
onSubmitted: (String s) {
_url = s;
_load();
},
decoration: const InputDecoration(hintText: 'URL'),
2023-07-29 02:17:26 +00:00
),
),
IconButton(
icon: Icon(
Icons.search,
semanticLabel: "Search".i18n,
),
onPressed: () => _load(),
)
],
),
),
Container(
height: 8.0,
),
if (_data == null && _loading)
const Row(
2023-07-29 02:17:26 +00:00
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[CircularProgressIndicator()],
),
if (_error)
ListTile(
title: Text('Error loading URL!'.i18n),
leading: const Icon(
2023-07-29 02:17:26 +00:00
Icons.error,
color: Colors.red,
),
),
//Playlist
if (_data != null) ...[
const FreezerDivider(),
2023-07-29 02:17:26 +00:00
ListTile(
title: Text(_data!.name!),
subtitle: Text((_data!.description ?? '') == ''
? '${_data!.tracks!.length} tracks'
: _data!.description!),
leading: Image.network(_data!.image ??
'http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg')),
const ImporterSettings(),
2023-07-29 02:17:26 +00:00
Padding(
padding:
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
2023-07-29 02:17:26 +00:00
child: ElevatedButton(
child: Text('Start import'.i18n),
onPressed: () async {
final navigator = Navigator.of(context);
2023-07-29 02:17:26 +00:00
await _start();
navigator.pushReplacement(MaterialPageRoute(
builder: (context) => const ImporterStatusScreen()));
2023-07-29 02:17:26 +00:00
},
),
),
]
],
),
);
}
}
class ImporterSettings extends StatefulWidget {
const ImporterSettings({super.key});
2023-07-29 02:17:26 +00:00
@override
State<ImporterSettings> createState() => _ImporterSettingsState();
2023-07-29 02:17:26 +00:00
}
class _ImporterSettingsState extends State<ImporterSettings> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text('Download imported tracks'.i18n),
leading: Switch(
value: importer.download,
onChanged: (v) => setState(() => importer.download = v),
),
),
],
);
}
}
class ImporterStatusScreen extends StatefulWidget {
const ImporterStatusScreen({super.key});
2023-07-29 02:17:26 +00:00
@override
State<ImporterStatusScreen> createState() => _ImporterStatusScreenState();
2023-07-29 02:17:26 +00:00
}
class _ImporterStatusScreenState extends State<ImporterStatusScreen> {
bool _done = false;
StreamSubscription? _subscription;
@override
void initState() {
//If import done mark as not done, to prevent double routing
if (importer.done) {
_done = true;
importer.done = false;
}
//Update
_subscription = importer.updateStream.listen((event) {
setState(() {
//Unset done so this page doesn't reopen
if (importer.done) {
_done = true;
importer.done = false;
}
});
});
super.initState();
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Importing...'.i18n)),
body: ListView(
children: [
// Spinner
if (!_done)
const Padding(
2023-07-29 02:17:26 +00:00
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[CircularProgressIndicator()],
),
),
// Progress indicator
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(
Icons.import_export,
size: 24.0,
),
Container(
width: 4.0,
),
Text(
'${importer.ok + importer.error}/${importer.tracks.length}',
style: const TextStyle(fontSize: 24.0),
2023-07-29 02:17:26 +00:00
)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(
Icons.done,
size: 24.0,
),
Container(
width: 4.0,
),
Text(
'${importer.ok}',
style: const TextStyle(fontSize: 24.0),
)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(
Icons.error,
size: 24.0,
),
Container(
width: 4.0,
),
Text(
'${importer.error}',
style: const TextStyle(fontSize: 24.0),
),
],
),
//When Done
if (_done)
TextButton(
child: Text('Playlist menu'.i18n),
onPressed: () {
MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(importer.playlist!);
},
)
],
2023-07-29 02:17:26 +00:00
),
const SizedBox(height: 8.0),
const FreezerDivider(),
2023-07-29 02:17:26 +00:00
//Tracks
...List.generate(importer.tracks.length, (i) {
ImporterTrack t = importer.tracks[i];
return ListTile(
leading: t.state.icon,
title: Text(t.title!),
subtitle: Text(
t.artists!.join(", "),
maxLines: 1,
),
);
})
],
),
);
}
}
class SpotifyImporterV2 extends StatefulWidget {
const SpotifyImporterV2({super.key});
2023-07-29 02:17:26 +00:00
@override
State<SpotifyImporterV2> createState() => _SpotifyImporterV2State();
2023-07-29 02:17:26 +00:00
}
class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
bool _authorizing = false;
String? _clientId;
String? _clientSecret;
final SpotifyAPIWrapper spotify = SpotifyAPIWrapper();
//Spotify authorization flow
Future _authorize() async {
setState(() => _authorizing = true);
await spotify.authorize(_clientId, _clientSecret);
//Save credentials
settings.spotifyClientId = _clientId;
settings.spotifyClientSecret = _clientSecret;
await settings.save();
setState(() => _authorizing = false);
//Redirect
if (context.mounted) {
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => SpotifyImporterV2Main(spotify)));
}
2023-07-29 02:17:26 +00:00
}
@override
void initState() {
_clientId = settings.spotifyClientId;
_clientSecret = settings.spotifyClientSecret;
//Try saved
spotify.trySaved().then((r) {
if (r) {
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => SpotifyImporterV2Main(spotify)));
}
});
super.initState();
}
@override
void dispose() {
//Stop server
spotify.cancelAuthorize();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Spotify Importer v2".i18n)),
body: ListView(
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
2023-07-29 02:17:26 +00:00
child: Text(
"This importer requires Spotify Client ID and Client Secret. To obtain them:"
.i18n,
textAlign: TextAlign.center,
style: const TextStyle(
2023-07-29 02:17:26 +00:00
fontSize: 18.0,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
2023-07-29 02:17:26 +00:00
child: Text(
"1. Go to: developer.spotify.com/dashboard and create an app."
.i18n,
textAlign: TextAlign.center,
style: const TextStyle(
2023-07-29 02:17:26 +00:00
fontSize: 16.0,
),
)),
Padding(
padding:
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
2023-07-29 02:17:26 +00:00
child: ElevatedButton(
child: Text("Open in Browser".i18n),
onPressed: () {
launchUrl(Uri.parse("https://developer.spotify.com/dashboard"));
},
),
),
const SizedBox(height: 16.0),
2023-07-29 02:17:26 +00:00
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
2023-07-29 02:17:26 +00:00
child: Text(
"${"2. In the app you just created go to settings, and set the Redirect URL to: ".i18n}http://localhost:42069",
2023-07-29 02:17:26 +00:00
textAlign: TextAlign.center,
style: const TextStyle(
2023-07-29 02:17:26 +00:00
fontSize: 16.0,
),
)),
Padding(
padding:
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
2023-07-29 02:17:26 +00:00
child: ElevatedButton(
child: Text("Copy the Redirect URL".i18n),
onPressed: () async {
await Clipboard.setData(
const ClipboardData(text: "http://localhost:42069"));
Fluttertoast.showToast(msg: "Copied".i18n);
2023-07-29 02:17:26 +00:00
},
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
2023-07-29 02:17:26 +00:00
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
child: TextField(
controller: TextEditingController(text: _clientId),
decoration: InputDecoration(labelText: "Client ID".i18n),
onChanged: (v) => setState(() => _clientId = v),
),
),
const SizedBox(width: 16.0),
Flexible(
child: TextField(
controller: TextEditingController(text: _clientSecret),
obscureText: true,
decoration:
InputDecoration(labelText: "Client Secret".i18n),
onChanged: (v) => setState(() => _clientSecret = v),
),
),
],
),
),
Padding(
padding:
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
2023-07-29 02:17:26 +00:00
child: ElevatedButton(
onPressed: (_clientId != null &&
_clientSecret != null &&
!_authorizing)
? () => _authorize()
: null,
child: Text("Authorize".i18n)),
2023-07-29 02:17:26 +00:00
),
if (_authorizing)
const Padding(
2023-07-29 02:17:26 +00:00
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [CircularProgressIndicator()],
),
)
],
),
);
}
}
class SpotifyImporterV2Main extends StatefulWidget {
final SpotifyAPIWrapper spotify;
const SpotifyImporterV2Main(this.spotify, {Key? key}) : super(key: key);
2023-07-29 02:17:26 +00:00
@override
State<SpotifyImporterV2Main> createState() => _SpotifyImporterV2MainState();
2023-07-29 02:17:26 +00:00
}
class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
late String _url;
bool _urlLoading = false;
spotify.Playlist? _urlPlaylist;
bool _playlistsLoading = true;
List<spotify.PlaylistSimple>? _playlists;
@override
void initState() {
_loadPlaylists();
super.initState();
}
//Load playlists
Future _loadPlaylists() async {
var pages = widget.spotify.spotify.users.playlists(widget.spotify.me.id!);
_playlists = List.from(await pages.all());
setState(() => _playlistsLoading = false);
}
Future _loadUrl() async {
setState(() => _urlLoading = true);
//Resolve URL
try {
String? uri = await SpotifyScrapper.resolveUrl(_url);
//Error/NonPlaylist
if (uri == null || uri.split(':')[1] != 'playlist') {
throw Exception();
}
//Get playlist
spotify.Playlist playlist =
await widget.spotify.spotify.playlists.get(uri.split(":")[2]);
setState(() {
_urlLoading = false;
_urlPlaylist = playlist;
});
} catch (e) {
Fluttertoast.showToast(msg: "Invalid/Unsupported URL".i18n);
2023-07-29 02:17:26 +00:00
setState(() => _urlLoading = false);
return;
}
}
Future _startImport(String? title, String? description, String? id) async {
//Show loading dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => WillPopScope(
onWillPop: () => Future.value(false),
child: AlertDialog(
title: Text("Please wait...".i18n),
content: const Row(
2023-07-29 02:17:26 +00:00
mainAxisAlignment: MainAxisAlignment.center,
children: [CircularProgressIndicator()],
))));
try {
//Fetch entire playlist
var pages = widget.spotify.spotify.playlists.getTracksByPlaylistId(id);
var all = await pages.all();
//Map to importer track
List<ImporterTrack> tracks = all
.map((t) => ImporterTrack(
t.name, t.artists!.map((a) => a.name).toList(),
isrc: t.externalIds!.isrc))
.toList();
if (!context.mounted) return;
2023-07-29 02:17:26 +00:00
await importer.start(context, title, description, tracks);
if (!context.mounted) return;
2023-07-29 02:17:26 +00:00
//Route
Navigator.of(context).pop();
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => const ImporterStatusScreen()));
2023-07-29 02:17:26 +00:00
} catch (e) {
ScaffoldMessenger.of(context).snack(e.toString());
Navigator.of(context).pop();
return;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Spotify Importer v2".i18n)),
body: ListView(
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
2023-07-29 02:17:26 +00:00
child: Text(
'Logged in as: '.i18n + widget.spotify.me.displayName!,
maxLines: 1,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18.0, fontWeight: FontWeight.bold)),
2023-07-29 02:17:26 +00:00
),
const FreezerDivider(),
2023-07-29 02:17:26 +00:00
Container(height: 4.0),
Text(
"Options".i18n,
textAlign: TextAlign.center,
style:
const TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),
2023-07-29 02:17:26 +00:00
),
const ImporterSettings(),
const FreezerDivider(),
2023-07-29 02:17:26 +00:00
Container(height: 4.0),
Text(
"Import playlists by URL".i18n,
textAlign: TextAlign.center,
style:
const TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),
2023-07-29 02:17:26 +00:00
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
2023-07-29 02:17:26 +00:00
child: Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(hintText: "URL".i18n),
onChanged: (v) => setState(() => _url = v)),
),
IconButton(
icon: const Icon(Icons.search),
2023-07-29 02:17:26 +00:00
onPressed: () => _loadUrl(),
)
],
)),
if (_urlLoading)
const Row(
2023-07-29 02:17:26 +00:00
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: CircularProgressIndicator(),
)
],
),
if (_urlPlaylist != null)
ListTile(
title: Text(_urlPlaylist!.name!),
subtitle: Text(_urlPlaylist!.description ?? ''),
leading: Image.network(_urlPlaylist!.images!.first.url ??
"http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg")),
if (_urlPlaylist != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
2023-07-29 02:17:26 +00:00
child: ElevatedButton(
child: Text("Import".i18n),
onPressed: () {
_startImport(_urlPlaylist!.name,
_urlPlaylist!.description, _urlPlaylist!.id);
})),
// Playlists
const FreezerDivider(),
2023-07-29 02:17:26 +00:00
Container(height: 4.0),
Text("Playlists".i18n,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18.0, fontWeight: FontWeight.bold)),
2023-07-29 02:17:26 +00:00
Container(height: 4.0),
if (_playlistsLoading)
const Row(
2023-07-29 02:17:26 +00:00
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: CircularProgressIndicator(),
)
],
),
if (!_playlistsLoading && _playlists != null)
...List.generate(_playlists!.length, (i) {
spotify.PlaylistSimple p = _playlists![i];
return ListTile(
title: Text(p.name!, maxLines: 1),
subtitle: Text(p.owner!.displayName!, maxLines: 1),
leading: Image.network(p.images!.first.url ??
"http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg"),
onTap: () {
_startImport(p.name, "", p.id);
},
);
})
],
));
}
}