2024-02-10 17:13:27 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:convert';
|
2023-10-20 23:12:33 +00:00
|
|
|
import 'dart:io';
|
2024-02-10 17:13:27 +00:00
|
|
|
import 'dart:math';
|
2023-10-20 23:12:33 +00:00
|
|
|
|
2024-02-12 02:37:26 +00:00
|
|
|
import 'package:crypto/crypto.dart';
|
|
|
|
import 'package:encrypt/encrypt.dart' as encrypt;
|
|
|
|
import 'package:pointycastle/asymmetric/api.dart';
|
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:freezer/translations.i18n.dart';
|
2024-02-12 02:37:26 +00:00
|
|
|
import 'package:freezer/utils.dart';
|
2024-02-10 17:13:27 +00:00
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:network_info_plus/network_info_plus.dart';
|
2023-10-20 23:12:33 +00:00
|
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
2024-01-29 00:24:04 +00:00
|
|
|
import 'package:webview_flutter/webview_flutter.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;
|
2024-03-01 18:22:57 +00:00
|
|
|
final deezerAPI = DeezerAPI.instance;
|
2023-07-29 02:17:26 +00:00
|
|
|
|
|
|
|
//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();
|
|
|
|
},
|
|
|
|
)
|
|
|
|
],
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-02-13 01:48:39 +00:00
|
|
|
void _update(String arl) async {
|
|
|
|
settings.arl = arl;
|
|
|
|
await settings.save();
|
2023-07-29 02:17:26 +00:00
|
|
|
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
|
|
|
Navigator.of(context).pop();
|
2024-02-13 01:48:39 +00:00
|
|
|
_update(_arl.trim());
|
2023-07-29 02:17:26 +00:00
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
});
|
2024-02-18 23:49:32 +00:00
|
|
|
return Scaffold(
|
|
|
|
body: SafeArea(
|
|
|
|
child: 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)))),
|
2024-02-18 23:49:32 +00:00
|
|
|
child: SingleChildScrollView(
|
|
|
|
child: Center(
|
|
|
|
child: ConstrainedBox(
|
|
|
|
constraints: const BoxConstraints(maxWidth: 700.0),
|
|
|
|
child: Column(
|
|
|
|
children: <Widget>[
|
|
|
|
ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(
|
|
|
|
minHeight:
|
|
|
|
MediaQuery.of(context).size.height - 250.0),
|
2023-10-24 22:32:28 +00:00
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
2024-02-18 23:49:32 +00:00
|
|
|
mainAxisSize: MainAxisSize.min,
|
2023-10-24 22:32:28 +00:00
|
|
|
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),
|
|
|
|
),
|
2024-02-12 02:37:26 +00:00
|
|
|
const SizedBox(height: 32.0),
|
2024-02-10 17:13:27 +00:00
|
|
|
ElevatedButton(
|
|
|
|
child:
|
|
|
|
Text('Login using other device'.i18n),
|
|
|
|
onPressed: () {
|
|
|
|
showDialog(
|
|
|
|
context: context,
|
|
|
|
builder: (context) =>
|
|
|
|
OtherDeviceLogin(_update));
|
|
|
|
}),
|
2024-02-13 01:48:39 +00:00
|
|
|
const SizedBox(height: 2.0),
|
2024-01-28 11:02:59 +00:00
|
|
|
//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),
|
2023-10-24 22:32:28 +00:00
|
|
|
|
|
|
|
// only supported on android
|
2024-02-13 01:48:39 +00:00
|
|
|
if (LoginBrowser.supported()) ...[
|
2023-10-24 22:32:28 +00:00
|
|
|
ElevatedButton(
|
|
|
|
onPressed: _loginBrowser,
|
|
|
|
child: Text('Login using browser'.i18n),
|
|
|
|
),
|
2024-02-12 02:37:26 +00:00
|
|
|
const SizedBox(height: 2.0),
|
|
|
|
],
|
2023-10-24 22:32:28 +00:00
|
|
|
ElevatedButton(
|
2024-02-13 01:48:39 +00:00
|
|
|
child: Text('Login with ARL'.i18n),
|
2023-10-24 22:32:28 +00:00
|
|
|
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),
|
|
|
|
)
|
|
|
|
],
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
),
|
2024-02-18 23:49:32 +00:00
|
|
|
])),
|
2023-10-24 22:32:28 +00:00
|
|
|
),
|
2024-02-18 23:49:32 +00:00
|
|
|
Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: [
|
|
|
|
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
|
|
|
),
|
|
|
|
),
|
2024-02-18 23:49:32 +00:00
|
|
|
),
|
|
|
|
));
|
2023-07-29 02:17:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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(),
|
|
|
|
]),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-29 00:24:04 +00:00
|
|
|
class LoginBrowser extends StatefulWidget {
|
2024-02-13 01:48:39 +00:00
|
|
|
static bool supported() => Platform.isAndroid || Platform.isIOS;
|
|
|
|
|
|
|
|
final void Function(String arl) arlCallback;
|
|
|
|
const LoginBrowser(this.arlCallback, {super.key});
|
2023-07-29 02:17:26 +00:00
|
|
|
|
|
|
|
@override
|
2024-01-29 00:24:04 +00:00
|
|
|
State<LoginBrowser> createState() => _LoginBrowserState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _LoginBrowserState extends State<LoginBrowser> {
|
|
|
|
late final WebViewController _controller;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
_controller = WebViewController()
|
|
|
|
..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) {
|
|
|
|
Navigator.of(context).pop();
|
2024-02-13 01:48:39 +00:00
|
|
|
widget.arlCallback(arl);
|
2024-01-29 00:24:04 +00:00
|
|
|
return NavigationDecision.prevent;
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
print(e);
|
2023-07-29 02:17:26 +00:00
|
|
|
}
|
2024-01-29 00:24:04 +00:00
|
|
|
} 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;
|
2023-07-29 02:17:26 +00:00
|
|
|
}
|
2024-01-29 00:24:04 +00:00
|
|
|
|
|
|
|
return NavigationDecision.navigate;
|
|
|
|
},
|
|
|
|
),
|
|
|
|
)
|
|
|
|
..loadRequest(Uri.parse('https://deezer.com/login'));
|
|
|
|
|
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
2024-02-13 16:53:25 +00:00
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
// clear everything
|
|
|
|
unawaited(_controller.clearCache());
|
|
|
|
unawaited(_controller.clearLocalStorage());
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
2024-01-29 00:24:04 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return SafeArea(
|
|
|
|
child: WebViewWidget(
|
|
|
|
controller: _controller,
|
2023-07-29 02:17:26 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-10 17:13:27 +00:00
|
|
|
class OtherDeviceLogin extends StatefulWidget {
|
2024-02-13 01:48:39 +00:00
|
|
|
final void Function(String arl) arlCallback;
|
|
|
|
const OtherDeviceLogin(this.arlCallback, {super.key});
|
2024-02-10 17:13:27 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
State<OtherDeviceLogin> createState() => _OtherDeviceLoginState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _OtherDeviceLoginState extends State<OtherDeviceLogin> {
|
|
|
|
late final HttpServer _server;
|
|
|
|
late final StreamSubscription _serverSubscription;
|
|
|
|
late final String? _deviceIP;
|
|
|
|
late final Future<void> _serverReady;
|
|
|
|
late int _code;
|
|
|
|
late Timer _codeTimer;
|
|
|
|
final _timerNotifier = ValueNotifier<double>(0.0);
|
2024-02-12 02:37:26 +00:00
|
|
|
bool _step2 = false;
|
2024-02-10 17:13:27 +00:00
|
|
|
final _logger = Logger('OtherDeviceLogin');
|
|
|
|
|
|
|
|
void _generateCode() {
|
|
|
|
_code = Random.secure().nextInt(899999) + 100000;
|
|
|
|
}
|
|
|
|
|
2024-02-12 02:37:26 +00:00
|
|
|
void _initTimer() {
|
2024-02-10 17:13:27 +00:00
|
|
|
_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());
|
|
|
|
}
|
|
|
|
});
|
2024-02-12 02:37:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _initServer() async {
|
|
|
|
_server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
|
|
|
|
_deviceIP = await NetworkInfo().getWifiIP();
|
|
|
|
_initTimer();
|
|
|
|
|
|
|
|
encrypt.Key? key;
|
|
|
|
|
|
|
|
void invalidRequest(HttpRequest request) {
|
|
|
|
request.response.statusCode = 400;
|
|
|
|
request.response.close();
|
|
|
|
}
|
|
|
|
|
2024-02-10 17:13:27 +00:00
|
|
|
_serverSubscription = _server.listen((request) async {
|
2024-02-12 02:37:26 +00:00
|
|
|
final buffer = <int>[];
|
2024-02-10 17:13:27 +00:00
|
|
|
final reqCompleter = Completer<void>();
|
|
|
|
final subs = request.listen(
|
|
|
|
(data) {
|
|
|
|
if (data.length + buffer.length > 8192) {
|
|
|
|
_logger.severe('Request too big!');
|
|
|
|
request.response.close();
|
2024-02-12 02:37:26 +00:00
|
|
|
reqCompleter.completeError("Request too big!");
|
|
|
|
return;
|
2024-02-10 17:13:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
buffer.addAll(data);
|
|
|
|
},
|
|
|
|
onDone: () {
|
|
|
|
reqCompleter.complete();
|
|
|
|
},
|
|
|
|
);
|
2024-02-12 02:37:26 +00:00
|
|
|
try {
|
|
|
|
await reqCompleter.future;
|
|
|
|
} catch (e) {
|
|
|
|
return;
|
|
|
|
}
|
2024-02-10 17:13:27 +00:00
|
|
|
subs.cancel();
|
2024-02-12 02:37:26 +00:00
|
|
|
late final Map data;
|
2024-02-10 17:13:27 +00:00
|
|
|
try {
|
2024-02-12 02:37:26 +00:00
|
|
|
data = jsonDecode(utf8.decode(buffer));
|
2024-02-10 17:13:27 +00:00
|
|
|
} catch (e) {
|
|
|
|
_logger.severe('Error $e');
|
2024-02-12 02:37:26 +00:00
|
|
|
return invalidRequest(request);
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if data is correct
|
|
|
|
if (!data.containsKey('_') || data['_'] is! String) {
|
|
|
|
return invalidRequest(request);
|
|
|
|
}
|
|
|
|
|
|
|
|
print(data);
|
|
|
|
switch (data['_']) {
|
|
|
|
case 'handshake':
|
|
|
|
if (key != null) {
|
|
|
|
request.response.statusCode = 400;
|
|
|
|
request.response.headers.contentType = ContentType.json;
|
|
|
|
request.response.write(jsonEncode({
|
|
|
|
'ok': false,
|
|
|
|
'message': 'Another client has already done the handshake.'
|
|
|
|
}));
|
|
|
|
request.response.close();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!data.containsKey('pubKey') ||
|
|
|
|
data['pubKey'] is! List ||
|
|
|
|
data['pubKey'].length != 2 ||
|
|
|
|
!data.containsKey('hash') ||
|
|
|
|
data['hash'] is! String) {
|
|
|
|
return invalidRequest(request);
|
|
|
|
}
|
|
|
|
|
|
|
|
final pubKey = RSAPublicKey(
|
|
|
|
Utils.deserializeBigInt(base64.decode(data['pubKey'][0])),
|
|
|
|
Utils.deserializeBigInt(base64.decode(data['pubKey'][1])));
|
|
|
|
final hash = Hmac(sha512, Utils.splitDigits(_code))
|
|
|
|
.convert(
|
|
|
|
Utils.serializeBigInt(pubKey.exponent! ^ pubKey.modulus!))
|
|
|
|
.toString();
|
|
|
|
if (hash != data['hash']) {
|
|
|
|
request.response.statusCode = 403;
|
|
|
|
request.response.write(
|
|
|
|
jsonEncode({'ok': false, 'message': 'Hash doesn\'t match'}));
|
|
|
|
request.response.close();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
final encrypter = encrypt.Encrypter(encrypt.RSA(publicKey: pubKey));
|
|
|
|
|
|
|
|
key = encrypt.Key.fromSecureRandom(32);
|
|
|
|
final encryptedKey = encrypter.encryptBytes(key!.bytes);
|
|
|
|
|
|
|
|
request.response.headers.contentType = ContentType.json;
|
|
|
|
request.response.write(jsonEncode({
|
|
|
|
'ok': true,
|
|
|
|
'key': encryptedKey.base64,
|
|
|
|
}));
|
|
|
|
request.response.close();
|
|
|
|
_codeTimer.cancel();
|
|
|
|
setState(() => _step2 = true);
|
|
|
|
break;
|
|
|
|
case 'arl':
|
|
|
|
if (!data.containsKey('arl') ||
|
|
|
|
data['arl'] is! String ||
|
|
|
|
!data.containsKey('iv') ||
|
|
|
|
data['iv'] is! String) {
|
|
|
|
return invalidRequest(request);
|
|
|
|
}
|
|
|
|
|
|
|
|
final encryptedArl = data['arl'] as String;
|
|
|
|
final iv = data['iv'] as String;
|
|
|
|
final encrypter = encrypt.Encrypter(encrypt.AES(key!));
|
|
|
|
final decryptedArl = encrypter.decrypt(
|
|
|
|
encrypt.Encrypted.fromBase64(encryptedArl),
|
|
|
|
iv: encrypt.IV.fromBase64(iv));
|
|
|
|
if (decryptedArl.length != 192) {
|
|
|
|
request.response.headers.contentType = ContentType.json;
|
|
|
|
request.response.write(
|
|
|
|
jsonEncode({'ok': false, 'message': 'Wrong ARL length!'}));
|
|
|
|
request.response.close();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
request.response.headers.contentType = ContentType.json;
|
|
|
|
request.response.write(jsonEncode({'ok': true}));
|
|
|
|
request.response.close();
|
|
|
|
|
|
|
|
Navigator.pop(context);
|
2024-02-13 01:48:39 +00:00
|
|
|
widget.arlCallback(decryptedArl);
|
2024-02-12 02:37:26 +00:00
|
|
|
break;
|
|
|
|
case 'cancel':
|
|
|
|
if (!data.containsKey('hash') || data['hash'] is! String) {
|
|
|
|
return invalidRequest(request);
|
|
|
|
}
|
|
|
|
|
|
|
|
final hash = Hmac(sha512, key!.bytes)
|
|
|
|
.convert(utf8.encode(_code.toString()))
|
|
|
|
.toString();
|
|
|
|
if (hash != data['hash']) {
|
|
|
|
request.response.statusCode = 403;
|
|
|
|
request.response.write(
|
|
|
|
jsonEncode({'ok': false, 'message': 'Hash doesn\'t match'}));
|
|
|
|
request.response.close();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
key = null;
|
|
|
|
_initTimer();
|
|
|
|
setState(() {
|
|
|
|
_step2 = false;
|
|
|
|
});
|
|
|
|
|
|
|
|
request.response.close();
|
|
|
|
break;
|
2024-02-10 17:13:27 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@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),
|
2024-02-12 02:37:26 +00:00
|
|
|
content: _step2
|
|
|
|
? Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
const LinearProgressIndicator(),
|
|
|
|
Padding(
|
|
|
|
padding: const EdgeInsets.fromLTRB(24, 18, 24, 24),
|
|
|
|
child: Text('Please follow the on-screen instructions'.i18n,
|
|
|
|
style: Theme.of(context).textTheme.bodyLarge),
|
|
|
|
),
|
|
|
|
])
|
2024-02-10 17:13:27 +00:00
|
|
|
: 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, _) =>
|
2024-02-12 02:37:26 +00:00
|
|
|
LinearProgressIndicator(value: 1.0 - value),
|
2024-02-10 17:13:27 +00:00
|
|
|
),
|
|
|
|
Padding(
|
|
|
|
padding: const EdgeInsets.fromLTRB(24, 18, 24, 24),
|
|
|
|
child: Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(
|
2024-02-12 02:37:26 +00:00
|
|
|
'On your other device, go to Freezer\'s settings > General > Login on other device and input the parameters below'
|
|
|
|
.i18n,
|
|
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
|
|
),
|
2024-02-10 17:13:27 +00:00
|
|
|
RichText(
|
|
|
|
text: TextSpan(
|
|
|
|
style:
|
|
|
|
Theme.of(context).textTheme.bodyMedium,
|
|
|
|
children: [
|
2024-02-12 02:37:26 +00:00
|
|
|
TextSpan(text: 'IP Address: '.i18n),
|
|
|
|
TextSpan(
|
|
|
|
text:
|
|
|
|
'${_deviceIP ?? 'Could not get IP!'}:${_server.port}',
|
|
|
|
style: Theme.of(context)
|
|
|
|
.textTheme
|
|
|
|
.displaySmall),
|
|
|
|
])),
|
2024-02-10 17:13:27 +00:00
|
|
|
RichText(
|
|
|
|
text: TextSpan(
|
|
|
|
style:
|
|
|
|
Theme.of(context).textTheme.bodyMedium,
|
|
|
|
children: [
|
|
|
|
TextSpan(text: 'Code: '.i18n),
|
|
|
|
TextSpan(
|
|
|
|
text: '$_code',
|
2024-02-12 02:37:26 +00:00
|
|
|
style: Theme.of(context)
|
|
|
|
.textTheme
|
|
|
|
.displaySmall),
|
2024-02-10 17:13:27 +00:00
|
|
|
])),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}),
|
|
|
|
actions: [
|
|
|
|
TextButton(
|
|
|
|
onPressed: () => Navigator.pop(context), child: Text('Cancel'.i18n))
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-28 11:02:59 +00:00
|
|
|
// email login is removed cuz not working = USELESS
|
2023-07-29 02:17:26 +00:00
|
|
|
|
2024-01-28 11:02:59 +00:00
|
|
|
//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'),
|
|
|
|
// )
|
|
|
|
// ],
|
|
|
|
// );
|
|
|
|
// }
|
|
|
|
// }
|