diff --git a/android/build.gradle b/android/build.gradle
index eab07ca5c..b8279233b 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,5 +1,5 @@
buildscript {
- ext.kotlin_version = '1.3.50'
+ ext.kotlin_version = '1.5.10'
repositories {
google()
jcenter()
@@ -7,7 +7,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath 'com.android.tools.build:gradle:4.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
classpath 'com.google.gms:google-services:4.3.3'
}
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index bc6a58afd..3df6b3389 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index aa93457cd..3886d2456 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -42,6 +42,12 @@
UIViewControllerBasedStatusBarAppearance
ITSAppUsesNonExemptEncryption
-
+
+ io.flutter.embedded_views_preview
+
+ NSCameraUsageDescription
+ This app needs camera access to scan QR codes
+ NSPhotoLibraryUsageDescription
+ This app needs photo library access to get QR codes from image
diff --git a/lib/common.dart b/lib/common.dart
index ef0ced2b0..3fb3ea502 100644
--- a/lib/common.dart
+++ b/lib/common.dart
@@ -42,6 +42,11 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom(
),
);
+void showToast(String text) {
+ EasyLoading.showToast(Translator.call(text),
+ maskType: EasyLoadingMaskType.black);
+}
+
void showLoading(String text) {
DialogManager.reset();
EasyLoading.dismiss();
diff --git a/lib/pages/connection_page.dart b/lib/pages/connection_page.dart
index deca3cee5..8a5ecdc22 100644
--- a/lib/pages/connection_page.dart
+++ b/lib/pages/connection_page.dart
@@ -19,7 +19,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
final title = translate("Connection");
@override
- final appBarActions = !isAndroid ? [WebMenu()] : [];
+ final appBarActions = isWeb ? [WebMenu()] : [];
@override
_ConnectionPageState createState() => _ConnectionPageState();
@@ -308,7 +308,7 @@ class _WebMenuState extends State {
},
onSelected: (value) {
if (value == 'server') {
- showServer();
+ showServerSettings();
}
if (value == 'about') {
showAbout();
diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart
index ef895c042..c4f9e0c08 100644
--- a/lib/pages/home_page.dart
+++ b/lib/pages/home_page.dart
@@ -25,12 +25,9 @@ class _HomePageState extends State {
@override
void initState() {
super.initState();
- _pages.addAll([
- ConnectionPage(),
- chatPage,
- ]);
+ _pages.add(ConnectionPage());
if (isAndroid) {
- _pages.add(ServerPage());
+ _pages.addAll([chatPage, ServerPage()]);
}
_pages.add(SettingsPage());
}
diff --git a/lib/pages/scan_page.dart b/lib/pages/scan_page.dart
new file mode 100644
index 000000000..52e1f1bf4
--- /dev/null
+++ b/lib/pages/scan_page.dart
@@ -0,0 +1,257 @@
+import 'package:flutter/material.dart';
+import 'package:qr_code_scanner/qr_code_scanner.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:image/image.dart' as img;
+import 'package:zxing2/qrcode.dart';
+import 'dart:io';
+import 'dart:convert';
+import '../common.dart';
+import '../models/model.dart';
+
+class ScanPage extends StatefulWidget {
+ @override
+ _ScanPageState createState() => _ScanPageState();
+}
+
+class _ScanPageState extends State {
+ QRViewController? controller;
+ final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
+
+ // In order to get hot reload to work we need to pause the camera if the platform
+ // is android, or resume the camera if the platform is iOS.
+ @override
+ void reassemble() {
+ super.reassemble();
+ if (isAndroid) {
+ controller!.pauseCamera();
+ }
+ controller!.resumeCamera();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Scan QR'),
+ actions: [
+ IconButton(
+ color: Colors.white,
+ icon: Icon(Icons.image_search),
+ iconSize: 32.0,
+ onPressed: () async {
+ final ImagePicker _picker = ImagePicker();
+ final XFile? file =
+ await _picker.pickImage(source: ImageSource.gallery);
+ if (file != null) {
+ var image = img.decodeNamedImage(
+ File(file.path).readAsBytesSync(), file.path)!;
+
+ LuminanceSource source = RGBLuminanceSource(
+ image.width,
+ image.height,
+ image
+ .getBytes(format: img.Format.abgr)
+ .buffer
+ .asInt32List());
+ var bitmap = BinaryBitmap(HybridBinarizer(source));
+
+ var reader = QRCodeReader();
+ try {
+ var result = reader.decode(bitmap);
+ showServerSettingFromQr(result.text);
+ } catch (e) {
+ showToast('No QR code found');
+ }
+ }
+ }),
+ IconButton(
+ color: Colors.yellow,
+ icon: Icon(Icons.flash_on),
+ iconSize: 32.0,
+ onPressed: () async {
+ await controller?.toggleFlash();
+ }),
+ IconButton(
+ color: Colors.white,
+ icon: Icon(Icons.switch_camera),
+ iconSize: 32.0,
+ onPressed: () async {
+ await controller?.flipCamera();
+ },
+ ),
+ ],
+ ),
+ body: _buildQrView(context));
+ }
+
+ Widget _buildQrView(BuildContext context) {
+ // For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
+ var scanArea = (MediaQuery.of(context).size.width < 400 ||
+ MediaQuery.of(context).size.height < 400)
+ ? 150.0
+ : 300.0;
+ // To ensure the Scanner view is properly sizes after rotation
+ // we need to listen for Flutter SizeChanged notification and update controller
+ return QRView(
+ key: qrKey,
+ onQRViewCreated: _onQRViewCreated,
+ overlay: QrScannerOverlayShape(
+ borderColor: Colors.red,
+ borderRadius: 10,
+ borderLength: 30,
+ borderWidth: 10,
+ cutOutSize: scanArea),
+ onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
+ );
+ }
+
+ void _onQRViewCreated(QRViewController controller) {
+ setState(() {
+ this.controller = controller;
+ });
+ controller.scannedDataStream.listen((scanData) {
+ if (scanData.code != null) {
+ showServerSettingFromQr(scanData.code!);
+ }
+ });
+ }
+
+ void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
+ if (!p) {
+ showToast('No permisssion');
+ }
+ }
+
+ @override
+ void dispose() {
+ controller?.dispose();
+ super.dispose();
+ }
+
+ void showServerSettingFromQr(String data) async {
+ backToHome();
+ await controller!.stopCamera();
+ if (!data.startsWith('config=')) {
+ showToast('Invalid QR code');
+ return;
+ }
+ try {
+ Map values = json.decode(data.substring(7));
+ var host = values['host'] != null ? values['host'] as String : '';
+ var key = values['key'] != null ? values['key'] as String : '';
+ var api = values['api'] != null ? values['api'] as String : '';
+ showServerSettingsWithValue(host, '', key, api);
+ } catch (e) {
+ showToast('Invalid QR code');
+ }
+ }
+}
+
+void showServerSettingsWithValue(
+ String id, String relay, String key, String api) {
+ final formKey = GlobalKey();
+ final id0 = FFI.getByName('option', 'custom-rendezvous-server');
+ final relay0 = FFI.getByName('option', 'relay-server');
+ final api0 = FFI.getByName('option', 'api-server');
+ final key0 = FFI.getByName('option', 'key');
+ DialogManager.show((setState, close) {
+ return CustomAlertDialog(
+ title: Text(translate('ID/Relay Server')),
+ content: Form(
+ key: formKey,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextFormField(
+ initialValue: id,
+ decoration: InputDecoration(
+ labelText: translate('ID Server'),
+ ),
+ validator: validate,
+ onSaved: (String? value) {
+ if (value != null) id = value.trim();
+ },
+ )
+ ] +
+ (isAndroid
+ ? [
+ TextFormField(
+ initialValue: relay,
+ decoration: InputDecoration(
+ labelText: translate('Relay Server'),
+ ),
+ validator: validate,
+ onSaved: (String? value) {
+ if (value != null) relay = value.trim();
+ },
+ )
+ ]
+ : []) +
+ [
+ TextFormField(
+ initialValue: api,
+ decoration: InputDecoration(
+ labelText: translate('API Server'),
+ ),
+ validator: validate,
+ onSaved: (String? value) {
+ if (value != null) api = value.trim();
+ },
+ ),
+ TextFormField(
+ initialValue: key,
+ decoration: InputDecoration(
+ labelText: 'Key',
+ ),
+ validator: null,
+ onSaved: (String? value) {
+ if (value != null) key = value.trim();
+ },
+ ),
+ ])),
+ actions: [
+ TextButton(
+ style: flatButtonStyle,
+ onPressed: () {
+ close();
+ },
+ child: Text(translate('Cancel')),
+ ),
+ TextButton(
+ style: flatButtonStyle,
+ onPressed: () {
+ if (formKey.currentState != null &&
+ formKey.currentState!.validate()) {
+ formKey.currentState!.save();
+ if (id != id0)
+ FFI.setByName('option',
+ '{"name": "custom-rendezvous-server", "value": "$id"}');
+ if (relay != relay0)
+ FFI.setByName(
+ 'option', '{"name": "relay-server", "value": "$relay"}');
+ if (key != key0)
+ FFI.setByName('option', '{"name": "key", "value": "$key"}');
+ if (api != api0)
+ FFI.setByName(
+ 'option', '{"name": "api-server", "value": "$api"}');
+ close();
+ }
+ },
+ child: Text(translate('OK')),
+ ),
+ ],
+ onWillPop: () async {
+ return true;
+ },
+ );
+ }, barrierDismissible: true);
+}
+
+String? validate(value) {
+ value = value.trim();
+ if (value.isEmpty) {
+ return null;
+ }
+ final res = FFI.getByName('test_if_valid_server', value);
+ return res.isEmpty ? null : res;
+}
diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart
index dff6b5957..d9527ba36 100644
--- a/lib/pages/settings_page.dart
+++ b/lib/pages/settings_page.dart
@@ -9,6 +9,7 @@ import '../common.dart';
import '../widgets/dialog.dart';
import '../models/model.dart';
import 'home_page.dart';
+import 'scan_page.dart';
class SettingsPage extends StatefulWidget implements PageShape {
@override
@@ -18,7 +19,7 @@ class SettingsPage extends StatefulWidget implements PageShape {
final icon = Icon(Icons.settings);
@override
- final appBarActions = [];
+ final appBarActions = [ScanButton()];
@override
_SettingsState createState() => _SettingsState();
@@ -58,7 +59,7 @@ class _SettingsState extends State {
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
- showServer();
+ showServerSettings();
},
),
],
@@ -67,20 +68,18 @@ class _SettingsState extends State {
title: Text(translate("About")),
tiles: [
SettingsTile.navigation(
+ onPressed: (context) async {
+ if (await canLaunch(url)) {
+ await launch(url);
+ }
+ },
title: Text(translate("Version: ") + version),
- value: InkWell(
- onTap: () async {
- if (await canLaunch(url)) {
- await launch(url);
- }
- },
- child: Padding(
- padding: EdgeInsets.symmetric(vertical: 8),
- child: Text('rustdesk.com',
- style: TextStyle(
- decoration: TextDecoration.underline,
- )),
- ),
+ value: Padding(
+ padding: EdgeInsets.symmetric(vertical: 8),
+ child: Text('rustdesk.com',
+ style: TextStyle(
+ decoration: TextDecoration.underline,
+ )),
),
leading: Icon(Icons.info)),
],
@@ -90,116 +89,12 @@ class _SettingsState extends State {
}
}
-void showServer() {
- final formKey = GlobalKey();
- final id0 = FFI.getByName('option', 'custom-rendezvous-server');
- final relay0 = FFI.getByName('option', 'relay-server');
- final api0 = FFI.getByName('option', 'api-server');
- final key0 = FFI.getByName('option', 'key');
- var id = '';
- var relay = '';
- var key = '';
- var api = '';
- DialogManager.show((setState, close) {
- return CustomAlertDialog(
- title: Text(translate('ID/Relay Server')),
- content: Form(
- key: formKey,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- TextFormField(
- initialValue: id0,
- decoration: InputDecoration(
- labelText: translate('ID Server'),
- ),
- validator: validate,
- onSaved: (String? value) {
- if (value != null) id = value.trim();
- },
- )
- ] +
- (isAndroid
- ? [
- TextFormField(
- initialValue: relay0,
- decoration: InputDecoration(
- labelText: translate('Relay Server'),
- ),
- validator: validate,
- onSaved: (String? value) {
- if (value != null) relay = value.trim();
- },
- )
- ]
- : []) +
- [
- TextFormField(
- initialValue: api0,
- decoration: InputDecoration(
- labelText: translate('API Server'),
- ),
- validator: validate,
- onSaved: (String? value) {
- if (value != null) api = value.trim();
- },
- ),
- TextFormField(
- initialValue: key0,
- decoration: InputDecoration(
- labelText: 'Key',
- ),
- validator: null,
- onSaved: (String? value) {
- if (value != null) key = value.trim();
- },
- ),
- ])),
- actions: [
- TextButton(
- style: flatButtonStyle,
- onPressed: () {
- close();
- },
- child: Text(translate('Cancel')),
- ),
- TextButton(
- style: flatButtonStyle,
- onPressed: () {
- if (formKey.currentState != null &&
- formKey.currentState!.validate()) {
- formKey.currentState!.save();
- if (id != id0)
- FFI.setByName('option',
- '{"name": "custom-rendezvous-server", "value": "$id"}');
- if (relay != relay0)
- FFI.setByName(
- 'option', '{"name": "relay-server", "value": "$relay"}');
- if (key != key0)
- FFI.setByName('option', '{"name": "key", "value": "$key"}');
- if (api != api0)
- FFI.setByName(
- 'option', '{"name": "api-server", "value": "$api"}');
- close();
- }
- },
- child: Text(translate('OK')),
- ),
- ],
- onWillPop: () async {
- return true;
- },
- );
- }, barrierDismissible: true);
-}
-
-String? validate(value) {
- value = value.trim();
- if (value.isEmpty) {
- return null;
- }
- final res = FFI.getByName('test_if_valid_server', value);
- return res.isEmpty ? null : res;
+void showServerSettings() {
+ final id = FFI.getByName('option', 'custom-rendezvous-server');
+ final relay = FFI.getByName('option', 'relay-server');
+ final api = FFI.getByName('option', 'api-server');
+ final key = FFI.getByName('option', 'key');
+ showServerSettingsWithValue(id, relay, key, api);
}
void showAbout() {
@@ -448,3 +343,20 @@ String? getUsername() {
}
return username;
}
+
+class ScanButton extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return IconButton(
+ icon: Icon(Icons.qr_code_scanner),
+ onPressed: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) => ScanPage(),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index 334ff42e9..72376a01c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -57,6 +57,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
+ cross_file:
+ dependency: transitive
+ description:
+ name: cross_file
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.3.2"
crypto:
dependency: transitive
description:
@@ -176,6 +183,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.2"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
flutter:
dependency: "direct main"
description: flutter
@@ -209,6 +223,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.5"
flutter_spinkit:
dependency: transitive
description:
@@ -241,12 +262,47 @@ packages:
source: hosted
version: "4.0.0"
image:
- dependency: transitive
+ dependency: "direct main"
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.3"
+ image_picker:
+ dependency: "direct main"
+ description:
+ name: image_picker
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.8.5"
+ image_picker_android:
+ dependency: transitive
+ description:
+ name: image_picker_android
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.8.4+11"
+ image_picker_for_web:
+ dependency: transitive
+ description:
+ name: image_picker_for_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.6"
+ image_picker_ios:
+ dependency: transitive
+ description:
+ name: image_picker_ios
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.8.4+11"
+ image_picker_platform_interface:
+ dependency: transitive
+ description:
+ name: image_picker_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.4.4"
intl:
dependency: transitive
description:
@@ -394,6 +450,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
+ qr_code_scanner:
+ dependency: "direct main"
+ description:
+ name: qr_code_scanner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.7.0"
quiver:
dependency: transitive
description:
@@ -672,6 +735,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
+ zxing2:
+ dependency: "direct main"
+ description:
+ name: zxing2
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.0"
sdks:
dart: ">=2.16.0 <3.0.0"
flutter: ">=2.10.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 44d9337ab..1309227f1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -46,6 +46,10 @@ dependencies:
settings_ui: ^2.0.2
flutter_breadcrumb: ^1.0.1
http: ^0.13.4
+ qr_code_scanner: ^0.7.0
+ zxing2: ^0.1.0
+ image_picker: ^0.8.5
+ image: ^3.1.3
dev_dependencies:
flutter_launcher_icons: ^0.9.1