import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:logging/logging.dart'; import 'package:network_info_plus/network_info_plus.dart'; import 'package:rxdart/rxdart.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 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), ElevatedButton( child: Text('Login using other device'.i18n), onPressed: () { showDialog( context: context, builder: (context) => OtherDeviceLogin(_update)); }), const SizedBox(height: 16.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 (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 StatefulWidget { final Function updateParent; const LoginBrowser(this.updateParent, {super.key}); @override State createState() => _LoginBrowserState(); } class _LoginBrowserState extends State { late final WebViewController _controller; @override void dispose() { super.dispose(); } @override void initState() { _controller = WebViewController() ..clearLocalStorage() ..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) { settings.arl = arl; Navigator.of(context).pop(); widget.updateParent(); 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 Widget build(BuildContext context) { return SafeArea( child: WebViewWidget( controller: _controller, ), ); } } class OtherDeviceLogin extends StatefulWidget { final VoidCallback callback; const OtherDeviceLogin(this.callback, {super.key}); @override State createState() => _OtherDeviceLoginState(); } class _OtherDeviceLoginState extends State { late final HttpServer _server; late final StreamSubscription _serverSubscription; late final String? _deviceIP; late final Future _serverReady; late int _code; late Timer _codeTimer; final _timerNotifier = ValueNotifier(0.0); bool step2 = false; final _logger = Logger('OtherDeviceLogin'); void _generateCode() { _code = Random.secure().nextInt(899999) + 100000; } Future _initServer() async { _server = await HttpServer.bind(InternetAddress.anyIPv4, 0); _deviceIP = await NetworkInfo().getWifiIP(); _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()); } }); _serverSubscription = _server.listen((request) async { final buffer = Uint8List(0); final reqCompleter = Completer(); final subs = request.listen( (data) { if (data.length + buffer.length > 8192) { _logger.severe('Request too big!'); request.response.close(); } buffer.addAll(data); }, onDone: () { reqCompleter.complete(); }, ); await reqCompleter.future; subs.cancel(); try { jsonDecode(utf8.decode(buffer)); } catch (e) { _logger.severe('Error $e'); request.response.close(); } }); } @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 ? 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: 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, input the parameters below and follow the on-screen instructions.' .i18n), RichText( textAlign: TextAlign.start, text: TextSpan( style: Theme.of(context).textTheme.bodyMedium, children: [ TextSpan(text: 'IP Address: '.i18n), TextSpan( text: _deviceIP ?? 'Could not get IP!', style: TextStyle(fontSize: 32.sp)), const TextSpan(text: ':'), TextSpan( text: _server.port.toString(), style: TextStyle(fontSize: 32.sp)), ])), RichText( text: TextSpan( style: Theme.of(context).textTheme.bodyMedium, children: [ TextSpan(text: 'Code: '.i18n), TextSpan( text: '$_code', style: TextStyle(fontSize: 32.sp)), ])), ], ), ) ]); }), 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 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'), // ) // ], // ); // } // }