2023-10-20 23:12:33 +00:00
|
|
|
import 'dart:io';
|
|
|
|
|
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';
|
2023-10-20 23:12:33 +00:00
|
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
2023-07-29 02:17:26 +00:00
|
|
|
|
|
|
|
import '../settings.dart';
|
|
|
|
import '../api/definitions.dart';
|
|
|
|
import 'home_screen.dart';
|
|
|
|
|
|
|
|
class LoginWidget extends StatefulWidget {
|
|
|
|
final Function? callback;
|
2023-10-12 22:09:37 +00:00
|
|
|
const LoginWidget({this.callback, Key? key}) : super(key: key);
|
2023-07-29 02:17:26 +00:00
|
|
|
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
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
|
2023-09-26 00:06:59 +00:00
|
|
|
if (!await HomePage.exists('')) {
|
2023-07-29 02:17:26 +00:00
|
|
|
await deezerAPI.authorize();
|
|
|
|
settings.offlineMode = false;
|
|
|
|
HomePage hp = await deezerAPI.homePage();
|
2023-09-26 00:06:59 +00:00
|
|
|
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) {
|
2023-10-12 22:09:37 +00:00
|
|
|
_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();
|
2023-10-12 22:09:37 +00:00
|
|
|
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
|
2023-10-12 22:09:37 +00:00
|
|
|
void goARL(FocusNode? node, TextEditingController controller) {
|
2023-07-29 02:17:26 +00:00
|
|
|
if (node != null) {
|
|
|
|
node.unfocus();
|
|
|
|
}
|
2023-10-12 22:09:37 +00:00
|
|
|
controller.clear();
|
2023-07-29 02:17:26 +00:00
|
|
|
settings.arl = _arl.trim();
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
_update();
|
|
|
|
}
|
|
|
|
|
2023-10-20 23:12:33 +00:00
|
|
|
void _loginBrowser() async {
|
|
|
|
Navigator.of(context)
|
|
|
|
.pushRoute(builder: (context) => LoginBrowser(_update));
|
|
|
|
}
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
//If arl non null, show loading
|
2023-10-12 22:09:37 +00:00
|
|
|
if (settings.arl != null) {
|
|
|
|
return const Scaffold(
|
2023-07-29 02:17:26 +00:00
|
|
|
body: Center(
|
|
|
|
child: CircularProgressIndicator(),
|
|
|
|
),
|
|
|
|
);
|
2023-10-12 22:09:37 +00:00
|
|
|
}
|
|
|
|
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) {
|
2023-10-12 22:09:37 +00:00
|
|
|
goARL(node, controller);
|
2023-07-29 02:17:26 +00:00
|
|
|
}
|
|
|
|
return KeyEventResult.handled;
|
|
|
|
});
|
2023-10-12 22:09:37 +00:00
|
|
|
if (settings.arl == null) {
|
2023-07-29 02:17:26 +00:00
|
|
|
return Scaffold(
|
|
|
|
body: Padding(
|
2023-10-12 22:09:37 +00:00
|
|
|
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)))),
|
2023-10-24 22:32:28 +00:00
|
|
|
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),
|
|
|
|
)
|
|
|
|
],
|
2023-07-29 02:17:26 +00:00
|
|
|
),
|
2023-10-24 22:32:28 +00:00
|
|
|
),
|
2023-07-29 02:17:26 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
));
|
2023-10-12 22:09:37 +00:00
|
|
|
}
|
2023-07-29 02:17:26 +00:00
|
|
|
return const SizedBox();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-20 23:12:33 +00:00
|
|
|
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(),
|
|
|
|
]),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
class LoginBrowser extends StatelessWidget {
|
|
|
|
final Function updateParent;
|
2023-10-12 22:09:37 +00:00
|
|
|
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;
|
2023-10-12 22:09:37 +00:00
|
|
|
const EmailLogin(this.callback, {Key? key}) : super(key: key);
|
2023-07-29 02:17:26 +00:00
|
|
|
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
State<EmailLogin> createState() => _EmailLoginState();
|
2023-07-29 02:17:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class _EmailLoginState extends State<EmailLogin> {
|
2023-10-20 23:12:33 +00:00
|
|
|
final _emailController = TextEditingController();
|
|
|
|
final _passwordController = TextEditingController();
|
2023-07-29 02:17:26 +00:00
|
|
|
bool _loading = false;
|
|
|
|
|
|
|
|
Future _login() async {
|
|
|
|
setState(() => _loading = true);
|
|
|
|
//Try logging in
|
|
|
|
String? arl;
|
|
|
|
String? exception;
|
|
|
|
try {
|
2023-10-20 23:12:33 +00:00
|
|
|
arl = await deezerAPI.getArlByEmail(
|
|
|
|
_emailController.text, _passwordController.text);
|
2023-07-29 02:17:26 +00:00
|
|
|
} 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(
|
2023-10-12 22:09:37 +00:00
|
|
|
"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),
|
2023-10-24 22:32:28 +00:00
|
|
|
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,
|
2023-07-29 02:17:26 +00:00
|
|
|
],
|
2023-10-24 22:32:28 +00:00
|
|
|
)
|
|
|
|
],
|
|
|
|
),
|
2023-07-29 02:17:26 +00:00
|
|
|
),
|
|
|
|
actions: [
|
2023-10-24 22:32:28 +00:00
|
|
|
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'),
|
|
|
|
)
|
2023-07-29 02:17:26 +00:00
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|