Pato05
87c9733f51
fix audio service stop on android getTrack backend improvements get new track token when expired move shuffle button into LibraryPlaylists as FAB move favoriteButton next to track title move lyrics button on top of album art search: fix chips, and remove checkbox when selected
838 lines
29 KiB
Dart
838 lines
29 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:encrypt/encrypt.dart' as encrypt;
|
|
import 'package:pointycastle/asymmetric/api.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/translations.i18n.dart';
|
|
import 'package:freezer/utils.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:network_info_plus/network_info_plus.dart';
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
|
import 'package:webview_flutter/webview_flutter.dart';
|
|
|
|
import '../settings.dart';
|
|
import '../api/definitions.dart';
|
|
import 'home_screen.dart';
|
|
|
|
class LoginWidget extends StatefulWidget {
|
|
final Function? callback;
|
|
const LoginWidget({this.callback, Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<LoginWidget> createState() => _LoginWidgetState();
|
|
}
|
|
|
|
class _LoginWidgetState extends State<LoginWidget> {
|
|
late String _arl;
|
|
String? _error;
|
|
|
|
//Initialize deezer etc
|
|
Future _init() async {
|
|
deezerAPI.arl = settings.arl;
|
|
|
|
//Pre-cache homepage
|
|
if (!await HomePage.exists('')) {
|
|
await deezerAPI.authorize();
|
|
settings.offlineMode = false;
|
|
HomePage hp = await deezerAPI.homePage();
|
|
await hp.save('');
|
|
}
|
|
}
|
|
|
|
//Call _init()
|
|
void _start() async {
|
|
if (settings.arl != null) {
|
|
_init().then((_) {
|
|
if (widget.callback != null) widget.callback!();
|
|
});
|
|
}
|
|
}
|
|
|
|
//Check if deezer available in current country
|
|
void _checkAvailability() async {
|
|
bool? available = await DeezerAPI.chceckAvailability();
|
|
if (!(available ?? true)) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text("Deezer is unavailable".i18n),
|
|
content: Text(
|
|
"Deezer is unavailable in your country, Freezer might not work properly. Please use a VPN"
|
|
.i18n),
|
|
actions: [
|
|
TextButton(
|
|
child: Text('Continue'.i18n),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
)
|
|
],
|
|
));
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(LoginWidget oldWidget) {
|
|
_start();
|
|
super.didUpdateWidget(oldWidget);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
_start();
|
|
_checkAvailability();
|
|
super.initState();
|
|
}
|
|
|
|
void errorDialog() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return AlertDialog(
|
|
title: Text('Error'.i18n),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'Error logging in! Please check your token and internet connection and try again.'
|
|
.i18n),
|
|
if (_error != null) Text('\n\n$_error')
|
|
],
|
|
),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text('Dismiss'.i18n),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
)
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
void _update(String arl) async {
|
|
settings.arl = arl;
|
|
await settings.save();
|
|
setState(() => {});
|
|
|
|
//Try logging in
|
|
try {
|
|
deezerAPI.arl = settings.arl;
|
|
bool resp = await deezerAPI.rawAuthorize(
|
|
onError: (e) => setState(() => _error = e.toString()));
|
|
if (resp == false) {
|
|
//false, not null
|
|
if (settings.arl!.length != 192) {
|
|
_error ??= '';
|
|
_error = 'Invalid ARL length!';
|
|
}
|
|
setState(() => settings.arl = null);
|
|
errorDialog();
|
|
}
|
|
//On error show dialog and reset to null
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
print('Login error: $e');
|
|
setState(() => settings.arl = null);
|
|
errorDialog();
|
|
}
|
|
|
|
await settings.save();
|
|
_start();
|
|
}
|
|
|
|
// ARL auth: called on "Save" click, Enter and DPAD_Center press
|
|
void goARL(FocusNode? node, TextEditingController controller) {
|
|
if (node != null) {
|
|
node.unfocus();
|
|
}
|
|
controller.clear();
|
|
Navigator.of(context).pop();
|
|
_update(_arl.trim());
|
|
}
|
|
|
|
void _loginBrowser() async {
|
|
Navigator.of(context)
|
|
.pushRoute(builder: (context) => LoginBrowser(_update));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
//If arl non null, show loading
|
|
if (settings.arl != null) {
|
|
return const Scaffold(
|
|
body: Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
}
|
|
TextEditingController controller = TextEditingController();
|
|
// For "DPAD center" key handling on remote controls
|
|
FocusNode focusNode = FocusNode(
|
|
skipTraversal: true,
|
|
descendantsAreFocusable: false,
|
|
onKey: (node, event) {
|
|
if (event.logicalKey == LogicalKeyboardKey.select) {
|
|
goARL(node, controller);
|
|
}
|
|
return KeyEventResult.handled;
|
|
});
|
|
return Scaffold(
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
|
|
child: Theme(
|
|
data: Theme.of(context).copyWith(
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: ButtonStyle(
|
|
foregroundColor:
|
|
MaterialStateProperty.all(Colors.white)))),
|
|
//data: ThemeData(
|
|
// outlinedButtonTheme: OutlinedButtonThemeData(
|
|
// style: ButtonStyle(
|
|
// foregroundColor:
|
|
// MaterialStateProperty.all(Colors.white)))),
|
|
child: SingleChildScrollView(
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 700.0),
|
|
child: Column(
|
|
children: <Widget>[
|
|
ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
minHeight:
|
|
MediaQuery.of(context).size.height - 250.0),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const FreezerTitle(),
|
|
const SizedBox(height: 16.0),
|
|
Text(
|
|
"Please login using your Deezer account."
|
|
.i18n,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(fontSize: 16.0),
|
|
),
|
|
const SizedBox(height: 32.0),
|
|
ElevatedButton(
|
|
child:
|
|
Text('Login using other device'.i18n),
|
|
onPressed: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) =>
|
|
OtherDeviceLogin(_update));
|
|
}),
|
|
const SizedBox(height: 2.0),
|
|
//Email login dialog (Not working anymore)
|
|
// ElevatedButton(
|
|
// child: Text(
|
|
// 'Login using email'.i18n,
|
|
// ),
|
|
// onPressed: () {
|
|
// showDialog(
|
|
// context: context,
|
|
// builder: (context) =>
|
|
// EmailLogin(_update));
|
|
// },
|
|
// ),
|
|
// const SizedBox(height: 2.0),
|
|
|
|
// only supported on android
|
|
if (LoginBrowser.supported()) ...[
|
|
ElevatedButton(
|
|
onPressed: _loginBrowser,
|
|
child: Text('Login using browser'.i18n),
|
|
),
|
|
const SizedBox(height: 2.0),
|
|
],
|
|
ElevatedButton(
|
|
child: Text('Login with ARL'.i18n),
|
|
onPressed: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
Future.delayed(
|
|
const Duration(seconds: 1),
|
|
() => {
|
|
focusNode.requestFocus()
|
|
}); // autofocus doesn't work - it's replacement
|
|
return AlertDialog(
|
|
title: Text('Enter ARL'.i18n),
|
|
content: TextField(
|
|
onChanged: (String s) => _arl = s,
|
|
decoration: InputDecoration(
|
|
labelText:
|
|
'Token (ARL)'.i18n),
|
|
focusNode: focusNode,
|
|
controller: controller,
|
|
onSubmitted: (String s) {
|
|
goARL(focusNode, controller);
|
|
},
|
|
),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text('Save'.i18n),
|
|
onPressed: () =>
|
|
goARL(null, controller),
|
|
)
|
|
],
|
|
);
|
|
});
|
|
},
|
|
),
|
|
])),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const SizedBox(height: 16.0),
|
|
Text(
|
|
"If you don't have account, you can register on deezer.com for free."
|
|
.i18n,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(fontSize: 16.0),
|
|
),
|
|
const SizedBox(height: 8.0),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
|
child: ElevatedButton(
|
|
child: Text('Open in browser'.i18n),
|
|
onPressed: () {
|
|
launchUrlString('https://deezer.com/register');
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 8.0),
|
|
const Divider(),
|
|
const SizedBox(height: 8.0),
|
|
Text(
|
|
"By using this app, you don't agree with the Deezer ToS"
|
|
.i18n,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(fontSize: 14.0),
|
|
)
|
|
],
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
}
|
|
}
|
|
|
|
class LoadingWindowWait extends StatelessWidget {
|
|
final VoidCallback update;
|
|
const LoadingWindowWait({super.key, required this.update});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'Please login by using the floating window'.i18n,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 16.0),
|
|
const CircularProgressIndicator(),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class LoginBrowser extends StatefulWidget {
|
|
static bool supported() => Platform.isAndroid || Platform.isIOS;
|
|
|
|
final void Function(String arl) arlCallback;
|
|
const LoginBrowser(this.arlCallback, {super.key});
|
|
|
|
@override
|
|
State<LoginBrowser> createState() => _LoginBrowserState();
|
|
}
|
|
|
|
class _LoginBrowserState extends State<LoginBrowser> {
|
|
late final WebViewController _controller;
|
|
|
|
@override
|
|
void initState() {
|
|
_controller = WebViewController()
|
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
|
..setBackgroundColor(const Color(0x00000000))
|
|
// Chrome on Android 14
|
|
..setUserAgent(
|
|
'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.101 Mobile Safari/537.36')
|
|
..setNavigationDelegate(
|
|
NavigationDelegate(
|
|
onProgress: (int progress) {
|
|
// Update loading bar.
|
|
},
|
|
onPageStarted: (String url) {},
|
|
onPageFinished: (String url) {},
|
|
onWebResourceError: (WebResourceError error) {},
|
|
onNavigationRequest: (NavigationRequest request) {
|
|
final uri = Uri.parse(request.url);
|
|
print('uri $uri');
|
|
|
|
//Parse arl from url
|
|
if (uri.host == 'deezer.page.link') {
|
|
try {
|
|
//Actual url is in `link` query parameter
|
|
Uri linkUri = Uri.parse(uri.queryParameters['link']!);
|
|
String? arl = linkUri.queryParameters['arl'];
|
|
if (arl != null) {
|
|
Navigator.of(context).pop();
|
|
widget.arlCallback(arl);
|
|
return NavigationDecision.prevent;
|
|
}
|
|
} catch (e) {
|
|
print(e);
|
|
}
|
|
} else if (!uri.path.contains('/login') &&
|
|
!uri.path.contains('/register') &&
|
|
!uri.path.contains('/open_app')) {
|
|
_controller.loadRequest(Uri.parse(
|
|
'https://deezer.com/open_app?page=home&source=MF_MenuDownloadApp'));
|
|
return NavigationDecision.prevent;
|
|
}
|
|
|
|
return NavigationDecision.navigate;
|
|
},
|
|
),
|
|
)
|
|
..loadRequest(Uri.parse('https://deezer.com/login'));
|
|
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// clear everything
|
|
unawaited(_controller.clearCache());
|
|
unawaited(_controller.clearLocalStorage());
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: WebViewWidget(
|
|
controller: _controller,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class OtherDeviceLogin extends StatefulWidget {
|
|
final void Function(String arl) arlCallback;
|
|
const OtherDeviceLogin(this.arlCallback, {super.key});
|
|
|
|
@override
|
|
State<OtherDeviceLogin> createState() => _OtherDeviceLoginState();
|
|
}
|
|
|
|
class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
|
|
late final HttpServer _server;
|
|
late final StreamSubscription _serverSubscription;
|
|
late final String? _deviceIP;
|
|
late final Future<void> _serverReady;
|
|
late int _code;
|
|
late Timer _codeTimer;
|
|
final _timerNotifier = ValueNotifier<double>(0.0);
|
|
bool _step2 = false;
|
|
final _logger = Logger('OtherDeviceLogin');
|
|
|
|
void _generateCode() {
|
|
_code = Random.secure().nextInt(899999) + 100000;
|
|
}
|
|
|
|
void _initTimer() {
|
|
_generateCode();
|
|
const tps = 30 * 1000 / 50;
|
|
_codeTimer = Timer.periodic(const Duration(milliseconds: 50), (timer) {
|
|
final a = timer.tick / tps;
|
|
_timerNotifier.value = a - a.truncate();
|
|
if (timer.tick % tps == 0) {
|
|
setState(() => _generateCode());
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _initServer() async {
|
|
_server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
|
|
_deviceIP = await NetworkInfo().getWifiIP();
|
|
_initTimer();
|
|
|
|
encrypt.Key? key;
|
|
|
|
void invalidRequest(HttpRequest request) {
|
|
request.response.statusCode = 400;
|
|
request.response.close();
|
|
}
|
|
|
|
_serverSubscription = _server.listen((request) async {
|
|
final buffer = <int>[];
|
|
final reqCompleter = Completer<void>();
|
|
final subs = request.listen(
|
|
(data) {
|
|
if (data.length + buffer.length > 8192) {
|
|
_logger.severe('Request too big!');
|
|
request.response.close();
|
|
reqCompleter.completeError("Request too big!");
|
|
return;
|
|
}
|
|
|
|
buffer.addAll(data);
|
|
},
|
|
onDone: () {
|
|
reqCompleter.complete();
|
|
},
|
|
);
|
|
try {
|
|
await reqCompleter.future;
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
subs.cancel();
|
|
late final Map data;
|
|
try {
|
|
data = jsonDecode(utf8.decode(buffer));
|
|
} catch (e) {
|
|
_logger.severe('Error $e');
|
|
return invalidRequest(request);
|
|
}
|
|
|
|
// check if data is correct
|
|
if (!data.containsKey('_') || data['_'] is! String) {
|
|
return invalidRequest(request);
|
|
}
|
|
|
|
print(data);
|
|
switch (data['_']) {
|
|
case 'handshake':
|
|
if (key != null) {
|
|
request.response.statusCode = 400;
|
|
request.response.headers.contentType = ContentType.json;
|
|
request.response.write(jsonEncode({
|
|
'ok': false,
|
|
'message': 'Another client has already done the handshake.'
|
|
}));
|
|
request.response.close();
|
|
return;
|
|
}
|
|
if (!data.containsKey('pubKey') ||
|
|
data['pubKey'] is! List ||
|
|
data['pubKey'].length != 2 ||
|
|
!data.containsKey('hash') ||
|
|
data['hash'] is! String) {
|
|
return invalidRequest(request);
|
|
}
|
|
|
|
final pubKey = RSAPublicKey(
|
|
Utils.deserializeBigInt(base64.decode(data['pubKey'][0])),
|
|
Utils.deserializeBigInt(base64.decode(data['pubKey'][1])));
|
|
final hash = Hmac(sha512, Utils.splitDigits(_code))
|
|
.convert(
|
|
Utils.serializeBigInt(pubKey.exponent! ^ pubKey.modulus!))
|
|
.toString();
|
|
if (hash != data['hash']) {
|
|
request.response.statusCode = 403;
|
|
request.response.write(
|
|
jsonEncode({'ok': false, 'message': 'Hash doesn\'t match'}));
|
|
request.response.close();
|
|
return;
|
|
}
|
|
|
|
final encrypter = encrypt.Encrypter(encrypt.RSA(publicKey: pubKey));
|
|
|
|
key = encrypt.Key.fromSecureRandom(32);
|
|
final encryptedKey = encrypter.encryptBytes(key!.bytes);
|
|
|
|
request.response.headers.contentType = ContentType.json;
|
|
request.response.write(jsonEncode({
|
|
'ok': true,
|
|
'key': encryptedKey.base64,
|
|
}));
|
|
request.response.close();
|
|
_codeTimer.cancel();
|
|
setState(() => _step2 = true);
|
|
break;
|
|
case 'arl':
|
|
if (!data.containsKey('arl') ||
|
|
data['arl'] is! String ||
|
|
!data.containsKey('iv') ||
|
|
data['iv'] is! String) {
|
|
return invalidRequest(request);
|
|
}
|
|
|
|
final encryptedArl = data['arl'] as String;
|
|
final iv = data['iv'] as String;
|
|
final encrypter = encrypt.Encrypter(encrypt.AES(key!));
|
|
final decryptedArl = encrypter.decrypt(
|
|
encrypt.Encrypted.fromBase64(encryptedArl),
|
|
iv: encrypt.IV.fromBase64(iv));
|
|
if (decryptedArl.length != 192) {
|
|
request.response.headers.contentType = ContentType.json;
|
|
request.response.write(
|
|
jsonEncode({'ok': false, 'message': 'Wrong ARL length!'}));
|
|
request.response.close();
|
|
return;
|
|
}
|
|
|
|
request.response.headers.contentType = ContentType.json;
|
|
request.response.write(jsonEncode({'ok': true}));
|
|
request.response.close();
|
|
|
|
Navigator.pop(context);
|
|
widget.arlCallback(decryptedArl);
|
|
break;
|
|
case 'cancel':
|
|
if (!data.containsKey('hash') || data['hash'] is! String) {
|
|
return invalidRequest(request);
|
|
}
|
|
|
|
final hash = Hmac(sha512, key!.bytes)
|
|
.convert(utf8.encode(_code.toString()))
|
|
.toString();
|
|
if (hash != data['hash']) {
|
|
request.response.statusCode = 403;
|
|
request.response.write(
|
|
jsonEncode({'ok': false, 'message': 'Hash doesn\'t match'}));
|
|
request.response.close();
|
|
return;
|
|
}
|
|
|
|
key = null;
|
|
_initTimer();
|
|
setState(() {
|
|
_step2 = false;
|
|
});
|
|
|
|
request.response.close();
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
_serverReady = _initServer();
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_server.close();
|
|
_serverSubscription.cancel();
|
|
_codeTimer.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Login using other device'.i18n),
|
|
contentPadding: const EdgeInsets.only(top: 12),
|
|
content: _step2
|
|
? Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const LinearProgressIndicator(),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 18, 24, 24),
|
|
child: Text('Please follow the on-screen instructions'.i18n,
|
|
style: Theme.of(context).textTheme.bodyLarge),
|
|
),
|
|
])
|
|
: FutureBuilder(
|
|
future: _serverReady,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState != ConnectionState.done) {
|
|
return const Row(children: [CircularProgressIndicator()]);
|
|
}
|
|
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ValueListenableBuilder(
|
|
valueListenable: _timerNotifier,
|
|
builder: (context, value, _) =>
|
|
LinearProgressIndicator(value: 1.0 - value),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 18, 24, 24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'On your other device, go to Freezer\'s settings > General > Login on other device and input the parameters below'
|
|
.i18n,
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
),
|
|
RichText(
|
|
text: TextSpan(
|
|
style:
|
|
Theme.of(context).textTheme.bodyMedium,
|
|
children: [
|
|
TextSpan(text: 'IP Address: '.i18n),
|
|
TextSpan(
|
|
text:
|
|
'${_deviceIP ?? 'Could not get IP!'}:${_server.port}',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.displaySmall),
|
|
])),
|
|
RichText(
|
|
text: TextSpan(
|
|
style:
|
|
Theme.of(context).textTheme.bodyMedium,
|
|
children: [
|
|
TextSpan(text: 'Code: '.i18n),
|
|
TextSpan(
|
|
text: '$_code',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.displaySmall),
|
|
])),
|
|
],
|
|
),
|
|
)
|
|
]);
|
|
}),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context), child: Text('Cancel'.i18n))
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// email login is removed cuz not working = USELESS
|
|
|
|
//class EmailLogin extends StatefulWidget {
|
|
// final Function callback;
|
|
// const EmailLogin(this.callback, {Key? key}) : super(key: key);
|
|
//
|
|
// @override
|
|
// State<EmailLogin> createState() => _EmailLoginState();
|
|
//}
|
|
//
|
|
//class _EmailLoginState extends State<EmailLogin> {
|
|
// final _emailController = TextEditingController();
|
|
// final _passwordController = TextEditingController();
|
|
// bool _loading = false;
|
|
//
|
|
// Future _login() async {
|
|
// setState(() => _loading = true);
|
|
// //Try logging in
|
|
// String? arl;
|
|
// String? exception;
|
|
// try {
|
|
// arl = await deezerAPI.getArlByEmail(
|
|
// _emailController.text, _passwordController.text);
|
|
// } catch (e, st) {
|
|
// exception = e.toString();
|
|
// print(e);
|
|
// print(st);
|
|
// }
|
|
// setState(() => _loading = false);
|
|
//
|
|
// //Success
|
|
// if (arl != null) {
|
|
// settings.arl = arl;
|
|
// Navigator.of(context).pop();
|
|
// widget.callback();
|
|
// return;
|
|
// }
|
|
//
|
|
// //Error
|
|
// showDialog(
|
|
// context: context,
|
|
// builder: (context) => AlertDialog(
|
|
// title: Text("Error logging in!".i18n),
|
|
// content: Text(
|
|
// "Error logging in using email, please check your credentials.\nError: ${exception ?? 'Unknown'}"),
|
|
// actions: [
|
|
// TextButton(
|
|
// child: Text('Dismiss'.i18n),
|
|
// onPressed: () {
|
|
// Navigator.of(context).pop();
|
|
// },
|
|
// )
|
|
// ],
|
|
// ));
|
|
// }
|
|
//
|
|
// @override
|
|
// Widget build(BuildContext context) {
|
|
// return AlertDialog(
|
|
// title: Text('Email Login'.i18n),
|
|
// content: AutofillGroup(
|
|
// child: Column(
|
|
// mainAxisSize: MainAxisSize.min,
|
|
// children: [
|
|
// TextFormField(
|
|
// enabled: !_loading,
|
|
// decoration: InputDecoration(labelText: 'Email'.i18n),
|
|
// controller: _emailController,
|
|
// autofillHints: const [
|
|
// AutofillHints.email,
|
|
// ],
|
|
// ),
|
|
// const SizedBox(height: 8.0),
|
|
// TextFormField(
|
|
// enabled: !_loading,
|
|
// obscureText: true,
|
|
// decoration: InputDecoration(labelText: "Password".i18n),
|
|
// controller: _passwordController,
|
|
// autofillHints: const [
|
|
// AutofillHints.password,
|
|
// ],
|
|
// )
|
|
// ],
|
|
// ),
|
|
// ),
|
|
// actions: [
|
|
// TextButton(
|
|
// onPressed: _loading
|
|
// ? null
|
|
// : () async {
|
|
// if (_emailController.text.isNotEmpty &&
|
|
// _passwordController.text.isNotEmpty) {
|
|
// await _login();
|
|
// } else {
|
|
// ScaffoldMessenger.of(context)
|
|
// .snack("Missing email or password!".i18n);
|
|
// }
|
|
// },
|
|
// child: _loading
|
|
// ? const CircularProgressIndicator()
|
|
// : const Text('Login'),
|
|
// )
|
|
// ],
|
|
// );
|
|
// }
|
|
// }
|