import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:freezer/api/deezer.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:url_launcher/url_launcher_string.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 createState() => _LoginWidgetState(); } class _LoginWidgetState extends State { 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: [ TextButton( child: Text('Dismiss'.i18n), onPressed: () { Navigator.of(context).pop(); }, ) ], ); }); } void _update() async { 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(); settings.arl = _arl.trim(); Navigator.of(context).pop(); _update(); } 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; }); if (settings.arl == null) { return Scaffold( body: 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: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 700.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, 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: 16.0), //Email login dialog 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 (Platform.isAndroid) ElevatedButton( onPressed: _loginBrowser, child: Text('Login using browser'.i18n), ), const SizedBox(height: 2.0), ElevatedButton( child: Text('Login using token'.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: [ TextButton( child: Text('Save'.i18n), onPressed: () => goARL(null, controller), ) ], ); }); }, ), ]))), 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), ) ], ), ), ), ), )); } return const SizedBox(); } } 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 StatelessWidget { final Function updateParent; const LoginBrowser(this.updateParent, {super.key}); @override Widget build(BuildContext context) { return SafeArea( child: InAppWebView( initialUrlRequest: URLRequest(url: Uri.parse('https://deezer.com/login')), onLoadStart: (InAppWebViewController controller, Uri? uri) async { //Offers URL if (!uri!.path.contains('/login') && !uri.path.contains('/register')) { controller.evaluateJavascript( source: 'window.location.href = "/open_app"'); } print('scheme ${uri.scheme}, host: ${uri.host}'); //Parse arl from url if (uri.scheme == 'intent' && 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) { settings.arl = arl; Navigator.of(context).pop(); updateParent(); } } catch (e) { print(e); } } }, ), ); } } class EmailLogin extends StatefulWidget { final Function callback; const EmailLogin(this.callback, {Key? key}) : super(key: key); @override State createState() => _EmailLoginState(); } class _EmailLoginState extends State { 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'), ) ], ); } }