From a15cd62fd4f92c06b655567bebe44428433cd3ec Mon Sep 17 00:00:00 2001
From: fufesou <fufesou@users.noreply.github.com>
Date: Sat, 23 Mar 2024 10:08:55 +0800
Subject: [PATCH] Refact. Flutter web, mid commit (#7482)

Signed-off-by: fufesou <shuanglongchen@yeah.net>
---
 flutter/lib/common.dart                       | 31 +++---
 flutter/lib/main.dart                         |  8 +-
 flutter/lib/mobile/pages/connection_page.dart |  2 +-
 flutter/lib/models/web_model.dart             | 15 ++-
 flutter/lib/web/bridge.dart                   | 98 ++++++++++++-------
 flutter/web/js/package.json                   |  6 +-
 flutter/web/js/src/connection.ts              | 16 ++-
 flutter/web/js/src/globals.js                 | 79 ++++++++++++++-
 8 files changed, 187 insertions(+), 68 deletions(-)

diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index 611cdecb3..21a1c3b5e 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -1,6 +1,5 @@
 import 'dart:async';
 import 'dart:convert';
-import 'dart:io';
 import 'dart:math';
 
 import 'package:back_button_interceptor/back_button_interceptor.dart';
@@ -1544,7 +1543,7 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
   late Size sz;
   late bool isMaximized;
   bool isFullscreen = stateGlobal.fullscreen.isTrue ||
-      (Platform.isMacOS && stateGlobal.closeOnFullscreen == true);
+      (isMacOS && stateGlobal.closeOnFullscreen == true);
   setFrameIfMaximized() {
     if (isMaximized) {
       final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
@@ -1583,7 +1582,7 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
       setFrameIfMaximized();
       break;
   }
-  if (Platform.isWindows) {
+  if (isWindows) {
     const kMinOffset = -10000;
     const kMaxOffset = 10000;
     if (position.dx < kMinOffset ||
@@ -1854,7 +1853,7 @@ Future<bool> restoreWindowPosition(WindowType type,
 /// initUniLinks should only be used on macos/windows.
 /// we use dbus for linux currently.
 Future<bool> initUniLinks() async {
-  if (Platform.isLinux) {
+  if (isLinux) {
     return false;
   }
   // check cold boot
@@ -1876,7 +1875,7 @@ Future<bool> initUniLinks() async {
 ///
 /// Returns a [StreamSubscription] which can listen the uni links.
 StreamSubscription? listenUniLinks({handleByFlutter = true}) {
-  if (Platform.isLinux) {
+  if (isLinux) {
     return null;
   }
 
@@ -2024,7 +2023,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
     command = '--connect';
     id = uri.path.substring("/new/".length);
   } else if (uri.authority == "config") {
-    if (Platform.isAndroid || Platform.isIOS) {
+    if (isAndroid || isIOS) {
       final config = uri.path.substring("/".length);
       // add a timer to make showToast work
       Timer(Duration(seconds: 1), () {
@@ -2033,7 +2032,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
     }
     return null;
   } else if (uri.authority == "password") {
-    if (Platform.isAndroid || Platform.isIOS) {
+    if (isAndroid || isIOS) {
       final password = uri.path.substring("/".length);
       if (password.isNotEmpty) {
         Timer(Duration(seconds: 1), () async {
@@ -2253,7 +2252,7 @@ Future<void> reloadAllWindows() async {
 /// [Note]
 /// Portable build is only available on Windows.
 bool isRunningInPortableMode() {
-  if (!Platform.isWindows) {
+  if (!isWindows) {
     return false;
   }
   return bool.hasEnvironment(kEnvPortableExecutable);
@@ -2266,7 +2265,7 @@ Future<void> onActiveWindowChanged() async {
   if (rustDeskWinManager.getActiveWindows().isEmpty) {
     // close all sub windows
     try {
-      if (Platform.isLinux) {
+      if (isLinux) {
         await Future.wait([
           saveWindowPosition(WindowType.Main),
           rustDeskWinManager.closeAllSubWindows()
@@ -2280,7 +2279,7 @@ Future<void> onActiveWindowChanged() async {
       debugPrint("Start closing RustDesk...");
       await windowManager.setPreventClose(false);
       await windowManager.close();
-      if (Platform.isMacOS) {
+      if (isMacOS) {
         RdPlatformChannel.instance.terminate();
       }
     }
@@ -2296,7 +2295,7 @@ Timer periodic_immediate(Duration duration, Future<void> Function() callback) {
 
 /// return a human readable windows version
 WindowsTarget getWindowsTarget(int buildNumber) {
-  if (!Platform.isWindows) {
+  if (!isWindows) {
     return WindowsTarget.naw;
   }
   if (buildNumber >= 22000) {
@@ -2330,7 +2329,7 @@ int getWindowsTargetBuildNumber() {
 /// [Conditions]
 /// - Windows 7, window will overflow when we use frameless ui.
 bool get kUseCompatibleUiMode =>
-    Platform.isWindows &&
+    isWindows &&
     const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
 
 class ServerConfig {
@@ -2460,7 +2459,7 @@ Future<void> updateSystemWindowTheme() async {
   // Set system window theme for macOS.
   final userPreference = MyTheme.getThemeModePreference();
   if (userPreference != ThemeMode.system) {
-    if (Platform.isMacOS) {
+    if (isMacOS) {
       await RdPlatformChannel.instance.changeSystemWindowTheme(
           userPreference == ThemeMode.light
               ? SystemWindowTheme.light
@@ -2548,7 +2547,7 @@ void onCopyFingerprint(String value) {
 
 Future<bool> callMainCheckSuperUserPermission() async {
   bool checked = await bind.mainCheckSuperUserPermission();
-  if (Platform.isMacOS) {
+  if (isMacOS) {
     await windowManager.show();
   }
   return checked;
@@ -2556,7 +2555,7 @@ Future<bool> callMainCheckSuperUserPermission() async {
 
 Future<void> start_service(bool is_start) async {
   bool checked = !bind.mainIsInstalled() ||
-      !Platform.isMacOS ||
+      !isMacOS ||
       await callMainCheckSuperUserPermission();
   if (checked) {
     bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y");
@@ -3114,7 +3113,7 @@ Widget loadIcon(double size) {
 
 var imcomingOnlyHomeSize = Size(280, 300);
 Size getIncomingOnlyHomeSize() {
-  final magicWidth = Platform.isWindows ? 11.0 : 2.0;
+  final magicWidth = isWindows ? 11.0 : 2.0;
   final magicHeight = 10.0;
   return imcomingOnlyHomeSize +
       Offset(magicWidth, kDesktopRemoteTabBarHeight + magicHeight);
diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart
index e1632184e..e7e5aee22 100644
--- a/flutter/lib/main.dart
+++ b/flutter/lib/main.dart
@@ -125,6 +125,10 @@ void runMainApp(bool startService) async {
   await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
   gFFI.userModel.refreshCurrentUser();
   runApp(App());
+  if (isWeb) {
+    // Web does not support window manager.
+    return;
+  }
   // Set window option.
   WindowOptions windowOptions = getHiddenTitleBarWindowOptions();
   windowManager.waitUntilReadyToShow(windowOptions, () async {
@@ -150,11 +154,11 @@ void runMainApp(bool startService) async {
 void runMobileApp() async {
   await initEnv(kAppTypeMain);
   if (isAndroid) androidChannelInit();
-  platformFFI.syncAndroidServiceAppDirConfigPath();
+  if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
   await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
   gFFI.userModel.refreshCurrentUser();
   runApp(App());
-  await initUniLinks();
+  if (!isWeb) await initUniLinks();
 }
 
 void runMultiWindow(
diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart
index 09c1e7ea6..d2a3c0347 100644
--- a/flutter/lib/mobile/pages/connection_page.dart
+++ b/flutter/lib/mobile/pages/connection_page.dart
@@ -53,7 +53,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
   @override
   void initState() {
     super.initState();
-    _uniLinksSubscription = listenUniLinks();
+    if (!isWeb) _uniLinksSubscription = listenUniLinks();
     if (_idController.text.isEmpty) {
       () async {
         final lastRemoteId = await bind.mainGetLastRemoteId();
diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart
index d8c8491c9..ef8bfff21 100644
--- a/flutter/lib/models/web_model.dart
+++ b/flutter/lib/models/web_model.dart
@@ -18,7 +18,7 @@ typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
 
 class PlatformFFI {
   final _eventHandlers = <String, Map<String, HandleEvent>>{};
-  late RustdeskImpl _ffiBind;
+  final RustdeskImpl _ffiBind = RustdeskImpl();
 
   static String getByName(String name, [String arg = '']) {
     return context.callMethod('getByName', [name, arg]);
@@ -101,6 +101,15 @@ class PlatformFFI {
     isWebDesktop = !context.callMethod('isMobile');
     context.callMethod('init');
     version = getByName('version');
+
+    context['onRegisteredEvent'] = (String message) {
+      try {
+        Map<String, dynamic> event = json.decode(message);
+        tryHandle(event);
+      } catch (e) {
+        print('json.decode fail(): $e');
+      }
+    };
   }
 
   void setEventCallback(void Function(Map<String, dynamic>) fun) {
@@ -145,7 +154,5 @@ class PlatformFFI {
   }
 
   // just for compilation
-  void syncAndroidServiceAppDirConfigPath() {
-    throw UnimplementedError();
-  }
+  void syncAndroidServiceAppDirConfigPath() {}
 }
diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart
index 8c55c1d7e..5c6dc8c98 100644
--- a/flutter/lib/web/bridge.dart
+++ b/flutter/lib/web/bridge.dart
@@ -1,4 +1,6 @@
 import 'dart:async';
+import 'dart:js' as js;
+import 'dart:convert';
 import 'dart:typed_data';
 import 'package:uuid/uuid.dart';
 
@@ -176,29 +178,35 @@ class RustdeskImpl {
 
   Future<String?> sessionGetFlutterOptionByPeerId(
       {required String id, required String k, dynamic hint}) {
-    throw UnimplementedError();
+    return Future.value(null);
   }
 
   int getNextTextureKey({dynamic hint}) {
-    throw UnimplementedError();
+    return 0;
   }
 
   String getLocalFlutterOption({required String k, dynamic hint}) {
-    throw UnimplementedError();
+    return js.context.callMethod('getByName', ['option:flutter:local', k]);
   }
 
   Future<void> setLocalFlutterOption(
       {required String k, required String v, dynamic hint}) {
-    throw UnimplementedError();
+    return Future(() => js.context.callMethod('setByName', [
+          'option:flutter:local',
+          jsonEncode({'name': k, 'value': v})
+        ]));
   }
 
   String getLocalKbLayoutType({dynamic hint}) {
-    throw UnimplementedError();
+    throw js.context.callMethod('getByName', ['option:local', 'kb_layout']);
   }
 
   Future<void> setLocalKbLayoutType(
       {required String kbLayoutType, dynamic hint}) {
-    throw UnimplementedError();
+    return Future(() => js.context.callMethod('setByName', [
+          'option:local',
+          jsonEncode({'name': 'kb_layout', 'value': kbLayoutType})
+        ]));
   }
 
   Future<String?> sessionGetViewStyle(
@@ -538,11 +546,11 @@ class RustdeskImpl {
   }
 
   Future<String> mainGetOption({required String key, dynamic hint}) {
-    throw UnimplementedError();
+    return Future.value(mainGetOptionSync(key: key));
   }
 
   String mainGetOptionSync({required String key, dynamic hint}) {
-    throw UnimplementedError();
+    return js.context.callMethod('getByName', ['option', key]);
   }
 
   Future<String> mainGetError({dynamic hint}) {
@@ -555,7 +563,10 @@ class RustdeskImpl {
 
   Future<void> mainSetOption(
       {required String key, required String value, dynamic hint}) {
-    throw UnimplementedError();
+    return js.context.callMethod('setByName', [
+      'option',
+      jsonEncode({'name': key, 'value': value})
+    ]);
   }
 
   Future<String> mainGetOptions({dynamic hint}) {
@@ -623,11 +634,13 @@ class RustdeskImpl {
   }
 
   Future<String> mainGetConnectStatus({dynamic hint}) {
-    throw UnimplementedError();
+    return Future(
+        () => js.context.callMethod('getByName', ["get_conn_status"]));
   }
 
   Future<void> mainCheckConnectStatus({dynamic hint}) {
-    throw UnimplementedError();
+    return Future(
+        () => js.context.callMethod('setByName', ["check_conn_status"]));
   }
 
   Future<bool> mainIsUsingPublicServer({dynamic hint}) {
@@ -635,7 +648,7 @@ class RustdeskImpl {
   }
 
   Future<void> mainDiscover({dynamic hint}) {
-    throw UnimplementedError();
+    return Future(() => js.context.callMethod('setByName', ['discover']));
   }
 
   Future<String> mainGetApiServer({dynamic hint}) {
@@ -651,7 +664,7 @@ class RustdeskImpl {
   }
 
   String mainGetLocalOption({required String key, dynamic hint}) {
-    throw UnimplementedError();
+    return js.context.callMethod('getByName', ['option:local', key]);
   }
 
   String mainGetEnv({required String key, dynamic hint}) {
@@ -660,7 +673,10 @@ class RustdeskImpl {
 
   Future<void> mainSetLocalOption(
       {required String key, required String value, dynamic hint}) {
-    throw UnimplementedError();
+    return Future(() => js.context.callMethod('setByName', [
+          'option:local',
+          jsonEncode({'name': key, 'value': value})
+        ]));
   }
 
   String mainGetInputSource({dynamic hint}) {
@@ -741,15 +757,16 @@ class RustdeskImpl {
   }
 
   Future<void> mainLoadRecentPeers({dynamic hint}) {
-    throw UnimplementedError();
+    return Future(
+        () => js.context.callMethod('getByName', ['load_recent_peers']));
   }
 
   String mainLoadRecentPeersSync({dynamic hint}) {
-    throw UnimplementedError();
+    return js.context.callMethod('getByName', ['load_recent_peers_sync']);
   }
 
   String mainLoadLanPeersSync({dynamic hint}) {
-    throw UnimplementedError();
+    return js.context.callMethod('getByName', ['load_lan_peers_sync']);
   }
 
   Future<String> mainLoadRecentPeersForAb(
@@ -758,15 +775,16 @@ class RustdeskImpl {
   }
 
   Future<void> mainLoadFavPeers({dynamic hint}) {
-    throw UnimplementedError();
+    return Future(() => js.context.callMethod('getByName', ['load_fav_peers']));
   }
 
   Future<void> mainLoadLanPeers({dynamic hint}) {
-    throw UnimplementedError();
+    return Future(() => js.context.callMethod('getByName', ['load_lan_peers']));
   }
 
   Future<void> mainRemoveDiscovered({required String id, dynamic hint}) {
-    throw UnimplementedError();
+    return Future(
+        () => js.context.callMethod('getByName', ['remove_discovered']));
   }
 
   Future<void> mainChangeTheme({required String dark, dynamic hint}) {
@@ -840,7 +858,8 @@ class RustdeskImpl {
   }
 
   Future<String> mainGetLastRemoteId({dynamic hint}) {
-    throw UnimplementedError();
+    return Future(
+        () => js.context.callMethod('getByName', ['option', 'last_remote_id']));
   }
 
   Future<String> mainGetSoftwareUpdateUrl({dynamic hint}) {
@@ -848,7 +867,7 @@ class RustdeskImpl {
   }
 
   Future<String> mainGetHomeDir({dynamic hint}) {
-    throw UnimplementedError();
+    return Future.value('');
   }
 
   Future<String> mainGetLangs({dynamic hint}) {
@@ -856,11 +875,11 @@ class RustdeskImpl {
   }
 
   Future<String> mainGetTemporaryPassword({dynamic hint}) {
-    throw UnimplementedError();
+    return Future.value('');
   }
 
   Future<String> mainGetPermanentPassword({dynamic hint}) {
-    throw UnimplementedError();
+    return Future.value('');
   }
 
   Future<String> mainGetFingerprint({dynamic hint}) {
@@ -880,7 +899,7 @@ class RustdeskImpl {
   }
 
   Future<void> mainInit({required String appDir, dynamic hint}) {
-    throw UnimplementedError();
+    return Future.value();
   }
 
   Future<void> mainDeviceId({required String id, dynamic hint}) {
@@ -1093,7 +1112,10 @@ class RustdeskImpl {
 
   String translate(
       {required String name, required String locale, dynamic hint}) {
-    throw UnimplementedError();
+    return js.context.callMethod('getByName', [
+      'translate',
+      jsonEncode({'locale': locale, 'text': name})
+    ]);
   }
 
   int sessionGetRgbaSize(
@@ -1131,7 +1153,7 @@ class RustdeskImpl {
   }
 
   Future<bool> optionSynced({dynamic hint}) {
-    throw UnimplementedError();
+    return Future.value(true);
   }
 
   bool mainIsInstalled({dynamic hint}) {
@@ -1257,43 +1279,45 @@ class RustdeskImpl {
   }
 
   Future<void> mainTestWallpaper({required int second, dynamic hint}) {
-    throw UnimplementedError();
+    // TODO: implement mainTestWallpaper
+    return Future.value();
   }
 
   Future<bool> mainSupportRemoveWallpaper({dynamic hint}) {
-    throw UnimplementedError();
+    // TODO: implement mainSupportRemoveWallpaper
+    return Future.value(false);
   }
 
   bool isIncomingOnly({dynamic hint}) {
-    throw UnimplementedError();
+    return false;
   }
 
   bool isOutgoingOnly({dynamic hint}) {
-    throw UnimplementedError();
+    return false;
   }
 
   bool isCustomClient({dynamic hint}) {
-    throw UnimplementedError();
+    return false;
   }
 
   bool isDisableSettings({dynamic hint}) {
-    throw UnimplementedError();
+    return false;
   }
 
   bool isDisableAb({dynamic hint}) {
-    throw UnimplementedError();
+    return false;
   }
 
   bool isDisableAccount({dynamic hint}) {
-    throw UnimplementedError();
+    return false;
   }
 
   bool isDisableInstallation({dynamic hint}) {
-    throw UnimplementedError();
+    return false;
   }
 
   Future<bool> isPresetPassword({dynamic hint}) {
-    throw UnimplementedError();
+    return Future.value(false);
   }
 
   Future<void> sendUrlScheme({required String url, dynamic hint}) {
diff --git a/flutter/web/js/package.json b/flutter/web/js/package.json
index 15e0e75b8..436806e8d 100644
--- a/flutter/web/js/package.json
+++ b/flutter/web/js/package.json
@@ -3,19 +3,19 @@
   "version": "1.0.0",
   "scripts": {
     "dev": "vite",
-    "build": "./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && ./ts_proto.py && tsc && vite build",
+    "build": "python ./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && python ./ts_proto.py && tsc && vite build",
     "preview": "vite preview"
   },
   "devDependencies": {
     "typescript": "^4.4.4",
-    "vite": "^2.7.2"
+    "vite": "2.8"
   },
   "dependencies": {
     "fast-sha256": "^1.3.0",
     "libsodium": "^0.7.9",
     "libsodium-wrappers": "^0.7.9",
     "pcm-player": "^0.0.11",
-    "ts-proto": "^1.101.0",
+    "ts-proto": "^1.169.1",
     "wasm-feature-detect": "^1.2.11",
     "zstddec": "^0.0.2"
   }
diff --git a/flutter/web/js/src/connection.ts b/flutter/web/js/src/connection.ts
index b0c479c90..b6707b4d6 100644
--- a/flutter/web/js/src/connection.ts
+++ b/flutter/web/js/src/connection.ts
@@ -443,9 +443,9 @@ export default class Connection {
             if (this._videoTestSpeed[0] >= 30) {
               console.log(
                 "video decoder: " +
-                  parseInt(
-                    "" + this._videoTestSpeed[1] / this._videoTestSpeed[0]
-                  )
+                parseInt(
+                  "" + this._videoTestSpeed[1] / this._videoTestSpeed[0]
+                )
               );
               this._videoTestSpeed = [0, 0];
             }
@@ -456,6 +456,7 @@ export default class Connection {
   }
 
   handlePeerInfo(pi: message.PeerInfo) {
+    localStorage.setItem('last_remote_id', this._id);
     this._peerInfo = pi;
     if (pi.displays.length == 0) {
       this.msgbox("error", "Remote Error", "No Display");
@@ -540,6 +541,15 @@ export default class Connection {
     return this._options[name];
   }
 
+  // TODO:
+  getStatus(): String {
+    return JSON.stringify({status_num: 10});
+  }
+
+  // TODO:
+  checkConnStatus() {
+  }
+
   setOption(name: string, value: any) {
     if (value == undefined) {
       delete this._options[name];
diff --git a/flutter/web/js/src/globals.js b/flutter/web/js/src/globals.js
index 953add18d..992a7f1a6 100644
--- a/flutter/web/js/src/globals.js
+++ b/flutter/web/js/src/globals.js
@@ -257,6 +257,14 @@ window.setByName = (name, value) => {
       value = JSON.parse(value);
       localStorage.setItem(value.name, value.value);
       break;
+    case 'option:local':
+      value = JSON.parse(value);
+      localStorage.setItem('option:local:' + value.name, value.value);
+      break;
+    case 'option:flutter:local':
+      value = JSON.parse(value);
+      localStorage.setItem('option:flutter:local:' + value.name, value.value);
+      break;
     case 'peer_option':
       value = JSON.parse(value);
       curConn.setOption(value.name, value.value);
@@ -264,6 +272,15 @@ window.setByName = (name, value) => {
     case 'input_os_password':
       curConn.inputOsPassword(value);
       break;
+    case 'check_conn_status':
+      curConn.checkConnStatus();
+      break;
+    case 'remove_discovered':
+      removeDiscovered(value);
+      break;
+    case 'discover':
+      // TODO: discover
+      break;
     default:
       break;
   }
@@ -300,6 +317,10 @@ function _getByName(name, arg) {
       return curConn.getOption(arg) || false;
     case 'option':
       return localStorage.getItem(arg);
+    case 'option:local':
+      return localStorage.getItem('option:local:' + arg);
+    case 'option:flutter:local':
+      return localStorage.getItem('option:flutter:local:' + arg);
     case 'image_quality':
       return curConn.getImageQuality();
     case 'translate':
@@ -307,10 +328,38 @@ function _getByName(name, arg) {
       return translate(arg.locale, arg.text);
     case 'peer_option':
       return curConn.getOption(arg);
+    case 'get_conn_status':
+      if (curConn) {
+        return curConn.getStatus();
+      } else {
+        return JSON.stringify({ status_num: 0 });
+      }
     case 'test_if_valid_server':
       break;
     case 'version':
       return version;
+    case 'load_recent_peers':
+      const peersRecent = localStorage.getItem('peers-recent');
+      if (peersRecent) {
+        onRegisteredEvent(JSON.stringify({ name: 'load_recent_peers', peers: peersRecent }));
+      }
+      break;
+    case 'load_fav_peers':
+      const peersFav = localStorage.getItem('peers-fav');
+      if (peersFav) {
+        onRegisteredEvent(JSON.stringify({ name: 'load_fav_peers', peers: peersFav }));
+      }
+      break;
+    case 'load_lan_peers':
+      const peersLan = localStorage.getItem('peers-lan');
+      if (peersLan) {
+        onRegisteredEvent(JSON.stringify({ name: 'load_lan_peers', peers: peersLan }));
+      }
+      break;
+    case 'load_recent_peers_sync':
+      return localStorage.getItem('peers-recent') ?? '{}';
+    case 'load_lan_peers_sync':
+      return localStorage.getItem('peers-lan') ?? '{}';
   }
   return '';
 }
@@ -342,8 +391,20 @@ window.init = async () => {
 }
 
 export function getPeers() {
+  return _getJsonObj('peers');
+}
+
+export function getRecentPeers() {
+  return _getJsonObj('peers-recent');
+}
+
+export function getLanPeers() {
+  return _getJsonObj('peers-lan');
+}
+
+export function getJsonObj(key) {
   try {
-    return JSON.parse(localStorage.getItem('peers')) || {};
+    return JSON.parse(localStorage.getItem(key)) || {};
   } catch (e) {
     return {};
   }
@@ -380,4 +441,18 @@ export function copyToClipboard(text) {
       document.body.removeChild(textarea);
     }
   }
-}
\ No newline at end of file
+}
+
+// ========================== peers begin ==========================
+function removeDiscovered(id) {
+  try {
+    const v = localStorage.getItem('discovered');
+    if (!v) return;
+    const discovered = JSON.parse(localStorage.getItem('discovered'));
+    delete discovered[id];
+    localStorage.setItem('discovered', JSON.stringify(discovered));
+  } catch (e) {
+    console.error(e);
+  }
+}
+// ========================== peers end ===========================