diff --git a/packages/app/linux/flutter/generated_plugins.cmake b/packages/app/linux/flutter/generated_plugins.cmake index 3932809f..7180e5e3 100644 --- a/packages/app/linux/flutter/generated_plugins.cmake +++ b/packages/app/linux/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_zxing ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index 750e7266..37d5efde 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansi_styles: + dependency: transitive + description: + name: ansi_styles + sha256: "9c656cc12b3c27b17dd982b2cc5c0cfdfbdabd7bc8f3ae5e8542d9867b47ce8a" + url: "https://pub.dev" + source: hosted + version: "0.3.2+1" archive: dependency: transitive description: @@ -65,6 +73,46 @@ packages: url: "https://pub.dev" source: hosted version: "8.6.1" + camera: + dependency: transitive + description: + name: camera + sha256: ebebead3d5ec3d148249331d751d462d7e8c98102b8830a9b45ec96a2bd4333f + url: "https://pub.dev" + source: hosted + version: "0.10.5+2" + camera_android: + dependency: transitive + description: + name: camera_android + sha256: f43d07f9d7228ea1ca87d22e30881bd68da4b78484a1fbd1f1408b412a41cefb + url: "https://pub.dev" + source: hosted + version: "0.10.8+3" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "1a416e452b30955b392f4efbf23291d3f2ba3660a85e1628859eb62d2a2bab26" + url: "https://pub.dev" + source: hosted + version: "0.9.13+2" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "60fa0bb62a4f3bf3a7c413e31e4cd01b69c779ccc8e4668904a24581b86c316b" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: bcbd775fb3a9d51cc3ece899d54ad66f6306410556bac5759f78e13f9228841f + url: "https://pub.dev" + source: hosted + version: "0.3.1+4" characters: dependency: transitive description: @@ -73,6 +121,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + cli_launcher: + dependency: transitive + description: + name: cli_launcher + sha256: "5e7e0282b79e8642edd6510ee468ae2976d847a0a29b3916e85f5fa1bfe24005" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" clock: dependency: transitive description: @@ -89,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.1" + conventional_commit: + dependency: transitive + description: + name: conventional_commit + sha256: dec15ad1118f029c618651a4359eb9135d8b88f761aa24e4016d061cd45948f2 + url: "https://pub.dev" + source: hosted + version: "0.6.0+1" convert: dependency: transitive description: @@ -344,11 +424,27 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_zxing: + dependency: transitive + description: + name: flutter_zxing + sha256: b25efe5ac91fe7a51aa8bfea7aca7435285b911b8758b7da38b7f037bad62c35 + url: "https://pub.dev" + source: hosted + version: "1.1.2" fuchsia_remote_debug_protocol: dependency: transitive description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" go_router: dependency: transitive description: @@ -357,6 +453,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.0.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" html: dependency: transitive description: @@ -474,6 +578,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" js: dependency: transitive description: @@ -538,6 +650,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.7296" + melos: + dependency: transitive + description: + name: melos + sha256: ccbb6ecd8bb3f08ae8f9ce22920d816bff325a98940c845eda0257cd395503ac + url: "https://pub.dev" + source: hosted + version: "3.1.0" menu_base: dependency: transitive description: @@ -562,6 +682,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mustache_template: + dependency: transitive + description: + name: mustache_template + sha256: a46e26f91445bfb0b60519be280555b06792460b27b19e2b19ad5b9740df5d1c + url: "https://pub.dev" + source: hosted + version: "2.0.0" neon: dependency: "direct main" description: @@ -789,6 +917,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" process: dependency: transitive description: @@ -797,6 +933,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + prompts: + dependency: transitive + description: + name: prompts + sha256: "3773b845e85a849f01e793c4fc18a45d52d7783b4cb6c0569fad19f9d0a774a1" + url: "https://pub.dev" + source: hosted + version: "2.0.0" provider: dependency: transitive description: @@ -805,6 +949,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pub_updater: + dependency: transitive + description: + name: pub_updater + sha256: "42890302ab2672adf567dc2b20e55b4ecc29d7e19c63b6b98143ab68dd717d3a" + url: "https://pub.dev" + source: hosted + version: "0.2.4" + pubspec: + dependency: transitive + description: + name: pubspec + sha256: f534a50a2b4d48dc3bc0ec147c8bd7c304280fff23b153f3f11803c4d49d927e + url: "https://pub.dev" + source: hosted + version: "2.3.0" queue: dependency: transitive description: @@ -845,6 +1013,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" rxdart: dependency: transitive description: @@ -1009,6 +1185,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1105,6 +1289,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + uri: + dependency: transitive + description: + name: uri + sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + url: "https://pub.dev" + source: hosted + version: "1.0.0" url_launcher: dependency: transitive description: @@ -1354,6 +1546,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" + url: "https://pub.dev" + source: hosted + version: "2.1.1" sdks: dart: ">=3.0.0 <4.0.0" flutter: ">=3.10.4" diff --git a/packages/neon/neon/lib/l10n/en.arb b/packages/neon/neon/lib/l10n/en.arb index e9535306..2372a31e 100644 --- a/packages/neon/neon/lib/l10n/en.arb +++ b/packages/neon/neon/lib/l10n/en.arb @@ -10,7 +10,8 @@ "loginOpenAgain": "Open again", "loginSwitchToBrowserWindow": "Please switch to the browser window that just opened and proceed there", "loginWorksWith": "works with", - "loginRestart": "Restart login", + "loginUsingQrcode": "Login using a QR code", + "loginUsingServerAddress": "Login using the server address", "loginCheckingServerVersion": "Checking server version", "loginSupportedServerVersion": "Supported server version: {version}", "@loginSupportedServerVersion": { @@ -66,6 +67,7 @@ }, "errorEmptyField": "This field can not be empty", "errorInvalidURL": "Invalid URL provided", + "errorInvalidQrcode": "Invalid QR-Code provided", "actionYes": "Yes", "actionNo": "No", "actionClose": "Close", diff --git a/packages/neon/neon/lib/l10n/localizations.dart b/packages/neon/neon/lib/l10n/localizations.dart index 4bcfa441..647b4cfa 100644 --- a/packages/neon/neon/lib/l10n/localizations.dart +++ b/packages/neon/neon/lib/l10n/localizations.dart @@ -119,11 +119,17 @@ abstract class AppLocalizations { /// **'works with'** String get loginWorksWith; - /// No description provided for @loginRestart. + /// No description provided for @loginUsingQrcode. /// /// In en, this message translates to: - /// **'Restart login'** - String get loginRestart; + /// **'Login using a QR code'** + String get loginUsingQrcode; + + /// No description provided for @loginUsingServerAddress. + /// + /// In en, this message translates to: + /// **'Login using the server address'** + String get loginUsingServerAddress; /// No description provided for @loginCheckingServerVersion. /// @@ -245,6 +251,12 @@ abstract class AppLocalizations { /// **'Invalid URL provided'** String get errorInvalidURL; + /// No description provided for @errorInvalidQrcode. + /// + /// In en, this message translates to: + /// **'Invalid QR-Code provided'** + String get errorInvalidQrcode; + /// No description provided for @actionYes. /// /// In en, this message translates to: diff --git a/packages/neon/neon/lib/l10n/localizations_en.dart b/packages/neon/neon/lib/l10n/localizations_en.dart index 5142f03b..90ba1a2d 100644 --- a/packages/neon/neon/lib/l10n/localizations_en.dart +++ b/packages/neon/neon/lib/l10n/localizations_en.dart @@ -36,7 +36,10 @@ class AppLocalizationsEn extends AppLocalizations { String get loginWorksWith => 'works with'; @override - String get loginRestart => 'Restart login'; + String get loginUsingQrcode => 'Login using a QR code'; + + @override + String get loginUsingServerAddress => 'Login using the server address'; @override String get loginCheckingServerVersion => 'Checking server version'; @@ -111,6 +114,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get errorInvalidURL => 'Invalid URL provided'; + @override + String get errorInvalidQrcode => 'Invalid QR-Code provided'; + @override String get actionYes => 'Yes'; diff --git a/packages/neon/neon/lib/src/pages/login.dart b/packages/neon/neon/lib/src/pages/login.dart index 48e95e7d..84d4945a 100644 --- a/packages/neon/neon/lib/src/pages/login.dart +++ b/packages/neon/neon/lib/src/pages/login.dart @@ -1,10 +1,14 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:neon/l10n/localizations.dart'; +import 'package:neon/src/platform/platform.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/theme/branding.dart'; import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/utils/validators.dart'; import 'package:neon/src/widgets/nextcloud_logo.dart'; +import 'package:provider/provider.dart'; class LoginPage extends StatefulWidget { const LoginPage({ @@ -52,6 +56,8 @@ class _LoginPageState extends State { @override Widget build(final BuildContext context) { final branding = Branding.of(context); + final platform = Provider.of(context, listen: false); + return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( @@ -72,7 +78,7 @@ class _LoginPageState extends State { style: Theme.of(context).textTheme.titleLarge, ), const SizedBox( - height: 30, + height: 20, ), if (branding.showLoginWithNextcloud) ...[ Text(AppLocalizations.of(context).loginWorksWith), @@ -81,6 +87,32 @@ class _LoginPageState extends State { ), ], const NextcloudLogo(), + const SizedBox( + height: 40, + ), + if (platform.canUseCamera) ...[ + ExcludeSemantics( + child: Center( + child: Text(AppLocalizations.of(context).loginUsingQrcode), + ), + ), + IconButton( + tooltip: AppLocalizations.of(context).loginUsingQrcode, + icon: const Icon( + Icons.qr_code_scanner, + size: 50, + ), + onPressed: () => unawaited(const LoginQrcodeRoute().push(context)), + ), + const SizedBox( + height: 20, + ), + ExcludeSemantics( + child: Center( + child: Text(AppLocalizations.of(context).loginUsingServerAddress), + ), + ), + ], Form( key: _formKey, child: TextFormField( diff --git a/packages/neon/neon/lib/src/pages/login_qrcode.dart b/packages/neon/neon/lib/src/pages/login_qrcode.dart new file mode 100644 index 00000000..e1810175 --- /dev/null +++ b/packages/neon/neon/lib/src/pages/login_qrcode.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_zxing/flutter_zxing.dart'; +import 'package:neon/src/router.dart'; +import 'package:neon/src/utils/exceptions.dart'; +import 'package:neon/src/widgets/exception.dart'; + +class LoginQrcodePage extends StatefulWidget { + const LoginQrcodePage({ + super.key, + }); + + @override + State createState() => _LoginQrcodePageState(); +} + +class _LoginQrcodePageState extends State { + final _urlRegex = RegExp(r'^nc://login/user:(.*)&password:(.*)&server:(.*)$'); + String? _lastErrorURL; + + @override + Widget build(final BuildContext context) => Scaffold( + appBar: AppBar(), + body: ReaderWidget( + codeFormat: Format.qrCode, + showGallery: false, + showToggleCamera: false, + showScannerOverlay: false, + tryHarder: true, + cropPercent: 0, + scanDelaySuccess: const Duration(seconds: 3), + onScan: (final code) async { + String? url; + try { + url = code.text; + if (url == null) { + throw InvalidQrcodeException(); + } + final match = _urlRegex.allMatches(url).single; + if (match.groupCount != 3) { + throw InvalidQrcodeException(); + } + final loginName = match.group(1)!; + final password = match.group(2)!; + final serverURL = match.group(3)!; + + final result = await LoginCheckServerStatusRoute(serverURL: serverURL).push(context); + if ((result ?? false) && mounted) { + LoginCheckAccountRoute( + serverURL: serverURL, + loginName: loginName, + password: password, + ).pushReplacement(context); + } + } catch (e, s) { + if (_lastErrorURL != url) { + debugPrint(e.toString()); + debugPrint(s.toString()); + + _lastErrorURL = url; + NeonException.showSnackbar(context, e); + } + } + }, + ), + ); +} diff --git a/packages/neon/neon/lib/src/router.dart b/packages/neon/neon/lib/src/router.dart index cad5b2af..f09f8924 100644 --- a/packages/neon/neon/lib/src/router.dart +++ b/packages/neon/neon/lib/src/router.dart @@ -10,6 +10,7 @@ import 'package:neon/src/pages/login.dart'; import 'package:neon/src/pages/login_check_account.dart'; import 'package:neon/src/pages/login_check_server_status.dart'; import 'package:neon/src/pages/login_flow.dart'; +import 'package:neon/src/pages/login_qrcode.dart'; import 'package:neon/src/pages/nextcloud_app_settings.dart'; import 'package:neon/src/pages/settings.dart'; import 'package:neon/src/utils/stream_listenable.dart'; @@ -105,6 +106,10 @@ class HomeRoute extends GoRouteData { path: 'flow/:serverURL', name: 'loginFlow', ), + TypedGoRoute( + path: 'qrcode', + name: 'loginQrcode', + ), TypedGoRoute( path: 'check/server/:serverURL', name: 'checkServerStatus', @@ -137,6 +142,14 @@ class LoginFlowRoute extends GoRouteData { Widget build(final BuildContext context, final GoRouterState state) => LoginFlowPage(serverURL: serverURL); } +@immutable +class LoginQrcodeRoute extends GoRouteData { + const LoginQrcodeRoute(); + + @override + Widget build(final BuildContext context, final GoRouterState state) => const LoginQrcodePage(); +} + @immutable class LoginCheckServerStatusRoute extends GoRouteData { const LoginCheckServerStatusRoute({ diff --git a/packages/neon/neon/lib/src/router.g.dart b/packages/neon/neon/lib/src/router.g.dart index bed7f49b..b823ce90 100644 --- a/packages/neon/neon/lib/src/router.g.dart +++ b/packages/neon/neon/lib/src/router.g.dart @@ -125,6 +125,11 @@ RouteBase get $loginRoute => GoRouteData.$route( name: 'loginFlow', factory: $LoginFlowRouteExtension._fromState, ), + GoRouteData.$route( + path: 'qrcode', + name: 'loginQrcode', + factory: $LoginQrcodeRouteExtension._fromState, + ), GoRouteData.$route( path: 'check/server/:serverURL', name: 'checkServerStatus', @@ -173,6 +178,20 @@ extension $LoginFlowRouteExtension on LoginFlowRoute { void pushReplacement(BuildContext context) => context.pushReplacement(location); } +extension $LoginQrcodeRouteExtension on LoginQrcodeRoute { + static LoginQrcodeRoute _fromState(GoRouterState state) => const LoginQrcodeRoute(); + + String get location => GoRouteData.$location( + '/login/qrcode', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => context.pushReplacement(location); +} + extension $LoginCheckServerStatusRouteExtension on LoginCheckServerStatusRoute { static LoginCheckServerStatusRoute _fromState(GoRouterState state) => LoginCheckServerStatusRoute( serverURL: state.pathParameters['serverURL']!, diff --git a/packages/neon/neon/lib/src/utils/exceptions.dart b/packages/neon/neon/lib/src/utils/exceptions.dart index a92f1d77..cf39f41c 100644 --- a/packages/neon/neon/lib/src/utils/exceptions.dart +++ b/packages/neon/neon/lib/src/utils/exceptions.dart @@ -7,3 +7,5 @@ class MissingPermissionException implements Exception { } class UnableToOpenFileException implements Exception {} + +class InvalidQrcodeException implements Exception {} diff --git a/packages/neon/neon/lib/src/widgets/exception.dart b/packages/neon/neon/lib/src/widgets/exception.dart index 70fd348a..aebf6816 100644 --- a/packages/neon/neon/lib/src/widgets/exception.dart +++ b/packages/neon/neon/lib/src/widgets/exception.dart @@ -128,6 +128,12 @@ class NeonException extends StatelessWidget { ); } + if (exception is InvalidQrcodeException) { + return _ExceptionDetails( + text: AppLocalizations.of(context).errorInvalidQrcode, + ); + } + if (exception is DynamiteApiException) { if (exception.statusCode == 401) { return _ExceptionDetails( diff --git a/packages/neon/neon/pubspec.yaml b/packages/neon/neon/pubspec.yaml index e8d7bdaf..b4616eef 100644 --- a/packages/neon/neon/pubspec.yaml +++ b/packages/neon/neon/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: sdk: flutter flutter_native_splash: ^2.2.19 flutter_svg: ^2.0.5 + flutter_zxing: ^1.1.2 # ^1.2.0 downgrades to image ^3.0.0 which breaks our dependencies. See https://github.com/khoren93/flutter_zxing/issues/94 go_router: ^8.0.3 http: ^0.13.6 intersperse: ^2.0.0