Merge branch 'master' into service

This commit is contained in:
rustdesk 2022-04-15 17:51:35 +08:00
commit d6eee37fbb
10 changed files with 388 additions and 137 deletions

View File

@ -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'
}

View File

@ -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

View File

@ -42,6 +42,12 @@
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<false/>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access to get QR codes from image</string>
</dict>
</plist>

View File

@ -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();

View File

@ -19,7 +19,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
final title = translate("Connection");
@override
final appBarActions = !isAndroid ? <Widget>[WebMenu()] : <Widget>[];
final appBarActions = isWeb ? <Widget>[WebMenu()] : <Widget>[];
@override
final scrollController = ScrollController();
@ -312,7 +312,7 @@ class _WebMenuState extends State<WebMenu> {
},
onSelected: (value) {
if (value == 'server') {
showServer();
showServerSettings();
}
if (value == 'about') {
showAbout();

View File

@ -26,12 +26,9 @@ class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
_pages.addAll([
ConnectionPage(),
chatPage,
]);
_pages.add(ConnectionPage());
if (isAndroid) {
_pages.add(ServerPage());
_pages.addAll([chatPage, ServerPage()]);
}
_pages.add(SettingsPage());
}

257
lib/pages/scan_page.dart Normal file
View File

@ -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<ScanPage> {
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<String, dynamic> 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<FormState>();
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: <Widget>[
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;
}

View File

@ -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
final scrollController = null;
@ -61,7 +62,7 @@ class _SettingsState extends State<SettingsPage> {
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
showServer();
showServerSettings();
},
),
],
@ -70,20 +71,18 @@ class _SettingsState extends State<SettingsPage> {
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)),
],
@ -93,116 +92,12 @@ class _SettingsState extends State<SettingsPage> {
}
}
void showServer() {
final formKey = GlobalKey<FormState>();
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: <Widget>[
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() {
@ -451,3 +346,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(),
),
);
},
);
}
}

View File

@ -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"

View File

@ -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