Pato05
2862c9ec05
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
474 lines
16 KiB
Dart
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'),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|