diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 326245c..20c1cd6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -31,6 +31,13 @@ + + + + + + + diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index a97b098..3a60312 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -139,5 +139,7 @@ "createMessageNewPollOptionPlaceholder": "Nová možnost", "createMessageErrorSelectRecipient": "Vyberte prosím příjemce", "createMessageErrorNoMessage": "Napište prosím zprávu", - "createMessageSend": "Odeslat" + "createMessageSend": "Odeslat", + "qrLoginPleaseLogin": "EduPage2 QR Přihlášení", + "qrLoginUseExistingCredentials": "Chystáte se přihlásit pomocí QR kódu" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 46ad383..3e91a08 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -71,5 +71,7 @@ "createMessageNewPollOptionPlaceholder": "New option", "createMessageErrorSelectRecipient": "Please select a recipient", "createMessageErrorNoMessage": "Please write a message", - "createMessageSend": "Send" + "createMessageSend": "Send", + "qrLoginPleaseLogin": "EduPage2 QR Login", + "qrLoginUseExistingCredentials": "You are about to login to EduPage2 using a QR code" } diff --git a/lib/main.dart b/lib/main.dart index e4185fc..f44eeea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,13 @@ +import 'dart:async'; + +import 'package:app_links/app_links.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:eduapge2/api.dart'; import 'package:eduapge2/homework.dart'; import 'package:eduapge2/icanteen.dart'; import 'package:eduapge2/load.dart'; import 'package:eduapge2/messages.dart'; +import 'package:eduapge2/qrlogin.dart'; import 'package:eduapge2/timetable.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; @@ -53,6 +57,8 @@ abstract class BaseState extends State { } } +final GlobalKey navigatorKey = GlobalKey(); + class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -78,6 +84,7 @@ class MyApp extends StatelessWidget { return DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) { return MaterialApp( title: 'EduPage2', + navigatorKey: navigatorKey, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, navigatorObservers: [SentryNavigatorObserver(), observer], @@ -107,6 +114,9 @@ class PageBaseState extends BaseState { int _selectedIndex = 0; String baseUrl = FirebaseRemoteConfig.instance.getString("testUrl"); + late AppLinks _appLinks; + StreamSubscription? _linkSubscription; + bool loaded = false; bool error = false; //for error status @@ -125,6 +135,28 @@ class PageBaseState extends BaseState { setOptimalDisplayMode(); if (!_isCheckingForUpdate) _checkForUpdate(); // ik that it's not necessary super.initState(); + initDeepLinks(); + } + + @override + void dispose() { + _linkSubscription?.cancel(); + super.dispose(); + } + + Future initDeepLinks() async { + _appLinks = AppLinks(); + + _linkSubscription = _appLinks.uriLinkStream.listen((uri) { + if (uri.path.startsWith('/l/')) { + final code = uri.pathSegments[1]; + navigatorKey.currentState?.push(MaterialPageRoute( + builder: (context) => QRLoginPage( + code: code, + ), + )); + } + }); } Future setOptimalDisplayMode() async { diff --git a/lib/qrlogin.dart b/lib/qrlogin.dart new file mode 100644 index 0000000..00bb01b --- /dev/null +++ b/lib/qrlogin.dart @@ -0,0 +1,176 @@ +import 'package:dio/dio.dart'; +import 'package:eduapge2/main.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class QRLoginPage extends StatefulWidget { + final String code; + const QRLoginPage({super.key, required this.code}); + + @override + BaseState createState() => QRLoinPageState(); +} + +class QRLoinPageState extends BaseState { + AppLocalizations? local; + late SharedPreferences sharedPreferences; + String email = ""; + String password = ""; + String server = ""; + bool _useCustomEndpoint = false; + String _customEndpoint = ''; + bool showPassword = false; + + @override + void initState() { + getPrefs(); + super.initState(); + } + + @override + void setState(VoidCallback fn) { + if (!mounted) return; + super.setState(fn); + } + + Future getPrefs() async { + sharedPreferences = await SharedPreferences.getInstance(); + String? sEmail = sharedPreferences.getString("email"); + String? sPassword = sharedPreferences.getString("password"); + String? sServer = sharedPreferences.getString("server"); + String? sEndpoint = sharedPreferences.getString("customEndpoint"); + + if (sEmail != null) { + email = sEmail; + } + if (sPassword != null) { + password = sPassword; + } + if (sServer != null) { + server = sServer; + } + if (sEndpoint != null && sEndpoint != "") { + _customEndpoint = sEndpoint; + _useCustomEndpoint = true; + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + local ??= AppLocalizations.of(context); + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + local!.qrLoginPleaseLogin, + style: const TextStyle( + fontSize: 18, + ), + ), + Text(local!.qrLoginUseExistingCredentials), + TextField( + decoration: InputDecoration( + icon: const Icon(Icons.email), + hintText: local!.loginUsername, + ), + onChanged: (text) => {email = text}, + keyboardType: TextInputType.emailAddress, + ), + TextField( + decoration: InputDecoration( + icon: const Icon(Icons.key), + hintText: local!.loginPassword, + suffixIcon: IconButton( + icon: Icon(showPassword + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + showPassword = !showPassword; + }); + }, + ), + ), + onChanged: (text) => {password = text}, + obscureText: true, + keyboardType: TextInputType.visiblePassword, + ), + Row( + children: [ + Checkbox( + value: _useCustomEndpoint, + onChanged: (value) { + setState(() { + _useCustomEndpoint = value!; + }); + }, + ), + Text(local!.loginCustomEndpointCheckbox), + ], + ), + if (_useCustomEndpoint) + TextField( + decoration: InputDecoration( + icon: const Icon(Icons.language), + hintText: local!.loginCustomEndpoint, + ), + onChanged: (value) => {_customEndpoint = value}), + TextField( + decoration: InputDecoration( + icon: const Icon(Icons.cloud_queue), + hintText: local!.loginServer, + ), + onChanged: (text) => {server = text}, + keyboardType: TextInputType.url, + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () async { + Dio dio = Dio(); + + var response = await dio.post( + 'https://ep2.vypal.me/qrlogin/${widget.code}', + options: Options( + headers: { + Headers.contentTypeHeader: + Headers.formUrlEncodedContentType, + }, + ), + data: { + 'username': email, + 'password': password, + 'server': server, + 'endpoint': _customEndpoint, + }, + ); + + if (response.statusCode == 200) { + navigatorKey.currentState!.pop(); + } else { + // If the server did not return a 200 OK response, throw an exception. + throw Exception('Failed to load data'); + } + }, + style: ButtonStyle( + elevation: MaterialStateProperty.all(3), + ), + child: Text(local!.loginLogin), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 2528735..aecfb5b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include @@ -14,6 +15,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b942fff..a98a528 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + gtk sentry_flutter url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 4216f5d..459ff28 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import app_links import connectivity_plus import dynamic_color import firebase_analytics @@ -18,6 +19,7 @@ import sqflite import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 741ac9e..cb61985 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.30" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "96e677810b83707ff5e10fac11e4839daa0ea4e0123c35864c092699165eb3db" + url: "https://pub.dev" + source: hosted + version: "6.1.1" args: dependency: transitive description: @@ -319,6 +327,14 @@ packages: description: flutter source: sdk version: "0.0.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" html_unescape: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5ed8aa1..6a28649 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: flutter_displaymode: ^0.6.0 install_referrer: ^1.2.1 in_app_update: ^4.2.2 + app_links: ^6.1.1 dev_dependencies: integration_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1a60256..511a95b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 928840d..5995934 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links connectivity_plus dynamic_color firebase_core