freezer/lib/ui/login_screen.dart

427 lines
14 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:freezer/api/deezer.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:freezer/translations.i18n.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);
2023-07-29 02:17:26 +00:00
@override
State<LoginWidget> createState() => _LoginWidgetState();
2023-07-29 02:17:26 +00:00
}
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('')) {
2023-07-29 02:17:26 +00:00
await deezerAPI.authorize();
settings.offlineMode = false;
HomePage hp = await deezerAPI.homePage();
await hp.save('');
2023-07-29 02:17:26 +00:00
}
}
//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() 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 ??= '';
2023-07-29 02:17:26 +00:00
_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');
2023-07-29 02:17:26 +00:00
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) {
2023-07-29 02:17:26 +00:00
if (node != null) {
node.unfocus();
}
controller.clear();
2023-07-29 02:17:26 +00:00
settings.arl = _arl.trim();
Navigator.of(context).pop();
_update();
}
@override
Widget build(BuildContext context) {
//If arl non null, show loading
if (settings.arl != null) {
return const Scaffold(
2023-07-29 02:17:26 +00:00
body: Center(
child: CircularProgressIndicator(),
),
);
}
TextEditingController controller = TextEditingController();
2023-07-29 02:17:26 +00:00
// 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);
2023-07-29 02:17:26 +00:00
}
return KeyEventResult.handled;
});
if (settings.arl == null) {
2023-07-29 02:17:26 +00:00
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
2023-07-29 02:17:26 +00:00
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: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
2023-07-29 02:17:26 +00:00
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const FreezerTitle(),
2023-07-29 02:17:26 +00:00
Container(height: 16.0),
Text(
"Please login using your Deezer account.".i18n,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16.0),
2023-07-29 02:17:26 +00:00
),
Container(
height: 16.0,
),
//Email login dialog
OutlinedButton(
child: Text(
'Login using email'.i18n,
),
onPressed: () {
showDialog(
context: context,
builder: (context) => EmailLogin(_update));
},
),
OutlinedButton(
child: Text('Login using browser'.i18n),
onPressed: () {
Navigator.of(context).pushRoute(
builder: (context) =>
LoginBrowser(_update));
},
),
OutlinedButton(
child: Text('Login using token'.i18n),
onPressed: () {
showDialog(
context: context,
builder: (context) {
Future.delayed(
const Duration(seconds: 1),
2023-07-29 02:17:26 +00:00
() => {
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);
},
2023-07-29 02:17:26 +00:00
),
actions: <Widget>[
TextButton(
child: Text('Save'.i18n),
onPressed: () =>
goARL(null, controller),
2023-07-29 02:17:26 +00:00
)
],
);
});
},
),
]))),
Container(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),
2023-07-29 02:17:26 +00:00
),
Container(height: 8.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
2023-07-29 02:17:26 +00:00
child: OutlinedButton(
child: Text('Open in browser'.i18n),
onPressed: () {
InAppBrowser.openWithSystemBrowser(
url: Uri.parse('https://deezer.com/register'));
},
),
),
Container(
height: 8.0,
),
const Divider(),
2023-07-29 02:17:26 +00:00
Container(
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),
2023-07-29 02:17:26 +00:00
)
],
),
),
));
}
2023-07-29 02:17:26 +00:00
return const SizedBox();
}
}
class LoginBrowser extends StatelessWidget {
final Function updateParent;
const LoginBrowser(this.updateParent, {super.key});
2023-07-29 02:17:26 +00:00
@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);
2023-07-29 02:17:26 +00:00
@override
State<EmailLogin> createState() => _EmailLoginState();
2023-07-29 02:17:26 +00:00
}
class _EmailLoginState extends State<EmailLogin> {
String? _email;
String? _password;
bool _loading = false;
Future _login() async {
setState(() => _loading = true);
//Try logging in
String? arl;
String? exception;
try {
arl = await DeezerAPI.getArlByEmail(_email, _password!);
} 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'}"),
2023-07-29 02:17:26 +00:00
actions: [
TextButton(
child: Text('Dismiss'.i18n),
onPressed: () {
Navigator.of(context).pop();
},
)
],
));
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Email Login'.i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: _loading
? [const CircularProgressIndicator()]
2023-07-29 02:17:26 +00:00
: [
TextField(
decoration: InputDecoration(labelText: 'Email'.i18n),
onChanged: (s) => _email = s,
),
Container(
height: 8.0,
),
TextField(
obscureText: true,
decoration: InputDecoration(labelText: "Password".i18n),
onChanged: (s) => _password = s,
)
],
),
actions: [
if (!_loading)
TextButton(
child: const Text('Login'),
2023-07-29 02:17:26 +00:00
onPressed: () async {
if (_email != null && _password != null) {
2023-07-29 02:17:26 +00:00
await _login();
} else {
2023-07-29 02:17:26 +00:00
ScaffoldMessenger.of(context)
.snack("Missing email or password!".i18n);
}
2023-07-29 02:17:26 +00:00
},
)
],
);
}
}