diff --git a/lib/common.dart b/lib/common.dart index 82c3c754a..410e9ca03 100644 --- a/lib/common.dart +++ b/lib/common.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:tuple/tuple.dart'; -import 'dart:io'; typedef F = String Function(String); @@ -29,13 +28,13 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom( ), ); -void Function() loadingCancelCallback = null; +void Function() loadingCancelCallback; void showLoading(String text, BuildContext context) { if (_hasDialog && context != null) { Navigator.pop(context); } dismissLoading(); - if (Platform.isAndroid) { + if (isAndroid) { EasyLoading.show(status: text, maskType: EasyLoadingMaskType.black); return; } @@ -202,3 +201,7 @@ Color str2color(String str, [alpha = 0xFF]) { hash = hash % 16777216; return Color((hash & 0xFF7FFF) | (alpha << 24)); } + +bool isAndroid; +bool isIOS; +bool isWeb; diff --git a/lib/home_page.dart b/lib/home_page.dart index 19ca26339..7f4778c88 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -5,9 +5,8 @@ import 'package:package_info/package_info.dart'; import 'package:url_launcher/url_launcher.dart'; import 'dart:async'; import 'common.dart'; -import 'model.dart'; +import 'model.dart' if (dart.library.html) 'web_model.dart'; import 'remote_page.dart'; -import 'dart:io'; class HomePage extends StatefulWidget { HomePage({Key key, this.title}) : super(key: key); @@ -25,7 +24,7 @@ class _HomePageState extends State { @override void initState() { super.initState(); - if (Platform.isAndroid) { + if (isAndroid) { Timer(Duration(seconds: 5), () { _updateUrl = FFI.getByName('software_update_url'); if (_updateUrl.isNotEmpty) setState(() {}); diff --git a/lib/main.dart b/lib/main.dart index 32af66665..29d9ad0ed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/observer.dart'; import 'package:firebase_core/firebase_core.dart'; -import 'model.dart'; +import 'model.dart' if (dart.library.html) 'web_model.dart'; import 'home_page.dart'; Future main() async { diff --git a/lib/model.dart b/lib/model.dart index db897443d..73dc375ec 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -1,6 +1,5 @@ import 'package:ffi/ffi.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/gestures.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:device_info/device_info.dart'; @@ -44,6 +43,9 @@ class FfiModel with ChangeNotifier { get pi => _pi; FfiModel() { + isIOS = Platform.isIOS; + isAndroid = Platform.isAndroid; + isWeb = false; Translator.call = translate; clear(); () async { @@ -295,7 +297,7 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - void clear([bool notify=false]) { + void clear([bool notify = false]) { _x = 0; _y = 0; _scale = 1.0; @@ -797,7 +799,7 @@ String translate(String name) { final v = tmp[name]; if (v == null) { var a = 'translate'; - var b = '{"locale": "${Platform.localeName}", "text": "${name}"}'; + var b = '{"locale": "${Platform.localeName}", "text": "$name"}'; return FFI.getByName(a, b); } else { return v; diff --git a/lib/remote_page.dart b/lib/remote_page.dart index 5fdaac60e..d36be7155 100644 --- a/lib/remote_page.dart +++ b/lib/remote_page.dart @@ -7,8 +7,7 @@ import 'dart:async'; import 'package:tuple/tuple.dart'; import 'package:wakelock/wakelock.dart'; import 'common.dart'; -import 'model.dart'; -import 'dart:io'; +import 'model.dart' if (dart.library.html) 'web_model.dart'; final initText = '\1' * 1024; @@ -124,7 +123,7 @@ class _RemotePageState extends State { void handleInput(String newValue) { var oldValue = _value; _value = newValue; - if (Platform.isIOS) { + if (isIOS) { var i = newValue.length - 1; for (; i >= 0 && newValue[i] != '\1'; --i) {} var j = oldValue.length - 1; diff --git a/lib/web_model.dart b/lib/web_model.dart new file mode 100644 index 000000000..c84d20059 --- /dev/null +++ b/lib/web_model.dart @@ -0,0 +1,700 @@ +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:device_info/device_info.dart'; +import 'dart:math'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:tuple/tuple.dart'; +import 'dart:async'; +import 'common.dart'; + +class FfiModel with ChangeNotifier { + PeerInfo _pi; + Display _display; + var _decoding = false; + bool _waitForImage; + bool _initialized = false; + final _permissions = Map(); + bool _secure; + bool _direct; + + get permissions => _permissions; + get initialized => _initialized; + get display => _display; + get secure => _secure; + get direct => _direct; + get pi => _pi; + + FfiModel() { + isIOS = false; + isAndroid = false; + isWeb = true; + Translator.call = translate; + clear(); + () async { + await FFI.init(); + _initialized = true; + print("FFI initialized"); + notifyListeners(); + }(); + } + + void updatePermission(Map evt) { + evt.forEach((k, v) { + if (k == 'name') return; + _permissions[k] = v == 'true'; + }); + print('$_permissions'); + } + + bool keyboard() => _permissions['keyboard'] != false; + + void clear() { + _pi = PeerInfo(); + _display = Display(); + _waitForImage = false; + _secure = null; + _direct = null; + clearPermissions(); + } + + void setConnectionType(bool secure, bool direct) { + _secure = secure; + _direct = direct; + } + + Image getConnectionImage() { + String icon; + if (secure == true && direct == true) { + icon = 'secure'; + } else if (secure == false && direct == true) { + icon = 'insecure'; + } else if (secure == false && direct == false) { + icon = 'insecure_relay'; + } else if (secure == true && direct == false) { + icon = 'secure_relay'; + } + return icon == null + ? null + : Image.asset('assets/$icon.png', width: 48, height: 48); + } + + void clearPermissions() { + _permissions.clear(); + } + + void update( + String id, + BuildContext context, + void Function( + Map evt, + String id, + ) + handleMsgbox) { + var pos; + for (;;) { + var evt = FFI.popEvent(); + if (evt == null) break; + var name = evt['name']; + if (name == 'msgbox') { + handleMsgbox(evt, id); + } else if (name == 'peer_info') { + handlePeerInfo(evt, context); + } else if (name == 'connection_ready') { + FFI.ffiModel.setConnectionType( + evt['secure'] == 'true', evt['direct'] == 'true'); + } else if (name == 'switch_display') { + handleSwitchDisplay(evt); + } else if (name == 'cursor_data') { + FFI.cursorModel.updateCursorData(evt); + } else if (name == 'cursor_id') { + FFI.cursorModel.updateCursorId(evt); + } else if (name == 'cursor_position') { + pos = evt; + } else if (name == 'clipboard') { + } else if (name == 'permission') { + FFI.ffiModel.updatePermission(evt); + } + } + if (pos != null) FFI.cursorModel.updateCursorPosition(pos); + if (!_decoding) { + var rgba = FFI.getRgba(); + if (rgba != null) { + if (_waitForImage) { + _waitForImage = false; + dismissLoading(); + } + _decoding = true; + final pid = FFI.id; + ui.decodeImageFromPixels( + rgba, _display.width, _display.height, ui.PixelFormat.bgra8888, + (image) { + FFI.clearRgbaFrame(); + _decoding = false; + if (FFI.id != pid) return; + try { + // my throw exception, because the listener maybe already dispose + FFI.imageModel.update(image); + } catch (e) { + print('update image: $e'); + } + }); + } + } + } + + void handleSwitchDisplay(Map evt) { + var old = _pi.currentDisplay; + _pi.currentDisplay = int.parse(evt['display']); + _display.x = double.parse(evt['x']); + _display.y = double.parse(evt['y']); + _display.width = int.parse(evt['width']); + _display.height = int.parse(evt['height']); + if (old != _pi.currentDisplay) + FFI.cursorModel.updateDisplayOrigin(_display.x, _display.y); + notifyListeners(); + } + + void handlePeerInfo(Map evt, BuildContext context) { + dismissLoading(); + _pi.version = evt['version']; + _pi.username = evt['username']; + _pi.hostname = evt['hostname']; + _pi.platform = evt['platform']; + _pi.sasEnabled = evt['sas_enabled'] == "true"; + _pi.currentDisplay = int.parse(evt['current_display']); + List displays = json.decode(evt['displays']); + _pi.displays = []; + for (int i = 0; i < displays.length; ++i) { + Map d0 = displays[i]; + var d = Display(); + d.x = d0['x'].toDouble(); + d.y = d0['y'].toDouble(); + d.width = d0['width']; + d.height = d0['height']; + _pi.displays.add(d); + } + if (_pi.currentDisplay < _pi.displays.length) { + _display = _pi.displays[_pi.currentDisplay]; + initializeCursorAndCanvas(); + } + if (displays.length > 0) { + showLoading(translate('Connected, waiting for image...'), context); + _waitForImage = true; + } + notifyListeners(); + } +} + +class ImageModel with ChangeNotifier { + ui.Image _image; + + ui.Image get image => _image; + + void update(ui.Image image) { + if (_image == null && image != null) { + final size = MediaQueryData.fromWindow(ui.window).size; + final xscale = size.width / image.width; + final yscale = size.height / image.height; + FFI.canvasModel.scale = max(xscale, yscale); + } + _image = image; + if (image != null) notifyListeners(); + } + + double get maxScale { + if (_image == null) return 1.0; + final size = MediaQueryData.fromWindow(ui.window).size; + final xscale = size.width / _image.width; + final yscale = size.height / _image.height; + return max(1.0, max(xscale, yscale)); + } + + double get minScale { + if (_image == null) return 1.0; + final size = MediaQueryData.fromWindow(ui.window).size; + final xscale = size.width / _image.width; + final yscale = size.height / _image.height; + return min(xscale, yscale); + } +} + +class CanvasModel with ChangeNotifier { + double _x; + double _y; + double _scale; + + CanvasModel() { + clear(); + } + + double get x => _x; + double get y => _y; + double get scale => _scale; + + void update(double x, double y, double scale) { + _x = x; + _y = y; + _scale = scale; + notifyListeners(); + } + + set scale(v) { + _scale = v; + notifyListeners(); + } + + void panX(double dx) { + _x += dx; + notifyListeners(); + } + + void resetOffset() { + _x = 0; + _y = 0; + notifyListeners(); + } + + void panY(double dy) { + _y += dy; + notifyListeners(); + } + + void updateScale(double v) { + if (FFI.imageModel.image == null) return; + final offset = FFI.cursorModel.offset; + var r = FFI.cursorModel.getVisibleRect(); + final px0 = (offset.dx - r.left) * _scale; + final py0 = (offset.dy - r.top) * _scale; + _scale *= v; + final maxs = FFI.imageModel.maxScale; + final mins = FFI.imageModel.minScale; + if (_scale > maxs) _scale = maxs; + if (_scale < mins) _scale = mins; + r = FFI.cursorModel.getVisibleRect(); + final px1 = (offset.dx - r.left) * _scale; + final py1 = (offset.dy - r.top) * _scale; + _x -= px1 - px0; + _y -= py1 - py0; + notifyListeners(); + } + + void clear([bool notify = false]) { + _x = 0; + _y = 0; + _scale = 1.0; + if (notify) notifyListeners(); + } +} + +class CursorModel with ChangeNotifier { + ui.Image _image; + final _images = Map>(); + double _x = -10000; + double _y = -10000; + double _hotx = 0; + double _hoty = 0; + double _displayOriginX = 0; + double _displayOriginY = 0; + + ui.Image get image => _image; + double get x => _x - _displayOriginX; + double get y => _y - _displayOriginY; + Offset get offset => Offset(_x, _y); + double get hotx => _hotx; + double get hoty => _hoty; + + // remote physical display coordinate + Rect getVisibleRect() { + final size = MediaQueryData.fromWindow(ui.window).size; + final xoffset = FFI.canvasModel.x; + final yoffset = FFI.canvasModel.y; + final scale = FFI.canvasModel.scale; + final x0 = _displayOriginX - xoffset / scale; + final y0 = _displayOriginY - yoffset / scale; + return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale); + } + + double adjustForKeyboard() { + final m = MediaQueryData.fromWindow(ui.window); + var keyboardHeight = m.viewInsets.bottom; + final size = m.size; + if (keyboardHeight < 100) return 0; + final s = FFI.canvasModel.scale; + final thresh = (size.height - keyboardHeight) / 2; + var h = (_y - getVisibleRect().top) * s; // local physical display height + return h - thresh; + } + + void touch(double x, double y, bool right) { + final scale = FFI.canvasModel.scale; + final xoffset = FFI.canvasModel.x; + final yoffset = FFI.canvasModel.y; + _x = (x - xoffset) / scale + _displayOriginX; + _y = (y - yoffset) / scale + _displayOriginY; + FFI.moveMouse(_x, _y); + FFI.tap(right); + notifyListeners(); + } + + void reset() { + _x = _displayOriginX; + _y = _displayOriginY; + FFI.moveMouse(_x, _y); + FFI.canvasModel.clear(true); + notifyListeners(); + } + + void updatePan(double dx, double dy, bool touchMode, bool drag) { + if (FFI.imageModel.image == null) return; + if (touchMode) { + if (drag) { + final scale = FFI.canvasModel.scale; + _x += dx / scale; + _y += dy / scale; + FFI.moveMouse(_x, _y); + notifyListeners(); + } else { + FFI.canvasModel.panX(dx); + FFI.canvasModel.panY(dy); + } + return; + } + final scale = FFI.canvasModel.scale; + dx /= scale; + dy /= scale; + final r = getVisibleRect(); + var cx = r.center.dx; + var cy = r.center.dy; + var tryMoveCanvasX = false; + if (dx > 0) { + final maxCanvasCanMove = + _displayOriginX + FFI.imageModel.image.width - r.right; + tryMoveCanvasX = _x + dx > cx && maxCanvasCanMove > 0; + if (tryMoveCanvasX) { + dx = min(dx, maxCanvasCanMove); + } else { + final maxCursorCanMove = r.right - _x; + dx = min(dx, maxCursorCanMove); + } + } else if (dx < 0) { + final maxCanvasCanMove = _displayOriginX - r.left; + tryMoveCanvasX = _x + dx < cx && maxCanvasCanMove < 0; + if (tryMoveCanvasX) { + dx = max(dx, maxCanvasCanMove); + } else { + final maxCursorCanMove = r.left - _x; + dx = max(dx, maxCursorCanMove); + } + } + var tryMoveCanvasY = false; + if (dy > 0) { + final mayCanvasCanMove = + _displayOriginY + FFI.imageModel.image.height - r.bottom; + tryMoveCanvasY = _y + dy > cy && mayCanvasCanMove > 0; + if (tryMoveCanvasY) { + dy = min(dy, mayCanvasCanMove); + } else { + final mayCursorCanMove = r.bottom - _y; + dy = min(dy, mayCursorCanMove); + } + } else if (dy < 0) { + final mayCanvasCanMove = _displayOriginY - r.top; + tryMoveCanvasY = _y + dy < cy && mayCanvasCanMove < 0; + if (tryMoveCanvasY) { + dy = max(dy, mayCanvasCanMove); + } else { + final mayCursorCanMove = r.top - _y; + dy = max(dy, mayCursorCanMove); + } + } + + if (dx == 0 && dy == 0) return; + _x += dx; + _y += dy; + if (tryMoveCanvasX && dx != 0) { + FFI.canvasModel.panX(-dx); + } + if (tryMoveCanvasY && dy != 0) { + FFI.canvasModel.panY(-dy); + } + + FFI.moveMouse(_x, _y); + notifyListeners(); + } + + void updateCursorData(Map evt) { + var id = int.parse(evt['id']); + _hotx = double.parse(evt['hotx']); + _hoty = double.parse(evt['hoty']); + var width = int.parse(evt['width']); + var height = int.parse(evt['height']); + List colors = json.decode(evt['colors']); + final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); + var pid = FFI.id; + ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, + (image) { + if (FFI.id != pid) return; + _image = image; + _images[id] = Tuple3(image, _hotx, _hoty); + try { + // my throw exception, because the listener maybe already dispose + notifyListeners(); + } catch (e) { + print('notify cursor: $e'); + } + }); + } + + void updateCursorId(Map evt) { + final tmp = _images[int.parse(evt['id'])]; + if (tmp != null) { + _image = tmp.item1; + _hotx = tmp.item2; + _hoty = tmp.item3; + notifyListeners(); + } + } + + void updateCursorPosition(Map evt) { + _x = double.parse(evt['x']); + _y = double.parse(evt['y']); + notifyListeners(); + } + + void updateDisplayOrigin(double x, double y) { + _displayOriginX = x; + _displayOriginY = y; + _x = x + 1; + _y = y + 1; + FFI.moveMouse(x, y); + FFI.canvasModel.resetOffset(); + notifyListeners(); + } + + void updateDisplayOriginWithCursor( + double x, double y, double xCursor, double yCursor) { + _displayOriginX = x; + _displayOriginY = y; + _x = xCursor; + _y = yCursor; + FFI.moveMouse(x, y); + notifyListeners(); + } + + void clear() { + _x = -10000; + _x = -10000; + _image = null; + _images.clear(); + } +} + +class FFI { + static String id = ""; + static String _dir = ''; + static var shift = false; + static var ctrl = false; + static var alt = false; + static var command = false; + static final imageModel = ImageModel(); + static final ffiModel = FfiModel(); + static final cursorModel = CursorModel(); + static final canvasModel = CanvasModel(); + + static String getId() { + return getByName('remote_id'); + } + + static void tap(bool right) { + sendMouse('down', right ? 'right' : 'left'); + sendMouse('up', right ? 'right' : 'left'); + } + + static void scroll(double y) { + var y2 = y.round(); + if (y2 == 0) return; + setByName('send_mouse', + json.encode(modify({'type': 'wheel', 'y': y2.toString()}))); + } + + static void reconnect() { + setByName('reconnect'); + FFI.ffiModel.clearPermissions(); + } + + static void resetModifiers() { + shift = ctrl = alt = command = false; + } + + static Map modify(Map evt) { + if (ctrl) evt['ctrl'] = 'true'; + if (shift) evt['shift'] = 'true'; + if (alt) evt['alt'] = 'true'; + if (command) evt['command'] = 'true'; + return evt; + } + + static void sendMouse(String type, String buttons) { + if (!ffiModel.keyboard()) return; + setByName( + 'send_mouse', json.encode(modify({'type': type, 'buttons': buttons}))); + } + + static void inputKey(String name) { + if (!ffiModel.keyboard()) return; + setByName('input_key', json.encode(modify({'name': name}))); + } + + static void moveMouse(double x, double y) { + if (!ffiModel.keyboard()) return; + var x2 = x.toInt(); + var y2 = y.toInt(); + setByName('send_mouse', json.encode(modify({'x': '$x2', 'y': '$y2'}))); + } + + static List peers() { + try { + List peers = json.decode(getByName('peers')); + return peers + .map((s) => s as List) + .map((s) => + Peer.fromJson(s[0] as String, s[1] as Map)) + .toList(); + } catch (e) { + print('peers(): $e'); + } + return []; + } + + static void connect(String id) { + setByName('connect', id); + FFI.id = id; + } + + static void clearRgbaFrame() {} + + static Uint8List getRgba() {} + + static Map popEvent() { + var s = getByName('event'); + if (s == '') return null; + try { + Map event = json.decode(s); + return event; + } catch (e) { + print('popEvent(): $e'); + } + return null; + } + + static void login(String password, bool remember) { + setByName( + 'login', + json.encode({ + 'password': password, + 'remember': remember ? 'true' : 'false', + })); + } + + static void close() { + savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x, + canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); + id = ""; + setByName('close', ''); + imageModel.update(null); + cursorModel.clear(); + ffiModel.clear(); + canvasModel.clear(); + resetModifiers(); + } + + static void setByName(String name, [String value = '']) {} + + static String getByName(String name, [String arg = '']) {} + + static Future init() async {} +} + +class Peer { + final String id; + final String username; + final String hostname; + final String platform; + + Peer.fromJson(String id, Map json) + : id = id, + username = json['username'], + hostname = json['hostname'], + platform = json['platform']; +} + +class Display { + double x = 0; + double y = 0; + int width = 0; + int height = 0; +} + +class PeerInfo { + String version; + String username; + String hostname; + String platform; + bool sasEnabled; + int currentDisplay; + List displays; +} + +void savePreference(String id, double xCursor, double yCursor, double xCanvas, + double yCanvas, double scale, int currentDisplay) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + final p = Map(); + p['xCursor'] = xCursor; + p['yCursor'] = yCursor; + p['xCanvas'] = xCanvas; + p['yCanvas'] = yCanvas; + p['scale'] = scale; + p['currentDisplay'] = currentDisplay; + prefs.setString('peer' + id, json.encode(p)); +} + +Future> getPreference(String id) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + var p = prefs.getString('peer' + id); + if (p == null) return null; + Map m = json.decode(p); + return m; +} + +void removePreference(String id) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.remove('peer' + id); +} + +void initializeCursorAndCanvas() async { + var p = await getPreference(FFI.id); + int currentDisplay = 0; + if (p != null) { + currentDisplay = p['currentDisplay']; + } + if (p == null || currentDisplay != FFI.ffiModel.pi.currentDisplay) { + FFI.cursorModel + .updateDisplayOrigin(FFI.ffiModel.display.x, FFI.ffiModel.display.y); + return; + } + double xCursor = p['xCursor']; + double yCursor = p['yCursor']; + double xCanvas = p['xCanvas']; + double yCanvas = p['yCanvas']; + double scale = p['scale']; + FFI.cursorModel.updateDisplayOriginWithCursor( + FFI.ffiModel.display.x, FFI.ffiModel.display.y, xCursor, yCursor); + FFI.canvasModel.update(xCanvas, yCanvas, scale); +} + +String translate(String name) { + return name; +} diff --git a/pubspec.lock b/pubspec.lock index 7f7f91a5d..4ec2db940 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,7 +21,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -35,14 +35,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -227,14 +227,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" nested: dependency: transitive description: @@ -428,7 +428,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.3" tuple: dependency: "direct main" description: @@ -491,7 +491,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" wakelock: dependency: "direct main" description: @@ -556,5 +556,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.13.0 <3.0.0" + dart: ">=2.14.0 <3.0.0" flutter: ">=2.0.0" diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 000000000..8aaa46ac1 Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 000000000..b749bfef0 Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 000000000..88cfd48df Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000..eb9b4d76e Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000..d69c56691 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 000000000..377445124 --- /dev/null +++ b/web/index.html @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + RustDesk + + + + + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 000000000..9723be242 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "rustdesk", + "short_name": "rustdesk", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Remote Desktop.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}