freezer/lib/ui/login_screen.dart
Pato05 2862c9ec05
remove browser login for desktop
restore translations functionality
make scrollViews handle mouse pointers like touch, so that pull to refresh functionality is available
exit app if opening cache or settings fails (another instance running)
remove draggable_scrollbar and use builtin widget instead
fix email login
better way to manage lyrics (less updates and lookups in the lyrics List)
fix player_screen on mobile (too big -> just average :))
right click: use TapUp events instead
desktop: show context menu on triple dots button also
avoid showing connection error if the homepage is cached and available offline
i'm probably forgetting something idk
2023-10-25 00:32:28 +02:00

474 lines
16 KiB
Dart

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<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() 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: <Widget>[
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: <Widget>[
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<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'),
)
],
);
}
}