From 4e8fd12fdc20fabc36f9b7115f54f4f8905e0f52 Mon Sep 17 00:00:00 2001 From: "Florian RICHER (MrDev023)" Date: Thu, 31 Mar 2022 21:34:28 +0200 Subject: [PATCH] Finish to add configuration tutorial --- lib/classes/routes.dart | 10 +- lib/utils/logger.dart | 30 ++++ lib/widgets/components/tutorials/step1.dart | 6 +- lib/widgets/components/tutorials/step2.dart | 10 +- lib/widgets/components/tutorials/step3.dart | 158 ++++++++++++++++---- lib/widgets/pages/home.dart | 10 ++ lib/widgets/pages/tutorial.dart | 36 ++++- pubspec.lock | 14 ++ pubspec.yaml | 1 + 9 files changed, 236 insertions(+), 39 deletions(-) create mode 100644 lib/utils/logger.dart create mode 100644 lib/widgets/pages/home.dart diff --git a/lib/classes/routes.dart b/lib/classes/routes.dart index aa964a0..3bb4fd7 100644 --- a/lib/classes/routes.dart +++ b/lib/classes/routes.dart @@ -1,19 +1,23 @@ +import 'package:desktopapp/widgets/pages/home.dart'; import 'package:desktopapp/widgets/pages/tutorial.dart'; import 'package:flutter/material.dart'; -enum Routes { streamlabsTutorial } +enum Routes { streamlabsTutorial, home } -extension on Routes { +extension RoutesExtension on Routes { String get path { switch (this) { case Routes.streamlabsTutorial: return '/streamlabs/tutorial'; + case Routes.home: + return '/home'; } } } var routes = { - Routes.streamlabsTutorial.path: (context) => const TutorialPage(), + Routes.streamlabsTutorial.path: (context) => TutorialPage(), + Routes.home.path: (context) => const HomePage(), }; var initialRoute = Routes.streamlabsTutorial.path; diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart new file mode 100644 index 0000000..5897a6b --- /dev/null +++ b/lib/utils/logger.dart @@ -0,0 +1,30 @@ +import 'package:flutter/foundation.dart'; + +enum LoggerType { info, warning, error } + +class Logger { + static void log(LoggerType logType, Object? instance, String message) { + if (kDebugMode) { + String className; + if (instance is Type) { + className = instance.toString(); + } else { + className = instance?.runtimeType.toString() ?? 'GLOBAL'; + } + String cat; + switch (logType) { + case LoggerType.info: + cat = 'INFO'; + break; + case LoggerType.warning: + cat = 'WARNING'; + break; + case LoggerType.error: + cat = 'ERROR'; + break; + } + // ignore: avoid_print + print('[$cat][$className] $message'); + } + } +} diff --git a/lib/widgets/components/tutorials/step1.dart b/lib/widgets/components/tutorials/step1.dart index b5147fd..29d65e5 100644 --- a/lib/widgets/components/tutorials/step1.dart +++ b/lib/widgets/components/tutorials/step1.dart @@ -2,7 +2,9 @@ import 'package:desktopapp/widgets/components/link.dart'; import 'package:flutter/material.dart'; class Step1 extends StatelessWidget { - const Step1({Key? key}) : super(key: key); + const Step1({Key? key, required this.onNext}) : super(key: key); + + final VoidCallback onNext; Widget buildFieldDescription(String field, String description) { return Row( @@ -60,7 +62,7 @@ class Step1 extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - TextButton(onPressed: () {}, child: const Text('Étape suivante')) + TextButton(onPressed: onNext, child: const Text('Étape suivante')) ], ), ) diff --git a/lib/widgets/components/tutorials/step2.dart b/lib/widgets/components/tutorials/step2.dart index 0f9fec9..7d9446c 100644 --- a/lib/widgets/components/tutorials/step2.dart +++ b/lib/widgets/components/tutorials/step2.dart @@ -4,7 +4,11 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class Step2 extends StatelessWidget { - Step2({Key? key}) : super(key: key); + Step2({Key? key, required this.onPrevious, required this.onNext}) + : super(key: key); + + final VoidCallback onPrevious; + final VoidCallback onNext; final TextEditingController _clientIdController = TextEditingController(); final TextEditingController _clientSecretController = TextEditingController(); @@ -86,8 +90,8 @@ class Step2 extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ TextButton( - onPressed: () {}, child: const Text('Étape précédente')), - TextButton(onPressed: () {}, child: const Text('Étape suivante')) + onPressed: onPrevious, child: const Text('Étape précédente')), + TextButton(onPressed: onNext, child: const Text('Étape suivante')) ], ), ) diff --git a/lib/widgets/components/tutorials/step3.dart b/lib/widgets/components/tutorials/step3.dart index 5f26424..0268c13 100644 --- a/lib/widgets/components/tutorials/step3.dart +++ b/lib/widgets/components/tutorials/step3.dart @@ -1,30 +1,45 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:desktopapp/classes/routes.dart'; +import 'package:desktopapp/utils/logger.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:desktopapp/widgets/components/link.dart' as link; +import 'package:url_launcher/url_launcher.dart'; class Step3 extends StatelessWidget { - const Step3({Key? key}) : super(key: key); + const Step3({Key? key, required this.onPrevious}) : super(key: key); + + final VoidCallback onPrevious; Widget buildContent(BuildContext context, SharedPreferences prefs) { - var clientId = prefs.getString('client_id'); return Column(children: [ const Padding( padding: EdgeInsets.symmetric(vertical: 20), - child: Text('Étape 3:Authorisation de l\'application', + child: Text('Étape 3: Autorisation de l\'application', style: TextStyle(fontSize: 20)), ), - link.Link( - uri: - 'https://streamlabs.com/api/v1.0/authorize?redirect_uri=http://localhost:1234/&client_id=$clientId&response_type=code&scope=alerts.create', - label: 'Cliquez ici pour vous connecter à Streamlabs', - ) + const Text( + 'Pour terminer la configuration, il reste plus qu\'à se connecter à Streamlabs pour autoriser l\'application à créer des alertes.'), + const SizedBox(height: 10), + TextButton( + onPressed: () async { + waitAuthorization(context, prefs); + var clientId = prefs.getString('client_id'); + + await Future.delayed(const Duration(seconds: 1)); + launch( + 'https://streamlabs.com/api/v1.0/authorize?redirect_uri=http://localhost:1234/&client_id=$clientId&response_type=code&scope=alerts.create'); + }, + child: const Text('Cliquez ici pour vous connecter à Streamlabs')), ]); } - Future waitAuthorizationCallback() async { - var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 1214); + Future waitAuthorizationCallback(SharedPreferences prefs) async { + var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 1234); + Logger.log(LoggerType.info, this, + 'Server started at ${server.address.address}:${server.port}'); var request = await server.first; var uri = Uri.dataFromString(request.requestedUri.toString()); @@ -32,30 +47,116 @@ class Step3 extends StatelessWidget { var code = query['code']; if (code != null) { - var prefs = await SharedPreferences.getInstance(); prefs.setString('code', code); + + request.response + ..headers.contentType = ContentType.text + ..write('You can close tab now') + ..close(); + } else { + request.response + ..headers.contentType = ContentType.text + ..write('Internal error') + ..close(); } - request.response - ..headers.contentType = ContentType.text - ..write('You can close tab now') - ..close(); + return code; + } - // SEND TOKEN ASK TO API - // POST https://streamlabs.com/api/v1.0/token - // { - // "grant_type": "authorization_code", - // "code": "CODE", - // "redirect_uri": "http://localhost:1234/", - // "client_id": "CLIENT_ID", - // "client_secret": "CLIENT_SECRET" - // } - // SAVE RESPONSE TOKEN AS CREDENTIALS IN SHARED PREFERENCES + Future waitAuthorization( + BuildContext context, SharedPreferences prefs) async { + var code = await waitAuthorizationCallback(prefs); + + if (code != null) { + if (prefs.containsKey('credentials')) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Info'), + content: const Text( + 'Connexion réussi. Appuyez sur OK pour acceder à l\'application.'), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.of(context) + .pushReplacementNamed(Routes.home.path); + }, + child: const Text('OK')) + ], + )); + } else { + Dio dio = Dio(BaseOptions( + contentType: Headers.jsonContentType, + responseType: ResponseType.json, + validateStatus: (_) => true)); + + var data = jsonEncode({ + 'grant_type': 'authorization_code', + 'client_id': prefs.getString('client_id'), + 'client_secret': prefs.getString('client_secret'), + 'redirect_uri': 'http://localhost:1234/', + 'code': code, + }); + + var response = + await dio.post('https://streamlabs.com/api/v1.0/token', data: data); + + if (response.statusCode == HttpStatus.ok) { + prefs.setString('credentials', response.data.toString()); + Logger.log(LoggerType.info, this, + 'Authorization success : ${response.data}'); + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Info'), + content: const Text( + 'Connexion réussi. Appuyez sur OK pour acceder à l\'application.'), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.of(context) + .pushReplacementNamed(Routes.home.path); + }, + child: const Text('OK')) + ], + )); + } else { + Logger.log(LoggerType.error, this, + 'Authorization error : ${response.data} with $data and ${response.requestOptions.headers}'); + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Erreur'), + content: const Text('Impossible de se connecter.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK')) + ], + )); + } + } + } else { + Logger.log(LoggerType.error, this, + 'Authorization error : impossible to get code'); + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Erreur'), + content: const Text('Impossible de se connecter.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK')) + ], + )); + } } @override Widget build(BuildContext context) { - waitAuthorizationCallback(); return Column( children: [ Expanded( @@ -76,8 +177,7 @@ class Step3 extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ TextButton( - onPressed: () {}, child: const Text('Étape précédente')), - TextButton(onPressed: () {}, child: const Text('Étape suivante')) + onPressed: onPrevious, child: const Text('Étape précédente')) ], ), ) diff --git a/lib/widgets/pages/home.dart b/lib/widgets/pages/home.dart new file mode 100644 index 0000000..6ab2b1e --- /dev/null +++ b/lib/widgets/pages/home.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Scaffold(body: Text('Test')); + } +} diff --git a/lib/widgets/pages/tutorial.dart b/lib/widgets/pages/tutorial.dart index 995599c..7f1f212 100644 --- a/lib/widgets/pages/tutorial.dart +++ b/lib/widgets/pages/tutorial.dart @@ -4,10 +4,42 @@ import 'package:desktopapp/widgets/components/tutorials/step3.dart'; import 'package:flutter/material.dart'; class TutorialPage extends StatelessWidget { - const TutorialPage({Key? key}) : super(key: key); + TutorialPage({Key? key}) : super(key: key); + + final PageController videoPageController = PageController( + initialPage: 0, + viewportFraction: 1, + ); + + void onPrevious() { + videoPageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } + + void onNext() { + videoPageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } @override Widget build(BuildContext context) { - return Scaffold(body: Step3()); + return Scaffold( + body: CustomScrollView( + controller: videoPageController, + scrollDirection: Axis.horizontal, + physics: const PageScrollPhysics(), + slivers: [ + SliverFillViewport( + delegate: SliverChildListDelegate([ + Step1(onNext: onNext), + Step2(onPrevious: onPrevious, onNext: onNext), + Step3(onPrevious: onPrevious), + ])), + ], + )); } } diff --git a/pubspec.lock b/pubspec.lock index 1fe2cce..4449ae5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + dio: + dependency: "direct main" + description: + name: dio + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.6" fake_async: dependency: transitive description: @@ -114,6 +121,13 @@ packages: description: flutter source: sdk version: "0.0.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d9f55a1..c0a4f9d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: url_launcher: ^6.0.20 shared_preferences: ^2.0.13 flash: ^2.0.3+2 + dio: ^4.0.6 dev_dependencies: flutter_test: